diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bd67617f5bf..eb4ca9801432 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4428,6 +4428,7 @@ Released 2018-09-13 [`try_err`]: https://rust-lang.github.io/rust-clippy/master/index.html#try_err [`type_complexity`]: https://rust-lang.github.io/rust-clippy/master/index.html#type_complexity [`type_repetition_in_bounds`]: https://rust-lang.github.io/rust-clippy/master/index.html#type_repetition_in_bounds +[`unchecked_duration_subtraction`]: https://rust-lang.github.io/rust-clippy/master/index.html#unchecked_duration_subtraction [`undocumented_unsafe_blocks`]: https://rust-lang.github.io/rust-clippy/master/index.html#undocumented_unsafe_blocks [`undropped_manually_drops`]: https://rust-lang.github.io/rust-clippy/master/index.html#undropped_manually_drops [`unicode_not_nfc`]: https://rust-lang.github.io/rust-clippy/master/index.html#unicode_not_nfc diff --git a/clippy_lints/src/lib.rs b/clippy_lints/src/lib.rs index a4bacb780349..a5407b65b88d 100644 --- a/clippy_lints/src/lib.rs +++ b/clippy_lints/src/lib.rs @@ -279,6 +279,7 @@ mod trailing_empty_array; mod trait_bounds; mod transmute; mod types; +mod unchecked_duration_subtraction; mod undocumented_unsafe_blocks; mod unicode; mod uninit_vec; @@ -921,6 +922,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf: store.register_late_pass(|_| Box::new(from_raw_with_void_ptr::FromRawWithVoidPtr)); store.register_late_pass(|_| Box::new(suspicious_xor_used_as_pow::ConfusingXorAndPow)); store.register_late_pass(move |_| Box::new(manual_is_ascii_check::ManualIsAsciiCheck::new(msrv))); + store.register_late_pass(move || Box::new(unchecked_duration_subtraction::UncheckedDurationSubtraction::new(msrv))); // add lints here, do not remove this comment, it's used in `new_lint` } diff --git a/clippy_lints/src/unchecked_duration_subtraction.rs b/clippy_lints/src/unchecked_duration_subtraction.rs new file mode 100644 index 000000000000..fb08067de8d4 --- /dev/null +++ b/clippy_lints/src/unchecked_duration_subtraction.rs @@ -0,0 +1,107 @@ +use clippy_utils::{diagnostics, meets_msrv, msrvs, source, ty}; +use rustc_errors::Applicability; +use rustc_hir::*; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_semver::RustcVersion; +use rustc_session::{declare_tool_lint, impl_lint_pass}; +use rustc_span::symbol::sym; + +declare_clippy_lint! { + /// ### What it does + /// Finds patterns of unchecked subtraction of [`Duration`] from [`Instant::now()`]. + /// + /// ### Why is this bad? + /// Unchecked subtraction could cause underflow on certain platforms, leading to bugs and/or + /// unintentional panics. + /// + /// ### Example + /// ```rust + /// let time_passed = Instant::now() - Duration::from_secs(5); + /// ``` + /// + /// Use instead: + /// ```rust + /// let time_passed = Instant::now().checked_sub(Duration::from_secs(5)); + /// ``` + /// + /// [`Duration`]: std::time::Duration + /// [`Instant::now()`]: std::time::Instant::now; + #[clippy::version = "1.65.0"] + pub UNCHECKED_DURATION_SUBTRACTION, + suspicious, + "finds unchecked subtraction of a 'Duration' from an 'Instant'" +} + +pub struct UncheckedDurationSubtraction { + msrv: Option, +} + +impl UncheckedDurationSubtraction { + #[must_use] + pub fn new(msrv: Option) -> Self { + Self { msrv } + } +} + +impl_lint_pass!(UncheckedDurationSubtraction => [UNCHECKED_DURATION_SUBTRACTION]); + +impl<'tcx> LateLintPass<'tcx> for UncheckedDurationSubtraction { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { + if expr.span.from_expansion() || !meets_msrv(self.msrv, msrvs::TRY_FROM) { + return; + } + + if_chain! { + if let ExprKind::Binary(op, lhs, rhs) = expr.kind; + + if let BinOpKind::Sub = op.node; + + // get types of left and right side + if is_an_instant(cx, lhs); + if is_a_duration(cx, rhs); + + then { + print_lint_and_sugg(cx, lhs, rhs, expr) + } + } + } +} + +fn is_an_instant(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { + let expr_ty = cx.typeck_results().expr_ty(expr); + + match expr_ty.kind() { + rustc_middle::ty::Adt(def, _) => { + clippy_utils::match_def_path(cx, dbg!(def).did(), &clippy_utils::paths::INSTANT) + }, + _ => false, + } +} + +fn is_a_duration(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { + let expr_ty = cx.typeck_results().expr_ty(expr); + ty::is_type_diagnostic_item(cx, expr_ty, sym::Duration) +} + +fn print_lint_and_sugg(cx: &LateContext<'_>, left_expr: &Expr<'_>, right_expr: &Expr<'_>, expr: &Expr<'_>) { + let mut applicability = Applicability::MachineApplicable; + + let left_expr = + source::snippet_with_applicability(cx, left_expr.span, "std::time::Instant::now()", &mut applicability); + let right_expr = source::snippet_with_applicability( + cx, + right_expr.span, + "std::time::Duration::from_secs(1)", + &mut applicability, + ); + + diagnostics::span_lint_and_sugg( + cx, + UNCHECKED_DURATION_SUBTRACTION, + expr.span, + "unchecked subtraction of a 'Duration' from an 'Instant'", + "try", + format!("{}.checked_sub({}).unwrap();", left_expr, right_expr), + applicability, + ); +} diff --git a/clippy_lints/src/utils/conf.rs b/clippy_lints/src/utils/conf.rs index 0cf49ca6d429..ab58e9b8b68d 100644 --- a/clippy_lints/src/utils/conf.rs +++ b/clippy_lints/src/utils/conf.rs @@ -213,7 +213,7 @@ define_Conf! { /// /// Suppress lints whenever the suggested change would cause breakage for other crates. (avoid_breaking_exported_api: bool = true), - /// 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. + /// 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. /// /// The minimum rust version that the project supports (msrv: Option = None),