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::debug_assert_eq_macro,
23 sym::debug_assert_macro,
24 sym::debug_assert_ne_macro,
25 sym::eprint_macro,
26 sym::eprintln_macro,
27 sym::format_args_macro,
28 sym::format_macro,
29 sym::print_macro,
30 sym::println_macro,
31 sym::std_panic_macro,
32 sym::todo_macro,
33 sym::unimplemented_macro,
34 sym::write_macro,
35 sym::writeln_macro,
36];
37
38pub fn is_format_macro(cx: &LateContext<'_>, macro_def_id: DefId) -> bool {
40 if let Some(name) = cx.tcx.get_diagnostic_name(macro_def_id) {
41 FORMAT_MACRO_DIAG_ITEMS.contains(&name)
42 } else {
43 get_unique_builtin_attr(
46 cx.sess(),
47 #[allow(deprecated)]
48 cx.tcx.get_all_attrs(macro_def_id),
49 sym::format_args,
50 )
51 .is_some()
52 }
53}
54
55#[derive(Debug)]
62pub struct MacroCall {
63 pub def_id: DefId,
65 pub kind: MacroKind,
67 pub expn: ExpnId,
69 pub span: Span,
71}
72
73impl MacroCall {
74 pub fn is_local(&self) -> bool {
75 span_is_local(self.span)
76 }
77}
78
79pub fn expn_backtrace(mut span: Span) -> impl Iterator<Item = (ExpnId, ExpnData)> {
81 std::iter::from_fn(move || {
82 let ctxt = span.ctxt();
83 if ctxt == SyntaxContext::root() {
84 return None;
85 }
86 let expn = ctxt.outer_expn();
87 let data = expn.expn_data();
88 span = data.call_site;
89 Some((expn, data))
90 })
91}
92
93pub fn span_is_local(span: Span) -> bool {
95 !span.from_expansion() || expn_is_local(span.ctxt().outer_expn())
96}
97
98pub fn expn_is_local(expn: ExpnId) -> bool {
100 if expn == ExpnId::root() {
101 return true;
102 }
103 let data = expn.expn_data();
104 let backtrace = expn_backtrace(data.call_site);
105 std::iter::once((expn, data))
106 .chain(backtrace)
107 .find_map(|(_, data)| data.macro_def_id)
108 .is_none_or(DefId::is_local)
109}
110
111pub fn macro_backtrace(span: Span) -> impl Iterator<Item = MacroCall> {
114 expn_backtrace(span).filter_map(|(expn, data)| match data {
115 ExpnData {
116 kind: ExpnKind::Macro(kind, _),
117 macro_def_id: Some(def_id),
118 call_site: span,
119 ..
120 } => Some(MacroCall {
121 def_id,
122 kind,
123 expn,
124 span,
125 }),
126 _ => None,
127 })
128}
129
130pub fn root_macro_call(span: Span) -> Option<MacroCall> {
136 macro_backtrace(span).last()
137}
138
139pub fn matching_root_macro_call(cx: &LateContext<'_>, span: Span, name: Symbol) -> Option<MacroCall> {
143 root_macro_call(span).filter(|mc| cx.tcx.is_diagnostic_item(name, mc.def_id))
144}
145
146pub fn root_macro_call_first_node(cx: &LateContext<'_>, node: &impl HirNode) -> Option<MacroCall> {
149 if first_node_in_macro(cx, node) != Some(ExpnId::root()) {
150 return None;
151 }
152 root_macro_call(node.span())
153}
154
155pub fn first_node_macro_backtrace(cx: &LateContext<'_>, node: &impl HirNode) -> impl Iterator<Item = MacroCall> {
158 let span = node.span();
159 first_node_in_macro(cx, node)
160 .into_iter()
161 .flat_map(move |expn| macro_backtrace(span).take_while(move |macro_call| macro_call.expn != expn))
162}
163
164pub fn first_node_in_macro(cx: &LateContext<'_>, node: &impl HirNode) -> Option<ExpnId> {
181 let expn = macro_backtrace(node.span()).next()?.expn;
184
185 let mut parent_iter = cx.tcx.hir_parent_iter(node.hir_id());
188 let (parent_id, _) = match parent_iter.next() {
189 None => return Some(ExpnId::root()),
190 Some((_, Node::Stmt(_))) => match parent_iter.next() {
191 None => return Some(ExpnId::root()),
192 Some(next) => next,
193 },
194 Some(next) => next,
195 };
196
197 let parent_span = cx.tcx.hir_span(parent_id);
199 let Some(parent_macro_call) = macro_backtrace(parent_span).next() else {
200 return Some(ExpnId::root());
202 };
203
204 if parent_macro_call.expn.is_descendant_of(expn) {
205 return None;
207 }
208
209 Some(parent_macro_call.expn)
210}
211
212pub fn is_panic(cx: &LateContext<'_>, def_id: DefId) -> bool {
216 let Some(name) = cx.tcx.get_diagnostic_name(def_id) else {
217 return false;
218 };
219 matches!(
220 name,
221 sym::core_panic_macro
222 | sym::std_panic_macro
223 | sym::core_panic_2015_macro
224 | sym::std_panic_2015_macro
225 | sym::core_panic_2021_macro
226 )
227}
228
229pub fn is_assert_macro(cx: &LateContext<'_>, def_id: DefId) -> bool {
231 let Some(name) = cx.tcx.get_diagnostic_name(def_id) else {
232 return false;
233 };
234 matches!(name, sym::assert_macro | sym::debug_assert_macro)
235}
236
237#[derive(Debug)]
241pub enum PanicCall<'a> {
242 DefaultMessage,
244 Str2015(&'a Expr<'a>),
250 Display(&'a Expr<'a>),
254 Format(&'a Expr<'a>),
258}
259
260impl<'a> PanicCall<'a> {
261 pub fn parse(expr: &'a Expr<'a>) -> Option<Self> {
262 let ExprKind::Call(callee, args) = &expr.kind else {
263 return None;
264 };
265 let ExprKind::Path(QPath::Resolved(_, path)) = &callee.kind else {
266 return None;
267 };
268 let name = path.segments.last().unwrap().ident.name;
269
270 let [arg, rest @ ..] = args else {
271 return None;
272 };
273 let result = match name {
274 sym::panic | sym::begin_panic | sym::panic_str_2015 => {
275 if arg.span.eq_ctxt(expr.span) || arg.span.is_dummy() {
276 Self::DefaultMessage
277 } else {
278 Self::Str2015(arg)
279 }
280 },
281 sym::panic_display => {
282 let ExprKind::AddrOf(_, _, e) = &arg.kind else {
283 return None;
284 };
285 Self::Display(e)
286 },
287 sym::panic_fmt => Self::Format(arg),
288 sym::assert_failed => {
291 if rest.len() != 3 {
294 return None;
295 }
296 let msg_arg = &rest[2];
298 match msg_arg.kind {
299 ExprKind::Call(_, [fmt_arg]) => Self::Format(fmt_arg),
300 _ => Self::DefaultMessage,
301 }
302 },
303 _ => return None,
304 };
305 Some(result)
306 }
307
308 pub fn is_default_message(&self) -> bool {
309 matches!(self, Self::DefaultMessage)
310 }
311}
312
313pub fn find_assert_args<'a>(
315 cx: &LateContext<'_>,
316 expr: &'a Expr<'a>,
317 expn: ExpnId,
318) -> Option<(&'a Expr<'a>, PanicCall<'a>)> {
319 find_assert_args_inner(cx, expr, expn).map(|([e], p)| (e, p))
320}
321
322pub fn find_assert_eq_args<'a>(
325 cx: &LateContext<'_>,
326 expr: &'a Expr<'a>,
327 expn: ExpnId,
328) -> Option<(&'a Expr<'a>, &'a Expr<'a>, PanicCall<'a>)> {
329 find_assert_args_inner(cx, expr, expn).map(|([a, b], p)| (a, b, p))
330}
331
332fn find_assert_args_inner<'a, const N: usize>(
333 cx: &LateContext<'_>,
334 expr: &'a Expr<'a>,
335 expn: ExpnId,
336) -> Option<([&'a Expr<'a>; N], PanicCall<'a>)> {
337 let macro_id = expn.expn_data().macro_def_id?;
338 let (expr, expn) = match cx.tcx.item_name(macro_id).as_str().strip_prefix("debug_") {
339 None => (expr, expn),
340 Some(inner_name) => find_assert_within_debug_assert(cx, expr, expn, Symbol::intern(inner_name))?,
341 };
342 let mut args = ArrayVec::new();
343 let panic_expn = for_each_expr_without_closures(expr, |e| {
344 if args.is_full() {
345 match PanicCall::parse(e) {
346 Some(expn) => ControlFlow::Break(expn),
347 None => ControlFlow::Continue(Descend::Yes),
348 }
349 } else if is_assert_arg(cx, e, expn) {
350 args.push(e);
351 ControlFlow::Continue(Descend::No)
352 } else {
353 ControlFlow::Continue(Descend::Yes)
354 }
355 });
356 let args = args.into_inner().ok()?;
357 Some((args, panic_expn?))
358}
359
360fn find_assert_within_debug_assert<'a>(
361 cx: &LateContext<'_>,
362 expr: &'a Expr<'a>,
363 expn: ExpnId,
364 assert_name: Symbol,
365) -> Option<(&'a Expr<'a>, ExpnId)> {
366 for_each_expr_without_closures(expr, |e| {
367 if !e.span.from_expansion() {
368 return ControlFlow::Continue(Descend::No);
369 }
370 let e_expn = e.span.ctxt().outer_expn();
371 if e_expn == expn {
372 ControlFlow::Continue(Descend::Yes)
373 } else if e_expn.expn_data().macro_def_id.map(|id| cx.tcx.item_name(id)) == Some(assert_name) {
374 ControlFlow::Break((e, e_expn))
375 } else {
376 ControlFlow::Continue(Descend::No)
377 }
378 })
379}
380
381fn is_assert_arg(cx: &LateContext<'_>, expr: &Expr<'_>, assert_expn: ExpnId) -> bool {
382 if !expr.span.from_expansion() {
383 return true;
384 }
385 let result = macro_backtrace(expr.span).try_for_each(|macro_call| {
386 if macro_call.expn == assert_expn {
387 ControlFlow::Break(false)
388 } else {
389 match cx.tcx.item_name(macro_call.def_id) {
390 sym::cfg => ControlFlow::Continue(()),
392 _ => ControlFlow::Break(true),
394 }
395 }
396 });
397 match result {
398 ControlFlow::Break(is_assert_arg) => is_assert_arg,
399 ControlFlow::Continue(()) => true,
400 }
401}
402
403#[derive(Default, Clone)]
406pub struct FormatArgsStorage(Arc<OnceLock<FxHashMap<Span, FormatArgs>>>);
407
408impl FormatArgsStorage {
409 pub fn get(&self, cx: &LateContext<'_>, start: &Expr<'_>, expn_id: ExpnId) -> Option<&FormatArgs> {
414 let format_args_expr = for_each_expr_without_closures(start, |expr| {
415 let ctxt = expr.span.ctxt();
416 if ctxt.outer_expn().is_descendant_of(expn_id) {
417 if macro_backtrace(expr.span)
418 .map(|macro_call| cx.tcx.item_name(macro_call.def_id))
419 .any(|name| matches!(name, sym::const_format_args | sym::format_args | sym::format_args_nl))
420 {
421 ControlFlow::Break(expr)
422 } else {
423 ControlFlow::Continue(Descend::Yes)
424 }
425 } else {
426 ControlFlow::Continue(Descend::No)
427 }
428 })?;
429
430 debug_assert!(self.0.get().is_some(), "`FormatArgsStorage` not yet populated");
431
432 self.0.get()?.get(&format_args_expr.span.with_parent(None))
433 }
434
435 pub fn set(&self, format_args: FxHashMap<Span, FormatArgs>) {
437 self.0
438 .set(format_args)
439 .expect("`FormatArgsStorage::set` should only be called once");
440 }
441}
442
443pub fn find_format_arg_expr<'hir>(start: &'hir Expr<'hir>, target: &FormatArgument) -> Option<&'hir Expr<'hir>> {
445 let SpanData {
446 lo,
447 hi,
448 ctxt,
449 parent: _,
450 } = target.expr.span.data();
451
452 for_each_expr_without_closures(start, |expr| {
453 let data = expr.span.data();
456 if data.lo == lo && data.hi == hi && data.ctxt == ctxt {
457 ControlFlow::Break(expr)
458 } else {
459 ControlFlow::Continue(())
460 }
461 })
462}
463
464pub fn format_placeholder_format_span(placeholder: &FormatPlaceholder) -> Option<Span> {
471 let base = placeholder.span?.data();
472
473 Some(Span::new(
476 placeholder.argument.span?.hi(),
477 base.hi - BytePos(1),
478 base.ctxt,
479 base.parent,
480 ))
481}
482
483pub fn format_args_inputs_span(format_args: &FormatArgs) -> Span {
490 match format_args.arguments.explicit_args() {
491 [] => format_args.span,
492 [.., last] => format_args
493 .span
494 .to(hygiene::walk_chain(last.expr.span, format_args.span.ctxt())),
495 }
496}
497
498pub fn format_arg_removal_span(format_args: &FormatArgs, index: usize) -> Option<Span> {
506 let ctxt = format_args.span.ctxt();
507
508 let current = hygiene::walk_chain(format_args.arguments.by_index(index)?.expr.span, ctxt);
509
510 let prev = if index == 0 {
511 format_args.span
512 } else {
513 hygiene::walk_chain(format_args.arguments.by_index(index - 1)?.expr.span, ctxt)
514 };
515
516 Some(current.with_lo(prev.hi()))
517}
518
519#[derive(Debug, Copy, Clone, PartialEq, Eq)]
521pub enum FormatParamUsage {
522 Argument,
524 Width,
526 Precision,
528}
529
530pub trait HirNode {
532 fn hir_id(&self) -> HirId;
533 fn span(&self) -> Span;
534}
535
536macro_rules! impl_hir_node {
537 ($($t:ident),*) => {
538 $(impl HirNode for hir::$t<'_> {
539 fn hir_id(&self) -> HirId {
540 self.hir_id
541 }
542 fn span(&self) -> Span {
543 self.span
544 }
545 })*
546 };
547}
548
549impl_hir_node!(Expr, Pat);
550
551impl HirNode for hir::Item<'_> {
552 fn hir_id(&self) -> HirId {
553 self.hir_id()
554 }
555
556 fn span(&self) -> Span {
557 self.span
558 }
559}