Skip to content

Commit a3a052d

Browse files
committed
Add new duration_suboptimal_units lint
1 parent 741b684 commit a3a052d

13 files changed

+624
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6406,6 +6406,7 @@ Released 2018-09-13
64066406
[`duplicate_mod`]: https://rust-lang.github.io/rust-clippy/master/index.html#duplicate_mod
64076407
[`duplicate_underscore_argument`]: https://rust-lang.github.io/rust-clippy/master/index.html#duplicate_underscore_argument
64086408
[`duplicated_attributes`]: https://rust-lang.github.io/rust-clippy/master/index.html#duplicated_attributes
6409+
[`duration_suboptimal_units`]: https://rust-lang.github.io/rust-clippy/master/index.html#duration_suboptimal_units
64096410
[`duration_subsec`]: https://rust-lang.github.io/rust-clippy/master/index.html#duration_subsec
64106411
[`eager_transmute`]: https://rust-lang.github.io/rust-clippy/master/index.html#eager_transmute
64116412
[`elidable_lifetime_names`]: https://rust-lang.github.io/rust-clippy/master/index.html#elidable_lifetime_names

clippy_dev/src/serve.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ pub fn run(port: u16, lint: Option<String>) -> ! {
5454
}
5555

5656
// Delay to avoid updating the metadata too aggressively.
57-
thread::sleep(Duration::from_millis(1000));
57+
thread::sleep(Duration::from_secs(1));
5858
}
5959
}
6060

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
135135
crate::drop_forget_ref::FORGET_NON_DROP_INFO,
136136
crate::drop_forget_ref::MEM_FORGET_INFO,
137137
crate::duplicate_mod::DUPLICATE_MOD_INFO,
138+
crate::duration_suboptimal_units::DURATION_SUBOPTIMAL_UNITS_INFO,
138139
crate::else_if_without_else::ELSE_IF_WITHOUT_ELSE_INFO,
139140
crate::empty_drop::EMPTY_DROP_INFO,
140141
crate::empty_enums::EMPTY_ENUMS_INFO,
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
use std::ops::ControlFlow;
2+
3+
use clippy_config::Conf;
4+
use clippy_utils::consts::{ConstEvalCtxt, Constant};
5+
use clippy_utils::diagnostics::span_lint_and_then;
6+
use clippy_utils::msrvs::{self, Msrv};
7+
use clippy_utils::res::MaybeDef;
8+
use clippy_utils::sym;
9+
use rustc_errors::Applicability;
10+
use rustc_hir::{Expr, ExprKind, QPath, RustcVersion};
11+
use rustc_lint::{LateContext, LateLintPass, LintContext};
12+
use rustc_middle::ty::TyCtxt;
13+
use rustc_session::impl_lint_pass;
14+
use rustc_span::Symbol;
15+
16+
declare_clippy_lint! {
17+
/// ### What it does
18+
///
19+
/// Checks for instances where a `std::time::Duration` is constructed using a smaller time unit
20+
/// when the value could be expressed more clearly using a larger unit.
21+
///
22+
/// ### Why is this bad?
23+
///
24+
/// Using a smaller unit for a duration that is evenly divisible by a larger unit reduces
25+
/// readability. Readers have to mentally convert values, which can be error-prone and makes
26+
/// the code less clear.
27+
///
28+
/// ### Example
29+
/// ```
30+
/// use std::time::Duration;
31+
///
32+
/// let dur = Duration::from_millis(5_000);
33+
/// let dur = Duration::from_secs(180);
34+
/// let dur = Duration::from_mins(10 * 60);
35+
/// ```
36+
///
37+
/// Use instead:
38+
/// ```
39+
/// use std::time::Duration;
40+
///
41+
/// let dur = Duration::from_secs(5);
42+
/// let dur = Duration::from_mins(3);
43+
/// let dur = Duration::from_hours(10);
44+
/// ```
45+
#[clippy::version = "1.94.0"]
46+
pub DURATION_SUBOPTIMAL_UNITS,
47+
pedantic,
48+
"constructing a `Duration` using a smaller unit when a larger unit would be more readable"
49+
}
50+
51+
impl_lint_pass!(DurationSuboptimalUnits => [DURATION_SUBOPTIMAL_UNITS]);
52+
53+
pub struct DurationSuboptimalUnits {
54+
msrv: Msrv,
55+
units: Vec<Unit>,
56+
}
57+
58+
impl DurationSuboptimalUnits {
59+
pub fn new(tcx: TyCtxt<'_>, conf: &'static Conf) -> Self {
60+
// The order of the units matters, as they are walked top to bottom
61+
let mut units = UNITS.to_vec();
62+
if tcx.features().enabled(sym::duration_constructors) {
63+
units.extend(EXTENDED_UNITS);
64+
}
65+
Self { msrv: conf.msrv, units }
66+
}
67+
}
68+
69+
impl LateLintPass<'_> for DurationSuboptimalUnits {
70+
fn check_expr(&mut self, cx: &LateContext<'_>, expr: &'_ Expr<'_>) {
71+
if !expr.span.in_external_macro(cx.sess().source_map())
72+
// Check if a function on std::time::Duration is called
73+
&& let ExprKind::Call(func, [arg]) = expr.kind
74+
&& let ExprKind::Path(QPath::TypeRelative(func_ty, func_name)) = func.kind
75+
&& cx
76+
.typeck_results()
77+
.node_type(func_ty.hir_id)
78+
.is_diag_item(cx, sym::Duration)
79+
// We intentionally don't want to evaluate referenced constants, as we don't want to
80+
// recommend a literal value over using constants:
81+
//
82+
// let dur = Duration::from_secs(SIXTY);
83+
// ^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `Duration::from_mins(1)`
84+
&& let Some(Constant::Int(value)) = ConstEvalCtxt::new(cx).eval_local(arg, expr.span.ctxt())
85+
&& let value = u64::try_from(value).expect("All Duration::from_<time-unit> constructors take a u64")
86+
// There is no need to promote e.g. 0 seconds to 0 hours
87+
&& value != 0
88+
&& let Some((promoted_constructor, promoted_value)) = self.promote(cx, expr, func_name.ident.name, value)
89+
{
90+
span_lint_and_then(
91+
cx,
92+
DURATION_SUBOPTIMAL_UNITS,
93+
expr.span,
94+
"constructing a `Duration` using a smaller unit when a larger unit would be more readable",
95+
|diag| {
96+
let suggestions = vec![
97+
(func_name.ident.span, promoted_constructor.to_string()),
98+
(arg.span, promoted_value.to_string()),
99+
];
100+
diag.multipart_suggestion_verbose(
101+
format!("try using {promoted_constructor}"),
102+
suggestions,
103+
Applicability::MachineApplicable,
104+
);
105+
},
106+
);
107+
}
108+
}
109+
}
110+
111+
impl DurationSuboptimalUnits {
112+
/// Tries to promote the given constructor and value to a bigger time unit and returns the
113+
/// promoted constructor name and value.
114+
///
115+
/// Returns [`None`] in case no promotion could be done.
116+
fn promote(
117+
&self,
118+
cx: &LateContext<'_>,
119+
expr: &'_ Expr<'_>,
120+
constructor_name: Symbol,
121+
value: u64,
122+
) -> Option<(Symbol, u64)> {
123+
let in_const = cx.tcx.hir_is_inside_const_context(expr.hir_id);
124+
125+
let (best_unit, best_value) = self
126+
.units
127+
.iter()
128+
.skip_while(|unit| unit.constructor_name != constructor_name)
129+
.skip(1)
130+
.try_fold(
131+
(constructor_name, value),
132+
|(current_unit, current_value), bigger_unit| {
133+
if let Some(bigger_value) = current_value.div_exact(u64::from(bigger_unit.factor))
134+
&& bigger_unit
135+
.stable_since(in_const)
136+
.is_none_or(|v| self.msrv.meets(cx, v))
137+
{
138+
ControlFlow::Continue((bigger_unit.constructor_name, bigger_value))
139+
} else {
140+
// We have to break early, as we can't skip versions, as they are needed to
141+
// correctly calculate the promoted value.
142+
ControlFlow::Break((current_unit, current_value))
143+
}
144+
},
145+
)
146+
.into_value();
147+
(best_unit != constructor_name).then_some((best_unit, best_value))
148+
}
149+
}
150+
151+
#[derive(Clone, Copy)]
152+
struct Unit {
153+
/// Name of the constructor on [`Duration`](std::time::Duration) to construct it from the given
154+
/// unit, e.g. [`Duration::from_secs`](std::time::Duration::from_secs)
155+
constructor_name: Symbol,
156+
157+
/// The increase factor over the previous (smaller) unit
158+
factor: u16,
159+
160+
/// In what rustc version stable support for this constructor was added.
161+
stable_since: Option<RustcVersion>,
162+
163+
/// In what rustc version stable support for this constructor in const contexts was added.
164+
const_stable_since: Option<RustcVersion>,
165+
}
166+
167+
impl Unit {
168+
/// In what rustc version stable support was added. This depends if the constructor call is in
169+
/// a const context or not.
170+
///
171+
/// If [`None`] is returned, there is no rustc version restriction (e.g. because it actually
172+
/// depends on a feature).
173+
pub fn stable_since(&self, in_const: bool) -> Option<RustcVersion> {
174+
if in_const {
175+
self.const_stable_since
176+
} else {
177+
self.stable_since
178+
}
179+
}
180+
}
181+
182+
/// Time unit constructors available on stable. The order matters!
183+
const UNITS: [Unit; 6] = [
184+
Unit {
185+
constructor_name: sym::from_nanos,
186+
// The value doesn't matter, as there is no previous unit
187+
factor: 0,
188+
stable_since: Some(msrvs::DURATION_FROM_NANOS_MICROS),
189+
const_stable_since: Some(msrvs::CONST_DURATION_FROM_NANOS_MICROS_MILLIS_SECS),
190+
},
191+
Unit {
192+
constructor_name: sym::from_micros,
193+
factor: 1_000,
194+
stable_since: Some(msrvs::DURATION_FROM_NANOS_MICROS),
195+
const_stable_since: Some(msrvs::CONST_DURATION_FROM_NANOS_MICROS_MILLIS_SECS),
196+
},
197+
Unit {
198+
constructor_name: sym::from_millis,
199+
factor: 1_000,
200+
stable_since: Some(msrvs::DURATION_FROM_MILLIS_SECS),
201+
const_stable_since: Some(msrvs::CONST_DURATION_FROM_NANOS_MICROS_MILLIS_SECS),
202+
},
203+
Unit {
204+
constructor_name: sym::from_secs,
205+
factor: 1_000,
206+
stable_since: Some(msrvs::DURATION_FROM_MILLIS_SECS),
207+
const_stable_since: Some(msrvs::CONST_DURATION_FROM_NANOS_MICROS_MILLIS_SECS),
208+
},
209+
Unit {
210+
constructor_name: sym::from_mins,
211+
factor: 60,
212+
stable_since: Some(msrvs::DURATION_FROM_MINUTES_HOURS),
213+
const_stable_since: Some(msrvs::DURATION_FROM_MINUTES_HOURS),
214+
},
215+
Unit {
216+
constructor_name: sym::from_hours,
217+
factor: 60,
218+
stable_since: Some(msrvs::DURATION_FROM_MINUTES_HOURS),
219+
const_stable_since: Some(msrvs::DURATION_FROM_MINUTES_HOURS),
220+
},
221+
];
222+
223+
/// Time unit constructors behind the `duration_constructors` feature. The order matters!
224+
const EXTENDED_UNITS: [Unit; 2] = [
225+
Unit {
226+
constructor_name: sym::from_days,
227+
factor: 24,
228+
stable_since: None,
229+
const_stable_since: None,
230+
},
231+
Unit {
232+
constructor_name: sym::from_weeks,
233+
factor: 7,
234+
stable_since: None,
235+
const_stable_since: None,
236+
},
237+
];

