Skip to main content

clippy_utils/
macros.rs

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