1#![expect(clippy::similar_names)] use 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
42pub 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 get_clippy_attr(cx.tcx, macro_def_id, sym::format_args).is_some()
50 }
51}
52
53#[derive(Debug)]
60pub struct MacroCall {
61 pub def_id: DefId,
63 pub kind: MacroKind,
65 pub expn: ExpnId,
67 pub span: Span,
69}
70
71impl MacroCall {
72 pub fn is_local(&self) -> bool {
74 span_is_local(self.span)
75 }
76}
77
78pub 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
92pub fn span_is_local(span: Span) -> bool {
94 !span.from_expansion() || expn_is_local(span.ctxt().outer_expn())
95}
96
97pub 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
110pub 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
129pub fn root_macro_call(span: Span) -> Option<MacroCall> {
135 macro_backtrace(span).last()
136}
137
138pub 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
145pub 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
154pub 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
163pub fn first_node_in_macro(cx: &LateContext<'_>, node: &impl HirNode) -> Option<ExpnId> {
180 if !node.span().from_expansion() {
183 return None;
184 }
185
186 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 let expn = macro_backtrace(node.span()).next()?.expn;
210
211 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 let parent_span = cx.tcx.hir_span(parent_id);
225 let Some(parent_macro_call) = macro_backtrace(parent_span).next() else {
226 return Some(ExpnId::root());
228 };
229
230 if parent_macro_call.expn.is_descendant_of(expn) {
231 return None;
233 }
234
235 Some(parent_macro_call.expn)
236}
237
238pub 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
255pub 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#[derive(Debug)]
267pub enum PanicCall<'a> {
268 DefaultMessage,
270 Str2015(&'a Expr<'a>),
276 Display(&'a Expr<'a>),
280 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 sym::assert_failed => {
317 if rest.len() != 3 {
320 return None;
321 }
322 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
339pub 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
348pub 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 sym::cfg => ControlFlow::Continue(()),
418 _ => 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#[derive(Default, Clone)]
432pub struct FormatArgsStorage(Arc<OnceLock<FxHashMap<Span, FormatArgs>>>);
433
434impl FormatArgsStorage {
435 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 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 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
487pub 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 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
508pub fn format_placeholder_format_span(placeholder: &FormatPlaceholder) -> Option<Span> {
515 let base = placeholder.span?.data();
516
517 Some(Span::new(
520 placeholder.argument.span?.hi(),
521 base.hi - BytePos(1),
522 base.ctxt,
523 base.parent,
524 ))
525}
526
527pub 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
542pub 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#[derive(Debug, Copy, Clone, PartialEq, Eq)]
565pub enum FormatParamUsage {
566 Argument,
568 Width,
570 Precision,
572}
573
574pub trait HirNode {
576 fn hir_id(&self) -> HirId;
578 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}