1#![allow(clippy::similar_names)] use std::sync::{Arc, OnceLock};
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_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, sym};
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_attr(cx.sess(), cx.tcx.get_attrs_unchecked(macro_def_id), "format_args").is_some()
46 }
47}
48
49#[derive(Debug)]
56pub struct MacroCall {
57 pub def_id: DefId,
59 pub kind: MacroKind,
61 pub expn: ExpnId,
63 pub span: Span,
65}
66
67impl MacroCall {
68 pub fn is_local(&self) -> bool {
69 span_is_local(self.span)
70 }
71}
72
73pub fn expn_backtrace(mut span: Span) -> impl Iterator<Item = (ExpnId, ExpnData)> {
75 std::iter::from_fn(move || {
76 let ctxt = span.ctxt();
77 if ctxt == SyntaxContext::root() {
78 return None;
79 }
80 let expn = ctxt.outer_expn();
81 let data = expn.expn_data();
82 span = data.call_site;
83 Some((expn, data))
84 })
85}
86
87pub fn span_is_local(span: Span) -> bool {
89 !span.from_expansion() || expn_is_local(span.ctxt().outer_expn())
90}
91
92pub fn expn_is_local(expn: ExpnId) -> bool {
94 if expn == ExpnId::root() {
95 return true;
96 }
97 let data = expn.expn_data();
98 let backtrace = expn_backtrace(data.call_site);
99 std::iter::once((expn, data))
100 .chain(backtrace)
101 .find_map(|(_, data)| data.macro_def_id)
102 .is_none_or(DefId::is_local)
103}
104
105pub fn macro_backtrace(span: Span) -> impl Iterator<Item = MacroCall> {
108 expn_backtrace(span).filter_map(|(expn, data)| match data {
109 ExpnData {
110 kind: ExpnKind::Macro(kind, _),
111 macro_def_id: Some(def_id),
112 call_site: span,
113 ..
114 } => Some(MacroCall {
115 def_id,
116 kind,
117 expn,
118 span,
119 }),
120 _ => None,
121 })
122}
123
124pub fn root_macro_call(span: Span) -> Option<MacroCall> {
130 macro_backtrace(span).last()
131}
132
133pub fn matching_root_macro_call(cx: &LateContext<'_>, span: Span, name: Symbol) -> Option<MacroCall> {
137 root_macro_call(span).filter(|mc| cx.tcx.is_diagnostic_item(name, mc.def_id))
138}
139
140pub fn root_macro_call_first_node(cx: &LateContext<'_>, node: &impl HirNode) -> Option<MacroCall> {
143 if first_node_in_macro(cx, node) != Some(ExpnId::root()) {
144 return None;
145 }
146 root_macro_call(node.span())
147}
148
149pub fn first_node_macro_backtrace(cx: &LateContext<'_>, node: &impl HirNode) -> impl Iterator<Item = MacroCall> {
152 let span = node.span();
153 first_node_in_macro(cx, node)
154 .into_iter()
155 .flat_map(move |expn| macro_backtrace(span).take_while(move |macro_call| macro_call.expn != expn))
156}
157
158pub fn first_node_in_macro(cx: &LateContext<'_>, node: &impl HirNode) -> Option<ExpnId> {
175 let expn = macro_backtrace(node.span()).next()?.expn;
178
179 let hir = cx.tcx.hir();
182 let mut parent_iter = cx.tcx.hir_parent_iter(node.hir_id());
183 let (parent_id, _) = match parent_iter.next() {
184 None => return Some(ExpnId::root()),
185 Some((_, Node::Stmt(_))) => match parent_iter.next() {
186 None => return Some(ExpnId::root()),
187 Some(next) => next,
188 },
189 Some(next) => next,
190 };
191
192 let parent_span = hir.span(parent_id);
194 let Some(parent_macro_call) = macro_backtrace(parent_span).next() else {
195 return Some(ExpnId::root());
197 };
198
199 if parent_macro_call.expn.is_descendant_of(expn) {
200 return None;
202 }
203
204 Some(parent_macro_call.expn)
205}
206
207pub fn is_panic(cx: &LateContext<'_>, def_id: DefId) -> bool {
211 let Some(name) = cx.tcx.get_diagnostic_name(def_id) else {
212 return false;
213 };
214 matches!(
215 name,
216 sym::core_panic_macro
217 | sym::std_panic_macro
218 | sym::core_panic_2015_macro
219 | sym::std_panic_2015_macro
220 | sym::core_panic_2021_macro
221 )
222}
223
224pub fn is_assert_macro(cx: &LateContext<'_>, def_id: DefId) -> bool {
226 let Some(name) = cx.tcx.get_diagnostic_name(def_id) else {
227 return false;
228 };
229 matches!(name, sym::assert_macro | sym::debug_assert_macro)
230}
231
232#[derive(Debug)]
233pub enum PanicExpn<'a> {
234 Empty,
236 Str(&'a Expr<'a>),
238 Display(&'a Expr<'a>),
240 Format(&'a Expr<'a>),
242}
243
244impl<'a> PanicExpn<'a> {
245 pub fn parse(expr: &'a Expr<'a>) -> Option<Self> {
246 let ExprKind::Call(callee, args) = &expr.kind else {
247 return None;
248 };
249 let ExprKind::Path(QPath::Resolved(_, path)) = &callee.kind else {
250 return None;
251 };
252 let name = path.segments.last().unwrap().ident.as_str();
253
254 if name == "panic_cold_explicit" {
256 return Some(Self::Empty);
257 }
258
259 let [arg, rest @ ..] = args else {
260 return None;
261 };
262 let result = match name {
263 "panic" if arg.span.eq_ctxt(expr.span) => Self::Empty,
264 "panic" | "panic_str" => Self::Str(arg),
265 "panic_display" | "panic_cold_display" => {
266 let ExprKind::AddrOf(_, _, e) = &arg.kind else {
267 return None;
268 };
269 Self::Display(e)
270 },
271 "panic_fmt" => Self::Format(arg),
272 "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}