Skip to content

Commit 5b0d727

Browse files
committed
Auto merge of rust-lang#9570 - nfejzic:lint-unchecked-duration-subtraction, r=llogiq
feat: lint unchecked subtraction of a 'Duration' from an 'Instant' Hello all, I tried to tackle the open issue rust-lang#9371 and this is what I came up with. I have a difficulty currently - some tests are failing: ``` failures: [ui] ui/manual_instant_elapsed.rs ``` The `manual_instant_elapsed` is failing because of `Instant::now() - duration` test, this now gets also picked by `unchecked_duration_subtraction` lint. What is the correct way to proceed in this case? Simply update the `.stderr` file for `manual_instant_elapsed` lint? changelog: [`unchecked_duration_subtraction`]: Add lint for unchecked subtraction of a `Duration` from an `Instant`. fixes rust-lang#9371
2 parents 3c7f74d + 0dd6ce0 commit 5b0d727

12 files changed

+256
-75
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4428,6 +4428,7 @@ Released 2018-09-13
44284428
[`try_err`]: https://rust-lang.github.io/rust-clippy/master/index.html#try_err
44294429
[`type_complexity`]: https://rust-lang.github.io/rust-clippy/master/index.html#type_complexity
44304430
[`type_repetition_in_bounds`]: https://rust-lang.github.io/rust-clippy/master/index.html#type_repetition_in_bounds
4431+
[`unchecked_duration_subtraction`]: https://rust-lang.github.io/rust-clippy/master/index.html#unchecked_duration_subtraction
44314432
[`undocumented_unsafe_blocks`]: https://rust-lang.github.io/rust-clippy/master/index.html#undocumented_unsafe_blocks
44324433
[`undropped_manually_drops`]: https://rust-lang.github.io/rust-clippy/master/index.html#undropped_manually_drops
44334434
[`unicode_not_nfc`]: https://rust-lang.github.io/rust-clippy/master/index.html#unicode_not_nfc

