Skip to content

Commit 993e4f1

Browse files
committed
Add new duration_suboptimal_units lint
1 parent 37330eb commit 993e4f1

File tree

9 files changed

+317
-2
lines changed

9 files changed

+317
-2
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: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
use clippy_config::Conf;
2+
use clippy_utils::consts::{ConstEvalCtxt, Constant};
3+
use clippy_utils::diagnostics::span_lint_and_sugg;
4+
use clippy_utils::msrvs::{self, Msrv};
5+
use clippy_utils::res::MaybeDef;
6+
use rustc_errors::Applicability;
7+
use rustc_hir::{Expr, ExprKind, QPath, RustcVersion};
8+
use rustc_lint::{LateContext, LateLintPass};
9+
use rustc_session::impl_lint_pass;
10+
use rustc_span::symbol::sym;
11+
12+
declare_clippy_lint! {
13+
/// ### What it does
14+
///
15+
/// Checks for instances where a `std::time::Duration` is constructed using a smaller time unit
16+
/// when the value could be expressed more clearly using a larger unit.
17+
///
18+
/// ### Why is this bad?
19+
///
20+
/// Using a smaller unit for a duration that is evenly divisible by a larger unit reduces
21+
/// readability. Readers have to mentally convert values, which can be error-prone and makes
22+
/// the code less clear.
23+
///
24+
/// ### Example
25+
/// ```
26+
/// use std::time::Duration;
27+
///
28+
/// let dur = Duration::from_millis(5_000);
29+
/// let dur = Duration::from_secs(180);
30+
/// let dur = Duration::from_mins(10 * 60);
31+
/// ```
32+
///
33+
/// Use instead:
34+
/// ```
35+
/// use std::time::Duration;
36+
///
37+
/// let dur = Duration::from_secs(5);
38+
/// let dur = Duration::from_mins(3);
39+
/// let dur = Duration::from_hours(10);
40+
/// ```
41+
#[clippy::version = "1.94.0"]
42+
pub DURATION_SUBOPTIMAL_UNITS,
43+
pedantic,
44+
"constructing a `Duration` using a smaller unit when a larger unit would be more readable"
45+
}
46+
47+
impl_lint_pass!(DurationSuboptimalUnits => [DURATION_SUBOPTIMAL_UNITS]);
48+
49+
pub struct DurationSuboptimalUnits {
50+
msrv: Msrv,
51+
}
52+
53+
impl DurationSuboptimalUnits {
54+
pub fn new(conf: &'static Conf) -> Self {
55+
Self { msrv: conf.msrv }
56+
}
57+
}
58+
59+
impl LateLintPass<'_> for DurationSuboptimalUnits {
60+
fn check_expr(&mut self, cx: &LateContext<'_>, expr: &'_ Expr<'_>) {
61+
// Check if a function on std::time::Duration is called
62+
if let ExprKind::Call(func, [arg]) = expr.kind
63+
&& let ExprKind::Path(QPath::TypeRelative(func_ty, func_name)) = func.kind
64+
&& cx
65+
.typeck_results()
66+
.node_type(func_ty.hir_id)
67+
.is_diag_item(cx, sym::Duration)
68+
{
69+
// We intentionally don't want to evaluate referenced constants, as we don't want to
70+
// recommend a literal value over using constants:
71+
//
72+
// let dur = Duration::from_secs(SIXTY);
73+
// ^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `Duration::from_mins(1)`
74+
let Some(Constant::Int(value)) = ConstEvalCtxt::new(cx).eval_local(arg, arg.span.ctxt()) else {
75+
return;
76+
};
77+
// There is no need to promote e.g. 0 seconds to 0 hours
78+
if value == 0 {
79+
return;
80+
}
81+
82+
let function_name = func_name.ident.name.as_str();
83+
let Some((promoted_function_name, promoted_value)) = self.promote(cx, function_name, value) else {
84+
return;
85+
};
86+
87+
span_lint_and_sugg(
88+
cx,
89+
DURATION_SUBOPTIMAL_UNITS,
90+
expr.span,
91+
"constructing a `Duration` using a smaller unit when a larger unit would be more readable",
92+
"try",
93+
format!("Duration::{promoted_function_name}({promoted_value})"),
94+
Applicability::MachineApplicable,
95+
);
96+
}
97+
}
98+
}
99+
100+
impl DurationSuboptimalUnits {
101+
/// Tries to promote the given function and value to a bigger time unit and returns the promoted
102+
/// function name and value.
103+
///
104+
/// Returns [`None`] in case no promotion could be done.
105+
fn promote<'a>(&self, cx: &LateContext<'_>, function_name: &'a str, value: u128) -> Option<(&'a str, u128)> {
106+
let mut promoted = false;
107+
let mut promoted_function_name = function_name;
108+
let mut promoted_value = value;
109+
110+
let bigger_units = UNITS
111+
.iter()
112+
.skip_while(|unit| unit.function_name != function_name)
113+
.skip(1);
114+
115+
for bigger_unit in bigger_units {
116+
if !self.msrv.meets(cx, bigger_unit.const_support_added) {
117+
// We have to break early, as we can not skip versions.
118+
// We can't skip any, as they are needed to correctly calculate the promoted value.
119+
break;
120+
}
121+
122+
if promoted_value.is_multiple_of(bigger_unit.factor) {
123+
promoted = true;
124+
promoted_value /= bigger_unit.factor;
125+
promoted_function_name = bigger_unit.function_name;
126+
} else {
127+
break;
128+
}
129+
}
130+
131+
promoted.then_some((promoted_function_name, promoted_value))
132+
}
133+
}
134+
135+
struct Unit {
136+
/// Name of the function on [`Duration`](std::time::Duration) to construct it from the given
137+
/// unit, e.g. [`Duration::from_secs`](std::time::Duration::from_secs)
138+
function_name: &'static str,
139+
140+
/// The increase factor over the previous (smaller) unit
141+
factor: u128,
142+
143+
/// In what rustc version support for this function in const contexts was added
144+
const_support_added: RustcVersion,
145+
}
146+
147+
const UNITS: &[Unit] = &[
148+
Unit {
149+
function_name: "from_nanos",
150+
// The value doesn't matter, as there is no previous unit
151+
factor: 0,
152+
const_support_added: msrvs::DURATION_CONSTS,
153+
},
154+
Unit {
155+
function_name: "from_micros",
156+
factor: 1_000,
157+
const_support_added: msrvs::DURATION_CONSTS,
158+
},
159+
Unit {
160+
function_name: "from_millis",
161+
factor: 1_000,
162+
const_support_added: msrvs::DURATION_CONSTS,
163+
},
164+
Unit {
165+
function_name: "from_secs",
166+
factor: 1_000,
167+
const_support_added: msrvs::DURATION_CONSTS,
168+
},
169+
Unit {
170+
function_name: "from_mins",
171+
factor: 60,
172+
const_support_added: msrvs::DURATION_CONSTRUCTORS_LITE,
173+
},
174+
Unit {
175+
function_name: "from_hours",
176+
factor: 60,
177+
const_support_added: msrvs::DURATION_CONSTRUCTORS_LITE,
178+
},
179+
// `from_days` and `from_weeks` are behind the nightly `duration_constructors` feature and can
180+
// be added once stabilized
181+
];

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ mod doc;
113113
mod double_parens;
114114
mod drop_forget_ref;
115115
mod duplicate_mod;
116+
mod duration_suboptimal_units;
116117
mod else_if_without_else;
117118
mod empty_drop;
118119
mod empty_enums;
@@ -855,6 +856,7 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co
855856
Box::new(|_| Box::new(volatile_composites::VolatileComposites)),
856857
Box::new(|_| Box::<replace_box::ReplaceBox>::default()),
857858
Box::new(move |_| Box::new(manual_ilog2::ManualIlog2::new(conf))),
859+
Box::new(move |_| Box::new(duration_suboptimal_units::DurationSuboptimalUnits::new(conf))),
858860
// add late passes here, used by `cargo dev new_lint`
859861
];
860862
store.late_passes.extend(late_lints);

