Skip to main content

clippy_utils/
macros.rs

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