1#![allow(clippy::similar_names)] use 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
39pub 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 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#[derive(Debug)]
63pub struct MacroCall {
64 pub def_id: DefId,
66 pub kind: MacroKind,
68 pub expn: ExpnId,
70 pub span: Span,
72}
73
74impl MacroCall {
75 pub fn is_local(&self) -> bool {
76 span_is_local(self.span)
77 }
78}
79
80pub 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
94pub fn span_is_local(span: Span) -> bool {
96 !span.from_expansion() || expn_is_local(span.ctxt().outer_expn())
97}
98
99pub 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
112pub 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
131pub fn root_macro_call(span: Span) -> Option<MacroCall> {
137 macro_backtrace(span).last()
138}
139
140pub 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
147pub 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
156pub 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
165pub fn first_node_in_macro(cx: &LateContext<'_>, node: &impl HirNode) -> Option<ExpnId> {
182 let expn = macro_backtrace(node.span()).next()?.expn;
185
186 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 let parent_span = cx.tcx.hir_span(parent_id);
200 let Some(parent_macro_call) = macro_backtrace(parent_span).next() else {
201 return Some(ExpnId::root());
203 };
204
205 if parent_macro_call.expn.is_descendant_of(expn) {
206 return None;
208 }
209
210 Some(parent_macro_call.expn)
211}
212
213pub 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
230pub 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#[derive(Debug)]
242pub enum PanicCall<'a> {
243 DefaultMessage,
245 Str2015(&'a Expr<'a>),
251 Display(&'a Expr<'a>),
255 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 sym::assert_failed => {
292 if rest.len() != 3 {
295 return None;
296 }
297 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
314pub 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
323pub 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 sym::cfg => ControlFlow::Continue(()),
393 _ => 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#[derive(Default, Clone)]
407pub struct FormatArgsStorage(Arc<OnceLock<FxHashMap<Span, FormatArgs>>>);
408
409impl FormatArgsStorage {
410 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 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
444pub 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 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
465pub fn format_placeholder_format_span(placeholder: &FormatPlaceholder) -> Option<Span> {
472 let base = placeholder.span?.data();
473
474 Some(Span::new(
477 placeholder.argument.span?.hi(),
478 base.hi - BytePos(1),
479 base.ctxt,
480 base.parent,
481 ))
482}
483
484pub 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
499pub 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#[derive(Debug, Copy, Clone, PartialEq, Eq)]
522pub enum FormatParamUsage {
523 Argument,
525 Width,
527 Precision,
529}
530
531pub 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}