| use std::ops::ControlFlow; |
| |
| use clippy_config::Conf; |
| use clippy_utils::consts::{ConstEvalCtxt, Constant}; |
| use clippy_utils::diagnostics::span_lint_and_then; |
| use clippy_utils::msrvs::{self, Msrv}; |
| use clippy_utils::res::MaybeDef; |
| use clippy_utils::sym; |
| use rustc_errors::Applicability; |
| use rustc_hir::{Expr, ExprKind, QPath, RustcVersion}; |
| use rustc_lint::{LateContext, LateLintPass, LintContext}; |
| use rustc_middle::ty::{self, TyCtxt, UintTy}; |
| use rustc_session::impl_lint_pass; |
| use rustc_span::Symbol; |
| |
| declare_clippy_lint! { |
| /// ### What it does |
| /// |
| /// Checks for instances where a `std::time::Duration` is constructed using a smaller time unit |
| /// when the value could be expressed more clearly using a larger unit. |
| /// |
| /// ### Why is this bad? |
| /// |
| /// Using a smaller unit for a duration that is evenly divisible by a larger unit reduces |
| /// readability. Readers have to mentally convert values, which can be error-prone and makes |
| /// the code less clear. |
| /// |
| /// ### Example |
| /// ``` |
| /// use std::time::Duration; |
| /// |
| /// let dur = Duration::from_millis(5_000); |
| /// let dur = Duration::from_secs(180); |
| /// let dur = Duration::from_mins(10 * 60); |
| /// ``` |
| /// |
| /// Use instead: |
| /// ``` |
| /// use std::time::Duration; |
| /// |
| /// let dur = Duration::from_secs(5); |
| /// let dur = Duration::from_mins(3); |
| /// let dur = Duration::from_hours(10); |
| /// ``` |
| #[clippy::version = "1.95.0"] |
| pub DURATION_SUBOPTIMAL_UNITS, |
| pedantic, |
| "constructing a `Duration` using a smaller unit when a larger unit would be more readable" |
| } |
| |
| impl_lint_pass!(DurationSuboptimalUnits => [DURATION_SUBOPTIMAL_UNITS]); |
| |
| pub struct DurationSuboptimalUnits { |
| msrv: Msrv, |
| units: Vec<Unit>, |
| } |
| |
| impl DurationSuboptimalUnits { |
| pub fn new(tcx: TyCtxt<'_>, conf: &'static Conf) -> Self { |
| // The order of the units matters, as they are walked top to bottom |
| let mut units = UNITS.to_vec(); |
| if tcx.features().enabled(sym::duration_constructors) { |
| units.extend(EXTENDED_UNITS); |
| } |
| Self { msrv: conf.msrv, units } |
| } |
| } |
| |
| impl LateLintPass<'_> for DurationSuboptimalUnits { |
| fn check_expr(&mut self, cx: &LateContext<'_>, expr: &'_ Expr<'_>) { |
| if !expr.span.in_external_macro(cx.sess().source_map()) |
| // Check if a function on std::time::Duration is called |
| && let ExprKind::Call(func, [arg]) = expr.kind |
| && let ExprKind::Path(QPath::TypeRelative(func_ty, func_name)) = func.kind |
| && cx |
| .typeck_results() |
| .node_type(func_ty.hir_id) |
| .is_diag_item(cx, sym::Duration) |
| && matches!(cx.typeck_results().expr_ty_adjusted(arg).kind(), ty::Uint(UintTy::U64)) |
| // We intentionally don't want to evaluate referenced constants, as we don't want to |
| // recommend a literal value over using constants: |
| // |
| // let dur = Duration::from_secs(SIXTY); |
| // ^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `Duration::from_mins(1)` |
| && let Some(Constant::Int(value)) = ConstEvalCtxt::new(cx).eval_local(arg, expr.span.ctxt()) |
| && let Ok(value) = u64::try_from(value) // Cannot fail |
| // There is no need to promote e.g. 0 seconds to 0 hours |
| && value != 0 |
| && let Some((promoted_constructor, promoted_value)) = self.promote(cx, func_name.ident.name, value) |
| { |
| span_lint_and_then( |
| cx, |
| DURATION_SUBOPTIMAL_UNITS, |
| expr.span, |
| "constructing a `Duration` using a smaller unit when a larger unit would be more readable", |
| |diag| { |
| let suggestions = vec![ |
| (func_name.ident.span, promoted_constructor.to_string()), |
| (arg.span, promoted_value.to_string()), |
| ]; |
| diag.multipart_suggestion( |
| format!("try using {promoted_constructor}"), |
| suggestions, |
| Applicability::MachineApplicable, |
| ); |
| }, |
| ); |
| } |
| } |
| } |
| |
| impl DurationSuboptimalUnits { |
| /// Tries to promote the given constructor and value to a bigger time unit and returns the |
| /// promoted constructor name and value. |
| /// |
| /// Returns [`None`] in case no promotion could be done. |
| fn promote(&self, cx: &LateContext<'_>, constructor_name: Symbol, value: u64) -> Option<(Symbol, u64)> { |
| let (best_unit, best_value) = self |
| .units |
| .iter() |
| .skip_while(|unit| unit.constructor_name != constructor_name) |
| .skip(1) |
| .try_fold( |
| (constructor_name, value), |
| |(current_unit, current_value), bigger_unit| { |
| if let Some(bigger_value) = current_value.div_exact(u64::from(bigger_unit.factor)) |
| && bigger_unit.stable_since.is_none_or(|v| self.msrv.meets(cx, v)) |
| { |
| ControlFlow::Continue((bigger_unit.constructor_name, bigger_value)) |
| } else { |
| // We have to break early, as we can't skip versions, as they are needed to |
| // correctly calculate the promoted value. |
| ControlFlow::Break((current_unit, current_value)) |
| } |
| }, |
| ) |
| .into_value(); |
| (best_unit != constructor_name).then_some((best_unit, best_value)) |
| } |
| } |
| |
| #[derive(Clone, Copy)] |
| struct Unit { |
| /// Name of the constructor on [`Duration`](std::time::Duration) to construct it from the given |
| /// unit, e.g. [`Duration::from_secs`](std::time::Duration::from_secs) |
| constructor_name: Symbol, |
| |
| /// The increase factor over the previous (smaller) unit |
| factor: u16, |
| |
| /// In what rustc version stable support for this constructor was added. |
| /// We do not need to track the version stable support in const contexts was added, as the const |
| /// stabilization was done in an ascending order of the time unites, so it's always valid to |
| /// promote a const constructor. |
| stable_since: Option<RustcVersion>, |
| } |
| |
| /// Time unit constructors available on stable. The order matters! |
| const UNITS: [Unit; 6] = [ |
| Unit { |
| constructor_name: sym::from_nanos, |
| // The value doesn't matter, as there is no previous unit |
| factor: 0, |
| stable_since: Some(msrvs::DURATION_FROM_NANOS_MICROS), |
| }, |
| Unit { |
| constructor_name: sym::from_micros, |
| factor: 1_000, |
| stable_since: Some(msrvs::DURATION_FROM_NANOS_MICROS), |
| }, |
| Unit { |
| constructor_name: sym::from_millis, |
| factor: 1_000, |
| stable_since: Some(msrvs::DURATION_FROM_MILLIS_SECS), |
| }, |
| Unit { |
| constructor_name: sym::from_secs, |
| factor: 1_000, |
| stable_since: Some(msrvs::DURATION_FROM_MILLIS_SECS), |
| }, |
| Unit { |
| constructor_name: sym::from_mins, |
| factor: 60, |
| stable_since: Some(msrvs::DURATION_FROM_MINUTES_HOURS), |
| }, |
| Unit { |
| constructor_name: sym::from_hours, |
| factor: 60, |
| stable_since: Some(msrvs::DURATION_FROM_MINUTES_HOURS), |
| }, |
| ]; |
| |
| /// Time unit constructors behind the `duration_constructors` feature. The order matters! |
| const EXTENDED_UNITS: [Unit; 2] = [ |
| Unit { |
| constructor_name: sym::from_days, |
| factor: 24, |
| stable_since: None, |
| }, |
| Unit { |
| constructor_name: sym::from_weeks, |
| factor: 7, |
| stable_since: None, |
| }, |
| ]; |