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)]
238pub enum PanicExpn<'a> {
239 Empty,
241 Str(&'a Expr<'a>),
243 Display(&'a Expr<'a>),
245 Format(&'a Expr<'a>),
247}
248
249impl<'a> PanicExpn<'a> {
250 pub fn parse(expr: &'a Expr<'a>) -> Option<Self> {
251 let ExprKind::Call(callee, args) = &expr.kind else {
252 return None;
253 };
254 let ExprKind::Path(QPath::Resolved(_, path)) = &callee.kind else {
255 return None;
256 };
257 let name = path.segments.last().unwrap().ident.name;
258
259 let [arg, rest @ ..] = args else {
260 return None;
261 };
262 let result = match name {
263 sym::panic if arg.span.eq_ctxt(expr.span) => Self::Empty,
264 sym::panic | sym::panic_str => Self::Str(arg),
265 sym::panic_display => {
266 let ExprKind::AddrOf(_, _, e) = &arg.kind else {
267 return None;
268 };
269 Self::Display(e)
270 },
271 sym::panic_fmt => Self::Format(arg),
272 sym::assert_failed => {
275 if rest.len() != 3 {
278 return None;
279 }
280 let msg_arg = &rest[2];
282 match msg_arg.kind {
283 ExprKind::Call(_, [fmt_arg]) => Self::Format(fmt_arg),
284 _ => Self::Empty,
285 }
286 },
287 _ => return None,
288 };
289 Some(result)
290 }
291}
292
293pub fn find_assert_args<'a>(
295 cx: &LateContext<'_>,
296 expr: &'a Expr<'a>,
297 expn: ExpnId,
298) -> Option<(&'a Expr<'a>, PanicExpn<'a>)> {
299 find_assert_args_inner(cx, expr, expn).map(|([e], mut p)| {
300 if let PanicExpn::Str(_) = p {
305 p = PanicExpn::Empty;
306 }
307
308 (e, p)
309 })
310}
311
312pub fn find_assert_eq_args<'a>(
315 cx: &LateContext<'_>,
316 expr: &'a Expr<'a>,
317 expn: ExpnId,
318) -> Option<(&'a Expr<'a>, &'a Expr<'a>, PanicExpn<'a>)> {
319 find_assert_args_inner(cx, expr, expn).map(|([a, b], p)| (a, b, p))
320}
321
322fn find_assert_args_inner<'a, const N: usize>(
323 cx: &LateContext<'_>,
324 expr: &'a Expr<'a>,
325 expn: ExpnId,
326) -> Option<([&'a Expr<'a>; N], PanicExpn<'a>)> {
327 let macro_id = expn.expn_data().macro_def_id?;
328 let (expr, expn) = match cx.tcx.item_name(macro_id).as_str().strip_prefix("debug_") {
329 None => (expr, expn),
330 Some(inner_name) => find_assert_within_debug_assert(cx, expr, expn, Symbol::intern(inner_name))?,
331 };
332 let mut args = ArrayVec::new();
333 let panic_expn = for_each_expr_without_closures(expr, |e| {
334 if args.is_full() {
335 match PanicExpn::parse(e) {
336 Some(expn) => ControlFlow::Break(expn),
337 None => ControlFlow::Continue(Descend::Yes),
338 }
339 } else if is_assert_arg(cx, e, expn) {
340 args.push(e);
341 ControlFlow::Continue(Descend::No)
342 } else {
343 ControlFlow::Continue(Descend::Yes)
344 }
345 });
346 let args = args.into_inner().ok()?;
347 let panic_expn = panic_expn.unwrap_or(PanicExpn::Empty);
350 Some((args, panic_expn))
351}
352
353fn find_assert_within_debug_assert<'a>(
354 cx: &LateContext<'_>,
355 expr: &'a Expr<'a>,
356 expn: ExpnId,
357 assert_name: Symbol,
358) -> Option<(&'a Expr<'a>, ExpnId)> {
359 for_each_expr_without_closures(expr, |e| {
360 if !e.span.from_expansion() {
361 return ControlFlow::Continue(Descend::No);
362 }
363 let e_expn = e.span.ctxt().outer_expn();
364 if e_expn == expn {
365 ControlFlow::Continue(Descend::Yes)
366 } else if e_expn.expn_data().macro_def_id.map(|id| cx.tcx.item_name(id)) == Some(assert_name) {
367 ControlFlow::Break((e, e_expn))
368 } else {
369 ControlFlow::Continue(Descend::No)
370 }
371 })
372}
373
374fn is_assert_arg(cx: &LateContext<'_>, expr: &Expr<'_>, assert_expn: ExpnId) -> bool {
375 if !expr.span.from_expansion() {
376 return true;
377 }
378 let result = macro_backtrace(expr.span).try_for_each(|macro_call| {
379 if macro_call.expn == assert_expn {
380 ControlFlow::Break(false)
381 } else {
382 match cx.tcx.item_name(macro_call.def_id) {
383 sym::cfg => ControlFlow::Continue(()),
385 _ => ControlFlow::Break(true),
387 }
388 }
389 });
390 match result {
391 ControlFlow::Break(is_assert_arg) => is_assert_arg,
392 ControlFlow::Continue(()) => true,
393 }
394}
395
396#[derive(Default, Clone)]
399pub struct FormatArgsStorage(Arc<OnceLock<FxHashMap<Span, FormatArgs>>>);
400
401impl FormatArgsStorage {
402 pub fn get(&self, cx: &LateContext<'_>, start: &Expr<'_>, expn_id: ExpnId) -> Option<&FormatArgs> {
407 let format_args_expr = for_each_expr_without_closures(start, |expr| {
408 let ctxt = expr.span.ctxt();
409 if ctxt.outer_expn().is_descendant_of(expn_id) {
410 if macro_backtrace(expr.span)
411 .map(|macro_call| cx.tcx.item_name(macro_call.def_id))
412 .any(|name| matches!(name, sym::const_format_args | sym::format_args | sym::format_args_nl))
413 {
414 ControlFlow::Break(expr)
415 } else {
416 ControlFlow::Continue(Descend::Yes)
417 }
418 } else {
419 ControlFlow::Continue(Descend::No)
420 }
421 })?;
422
423 debug_assert!(self.0.get().is_some(), "`FormatArgsStorage` not yet populated");
424
425 self.0.get()?.get(&format_args_expr.span.with_parent(None))
426 }
427
428 pub fn set(&self, format_args: FxHashMap<Span, FormatArgs>) {
430 self.0
431 .set(format_args)
432 .expect("`FormatArgsStorage::set` should only be called once");
433 }
434}
435
436pub fn find_format_arg_expr<'hir>(start: &'hir Expr<'hir>, target: &FormatArgument) -> Option<&'hir Expr<'hir>> {
438 let SpanData {
439 lo,
440 hi,
441 ctxt,
442 parent: _,
443 } = target.expr.span.data();
444
445 for_each_expr_without_closures(start, |expr| {
446 let data = expr.span.data();
449 if data.lo == lo && data.hi == hi && data.ctxt == ctxt {
450 ControlFlow::Break(expr)
451 } else {
452 ControlFlow::Continue(())
453 }
454 })
455}
456
457pub fn format_placeholder_format_span(placeholder: &FormatPlaceholder) -> Option<Span> {
464 let base = placeholder.span?.data();
465
466 Some(Span::new(
469 placeholder.argument.span?.hi(),
470 base.hi - BytePos(1),
471 base.ctxt,
472 base.parent,
473 ))
474}
475
476pub fn format_args_inputs_span(format_args: &FormatArgs) -> Span {
483 match format_args.arguments.explicit_args() {
484 [] => format_args.span,
485 [.., last] => format_args
486 .span
487 .to(hygiene::walk_chain(last.expr.span, format_args.span.ctxt())),
488 }
489}
490
491pub fn format_arg_removal_span(format_args: &FormatArgs, index: usize) -> Option<Span> {
499 let ctxt = format_args.span.ctxt();
500
501 let current = hygiene::walk_chain(format_args.arguments.by_index(index)?.expr.span, ctxt);
502
503 let prev = if index == 0 {
504 format_args.span
505 } else {
506 hygiene::walk_chain(format_args.arguments.by_index(index - 1)?.expr.span, ctxt)
507 };
508
509 Some(current.with_lo(prev.hi()))
510}
511
512#[derive(Debug, Copy, Clone, PartialEq, Eq)]
514pub enum FormatParamUsage {
515 Argument,
517 Width,
519 Precision,
521}
522
523pub trait HirNode {
525 fn hir_id(&self) -> HirId;
526 fn span(&self) -> Span;
527}
528
529macro_rules! impl_hir_node {
530 ($($t:ident),*) => {
531 $(impl HirNode for hir::$t<'_> {
532 fn hir_id(&self) -> HirId {
533 self.hir_id
534 }
535 fn span(&self) -> Span {
536 self.span
537 }
538 })*
539 };
540}
541
542impl_hir_node!(Expr, Pat);
543
544impl HirNode for hir::Item<'_> {
545 fn hir_id(&self) -> HirId {
546 self.hir_id()
547 }
548
549 fn span(&self) -> Span {
550 self.span
551 }
552}