Skip to content

Commit ba03dc7

Browse files
committed
Auto merge of #8219 - camsteffen:macro-decoupling, r=llogiq
New macro utils changelog: none Sorry, this is a big one. A lot of interrelated changes and I wanted to put the new utils to use to make sure they are somewhat battle-tested. We may want to divide some of the lint-specific refactoring commits into batches for smaller reviewing tasks. I could also split into more PRs. Introduces a bunch of new utils at `clippy_utils::macros::...`. Please read through the docs and give any feedback! I'm happy to introduce `MacroCall` and various functions to retrieve an instance. It feels like the missing puzzle piece. I'm also introducing `ExpnId` from rustc as "useful for Clippy too". `@rust-lang/clippy` Fixes #7843 by not parsing every node of macro implementations, at least the major offenders. I probably want to get rid of `is_expn_of` at some point.
2 parents 0e28e38 + 786f874 commit ba03dc7

31 files changed

+868
-865
lines changed
+25-95
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
use clippy_utils::consts::{constant, Constant};
22
use clippy_utils::diagnostics::span_lint_and_help;
3-
use clippy_utils::higher;
4-
use clippy_utils::source::snippet_opt;
5-
use clippy_utils::{is_direct_expn_of, is_expn_of, match_panic_call, peel_blocks};
6-
use if_chain::if_chain;
7-
use rustc_hir::{Expr, ExprKind, UnOp};
3+
use clippy_utils::macros::{find_assert_args, root_macro_call_first_node, PanicExpn};
4+
use rustc_hir::Expr;
85
use rustc_lint::{LateContext, LateLintPass};
96
use rustc_session::{declare_lint_pass, declare_tool_lint};
7+
use rustc_span::sym;
108