clippy_utils/src/msrvs.rs

Lines changed: 2 additions & 1 deletion
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_CONSTRUCTORS_LITE }
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,7 +70,7 @@ 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, DURATION_CONSTS }
7374
1,31,0 { OPTION_REPLACE }
7475
1,30,0 { ITERATOR_FIND_MAP, TOOL_ATTRIBUTES }
7576
1,29,0 { ITER_FLATTEN }
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#![warn(clippy::duration_suboptimal_units)]
2+
3+
use std::time::Duration;
4+
5+
fn main() {
6+
let dur = Duration::from_secs(0);
7+
let dur = Duration::from_secs(42);
8+
let dur = Duration::from_hours(3);
9+
10+
let dur = Duration::from_mins(1);
11+
//~^ duration_suboptimal_units
12+
let dur = Duration::from_mins(3);
13+
//~^ duration_suboptimal_units
14+
let dur = Duration::from_mins(10);
15+
//~^ duration_suboptimal_units
16+
let dur = Duration::from_hours(24);
17+
//~^ duration_suboptimal_units
18+
let dur = Duration::from_secs(5);
19+
//~^ duration_suboptimal_units
20+
let dur = Duration::from_hours(13);
21+
//~^ duration_suboptimal_units
22+
23+
// Constants are intentionally not resolved, as we don't want to recommend a literal value over
24+
// using constants.
25+
const SIXTY: u64 = 60;
26+
let dur = Duration::from_secs(SIXTY);
27+
// Technically it would be nice to use Duration::from_mins(SIXTY) here, but that is a follow-up
28+
let dur = Duration::from_secs(SIXTY * 60);
29+
}
30+
31+
mod my_duration {
32+
struct Duration {}
33+
34+
impl Duration {
35+
pub const fn from_secs(_secs: u64) -> Self {
36+
Self {}
37+
}
38+
}
39+
40+
fn test() {
41+
// Only suggest the change for std::time::Duration, not for other Duration structs
42+
let dur = Duration::from_secs(60);
43+
}
44+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#![warn(clippy::duration_suboptimal_units)]
2+
3+
use std::time::Duration;
4+
5+
fn main() {
6+
let dur = Duration::from_secs(0);
7+
let dur = Duration::from_secs(42);
8+
let dur = Duration::from_hours(3);
9+
10+
let dur = Duration::from_secs(60);
11+
//~^ duration_suboptimal_units
12+
let dur = Duration::from_secs(180);
13+
//~^ duration_suboptimal_units
14+
let dur = Duration::from_secs(10 * 60);
15+
//~^ duration_suboptimal_units
16+
let dur = Duration::from_mins(24 * 60);
17+
//~^ duration_suboptimal_units
18+
let dur = Duration::from_millis(5_000);
19+
//~^ duration_suboptimal_units
20+
let dur = Duration::from_nanos(13 * 60 * 60 * 1_000 * 1_000 * 1_000);
21+
//~^ duration_suboptimal_units
22+
23+
// Constants are intentionally not resolved, as we don't want to recommend a literal value over
24+
// using constants.
25+
const SIXTY: u64 = 60;
26+
let dur = Duration::from_secs(SIXTY);
27+
// Technically it would be nice to use Duration::from_mins(SIXTY) here, but that is a follow-up
28+
let dur = Duration::from_secs(SIXTY * 60);
29+
}
30+
31+
mod my_duration {
32+
struct Duration {}
33+
34+
impl Duration {
35+
pub const fn from_secs(_secs: u64) -> Self {
36+
Self {}
37+
}
38+
}
39+
40+
fn test() {
41+
// Only suggest the change for std::time::Duration, not for other Duration structs
42+
let dur = Duration::from_secs(60);
43+
}
44+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
error: constructing a `Duration` using a smaller unit when a larger unit would be more readable
2+
--> tests/ui/duration_suboptimal_units.rs:10:15
3+
|
4+
LL | let dur = Duration::from_secs(60);
5+
| ^^^^^^^^^^^^^^^^^^^^^^^ help: try: `Duration::from_mins(1)`
6+
|
7+
= note: `-D clippy::duration-suboptimal-units` implied by `-D warnings`
8+
= help: to override `-D warnings` add `#[allow(clippy::duration_suboptimal_units)]`
9+
10+
error: constructing a `Duration` using a smaller unit when a larger unit would be more readable
11+
--> tests/ui/duration_suboptimal_units.rs:12:15
12+
|
13+
LL | let dur = Duration::from_secs(180);
14+
| ^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `Duration::from_mins(3)`
15+
16+
error: constructing a `Duration` using a smaller unit when a larger unit would be more readable
17+
--> tests/ui/duration_suboptimal_units.rs:14:15
18+
|
19+
LL | let dur = Duration::from_secs(10 * 60);
20+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `Duration::from_mins(10)`
21+
22+
error: constructing a `Duration` using a smaller unit when a larger unit would be more readable
23+
--> tests/ui/duration_suboptimal_units.rs:16:15
24+
|
25+
LL | let dur = Duration::from_mins(24 * 60);
26+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `Duration::from_hours(24)`
27+
28+
error: constructing a `Duration` using a smaller unit when a larger unit would be more readable
29+
--> tests/ui/duration_suboptimal_units.rs:18:15
30+
|
31+
LL | let dur = Duration::from_millis(5_000);
32+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `Duration::from_secs(5)`
33+
34+
error: constructing a `Duration` using a smaller unit when a larger unit would be more readable
35+
--> tests/ui/duration_suboptimal_units.rs:20:15
36+
|
37+
LL | let dur = Duration::from_nanos(13 * 60 * 60 * 1_000 * 1_000 * 1_000);
38+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `Duration::from_hours(13)`
39+
40+
error: aborting due to 6 previous errors
41+

0 commit comments

Comments
 (0)