clippy_lints/src/lib.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
#![cfg_attr(bootstrap, feature(array_windows))]
22
#![feature(box_patterns)]
3-
#![feature(macro_metavar_expr_concat)]
3+
#![feature(control_flow_into_value)]
4+
#![feature(exact_div)]
45
#![feature(f128)]
56
#![feature(f16)]
67
#![feature(if_let_guard)]
78
#![feature(iter_intersperse)]
89
#![feature(iter_partition_in_place)]
10+
#![feature(macro_metavar_expr_concat)]
911
#![feature(never_type)]
1012
#![feature(rustc_private)]
1113
#![feature(stmt_expr_attributes)]
@@ -113,6 +115,7 @@ mod doc;
113115
mod double_parens;
114116
mod drop_forget_ref;
115117
mod duplicate_mod;
118+
mod duration_suboptimal_units;
116119
mod else_if_without_else;
117120
mod empty_drop;
118121
mod empty_enums;
@@ -857,6 +860,7 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co
857860
Box::new(|_| Box::<replace_box::ReplaceBox>::default()),
858861
Box::new(move |_| Box::new(manual_ilog2::ManualIlog2::new(conf))),
859862
Box::new(|_| Box::new(same_length_and_capacity::SameLengthAndCapacity)),
863+
Box::new(move |tcx| Box::new(duration_suboptimal_units::DurationSuboptimalUnits::new(tcx, conf))),
860864
// add late passes here, used by `cargo dev new_lint`
861865
];
862866
store.late_passes.extend(late_lints);

