Skip to main content

clippy_utils/
macros.rs

1//! Utilities for analyzing macro invocations and expansions.
2
3#![expect(clippy::similar_names)] // `expr` and `expn`
4
5use std::cell::Cell;
6use std::sync::{Arc, OnceLock};
7
8use crate::visitors::{Descend, for_each_expr_without_closures};
9use crate::{get_clippy_attr, sym};
10
11use arrayvec::ArrayVec;
12use rustc_ast::{FormatArgs, FormatArgument, FormatPlaceholder};
13use rustc_data_structures::fx::FxHashMap;
14use rustc_hir::{self as hir, Expr, ExprKind, HirId, Node, QPath};
15use rustc_lint::LateContext;
16use rustc_span::def_id::DefId;
17use rustc_span::hygiene::{self, MacroKind, SyntaxContext};
18use rustc_span::{BytePos, ExpnData, ExpnId, ExpnKind, Span, SpanData, Symbol};
19use std::ops::ControlFlow;
20
21const FORMAT_MACRO_DIAG_ITEMS: &[Symbol] = &[
22    sym::assert_eq_macro,
23    sym::assert_macro,
24    sym::assert_ne_macro,
25    sym::core_panic_macro,
26    sym::debug_assert_eq_macro,
27    sym::debug_assert_macro,
28    sym::debug_assert_ne_macro,
29    sym::eprint_macro,
30    sym::eprintln_macro,
31    sym::format_args_macro,
32    sym::format_macro,
33    sym::print_macro,
34    sym::println_macro,
35    sym::std_panic_macro,
36    sym::todo_macro,
37    sym::unimplemented_macro,
38    sym::write_macro,
39    sym::writeln_macro,
40];
41
42/// Returns true if a given Macro `DefId` is a format macro (e.g. `println!`)
43pub fn is_format_macro(cx: &LateContext<'_>, macro_def_id: DefId) -> bool {
44    if let Some(name) = cx.tcx.get_diagnostic_name(macro_def_id) {
45        FORMAT_MACRO_DIAG_ITEMS.contains(&name)
46    } else {
47        // Allow users to tag any macro as being format!-like
48        // TODO: consider deleting FORMAT_MACRO_DIAG_ITEMS and using just this method
49        get_clippy_attr(cx.tcx, macro_def_id, sym::format_args).is_some()
50    }
51}
52
53/// A macro call, like `vec![1, 2, 3]`.
54///
55/// Use `tcx.item_name(macro_call.def_id)` to get the macro name.
56/// Even better is to check if it is a diagnostic item.
57///
58/// This structure is similar to `ExpnData` but it precludes desugaring expansions.
59#[derive(Debug)]
60pub struct MacroCall {
61    /// Macro `DefId`
62    pub def_id: DefId,
63    /// Kind of macro
64    pub kind: MacroKind,
65    /// The expansion produced by the macro call
66    pub expn: ExpnId,
67    /// Span of the macro call site
68    pub span: Span,
69}
70
71impl MacroCall {
72    /// Returns true if this macro call is from the root expansion or a locally defined macro
73    pub fn is_local(&self) -> bool {
74        span_is_local(self.span)
75    }
76}
77
78/// Returns an iterator of expansions that created the given span
79pub fn expn_backtrace(mut span: Span) -> impl Iterator<Item = (ExpnId, ExpnData)> {
80    std::iter::from_fn(move || {
81        let ctxt = span.ctxt();
82        if ctxt == SyntaxContext::root() {
83            return None;
84        }
85        let expn = ctxt.outer_expn();
86        let data = expn.expn_data();
87        span = data.call_site;
88        Some((expn, data))
89    })
90}
91
92/// Checks whether the span is from the root expansion or a locally defined macro
93pub fn span_is_local(span: Span) -> bool {
94    !span.from_expansion() || expn_is_local(span.ctxt().outer_expn())
95}
96
97/// Checks whether the expansion is the root expansion or a locally defined macro
98pub fn expn_is_local(expn: ExpnId) -> bool {
99    if expn == ExpnId::root() {
100        return true;
101    }
102    let data = expn.expn_data();
103    let backtrace = expn_backtrace(data.call_site);
104    std::iter::once((expn, data))
105        .chain(backtrace)
106        .find_map(|(_, data)| data.macro_def_id)
107        .is_none_or(DefId::is_local)
108}
109
110/// Returns an iterator of macro expansions that created the given span.
111/// Note that desugaring expansions are skipped.
112pub fn macro_backtrace(span: Span) -> impl Iterator<Item = MacroCall> {
113    expn_backtrace(span).filter_map(|(expn, data)| match data {
114        ExpnData {
115            kind: ExpnKind::Macro(kind, _),
116            macro_def_id: Some(def_id),
117            call_site: span,
118            ..
119        } => Some(MacroCall {
120            def_id,
121            kind,
122            expn,
123            span,
124        }),
125        _ => None,
126    })
127}
128
129/// If the macro backtrace of `span` has a macro call at the root expansion
130/// (i.e. not a nested macro call), returns `Some` with the `MacroCall`
131///
132/// If you only want to check whether the root macro has a specific name,
133/// consider using [`matching_root_macro_call`] instead.
134pub fn root_macro_call(span: Span) -> Option<MacroCall> {
135    macro_backtrace(span).last()
136}
137
138/// A combination of [`root_macro_call`] and
139/// [`is_diagnostic_item`](rustc_middle::ty::TyCtxt::is_diagnostic_item) that returns a `MacroCall`
140/// at the root expansion if only it matches the given name.
141pub fn matching_root_macro_call(cx: &LateContext<'_>, span: Span, name: Symbol) -> Option<MacroCall> {
142    root_macro_call(span).filter(|mc| cx.tcx.is_diagnostic_item(name, mc.def_id))
143}
144
145/// Like [`root_macro_call`], but only returns `Some` if `node` is the "first node"
146/// produced by the macro call, as in [`first_node_in_macro`].
147pub fn root_macro_call_first_node(cx: &LateContext<'_>, node: &impl HirNode) -> Option<MacroCall> {
148    if first_node_in_macro(cx, node) != Some(ExpnId::root()) {
149        return None;
150    }
151    root_macro_call(node.span())
152}
153
154/// Like [`macro_backtrace`], but only returns macro calls where `node` is the "first node" of the
155/// macro call, as in [`first_node_in_macro`].
156pub fn first_node_macro_backtrace(cx: &LateContext<'_>, node: &impl HirNode) -> impl Iterator<Item = MacroCall> {
157    let span = node.span();
158    first_node_in_macro(cx, node)
159        .into_iter()
160        .flat_map(move |expn| macro_backtrace(span).take_while(move |macro_call| macro_call.expn != expn))
161}
162
163/// If `node` is the "first node" in a macro expansion, returns `Some` with the `ExpnId` of the
164/// macro call site (i.e. the parent of the macro expansion).
165///
166/// This generally means that `node` is the outermost node of an entire macro expansion, but there
167/// are some caveats noted below. This is useful for finding macro calls while visiting the HIR
168/// without processing the macro call at every node within its expansion.
169///
170/// If you already have immediate access to the parent node, it is simpler to
171/// just check the context of that span directly (e.g. `parent.span.from_expansion()`).
172///
173/// If a macro call is in statement position, it expands to one or more statements.
174/// In that case, each statement *and* their immediate descendants will all yield `Some`
175/// with the `ExpnId` of the containing block.
176///
177/// A node may be the "first node" of multiple macro calls in a macro backtrace.
178/// The expansion of the outermost macro call site is returned in such cases.
179pub fn first_node_in_macro(cx: &LateContext<'_>, node: &impl HirNode) -> Option<ExpnId> {
180    // A node that is not from an expansion can never be the first node in a macro. This is the
181    // common case, so return before touching the cache.
182    if !node.span().from_expansion() {
183        return None;
184    }
185
186    // The macro lints each query this for the node currently being visited, and the combined late
187    // pass runs all their `check_*` callbacks on that node back to back, so the redundant queries
188    // are consecutive. A single-slot memo of the last node collapses them to one backtrace walk in
189    // O(1) memory. The result is a pure function of the `HirId` (the HIR is frozen during lint
190    // passes), so returning the stored value on a key match is always correct.
191    let hir_id = node.hir_id();
192    if let Some((cached_id, cached)) = LAST_FIRST_NODE_IN_MACRO.get()
193        && cached_id == hir_id
194    {
195        return cached;
196    }
197    let result = first_node_in_macro_uncached(cx, node);
198    LAST_FIRST_NODE_IN_MACRO.set(Some((hir_id, result)));
199    result
200}
201
202thread_local! {
203    static LAST_FIRST_NODE_IN_MACRO: Cell<Option<(HirId, Option<ExpnId>)>> = const { Cell::new(None) };
204}
205
206fn first_node_in_macro_uncached(cx: &LateContext<'_>, node: &impl HirNode) -> Option<ExpnId> {
207    // get the macro expansion or return `None` if not found
208    // `macro_backtrace` importantly ignores desugaring expansions
209    let expn = macro_backtrace(node.span()).next()?.expn;
210
211    // get the parent node, possibly skipping over a statement
212    // if the parent is not found, it is sensible to return `Some(root)`
213    let mut parent_iter = cx.tcx.hir_parent_iter(node.hir_id());
214    let (parent_id, _) = match parent_iter.next() {
215        None => return Some(ExpnId::root()),
216        Some((_, Node::Stmt(_))) => match parent_iter.next() {
217            None => return Some(ExpnId::root()),
218            Some(next) => next,
219        },
220        Some(next) => next,
221    };
222
223    // get the macro expansion of the parent node
224    let parent_span = cx.tcx.hir_span(parent_id);
225    let Some(parent_macro_call) = macro_backtrace(parent_span).next() else {
226        // the parent node is not in a macro
227        return Some(ExpnId::root());
228    };
229
230    if parent_macro_call.expn.is_descendant_of(expn) {
231        // `node` is input to a macro call
232        return None;
233    }
234
235    Some(parent_macro_call.expn)
236}
237
238/* Specific Macro Utils */
239
240/// Is `def_id` of `std::panic`, `core::panic` or any inner implementation macros
241pub fn is_panic(cx: &LateContext<'_>, def_id: DefId) -> bool {
242    let Some(name) = cx.tcx.get_diagnostic_name(def_id) else {
243        return false;
244    };
245    matches!(
246        name,
247        sym::core_panic_macro
248            | sym::std_panic_macro
249            | sym::core_panic_2015_macro
250            | sym::std_panic_2015_macro
251            | sym::core_panic_2021_macro
252    )
253}
254
255/// Is `def_id` of `assert!` or `debug_assert!`
256pub fn is_assert_macro(cx: &LateContext<'_>, def_id: DefId) -> bool {
257    let Some(name) = cx.tcx.get_diagnostic_name(def_id) else {
258        return false;
259    };
260    matches!(name, sym::assert_macro | sym::debug_assert_macro)
261}
262
263/// A call to a function in [`std::rt`] or [`core::panicking`] that results in a panic, typically
264/// part of a `panic!()` expansion (often wrapped in a block) but may be called directly by other
265/// macros such as `assert`.
266#[derive(Debug)]
267pub enum PanicCall<'a> {
268    // The default message - `panic!()`, `assert!(true)`, etc.
269    DefaultMessage,
270    /// A string literal or any `&str` in edition 2015/2018 - `panic!("message")` or
271    /// `panic!(message)`.
272    ///
273    /// In edition 2021+ `panic!("message")` will be a [`PanicCall::Format`] and `panic!(message)` a
274    /// compile error.
275    Str2015(&'a Expr<'a>),
276    /// A single argument that implements `Display` - `panic!("{}", object)`.
277    ///
278    /// `panic!("{object}")` will still be a [`PanicCall::Format`].
279    Display(&'a Expr<'a>),
280    /// Anything else - `panic!("error {}: {}", a, b)`, `panic!("on edition 2021+")`.
281    ///
282    /// See [`FormatArgsStorage::get`] to examine the contents of the formatting.
283    Format(&'a Expr<'a>),
284}
285
286impl<'a> PanicCall<'a> {
287    pub fn parse(expr: &'a Expr<'a>) -> Option<Self> {
288        let ExprKind::Call(callee, args) = &expr.kind else {
289            return None;
290        };
291        let ExprKind::Path(QPath::Resolved(_, path)) = &callee.kind else {
292            return None;
293        };
294        let name = path.segments.last().unwrap().ident.name;
295
296        let [arg, rest @ ..] = args else {
297            return None;
298        };
299        let result = match name {
300            sym::panic | sym::begin_panic | sym::panic_str_2015 => {
301                if arg.span.ctxt() == expr.span.ctxt() || arg.span.is_dummy() {
302                    Self::DefaultMessage
303                } else {
304                    Self::Str2015(arg)
305                }
306            },
307            sym::panic_display => {
308                let ExprKind::AddrOf(_, _, e) = &arg.kind else {
309                    return None;
310                };
311                Self::Display(e)
312            },
313            sym::panic_fmt => Self::Format(arg),
314            // Since Rust 1.52, `assert_{eq,ne}` macros expand to use:
315            // `core::panicking::assert_failed(.., left_val, right_val, None | Some(format_args!(..)));`
316            sym::assert_failed => {
317                // It should have 4 arguments in total (we already matched with the first argument,
318                // so we're just checking for 3)
319                if rest.len() != 3 {
320                    return None;
321                }
322                // `msg_arg` is either `None` (no custom message) or `Some(format_args!(..))` (custom message)
323                let msg_arg = &rest[2];
324                match msg_arg.kind {
325                    ExprKind::Call(_, [fmt_arg]) => Self::Format(fmt_arg),
326                    _ => Self::DefaultMessage,
327                }
328            },
329            _ => return None,
330        };
331        Some(result)
332    }
333
334    pub fn is_default_message(&self) -> bool {
335        matches!(self, Self::DefaultMessage)
336    }
337}
338
339/// Finds the arguments of an `assert!` or `debug_assert!` macro call within the macro expansion
340pub fn find_assert_args<'a>(
341    cx: &LateContext<'_>,
342    expr: &'a Expr<'a>,
343    expn: ExpnId,
344) -> Option<(&'a Expr<'a>, PanicCall<'a>)> {
345    find_assert_args_inner(cx, expr, expn).map(|([e], p)| (e, p))
346}
347
348/// Finds the arguments of an `assert_eq!` or `debug_assert_eq!` macro call within the macro
349/// expansion
350pub fn find_assert_eq_args<'a>(
351    cx: &LateContext<'_>,
352    expr: &'a Expr<'a>,
353    expn: ExpnId,
354) -> Option<(&'a Expr<'a>, &'a Expr<'a>, PanicCall<'a>)> {
355    find_assert_args_inner(cx, expr, expn).map(|([a, b], p)| (a, b, p))
356}
357
358fn find_assert_args_inner<'a, const N: usize>(
359    cx: &LateContext<'_>,
360    expr: &'a Expr<'a>,
361    expn: ExpnId,
362) -> Option<([&'a Expr<'a>; N], PanicCall<'a>)> {
363    let macro_id = expn.expn_data().macro_def_id?;
364    let (expr, expn) = match cx.tcx.item_name(macro_id).as_str().strip_prefix("debug_") {
365        None => (expr, expn),
366        Some(inner_name) => find_assert_within_debug_assert(cx, expr, expn, Symbol::intern(inner_name))?,
367    };
368    let mut args = ArrayVec::new();
369    let panic_expn = for_each_expr_without_closures(expr, |e| {
370        if args.is_full() {
371            match PanicCall::parse(e) {
372                Some(expn) => ControlFlow::Break(expn),
373                None => ControlFlow::Continue(Descend::Yes),
374            }
375        } else if is_assert_arg(cx, e, expn) {
376            args.push(e);
377            ControlFlow::Continue(Descend::No)
378        } else {
379            ControlFlow::Continue(Descend::Yes)
380        }
381    });
382    let args = args.into_inner().ok()?;
383    Some((args, panic_expn?))
384}
385
386fn find_assert_within_debug_assert<'a>(
387    cx: &LateContext<'_>,
388    expr: &'a Expr<'a>,
389    expn: ExpnId,
390    assert_name: Symbol,
391) -> Option<(&'a Expr<'a>, ExpnId)> {
392    for_each_expr_without_closures(expr, |e| {
393        if !e.span.from_expansion() {
394            return ControlFlow::Continue(Descend::No);
395        }
396        let e_expn = e.span.ctxt().outer_expn();
397        if e_expn == expn {
398            ControlFlow::Continue(Descend::Yes)
399        } else if e_expn.expn_data().macro_def_id.map(|id| cx.tcx.item_name(id)) == Some(assert_name) {
400            ControlFlow::Break((e, e_expn))
401        } else {
402            ControlFlow::Continue(Descend::No)
403        }
404    })
405}
406
407fn is_assert_arg(cx: &LateContext<'_>, expr: &Expr<'_>, assert_expn: ExpnId) -> bool {
408    if !expr.span.from_expansion() {
409        return true;
410    }
411    let result = macro_backtrace(expr.span).try_for_each(|macro_call| {
412        if macro_call.expn == assert_expn {
413            ControlFlow::Break(false)
414        } else {
415            match cx.tcx.item_name(macro_call.def_id) {
416                // `cfg!(debug_assertions)` in `debug_assert!`
417                sym::cfg => ControlFlow::Continue(()),
418                // assert!(other_macro!(..))
419                _ => ControlFlow::Break(true),
420            }
421        }
422    });
423    match result {
424        ControlFlow::Break(is_assert_arg) => is_assert_arg,
425        ControlFlow::Continue(()) => true,
426    }
427}
428
429/// Stores AST [`FormatArgs`] nodes for use in late lint passes, as they are in a desugared form in
430/// the HIR
431#[derive(Default, Clone)]
432pub struct FormatArgsStorage(Arc<OnceLock<FxHashMap<Span, FormatArgs>>>);
433
434impl FormatArgsStorage {
435    /// Returns an AST [`FormatArgs`] node if a `format_args` expansion is found as a descendant of
436    /// `expn_id`
437    ///
438    /// See also [`find_format_arg_expr`]
439    pub fn get(&self, cx: &LateContext<'_>, start: &Expr<'_>, expn_id: ExpnId) -> Option<&FormatArgs> {
440        let format_args_expr = for_each_expr_without_closures(start, |expr| {
441            let ctxt = expr.span.ctxt();
442            if ctxt.outer_expn().is_descendant_of(expn_id) {
443                if macro_backtrace(expr.span)
444                    .map(|macro_call| cx.tcx.item_name(macro_call.def_id))
445                    .any(|name| matches!(name, sym::const_format_args | sym::format_args | sym::format_args_nl))
446                {
447                    ControlFlow::Break(expr)
448                } else {
449                    ControlFlow::Continue(Descend::Yes)
450                }
451            } else {
452                ControlFlow::Continue(Descend::No)
453            }
454        })?;
455
456        debug_assert!(self.0.get().is_some(), "`FormatArgsStorage` not yet populated");
457
458        self.0.get()?.get(&format_args_expr.span.with_parent(None))
459    }
460
461    /// Returns AST [`FormatArgs`] nodes if there are any nested inside `parent_format_args`.
462    pub fn get_nested(&self, parent_format_args: &FormatArgs) -> Vec<&FormatArgs> {
463        let mut nested = Vec::new();
464        let Some(format_args_map) = self.0.get() else {
465            return nested;
466        };
467
468        for arg in parent_format_args.arguments.all_args() {
469            if matches!(arg.expr.kind, rustc_ast::ExprKind::FormatArgs(_))
470                && let Some(format_args) = format_args_map.get(&arg.expr.span.with_parent(None))
471            {
472                nested.push(format_args);
473            }
474        }
475
476        nested
477    }
478
479    /// Should only be called by `FormatArgsCollector`
480    pub fn set(&self, format_args: FxHashMap<Span, FormatArgs>) {
481        self.0
482            .set(format_args)
483            .expect("`FormatArgsStorage::set` should only be called once");
484    }
485}
486
487/// Attempt to find the [`rustc_hir::Expr`] that corresponds to the [`FormatArgument`]'s value
488pub fn find_format_arg_expr<'hir>(start: &'hir Expr<'hir>, target: &FormatArgument) -> Option<&'hir Expr<'hir>> {
489    let SpanData {
490        lo,
491        hi,
492        ctxt,
493        parent: _,
494    } = target.expr.span.data();
495
496    for_each_expr_without_closures(start, |expr| {
497        // When incremental compilation is enabled spans gain a parent during AST to HIR lowering,
498        // since we're comparing an AST span to a HIR one we need to ignore the parent field
499        let data = expr.span.data();
500        if data.lo == lo && data.hi == hi && data.ctxt == ctxt {
501            ControlFlow::Break(expr)
502        } else {
503            ControlFlow::Continue(())
504        }
505    })
506}
507
508/// Span of the `:` and format specifiers
509///
510/// ```ignore
511/// format!("{:.}"), format!("{foo:.}")
512///           ^^                  ^^
513/// ```
514pub fn format_placeholder_format_span(placeholder: &FormatPlaceholder) -> Option<Span> {
515    let base = placeholder.span?.data();
516
517    // `base.hi` is `{...}|`, subtract 1 byte (the length of '}') so that it points before the closing
518    // brace `{...|}`
519    Some(Span::new(
520        placeholder.argument.span?.hi(),
521        base.hi - BytePos(1),
522        base.ctxt,
523        base.parent,
524    ))
525}
526
527/// Span covering the format string and values
528///
529/// ```ignore
530/// format("{}.{}", 10, 11)
531/// //     ^^^^^^^^^^^^^^^
532/// ```
533pub fn format_args_inputs_span(format_args: &FormatArgs) -> Span {
534    match format_args.arguments.explicit_args() {
535        [] => format_args.span,
536        [.., last] => format_args
537            .span
538            .to(hygiene::walk_chain(last.expr.span, format_args.span.ctxt())),
539    }
540}
541
542/// Returns the [`Span`] of the value at `index` extended to the previous comma, e.g. for the value
543/// `10`
544///
545/// ```ignore
546/// format("{}.{}", 10, 11)
547/// //            ^^^^
548/// ```
549pub fn format_arg_removal_span(format_args: &FormatArgs, index: usize) -> Option<Span> {
550    let ctxt = format_args.span.ctxt();
551
552    let current = hygiene::walk_chain(format_args.arguments.by_index(index)?.expr.span, ctxt);
553
554    let prev = if index == 0 {
555        format_args.span
556    } else {
557        hygiene::walk_chain(format_args.arguments.by_index(index - 1)?.expr.span, ctxt)
558    };
559
560    Some(current.with_lo(prev.hi()))
561}
562
563/// Where a format parameter is being used in the format string
564#[derive(Debug, Copy, Clone, PartialEq, Eq)]
565pub enum FormatParamUsage {
566    /// Appears as an argument, e.g. `format!("{}", foo)`
567    Argument,
568    /// Appears as a width, e.g. `format!("{:width$}", foo, width = 1)`
569    Width,
570    /// Appears as a precision, e.g. `format!("{:.precision$}", foo, precision = 1)`
571    Precision,
572}
573
574/// A node with a `HirId` and a `Span`
575pub trait HirNode {
576    /// Returns this node's [`HirId`]
577    fn hir_id(&self) -> HirId;
578    /// Returns this node's [`Span`]
579    fn span(&self) -> Span;
580}
581
582macro_rules! impl_hir_node {
583    ($($t:ident),*) => {
584        $(impl HirNode for hir::$t<'_> {
585            fn hir_id(&self) -> HirId {
586                self.hir_id
587            }
588            fn span(&self) -> Span {
589                self.span
590            }
591        })*
592    };
593}
594
595impl_hir_node!(Expr, Pat);
596
597impl HirNode for hir::Item<'_> {
598    fn hir_id(&self) -> HirId {
599        self.hir_id()
600    }
601
602    fn span(&self) -> Span {
603        self.span
604    }
605}