-
Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(manual_assert_eq): new lint #16025
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| use clippy_utils::diagnostics::span_lint_and_then; | ||
| use clippy_utils::macros::{find_assert_args, root_macro_call_first_node}; | ||
| use clippy_utils::source::walk_span_to_context; | ||
| use clippy_utils::ty::implements_trait; | ||
| use clippy_utils::{is_in_const_context, sym}; | ||
| use rustc_errors::Applicability; | ||
| use rustc_hir::{BinOpKind, Expr, ExprKind}; | ||
| use rustc_lint::{LateContext, LateLintPass, LintContext}; | ||
| use rustc_session::declare_lint_pass; | ||
|
|
||
| declare_clippy_lint! { | ||
| /// ### What it does | ||
| /// Checks for `assert!` and `debug_assert!` that consist of only an (in)equality check | ||
| /// | ||
| /// ### Why is this bad? | ||
| /// `assert_{eq,ne}!` and `debug_assert_{eq,ne}!` achieves the same goal, and provides some | ||
| /// additional debug information | ||
| /// | ||
| /// ### Example | ||
| /// ```no_run | ||
| /// assert!(2 * 2 == 4); | ||
| /// assert!(2 * 2 != 5); | ||
| /// debug_assert!(2 * 2 == 4); | ||
| /// debug_assert!(2 * 2 != 5); | ||
| /// ``` | ||
| /// Use instead: | ||
| /// ```no_run | ||
| /// assert_eq!(2 * 2, 4); | ||
| /// assert_ne!(2 * 2, 5); | ||
| /// debug_assert_eq!(2 * 2, 4); | ||
| /// debug_assert_ne!(2 * 2, 5); | ||
| /// ``` | ||
| #[clippy::version = "1.92.0"] | ||
| pub MANUAL_ASSERT_EQ, | ||
| complexity, | ||
| "checks for assertions consisting of an (in)equality check" | ||
| } | ||
| declare_lint_pass!(ManualAssertEq => [MANUAL_ASSERT_EQ]); | ||
|
|
||
| impl LateLintPass<'_> for ManualAssertEq { | ||
| fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) { | ||
| if let Some(macro_call) = root_macro_call_first_node(cx, expr) | ||
| && let macro_name = match cx.tcx.get_diagnostic_name(macro_call.def_id) { | ||
| Some(sym::assert_macro) => "assert", | ||
| Some(sym::debug_assert_macro) => "debug_assert", | ||
| _ => return, | ||
| } | ||
| // `assert_eq` isn't allowed in const context because it calls non-const `core::panicking::assert_failed` | ||
| // XXX: this might change in the future, so might want to relax this restriction | ||
| && !is_in_const_context(cx) | ||
| && let Some((cond, _)) = find_assert_args(cx, expr, macro_call.expn) | ||
| && let ExprKind::Binary(op, lhs, rhs) = cond.kind | ||
| && matches!(op.node, BinOpKind::Eq | BinOpKind::Ne) | ||
| && !cond.span.from_expansion() | ||
| && let Some(debug_trait) = cx.tcx.get_diagnostic_item(sym::Debug) | ||
| && implements_trait(cx, cx.typeck_results().expr_ty(lhs), debug_trait, &[]) | ||
| && implements_trait(cx, cx.typeck_results().expr_ty(rhs), debug_trait, &[]) | ||
| { | ||
| span_lint_and_then( | ||
| cx, | ||
| MANUAL_ASSERT_EQ, | ||
| macro_call.span, | ||
| format!("used `{macro_name}!` with an equality comparison"), | ||
| |diag| { | ||
| let kind = if op.node == BinOpKind::Eq { "eq" } else { "ne" }; | ||
| let new_name = format!("{macro_name}_{kind}"); | ||
| let msg = format!("replace it with `{new_name}!(..)`"); | ||
|
|
||
| let ctxt = cond.span.ctxt(); | ||
| if let Some(lhs_span) = walk_span_to_context(lhs.span, ctxt) | ||
| && let Some(rhs_span) = walk_span_to_context(rhs.span, ctxt) | ||
| { | ||
| let macro_name_span = cx.sess().source_map().span_until_char(macro_call.span, '!'); | ||
| let eq_span = cond.span.with_lo(lhs_span.hi()).with_hi(rhs_span.lo()); | ||
| let suggestions = vec![ | ||
| (macro_name_span.shrink_to_hi(), format!("_{kind}")), | ||
| (eq_span, ", ".to_string()), | ||
| ]; | ||
|
|
||
| diag.multipart_suggestion(msg, suggestions, Applicability::MachineApplicable); | ||
| } else { | ||
| diag.span_help(expr.span, msg); | ||
| } | ||
| }, | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -915,14 +915,20 @@ fn assert_generic_args_match<'tcx>(tcx: TyCtxt<'tcx>, did: DefId, args: &[Generi | |
| .chain(&g.own_params) | ||
| .map(|x| &x.kind); | ||
|
|
||
| assert!( | ||
| count == args.len(), | ||
| "wrong number of arguments for `{did:?}`: expected `{count}`, found {}\n\ | ||
| #[expect( | ||
| clippy::manual_assert_eq, | ||
| reason = "the message contains `assert_eq!`-like formatting itself" | ||
| )] | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We shouldn't lint if there is an assert message provided
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree that the suggestion is unfortunate in this case, but in general, a provided assert message can often complement the default one from
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe reduce the applicability in this case? |
||
| { | ||
| assert!( | ||
| count == args.len(), | ||
| "wrong number of arguments for `{did:?}`: expected `{count}`, found {}\n\ | ||
| note: the expected arguments are: `[{}]`\n\ | ||
| the given arguments are: `{args:#?}`", | ||
| args.len(), | ||
| params.clone().map(ty::GenericParamDefKind::descr).format(", "), | ||
| ); | ||
| args.len(), | ||
| params.clone().map(ty::GenericParamDefKind::descr).format(", "), | ||
| ); | ||
| } | ||
|
|
||
| if let Some((idx, (param, arg))) = | ||
| params | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -54,8 +54,11 @@ fn main() { | |
| const _: () = assert!(true); | ||
| //~^ assertions_on_constants | ||
|
|
||
| assert!(8 == (7 + 1)); | ||
| //~^ assertions_on_constants | ||
| #[allow(clippy::manual_assert_eq, reason = "tests `assert!` specifically")] | ||
| { | ||
| assert!(8 == (7 + 1)); | ||
| //~^ assertions_on_constants | ||
| } | ||
|
Comment on lines
+57
to
+61
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In my experience, putting such specific allows (and not things like |
||
|
|
||
| // Don't lint if the value is dependent on a defined constant: | ||
| const N: usize = 1024; | ||
|
|
@@ -68,8 +71,11 @@ const _: () = { | |
| assert!(true); | ||
| //~^ assertions_on_constants | ||
|
|
||
| assert!(8 == (7 + 1)); | ||
| //~^ assertions_on_constants | ||
| #[allow(clippy::manual_assert_eq, reason = "tests `assert!` specifically")] | ||
| { | ||
| assert!(8 == (7 + 1)); | ||
| //~^ assertions_on_constants | ||
| } | ||
|
|
||
| assert!(C); | ||
| }; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.