clippy_utils/src/msrvs.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ macro_rules! msrv_aliases {
2323

2424
// names may refer to stabilized feature flags or library items
2525
msrv_aliases! {
26+
1,91,0 { DURATION_FROM_MINUTES_HOURS }
2627
1,88,0 { LET_CHAINS }
2728
1,87,0 { OS_STR_DISPLAY, INT_MIDPOINT, CONST_CHAR_IS_DIGIT, UNSIGNED_IS_MULTIPLE_OF, INTEGER_SIGN_CAST }
2829
1,85,0 { UINT_FLOAT_MIDPOINT, CONST_SIZE_OF_VAL }
@@ -69,19 +70,20 @@ msrv_aliases! {
6970
1,35,0 { OPTION_COPIED, RANGE_CONTAINS }
7071
1,34,0 { TRY_FROM }
7172
1,33,0 { UNDERSCORE_IMPORTS }
72-
1,32,0 { CONST_IS_POWER_OF_TWO }
73+
1,32,0 { CONST_IS_POWER_OF_TWO, CONST_DURATION_FROM_NANOS_MICROS_MILLIS_SECS }
7374
1,31,0 { OPTION_REPLACE }
7475
1,30,0 { ITERATOR_FIND_MAP, TOOL_ATTRIBUTES }
7576
1,29,0 { ITER_FLATTEN }
7677
1,28,0 { FROM_BOOL, REPEAT_WITH, SLICE_FROM_REF }
77-
1,27,0 { ITERATOR_TRY_FOLD, DOUBLE_ENDED_ITERATOR_RFIND }
78+
1,27,0 { ITERATOR_TRY_FOLD, DOUBLE_ENDED_ITERATOR_RFIND, DURATION_FROM_NANOS_MICROS }
7879
1,26,0 { RANGE_INCLUSIVE, STRING_RETAIN, POINTER_ADD_SUB_METHODS }
7980
1,24,0 { IS_ASCII_DIGIT, PTR_NULL }
8081
1,18,0 { HASH_MAP_RETAIN, HASH_SET_RETAIN }
8182
1,17,0 { FIELD_INIT_SHORTHAND, STATIC_IN_CONST, EXPECT_ERR }
8283
1,16,0 { STR_REPEAT, RESULT_UNWRAP_OR_DEFAULT }
8384
1,15,0 { MAYBE_BOUND_IN_WHERE }
8485
1,13,0 { QUESTION_MARK_OPERATOR }
86+
1,3,0 { DURATION_FROM_MILLIS_SECS }
8587
}
8688

8789
/// `#[clippy::msrv]` attributes are rarely used outside of Clippy's test suite, as a basic

clippy_utils/src/sym.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ generate! {
140140
disallowed_types,
141141
drain,
142142
dump,
143+
duration_constructors,
143144
ends_with,
144145
enum_glob_use,
145146
enumerate,
@@ -164,12 +165,20 @@ generate! {
164165
from_be_bytes,
165166
from_bytes_with_nul,
166167
from_bytes_with_nul_unchecked,
168+
from_days,
169+
from_hours,
167170
from_le_bytes,
171+
from_micros,
172+
from_millis,
173+
from_mins,
174+
from_nanos,
168175
from_ne_bytes,
169176
from_ptr,
170177
from_raw,
171178
from_raw_parts,
179+
from_secs,
172180
from_str_radix,
181+
from_weeks,
173182
fs,
174183
fuse,
175184
futures_util,

0 commit comments

Comments
 (0)