119
declare_clippy_lint! {
1210
/// ### What it does
@@ -36,107 +34,39 @@ declare_lint_pass!(AssertionsOnConstants => [ASSERTIONS_ON_CONSTANTS]);
3634

3735
impl<'tcx> LateLintPass<'tcx> for AssertionsOnConstants {
3836
fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) {
39-
let lint_true = |is_debug: bool| {
40-
span_lint_and_help(
41-
cx,
42-
ASSERTIONS_ON_CONSTANTS,
43-
e.span,
44-
if is_debug {
45-
"`debug_assert!(true)` will be optimized out by the compiler"
46-
} else {
47-
"`assert!(true)` will be optimized out by the compiler"
48-
},
49-
None,
50-
"remove it",
51-
);
37+
let Some(macro_call) = root_macro_call_first_node(cx, e) else { return };
38+
let is_debug = match cx.tcx.get_diagnostic_name(macro_call.def_id) {
39+
Some(sym::debug_assert_macro) => true,
40+
Some(sym::assert_macro) => false,
41+
_ => return,
5242
};
53-
let lint_false_without_message = || {
43+
let Some((condition, panic_expn)) = find_assert_args(cx, e, macro_call.expn) else { return };
44+
let Some((Constant::Bool(val), _)) = constant(cx, cx.typeck_results(), condition) else { return };
45+
if val {
5446
span_lint_and_help(
5547
cx,
5648
ASSERTIONS_ON_CONSTANTS,
57-
e.span,
58-
"`assert!(false)` should probably be replaced",
49+
macro_call.span,
50+
&format!(
51+
"`{}!(true)` will be optimized out by the compiler",
52+
cx.tcx.item_name(macro_call.def_id)
53+
),
5954
None,
60-
"use `panic!()` or `unreachable!()`",
55+
"remove it",
6156
);
62-
};
63-
let lint_false_with_message = |panic_message: String| {
57+
} else if !is_debug {
58+
let (assert_arg, panic_arg) = match panic_expn {
59+
PanicExpn::Empty => ("", ""),
60+
_ => (", ..", ".."),
61+
};
6462
span_lint_and_help(
6563
cx,
6664
ASSERTIONS_ON_CONSTANTS,
67-
e.span,
68-
&format!("`assert!(false, {})` should probably be replaced", panic_message),
65+
macro_call.span,
66+
&format!("`assert!(false{})` should probably be replaced", assert_arg),
6967
None,
70-
&format!("use `panic!({})` or `unreachable!({})`", panic_message, panic_message),
68+
&format!("use `panic!({})` or `unreachable!({0})`", panic_arg),
7169
);
72-
};
73-
74-
if let Some(debug_assert_span) = is_expn_of(e.span, "debug_assert") {
75-
if debug_assert_span.from_expansion() {
76-
return;
77-
}
78-
if_chain! {
79-
if let ExprKind::Unary(_, lit) = e.kind;
80-
if let Some((Constant::Bool(is_true), _)) = constant(cx, cx.typeck_results(), lit);
81-
if is_true;
82-
then {
83-
lint_true(true);
84-
}
85-
};
86-
} else if let Some(assert_span) = is_direct_expn_of(e.span, "assert") {
87-
if assert_span.from_expansion() {
88-
return;
89-
}
90-
if let Some(assert_match) = match_assert_with_message(cx, e) {
91-
match assert_match {
92-
// matched assert but not message
93-
AssertKind::WithoutMessage(false) => lint_false_without_message(),
94-
AssertKind::WithoutMessage(true) | AssertKind::WithMessage(_, true) => lint_true(false),
95-
AssertKind::WithMessage(panic_message, false) => lint_false_with_message(panic_message),
96-
};
97-
}
98-
}
99-
}
100-
}
101-
102-
/// Result of calling `match_assert_with_message`.
103-
enum AssertKind {
104-
WithMessage(String, bool),
105-
WithoutMessage(bool),
106-
}
107-
108-
/// Check if the expression matches
109-
///
110-
/// ```rust,ignore
111-
/// if !c {
112-
/// {
113-
/// ::std::rt::begin_panic(message, _)
114-
/// }
115-
/// }
116-
/// ```
117-
///
118-
/// where `message` is any expression and `c` is a constant bool.
119-
fn match_assert_with_message<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) -> Option<AssertKind> {
120-
if_chain! {
121-
if let Some(higher::If { cond, then, .. }) = higher::If::hir(expr);
122-
if let ExprKind::Unary(UnOp::Not, expr) = cond.kind;
123-
// bind the first argument of the `assert!` macro
124-
if let Some((Constant::Bool(is_true), _)) = constant(cx, cx.typeck_results(), expr);
125-
let begin_panic_call = peel_blocks(then);
126-
// function call
127-
if let Some(arg) = match_panic_call(cx, begin_panic_call);
128-
// bind the second argument of the `assert!` macro if it exists
129-
if let panic_message = snippet_opt(cx, arg.span);
130-
// second argument of begin_panic is irrelevant
131-
// as is the second match arm
132-
then {
133-
// an empty message occurs when it was generated by the macro
134-
// (and not passed by the user)
135-
return panic_message
136-
.filter(|msg| !msg.is_empty())
137-
.map(|msg| AssertKind::WithMessage(msg, is_true))
138-
.or(Some(AssertKind::WithoutMessage(is_true)));
13970
}
14071
}
141-
None
14272
}

clippy_lints/src/attrs.rs

+7-11
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
//! checks for attributes
22
33
use clippy_utils::diagnostics::{span_lint, span_lint_and_help, span_lint_and_sugg, span_lint_and_then};
4+
use clippy_utils::macros::{is_panic, macro_backtrace};
45
use clippy_utils::msrvs;
56
use clippy_utils::source::{first_line_of_span, is_present_in_source, snippet_opt, without_block_comments};
6-
use clippy_utils::{extract_msrv_attr, match_panic_def_id, meets_msrv};
7+
use clippy_utils::{extract_msrv_attr, meets_msrv};
78
use if_chain::if_chain;
89
use rustc_ast::{AttrKind, AttrStyle, Attribute, Lit, LitKind, MetaItemKind, NestedMetaItem};
910
use rustc_errors::Applicability;
@@ -443,20 +444,15 @@ fn is_relevant_block(cx: &LateContext<'_>, typeck_results: &ty::TypeckResults<'_
443444
}
444445

445446
fn is_relevant_expr(cx: &LateContext<'_>, typeck_results: &ty::TypeckResults<'_>, expr: &Expr<'_>) -> bool {
447+
if macro_backtrace(expr.span).last().map_or(false, |macro_call| {
448+
is_panic(cx, macro_call.def_id) || cx.tcx.item_name(macro_call.def_id) == sym::unreachable
449+
}) {
450+
return false;
451+
}
446452
match &expr.kind {
447453
ExprKind::Block(block, _) => is_relevant_block(cx, typeck_results, block),
448454
ExprKind::Ret(Some(e)) => is_relevant_expr(cx, typeck_results, e),
449455
ExprKind::Ret(None) | ExprKind::Break(_, None) => false,
450-
ExprKind::Call(path_expr, _) => {
451-
if let ExprKind::Path(qpath) = &path_expr.kind {
452-
typeck_results
453-
.qpath_res(qpath, path_expr.hir_id)
454-
.opt_def_id()
455-
.map_or(true, |fun_id| !match_panic_def_id(cx, fun_id))
456-
} else {
457-
true
458-
}
459-
},
460456
_ => true,
461457
}
462458
}

clippy_lints/src/bool_assert_comparison.rs

+35-38
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
use clippy_utils::{diagnostics::span_lint_and_sugg, higher, is_direct_expn_of, ty::implements_trait};
1+
use clippy_utils::macros::{find_assert_eq_args, root_macro_call_first_node};
2+
use clippy_utils::{diagnostics::span_lint_and_sugg, ty::implements_trait};
23
use rustc_ast::ast::LitKind;
34
use rustc_errors::Applicability;
45
use rustc_hir::{Expr, ExprKind, Lit};
@@ -66,44 +67,40 @@ fn is_impl_not_trait_with_bool_out(cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) ->
6667

6768
impl<'tcx> LateLintPass<'tcx> for BoolAssertComparison {
6869
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
69-
let macros = ["assert_eq", "debug_assert_eq"];
70-
let inverted_macros = ["assert_ne", "debug_assert_ne"];
71-
72-
for mac in macros.iter().chain(inverted_macros.iter()) {
73-
if let Some(span) = is_direct_expn_of(expr.span, mac) {
74-
if let Some(args) = higher::extract_assert_macro_args(expr) {
75-
if let [a, b, ..] = args[..] {
76-
let nb_bool_args = usize::from(is_bool_lit(a)) + usize::from(is_bool_lit(b));
77-
78-
if nb_bool_args != 1 {
79-
// If there are two boolean arguments, we definitely don't understand
80-
// what's going on, so better leave things as is...
81-
//
82-
// Or there is simply no boolean and then we can leave things as is!
83-
return;
84-
}
85-
86-
if !is_impl_not_trait_with_bool_out(cx, a) || !is_impl_not_trait_with_bool_out(cx, b) {
87-
// At this point the expression which is not a boolean
88-
// literal does not implement Not trait with a bool output,
89-
// so we cannot suggest to rewrite our code
90-
return;
91-
}
70+
let Some(macro_call) = root_macro_call_first_node(cx, expr) else { return };
71+
let macro_name = cx.tcx.item_name(macro_call.def_id);
72+
if !matches!(
73+
macro_name.as_str(),
74+
"assert_eq" | "debug_assert_eq" | "assert_ne" | "debug_assert_ne"
75+
) {
76+
return;
77+
}
78+
let Some ((a, b, _)) = find_assert_eq_args(cx, expr, macro_call.expn) else { return };
79+
if !(is_bool_lit(a) ^ is_bool_lit(b)) {
80+
// If there are two boolean arguments, we definitely don't understand
81+
// what's going on, so better leave things as is...
82+
//
83+
// Or there is simply no boolean and then we can leave things as is!
84+
return;
85+
}
9286

93-
let non_eq_mac = &mac[..mac.len() - 3];
94-
span_lint_and_sugg(
95-
cx,
96-
BOOL_ASSERT_COMPARISON,
97-
span,
98-
&format!("used `{}!` with a literal bool", mac),
99-
"replace it with",
100-
format!("{}!(..)", non_eq_mac),
101-
Applicability::MaybeIncorrect,
102-
);
103-
return;
104-
}
105-
}
106-
}
87+
if !is_impl_not_trait_with_bool_out(cx, a) || !is_impl_not_trait_with_bool_out(cx, b) {
88+
// At this point the expression which is not a boolean
89+
// literal does not implement Not trait with a bool output,
90+
// so we cannot suggest to rewrite our code
91+
return;
10792
}
93+
94+
let macro_name = macro_name.as_str();
95+
let non_eq_mac = &macro_name[..macro_name.len() - 3];
96+
span_lint_and_sugg(
97+
cx,
98+
BOOL_ASSERT_COMPARISON,
99+
macro_call.span,
100+
&format!("used `{}!` with a literal bool", macro_name),
101+
"replace it with",
102+
format!("{}!(..)", non_eq_mac),
103+
Applicability::MaybeIncorrect,
104+
);
108105
}
109106
}

clippy_lints/src/doc.rs

+11-22
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
use clippy_utils::attrs::is_doc_hidden;
22
use clippy_utils::diagnostics::{span_lint, span_lint_and_help, span_lint_and_note, span_lint_and_then};
3+
use clippy_utils::macros::{is_panic, root_macro_call_first_node};
34
use clippy_utils::source::{first_line_of_span, snippet_with_applicability};
45
use clippy_utils::ty::{implements_trait, is_type_diagnostic_item};
5-
use clippy_utils::{is_entrypoint_fn, is_expn_of, match_panic_def_id, method_chain_args, return_ty};
6+
use clippy_utils::{is_entrypoint_fn, method_chain_args, return_ty};
67
use if_chain::if_chain;
78
use itertools::Itertools;
89
use rustc_ast::ast::{Async, AttrKind, Attribute, Fn, FnRetTy, ItemKind};
@@ -13,7 +14,7 @@ use rustc_errors::emitter::EmitterWriter;
1314
use rustc_errors::{Applicability, Handler, SuggestionStyle};
1415
use rustc_hir as hir;
1516
use rustc_hir::intravisit::{self, NestedVisitorMap, Visitor};
16-
use rustc_hir::{AnonConst, Expr, ExprKind, QPath};
17+
use rustc_hir::{AnonConst, Expr};
1718
use rustc_lint::{LateContext, LateLintPass};
1819
use rustc_middle::hir::map::Map;
1920
use rustc_middle::lint::in_external_macro;
@@ -805,24 +806,17 @@ impl<'a, 'tcx> Visitor<'tcx> for FindPanicUnwrap<'a, 'tcx> {
805806
return;
806807
}
807808

