Skip to content

Commit 6f9d615

Browse files
committed
Add new duration_suboptimal_units lint
1 parent 37330eb commit 6f9d615

File tree

9 files changed

+301
-2
lines changed

9 files changed

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

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: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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_hours(13);
19+
//~^ duration_suboptimal_units
20+
21+
// Constants are intentionally not resolved, as we don't want to recommend a literal value over
22+
// using constants.
23+
const SIXTY: u64 = 60;
24+
let dur = Duration::from_secs(SIXTY);
25+
// Technically it would be nice to use Duration::from_mins(SIXTY) here, but that is a follow-up
26+
let dur = Duration::from_secs(SIXTY * 60);
27+
}
28+
29+
mod my_duration {
30+
struct Duration {}
31+
32+
impl Duration {
33+
pub const fn from_secs(_secs: u64) -> Self {
34+
Self {}
35+
}
36+
}
37+
38+
fn test() {
39+
// Only suggest the change for std::time::Duration, not for other Duration structs
40+
let dur = Duration::from_secs(60);
41+
}
42+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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_nanos(13 * 60 * 60 * 1_000 * 1_000 * 1_000);
19+
//~^ duration_suboptimal_units
20+
21+
// Constants are intentionally not resolved, as we don't want to recommend a literal value over
22+
// using constants.
23+
const SIXTY: u64 = 60;
24+
let dur = Duration::from_secs(SIXTY);
25+
// Technically it would be nice to use Duration::from_mins(SIXTY) here, but that is a follow-up
26+
let dur = Duration::from_secs(SIXTY * 60);
27+
}
28+
29+
mod my_duration {
30+
struct Duration {}
31+
32+
impl Duration {
33+
pub const fn from_secs(_secs: u64) -> Self {
34+
Self {}
35+
}
36+
}
37+
38+
fn test() {
39+
// Only suggest the change for std::time::Duration, not for other Duration structs
40+
let dur = Duration::from_secs(60);
41+
}
42+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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_nanos(13 * 60 * 60 * 1_000 * 1_000 * 1_000);
32+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `Duration::from_hours(13)`
33+
34+
error: aborting due to 5 previous errors
35+

0 commit comments

Comments
 (0)