clippy_lints/src/declared_lints.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,8 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
203203
crate::inherent_to_string::INHERENT_TO_STRING_SHADOW_DISPLAY_INFO,
204204
crate::init_numbered_fields::INIT_NUMBERED_FIELDS_INFO,
205205
crate::inline_fn_without_body::INLINE_FN_WITHOUT_BODY_INFO,
206+
crate::instant_subtraction::MANUAL_INSTANT_ELAPSED_INFO,
207+
crate::instant_subtraction::UNCHECKED_DURATION_SUBTRACTION_INFO,
206208
crate::int_plus_one::INT_PLUS_ONE_INFO,
207209
crate::invalid_upcast_comparisons::INVALID_UPCAST_COMPARISONS_INFO,
208210
crate::invalid_utf8_in_unchecked::INVALID_UTF8_IN_UNCHECKED_INFO,
@@ -251,7 +253,6 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
251253
crate::manual_async_fn::MANUAL_ASYNC_FN_INFO,
252254
crate::manual_bits::MANUAL_BITS_INFO,
253255
crate::manual_clamp::MANUAL_CLAMP_INFO,
254-
crate::manual_instant_elapsed::MANUAL_INSTANT_ELAPSED_INFO,
255256
crate::manual_is_ascii_check::MANUAL_IS_ASCII_CHECK_INFO,
256257
crate::manual_let_else::MANUAL_LET_ELSE_INFO,
257258
crate::manual_non_exhaustive::MANUAL_NON_EXHAUSTIVE_INFO,
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
use clippy_utils::{
2+
diagnostics::{self, span_lint_and_sugg},
3+
meets_msrv, msrvs, source,
4+
sugg::Sugg,
5+
ty,
6+
};
7+
use rustc_errors::Applicability;
8+
use rustc_hir::{BinOpKind, Expr, ExprKind};
9+
use rustc_lint::{LateContext, LateLintPass};
10+
use rustc_semver::RustcVersion;
11+
use rustc_session::{declare_tool_lint, impl_lint_pass};
12+
use rustc_span::{source_map::Spanned, sym};
13+
14+
declare_clippy_lint! {
15+
/// ### What it does
16+
/// Lints subtraction between `Instant::now()` and another `Instant`.
17+
///
18+
/// ### Why is this bad?
19+
/// It is easy to accidentally write `prev_instant - Instant::now()`, which will always be 0ns
20+
/// as `Instant` subtraction saturates.
21+
///
22+
/// `prev_instant.elapsed()` also more clearly signals intention.
23+
///
24+
/// ### Example
25+
/// ```rust
26+
/// use std::time::Instant;
27+
/// let prev_instant = Instant::now();
28+
/// let duration = Instant::now() - prev_instant;
29+
/// ```
30+
/// Use instead:
31+
/// ```rust
32+
/// use std::time::Instant;
33+
/// let prev_instant = Instant::now();
34+
/// let duration = prev_instant.elapsed();
35+
/// ```
36+
#[clippy::version = "1.65.0"]
37+
pub MANUAL_INSTANT_ELAPSED,
38+
pedantic,
39+
"subtraction between `Instant::now()` and previous `Instant`"
40+
}
41+
42+
declare_clippy_lint! {
43+
/// ### What it does
44+
/// Lints subtraction between an [`Instant`] and a [`Duration`].
45+
///
46+
/// ### Why is this bad?
47+
/// Unchecked subtraction could cause underflow on certain platforms, leading to
48+
/// unintentional panics.
49+
///
50+
/// ### Example
51+
/// ```rust
52+
/// # use std::time::{Instant, Duration};
53+
/// let time_passed = Instant::now() - Duration::from_secs(5);
54+
/// ```
55+
///
56+
/// Use instead:
57+
/// ```rust
58+
/// # use std::time::{Instant, Duration};
59+
/// let time_passed = Instant::now().checked_sub(Duration::from_secs(5));
60+
/// ```
61+
///
62+
/// [`Duration`]: std::time::Duration
63+
/// [`Instant::now()`]: std::time::Instant::now;
64+
#[clippy::version = "1.65.0"]
65+
pub UNCHECKED_DURATION_SUBTRACTION,
66+
suspicious,
67+
"finds unchecked subtraction of a 'Duration' from an 'Instant'"
68+
}
69+
70+
pub struct InstantSubtraction {
71+
msrv: Option<RustcVersion>,
72+
}
73+
74+
impl InstantSubtraction {
75+
#[must_use]
76+
pub fn new(msrv: Option<RustcVersion>) -> Self {
77+
Self { msrv }
78+
}
79+
}
80+
81+
impl_lint_pass!(InstantSubtraction => [MANUAL_INSTANT_ELAPSED, UNCHECKED_DURATION_SUBTRACTION]);
82+
83+
impl LateLintPass<'_> for InstantSubtraction {
84+
fn check_expr(&mut self, cx: &LateContext<'_>, expr: &'_ Expr<'_>) {
85+
if let ExprKind::Binary(
86+
Spanned {
87+
node: BinOpKind::Sub, ..
88+
},
89+
lhs,
90+
rhs,
91+
) = expr.kind
92+
{
93+
if_chain! {
94+
if is_instant_now_call(cx, lhs);
95+
96+
if is_an_instant(cx, rhs);
97+
if let Some(sugg) = Sugg::hir_opt(cx, rhs);
98+
99+
then {
100+
print_manual_instant_elapsed_sugg(cx, expr, sugg)
101+
} else {
102+
if_chain! {
103+
if !expr.span.from_expansion();
104+
if meets_msrv(self.msrv, msrvs::TRY_FROM);
105+
106+
if is_an_instant(cx, lhs);
107+
if is_a_duration(cx, rhs);
108+
109+
then {
110+
print_unchecked_duration_subtraction_sugg(cx, lhs, rhs, expr)
111+
}
112+
}
113+
}
114+
}
115+
}
116+
}
117+
118+
extract_msrv_attr!(LateContext);
119+
}
120+
121+
fn is_instant_now_call(cx: &LateContext<'_>, expr_block: &'_ Expr<'_>) -> bool {
122+
if let ExprKind::Call(fn_expr, []) = expr_block.kind
123+
&& let Some(fn_id) = clippy_utils::path_def_id(cx, fn_expr)
124+
&& clippy_utils::match_def_path(cx, fn_id, &clippy_utils::paths::INSTANT_NOW)
125+
{
126+
true
127+
} else {
128+
false
129+
}
130+
}
131+
132+
fn is_an_instant(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
133+
let expr_ty = cx.typeck_results().expr_ty(expr);
134+
135+
match expr_ty.kind() {
136+
rustc_middle::ty::Adt(def, _) => clippy_utils::match_def_path(cx, def.did(), &clippy_utils::paths::INSTANT),
137+
_ => false,
138+
}
139+
}
140+
141+
fn is_a_duration(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
142+
let expr_ty = cx.typeck_results().expr_ty(expr);
143+
ty::is_type_diagnostic_item(cx, expr_ty, sym::Duration)
144+
}
145+
146+
fn print_manual_instant_elapsed_sugg(cx: &LateContext<'_>, expr: &Expr<'_>, sugg: Sugg<'_>) {
147+
span_lint_and_sugg(
148+
cx,
149+
MANUAL_INSTANT_ELAPSED,
150+
expr.span,
151+
"manual implementation of `Instant::elapsed`",
152+
"try",
153+
format!("{}.elapsed()", sugg.maybe_par()),
154+
Applicability::MachineApplicable,
155+
);
156+
}
157+
158+
fn print_unchecked_duration_subtraction_sugg(
159+
cx: &LateContext<'_>,
160+
left_expr: &Expr<'_>,
161+
right_expr: &Expr<'_>,
162+
expr: &Expr<'_>,
163+
) {
164+
let mut applicability = Applicability::MachineApplicable;
165+
166+
let left_expr =
167+
source::snippet_with_applicability(cx, left_expr.span, "std::time::Instant::now()", &mut applicability);
168+
let right_expr = source::snippet_with_applicability(
169+
cx,
170+
right_expr.span,
171+
"std::time::Duration::from_secs(1)",
172+
&mut applicability,
173+
);
174+
175+
diagnostics::span_lint_and_sugg(
176+
cx,
177+
UNCHECKED_DURATION_SUBTRACTION,
178+
expr.span,
179+
"unchecked subtraction of a 'Duration' from an 'Instant'",
180+
"try",
181+
format!("{left_expr}.checked_sub({right_expr}).unwrap()"),
182+
applicability,
183+
);
184+
}