808-
// check for `begin_panic`
809-
if_chain! {
810-
if let ExprKind::Call(func_expr, _) = expr.kind;
811-
if let ExprKind::Path(QPath::Resolved(_, path)) = func_expr.kind;
812-
if let Some(path_def_id) = path.res.opt_def_id();
813-
if match_panic_def_id(self.cx, path_def_id);
814-
if is_expn_of(expr.span, "unreachable").is_none();
815-
if !is_expn_of_debug_assertions(expr.span);
816-
then {
817-
self.panic_span = Some(expr.span);
809+
if let Some(macro_call) = root_macro_call_first_node(self.cx, expr) {
810+
if is_panic(self.cx, macro_call.def_id)
811+
|| matches!(
812+
self.cx.tcx.item_name(macro_call.def_id).as_str(),
813+
"assert" | "assert_eq" | "assert_ne" | "todo"
814+
)
815+
{
816+
self.panic_span = Some(macro_call.span);
818817
}
819818
}
820819

821-
// check for `assert_eq` or `assert_ne`
822-
if is_expn_of(expr.span, "assert_eq").is_some() || is_expn_of(expr.span, "assert_ne").is_some() {
823-
self.panic_span = Some(expr.span);
824-
}
825-
826820
// check for `unwrap`
827821
if let Some(arglists) = method_chain_args(expr, &["unwrap"]) {
828822
let receiver_ty = self.typeck_results.expr_ty(&arglists[0][0]).peel_refs();
@@ -844,8 +838,3 @@ impl<'a, 'tcx> Visitor<'tcx> for FindPanicUnwrap<'a, 'tcx> {
844838
NestedVisitorMap::OnlyBodies(self.cx.tcx.hir())
845839
}
846840
}
847-
848-
fn is_expn_of_debug_assertions(span: Span) -> bool {
849-
const MACRO_NAMES: &[&str] = &["debug_assert", "debug_assert_eq", "debug_assert_ne"];
850-
MACRO_NAMES.iter().any(|name| is_expn_of(span, name).is_some())
851-
}

clippy_lints/src/eq_op.rs

+20-25
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
use clippy_utils::diagnostics::{multispan_sugg, span_lint, span_lint_and_then};
2+
use clippy_utils::macros::{find_assert_eq_args, first_node_macro_backtrace};
23
use clippy_utils::source::snippet;
34
use clippy_utils::ty::{implements_trait, is_copy};
4-
use clippy_utils::{ast_utils::is_useless_with_eq_exprs, eq_expr_value, higher, is_expn_of, is_in_test_function};
5+
use clippy_utils::{ast_utils::is_useless_with_eq_exprs, eq_expr_value, is_in_test_function};
56
use if_chain::if_chain;
67
use rustc_errors::Applicability;
7-
use rustc_hir::{BinOpKind, BorrowKind, Expr, ExprKind, StmtKind};
8+
use rustc_hir::{BinOpKind, BorrowKind, Expr, ExprKind};
89
use rustc_lint::{LateContext, LateLintPass};
910
use rustc_session::{declare_lint_pass, declare_tool_lint};
1011

@@ -68,32 +69,26 @@ declare_clippy_lint! {
6869

6970
declare_lint_pass!(EqOp => [EQ_OP, OP_REF]);
7071

71-
const ASSERT_MACRO_NAMES: [&str; 4] = ["assert_eq", "assert_ne", "debug_assert_eq", "debug_assert_ne"];
72-
7372
impl<'tcx> LateLintPass<'tcx> for EqOp {
7473
#[allow(clippy::similar_names, clippy::too_many_lines)]
7574
fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) {
76-
if let ExprKind::Block(block, _) = e.kind {
77-
for stmt in block.stmts {
78-
for amn in &ASSERT_MACRO_NAMES {
79-
if_chain! {
80-
if is_expn_of(stmt.span, amn).is_some();
81-
if let StmtKind::Semi(matchexpr) = stmt.kind;
82-
if let Some(macro_args) = higher::extract_assert_macro_args(matchexpr);
83-
if macro_args.len() == 2;
84-
let (lhs, rhs) = (macro_args[0], macro_args[1]);
85-
if eq_expr_value(cx, lhs, rhs);
86-
if !is_in_test_function(cx.tcx, e.hir_id);
87-
then {
88-
span_lint(
89-
cx,
90-
EQ_OP,
91-
lhs.span.to(rhs.span),
92-
&format!("identical args used in this `{}!` macro call", amn),
93-
);
94-
}
95-
}
96-
}
75+
if_chain! {
76+
if let Some((macro_call, macro_name)) = first_node_macro_backtrace(cx, e).find_map(|macro_call| {
77+
let name = cx.tcx.item_name(macro_call.def_id);
78+
matches!(name.as_str(), "assert_eq" | "assert_ne" | "debug_assert_eq" | "debug_assert_ne")
79+
.then(|| (macro_call, name))
80+
});
81+
if let Some((lhs, rhs, _)) = find_assert_eq_args(cx, e, macro_call.expn);
82+
if eq_expr_value(cx, lhs, rhs);
83+
if macro_call.is_local();
84+
if !is_in_test_function(cx.tcx, e.hir_id);
85+
then {
86+
span_lint(
87+
cx,
88+
EQ_OP,
89+
lhs.span.to(rhs.span),
90+
&format!("identical args used in this `{}!` macro call", macro_name),
91+
);
9792
}
9893
}
9994
if let ExprKind::Binary(op, left, right) = e.kind {

0 commit comments

Comments
 (0)