1#![allow(clippy::similar_names)] use std::sync::Arc;
4
5use crate::get_unique_attr;
6use crate::visitors::{Descend, for_each_expr_without_closures};
7
8use arrayvec::ArrayVec;
9use rustc_ast::{FormatArgs, FormatArgument, FormatPlaceholder};
10use rustc_data_structures::fx::FxHashMap;
11use rustc_data_structures::sync::OnceLock;
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, sym};
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::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::write_macro,
34 sym::writeln_macro,
35];
36
37pub fn is_format_macro(cx: &LateContext<'_>, macro_def_id: DefId) -> bool {
39 if let Some(name) = cx.tcx.get_diagnostic_name(macro_def_id) {
40 FORMAT_MACRO_DIAG_ITEMS.contains(&name)
41 } else {
42 get_unique_attr(cx.sess(), cx.tcx.get_attrs_unchecked(macro_def_id), "format_args").is_some()
45 }
46}
47
48#[derive(Debug)]
55pub struct MacroCall {
56 pub def_id: DefId,
58 pub kind: MacroKind,
60 pub expn: ExpnId,
62 pub span: Span,
64}
65
66impl MacroCall {
67 pub fn is_local(&self) -> bool {
68 span_is_local(self.span)
69 }
70}
71
72pub fn expn_backtrace(mut span: Span) -> impl Iterator<Item = (ExpnId, ExpnData)> {
74 std::iter::from_fn(move || {
75 let ctxt = span.ctxt();
76 if ctxt == SyntaxContext::root() {
77 return None;
78 }
79 let expn = ctxt.outer_expn();
80 let data = expn.expn_data();
81 span = data.call_site;
82 Some((expn, data))
83 })
84}
85
86pub fn span_is_local(span: Span) -> bool {
88 !span.from_expansion() || expn_is_local(span.ctxt().outer_expn())
89}
90
91pub fn expn_is_local(expn: ExpnId) -> bool {
93 if expn == ExpnId::root() {
94 return true;
95 }
96 let data = expn.expn_data();
97 let backtrace = expn_backtrace(data.call_site);
98 std::iter::once((expn, data))
99 .chain(backtrace)
100 .find_map(|(_, data)| data.macro_def_id)
101 .is_none_or(DefId::is_local)
102}
103
104pub fn macro_backtrace(span: Span) -> impl Iterator<Item = MacroCall> {
107 expn_backtrace(span).filter_map(|(expn, data)| match data {
108 ExpnData {
109 kind: ExpnKind::Macro(kind, _),
110 macro_def_id: Some(def_id),
111 call_site: span,
112 ..
113 } => Some(MacroCall {
114 def_id,
115 kind,
116 expn,
117 span,
118 }),
119 _ => None,
120 })
121}
122
123pub fn root_macro_call(span: Span) -> Option<MacroCall> {
129 macro_backtrace(span).last()
130}
131
132pub fn matching_root_macro_call(cx: &LateContext<'_>, span: Span, name: Symbol) -> Option<MacroCall> {
136 root_macro_call(span).filter(|mc| cx.tcx.is_diagnostic_item(name, mc.def_id))
137}
138
139pub fn root_macro_call_first_node(cx: &LateContext<'_>, node: &impl HirNode) -> Option<MacroCall> {
142 if first_node_in_macro(cx, node) != Some(ExpnId::root()) {
143 return None;
144 }
145 root_macro_call(node.span())
146}
147
148pub fn first_node_macro_backtrace(cx: &LateContext<'_>, node: &impl HirNode) -> impl Iterator<Item = MacroCall> {
151 let span = node.span();
152 first_node_in_macro(cx, node)
153 .into_iter()
154 .flat_map(move |expn| macro_backtrace(span).take_while(move |macro_call| macro_call.expn != expn))
155}
156
157pub fn first_node_in_macro(cx: &LateContext<'_>, node: &impl HirNode) -> Option<ExpnId> {
174 let expn = macro_backtrace(node.span()).next()?.expn;
177
178 let hir = cx.tcx.hir();
181 let mut parent_iter = hir.parent_iter(node.hir_id());
182 let (parent_id, _) = match parent_iter.next() {
183 None => return Some(ExpnId::root()),
184 Some((_, Node::Stmt(_))) => match parent_iter.next() {
185 None => return Some(ExpnId::root()),
186 Some(next) => next,
187 },
188 Some(next) => next,
189 };
190
191 let parent_span = hir.span(parent_id);
193 let Some(parent_macro_call) = macro_backtrace(parent_span).next() else {
194 return Some(ExpnId::root());
196 };
197
198 if parent_macro_call.expn.is_descendant_of(expn) {
199 return None;
201 }
202
203 Some(parent_macro_call.expn)
204}
205
206pub fn is_panic(cx: &LateContext<'_>, def_id: DefId) -> bool {
210 let Some(name) = cx.tcx.get_diagnostic_name(def_id) else {
211 return false;
212 };
213 matches!(
214 name,
215 sym::core_panic_macro
216 | sym::std_panic_macro
217 | sym::core_panic_2015_macro
218 | sym::std_panic_2015_macro
219 | sym::core_panic_2021_macro
220 )
221}
222
223pub fn is_assert_macro(cx: &LateContext<'_>, def_id: DefId) -> bool {
225 let Some(name) = cx.tcx.get_diagnostic_name(def_id) else {
226 return false;
227 };
228 matches!(name, sym::assert_macro | sym::debug_assert_macro)
229}
230
231#[derive(Debug)]
232pub enum PanicExpn<'a> {
233 Empty,
235 Str(&'a Expr<'a>),
237 Display(&'a Expr<'a>),
239 Format(&'a Expr<'a>),
241}
242
243impl<'a> PanicExpn<'a> {
244 pub fn parse(expr: &'a Expr<'a>) -> Option<Self> {
245 let ExprKind::Call(callee, args) = &expr.kind else {
246 return None;
247 };
248 let ExprKind::Path(QPath::Resolved(_, path)) = &callee.kind else {
249 return None;
250 };
251 let name = path.segments.last().unwrap().ident.as_str();
252
253 if name == "panic_cold_explicit" {
255 return Some(Self::Empty);
256 }
257
258 let [arg, rest @ ..] = args else {
259 return None;
260 };
261 let result = match name {
262 "panic" if arg.span.eq_ctxt(expr.span) => Self::Empty,
263 "panic" | "panic_str" => Self::Str(arg),
264 "panic_display" | "panic_cold_display" => {
265 let ExprKind::AddrOf(_, _, e) = &arg.kind else {
266 return None;
267 };
268 Self::Display(e)
269 },
270 "panic_fmt" => Self::Format(arg),
271 "assert_failed" => {
274 if rest.len() != 3 {
277 return None;
278 }
279 let msg_arg = &rest[2];
281 match msg_arg.kind {
282 ExprKind::Call(_, [fmt_arg]) => Self::Format(fmt_arg),
283 _ => Self::Empty,
284 }
285 },
286 _ => return None,
287 };
288 Some(result)
289 }
290}
291
292pub fn find_assert_args<'a>(
294 cx: &LateContext<'_>,
295 expr: &'a Expr<'a>,
296 expn: ExpnId,
297) -> Option<(&'a Expr<'a>, PanicExpn<'a>)> {
298 find_assert_args_inner(cx, expr, expn).map(|([e], mut p)| {
299 if let PanicExpn::Str(_) = p {
304 p = PanicExpn::Empty;
305 }
306
307 (e, p)
308 })
309}
310
311pub fn find_assert_eq_args<'a>(
314 cx: &LateContext<'_>,
315 expr: &'a Expr<'a>,
316 expn: ExpnId,
317) -> Option<(&'a Expr<'a>, &'a Expr<'a>, PanicExpn<'a>)> {
318 find_assert_args_inner(cx, expr, expn).map(|([a, b], p)| (a, b, p))
319}
320
321fn find_assert_args_inner<'a, const N: usize>(
322 cx: &LateContext<'_>,
323 expr: &'a Expr<'a>,
324 expn: ExpnId,
325) -> Option<([&'a Expr<'a>; N], PanicExpn<'a>)> {
326 let macro_id = expn.expn_data().macro_def_id?;
327 let (expr, expn) = match cx.tcx.item_name(macro_id).as_str().strip_prefix("debug_") {
328 None => (expr, expn),
329 Some(inner_name) => find_assert_within_debug_assert(cx, expr, expn, Symbol::intern(inner_name))?,
330 };
331 let mut args = ArrayVec::new();
332 let panic_expn = for_each_expr_without_closures(expr, |e| {
333 if args.is_full() {
334 match PanicExpn::parse(e) {
335 Some(expn) => ControlFlow::Break(expn),
336 None => ControlFlow::Continue(Descend::Yes),
337 }
338 } else if is_assert_arg(cx, e, expn) {
339 args.push(e);
340 ControlFlow::Continue(Descend::No)
341 } else {
342 ControlFlow::Continue(Descend::Yes)
343 }
344 });
345 let args = args.into_inner().ok()?;
346 let panic_expn = panic_expn.unwrap_or(PanicExpn::Empty);
349 Some((args, panic_expn))
350}
351
352fn find_assert_within_debug_assert<'a>(
353 cx: &LateContext<'_>,
354 expr: &'a Expr<'a>,
355 expn: ExpnId,
356 assert_name: Symbol,
357) -> Option<(&'a Expr<'a>, ExpnId)> {
358 for_each_expr_without_closures(expr, |e| {
359 if !e.span.from_expansion() {
360 return ControlFlow::Continue(Descend::No);
361 }
362 let e_expn = e.span.ctxt().outer_expn();
363 if e_expn == expn {
364 ControlFlow::Continue(Descend::Yes)
365 } else if e_expn.expn_data().macro_def_id.map(|id| cx.tcx.item_name(id)) == Some(assert_name) {
366 ControlFlow::Break((e, e_expn))
367 } else {
368 ControlFlow::Continue(Descend::No)
369 }
370 })
371}
372
373fn is_assert_arg(cx: &LateContext<'_>, expr: &Expr<'_>, assert_expn: ExpnId) -> bool {
374 if !expr.span.from_expansion() {
375 return true;
376 }
377 let result = macro_backtrace(expr.span).try_for_each(|macro_call| {
378 if macro_call.expn == assert_expn {
379 ControlFlow::Break(false)
380 } else {
381 match cx.tcx.item_name(macro_call.def_id) {
382 sym::cfg => ControlFlow::Continue(()),
384 _ => ControlFlow::Break(true),
386 }
387 }
388 });
389 match result {
390 ControlFlow::Break(is_assert_arg) => is_assert_arg,
391 ControlFlow::Continue(()) => true,
392 }
393}
394
395#[derive(Default, Clone)]
398pub struct FormatArgsStorage(Arc<OnceLock<FxHashMap<Span, FormatArgs>>>);
399
400impl FormatArgsStorage {
401 pub fn get(&self, cx: &LateContext<'_>, start: &Expr<'_>, expn_id: ExpnId) -> Option<&FormatArgs> {
406 let format_args_expr = for_each_expr_without_closures(start, |expr| {
407 let ctxt = expr.span.ctxt();
408 if ctxt.outer_expn().is_descendant_of(expn_id) {
409 if macro_backtrace(expr.span)
410 .map(|macro_call| cx.tcx.item_name(macro_call.def_id))
411 .any(|name| matches!(name, sym::const_format_args | sym::format_args | sym::format_args_nl))
412 {
413 ControlFlow::Break(expr)
414 } else {
415 ControlFlow::Continue(Descend::Yes)
416 }
417 } else {
418 ControlFlow::Continue(Descend::No)
419 }
420 })?;
421
422 debug_assert!(self.0.get().is_some(), "`FormatArgsStorage` not yet populated");
423
424 self.0.get()?.get(&format_args_expr.span.with_parent(None))
425 }
426
427 pub fn set(&self, format_args: FxHashMap<Span, FormatArgs>) {
429 self.0
430 .set(format_args)
431 .expect("`FormatArgsStorage::set` should only be called once");
432 }
433}
434
435pub fn find_format_arg_expr<'hir>(start: &'hir Expr<'hir>, target: &FormatArgument) -> Option<&'hir Expr<'hir>> {
437 let SpanData {
438 lo,
439 hi,
440 ctxt,
441 parent: _,
442 } = target.expr.span.data();
443
444 for_each_expr_without_closures(start, |expr| {
445 let data = expr.span.data();
448 if data.lo == lo && data.hi == hi && data.ctxt == ctxt {
449 ControlFlow::Break(expr)
450 } else {
451 ControlFlow::Continue(())
452 }
453 })
454}
455
456pub fn format_placeholder_format_span(placeholder: &FormatPlaceholder) -> Option<Span> {
463 let base = placeholder.span?.data();
464
465 Some(Span::new(
468 placeholder.argument.span?.hi(),
469 base.hi - BytePos(1),
470 base.ctxt,
471 base.parent,
472 ))
473}
474
475pub fn format_args_inputs_span(format_args: &FormatArgs) -> Span {
482 match format_args.arguments.explicit_args() {
483 [] => format_args.span,
484 [.., last] => format_args
485 .span
486 .to(hygiene::walk_chain(last.expr.span, format_args.span.ctxt())),
487 }
488}
489
490pub fn format_arg_removal_span(format_args: &FormatArgs, index: usize) -> Option<Span> {
498 let ctxt = format_args.span.ctxt();
499
500 let current = hygiene::walk_chain(format_args.arguments.by_index(index)?.expr.span, ctxt);
501
502 let prev = if index == 0 {
503 format_args.span
504 } else {
505 hygiene::walk_chain(format_args.arguments.by_index(index - 1)?.expr.span, ctxt)
506 };
507
508 Some(current.with_lo(prev.hi()))
509}
510
511#[derive(Debug, Copy, Clone, PartialEq, Eq)]
513pub enum FormatParamUsage {
514 Argument,
516 Width,
518 Precision,
520}
521
522pub trait HirNode {
524 fn hir_id(&self) -> HirId;
525 fn span(&self) -> Span;
526}
527
528macro_rules! impl_hir_node {
529 ($($t:ident),*) => {
530 $(impl HirNode for hir::$t<'_> {
531 fn hir_id(&self) -> HirId {
532 self.hir_id
533 }
534 fn span(&self) -> Span {
535 self.span
536 }
537 })*
538 };
539}
540
541impl_hir_node!(Expr, Pat);
542
543impl HirNode for hir::Item<'_> {
544 fn hir_id(&self) -> HirId {
545 self.hir_id()
546 }
547
548 fn span(&self) -> Span {
549 self.span
550 }
551}