clippy_lints/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ mod inherent_impl;
151151
mod inherent_to_string;
152152
mod init_numbered_fields;
153153
mod inline_fn_without_body;
154+
mod instant_subtraction;
154155
mod int_plus_one;
155156
mod invalid_upcast_comparisons;
156157
mod invalid_utf8_in_unchecked;
@@ -172,7 +173,6 @@ mod manual_assert;
172173
mod manual_async_fn;
173174
mod manual_bits;
174175
mod manual_clamp;
175-
mod manual_instant_elapsed;
176176
mod manual_is_ascii_check;
177177
mod manual_let_else;
178178
mod manual_non_exhaustive;
@@ -907,7 +907,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
907907
store.register_late_pass(move |_| Box::new(operators::Operators::new(verbose_bit_mask_threshold)));
908908
store.register_late_pass(|_| Box::new(invalid_utf8_in_unchecked::InvalidUtf8InUnchecked));
909909
store.register_late_pass(|_| Box::<std_instead_of_core::StdReexports>::default());
910-
store.register_late_pass(|_| Box::new(manual_instant_elapsed::ManualInstantElapsed));
910+
store.register_late_pass(move |_| Box::new(instant_subtraction::InstantSubtraction::new(msrv)));
911911
store.register_late_pass(|_| Box::new(partialeq_to_none::PartialeqToNone));
912912
store.register_late_pass(move |_| Box::new(manual_clamp::ManualClamp::new(msrv)));
913913
store.register_late_pass(|_| Box::new(manual_string_new::ManualStringNew));

clippy_lints/src/manual_instant_elapsed.rs

Lines changed: 0 additions & 69 deletions
This file was deleted.

clippy_lints/src/utils/conf.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ define_Conf! {
213213
///
214214
/// Suppress lints whenever the suggested change would cause breakage for other crates.
215215
(avoid_breaking_exported_api: bool = true),
216-
/// Lint: MANUAL_SPLIT_ONCE, MANUAL_STR_REPEAT, CLONED_INSTEAD_OF_COPIED, REDUNDANT_FIELD_NAMES, REDUNDANT_STATIC_LIFETIMES, FILTER_MAP_NEXT, CHECKED_CONVERSIONS, MANUAL_RANGE_CONTAINS, USE_SELF, MEM_REPLACE_WITH_DEFAULT, MANUAL_NON_EXHAUSTIVE, OPTION_AS_REF_DEREF, MAP_UNWRAP_OR, MATCH_LIKE_MATCHES_MACRO, MANUAL_STRIP, MISSING_CONST_FOR_FN, UNNESTED_OR_PATTERNS, FROM_OVER_INTO, PTR_AS_PTR, IF_THEN_SOME_ELSE_NONE, APPROX_CONSTANT, DEPRECATED_CFG_ATTR, INDEX_REFUTABLE_SLICE, MAP_CLONE, BORROW_AS_PTR, MANUAL_BITS, ERR_EXPECT, CAST_ABS_TO_UNSIGNED, UNINLINED_FORMAT_ARGS, MANUAL_CLAMP, MANUAL_LET_ELSE.
216+
/// Lint: MANUAL_SPLIT_ONCE, MANUAL_STR_REPEAT, CLONED_INSTEAD_OF_COPIED, REDUNDANT_FIELD_NAMES, REDUNDANT_STATIC_LIFETIMES, FILTER_MAP_NEXT, CHECKED_CONVERSIONS, MANUAL_RANGE_CONTAINS, USE_SELF, MEM_REPLACE_WITH_DEFAULT, MANUAL_NON_EXHAUSTIVE, OPTION_AS_REF_DEREF, MAP_UNWRAP_OR, MATCH_LIKE_MATCHES_MACRO, MANUAL_STRIP, MISSING_CONST_FOR_FN, UNNESTED_OR_PATTERNS, FROM_OVER_INTO, PTR_AS_PTR, IF_THEN_SOME_ELSE_NONE, APPROX_CONSTANT, DEPRECATED_CFG_ATTR, INDEX_REFUTABLE_SLICE, MAP_CLONE, BORROW_AS_PTR, MANUAL_BITS, ERR_EXPECT, CAST_ABS_TO_UNSIGNED, UNINLINED_FORMAT_ARGS, MANUAL_CLAMP, MANUAL_LET_ELSE, UNCHECKED_DURATION_SUBTRACTION.
217217
///
218218
/// The minimum rust version that the project supports
219219
(msrv: Option<String> = None),

tests/ui/manual_instant_elapsed.fixed

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// run-rustfix
22
#![warn(clippy::manual_instant_elapsed)]
33
#![allow(clippy::unnecessary_operation)]
4+
#![allow(clippy::unchecked_duration_subtraction)]
45
#![allow(unused_variables)]
56
#![allow(unused_must_use)]
67

tests/ui/manual_instant_elapsed.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// run-rustfix
22
#![warn(clippy::manual_instant_elapsed)]
33
#![allow(clippy::unnecessary_operation)]
4+
#![allow(clippy::unchecked_duration_subtraction)]
45
#![allow(unused_variables)]
56
#![allow(unused_must_use)]
67

tests/ui/manual_instant_elapsed.stderr

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
error: manual implementation of `Instant::elapsed`
2-
--> $DIR/manual_instant_elapsed.rs:17:20
2+
--> $DIR/manual_instant_elapsed.rs:18:20
33
|
44
LL | let duration = Instant::now() - prev_instant;
55
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `prev_instant.elapsed()`
66
|
77
= note: `-D clippy::manual-instant-elapsed` implied by `-D warnings`
88

99
error: manual implementation of `Instant::elapsed`
10-
--> $DIR/manual_instant_elapsed.rs:26:5
10+
--> $DIR/manual_instant_elapsed.rs:27:5
1111
|
1212
LL | Instant::now() - *ref_to_instant; // to ensure parens are added correctly
1313
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `(*ref_to_instant).elapsed()`
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// run-rustfix
2+
#![warn(clippy::unchecked_duration_subtraction)]
3+
4+
use std::time::{Duration, Instant};
5+
6+
fn main() {
7+
let _first = Instant::now();
8+
let second = Duration::from_secs(3);
9+
10+
let _ = _first.checked_sub(second).unwrap();
11+
12+
let _ = Instant::now().checked_sub(Duration::from_secs(5)).unwrap();
13+
14+
let _ = _first.checked_sub(Duration::from_secs(5)).unwrap();
15+
16+
let _ = Instant::now().checked_sub(second).unwrap();
17+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// run-rustfix
2+
#![warn(clippy::unchecked_duration_subtraction)]
3+
4+
use std::time::{Duration, Instant};
5+
6+
fn main() {
7+
let _first = Instant::now();
8+
let second = Duration::from_secs(3);
9+
10+
let _ = _first - second;
11+
12+
let _ = Instant::now() - Duration::from_secs(5);
13+
14+
let _ = _first - Duration::from_secs(5);
15+
16+
let _ = Instant::now() - second;
17+
}

0 commit comments

Comments
 (0)