1#![allow(clippy::similar_names)] use std::sync::{Arc, OnceLock};
4
5use crate::visitors::{Descend, for_each_expr_without_closures};
6use crate::{get_unique_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_attr(cx.sess(), cx.tcx.get_all_attrs(macro_def_id), sym::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 mut parent_iter = cx.tcx.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 = cx.tcx.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.name;
252
253 let [arg, rest @ ..] = args else {
254 return None;
255 };
256 let result = match name {
257 sym::panic if arg.span.eq_ctxt(expr.span) => Self::Empty,
258 sym::panic | sym::panic_str => Self::Str(arg),
259 sym::panic_display => {
260 let ExprKind::AddrOf(_, _, e) = &arg.kind else {
261 return None;
262 };
263 Self::Display(e)
264 },
265 sym::panic_fmt => Self::Format(arg),
266 sym::assert_failed => {
269 if rest.len() != 3 {
272 return None;
273 }
274 let msg_arg = &rest[2];
276 match msg_arg.kind {
277 ExprKind::Call(_, [fmt_arg]) => Self::Format(fmt_arg),
278 _ => Self::Empty,
279 }
280 },
281 _ => return None,
282 };
283 Some(result)
284 }
285}
286
287pub fn find_assert_args<'a>(
289 cx: &LateContext<'_>,
290 expr: &'a Expr<'a>,
291 expn: ExpnId,
292) -> Option<(&'a Expr<'a>, PanicExpn<'a>)> {
293 find_assert_args_inner(cx, expr, expn).map(|([e], mut p)| {
294 if let PanicExpn::Str(_) = p {
299 p = PanicExpn::Empty;
300 }
301
302 (e, p)
303 })
304}
305
306pub fn find_assert_eq_args<'a>(
309 cx: &LateContext<'_>,
310 expr: &'a Expr<'a>,
311 expn: ExpnId,
312) -> Option<(&'a Expr<'a>, &'a Expr<'a>, PanicExpn<'a>)> {
313 find_assert_args_inner(cx, expr, expn).map(|([a, b], p)| (a, b, p))
314}
315
316fn find_assert_args_inner<'a, const N: usize>(
317 cx: &LateContext<'_>,
318 expr: &'a Expr<'a>,
319 expn: ExpnId,
320) -> Option<([&'a Expr<'a>; N], PanicExpn<'a>)> {
321 let macro_id = expn.expn_data().macro_def_id?;
322 let (expr, expn) = match cx.tcx.item_name(macro_id).as_str().strip_prefix("debug_") {
323 None => (expr, expn),
324 Some(inner_name) => find_assert_within_debug_assert(cx, expr, expn, Symbol::intern(inner_name))?,
325 };
326 let mut args = ArrayVec::new();
327 let panic_expn = for_each_expr_without_closures(expr, |e| {
328 if args.is_full() {
329 match PanicExpn::parse(e) {
330 Some(expn) => ControlFlow::Break(expn),
331 None => ControlFlow::Continue(Descend::Yes),
332 }
333 } else if is_assert_arg(cx, e, expn) {
334 args.push(e);
335 ControlFlow::Continue(Descend::No)
336 } else {
337 ControlFlow::Continue(Descend::Yes)
338 }
339 });
340 let args = args.into_inner().ok()?;
341 let panic_expn = panic_expn.unwrap_or(PanicExpn::Empty);
344 Some((args, panic_expn))
345}
346
347fn find_assert_within_debug_assert<'a>(
348 cx: &LateContext<'_>,
349 expr: &'a Expr<'a>,
350 expn: ExpnId,
351 assert_name: Symbol,
352) -> Option<(&'a Expr<'a>, ExpnId)> {
353 for_each_expr_without_closures(expr, |e| {
354 if !e.span.from_expansion() {
355 return ControlFlow::Continue(Descend::No);
356 }
357 let e_expn = e.span.ctxt().outer_expn();
358 if e_expn == expn {
359 ControlFlow::Continue(Descend::Yes)
360 } else if e_expn.expn_data().macro_def_id.map(|id| cx.tcx.item_name(id)) == Some(assert_name) {
361 ControlFlow::Break((e, e_expn))
362 } else {
363 ControlFlow::Continue(Descend::No)
364 }
365 })
366}
367
368fn is_assert_arg(cx: &LateContext<'_>, expr: &Expr<'_>, assert_expn: ExpnId) -> bool {
369 if !expr.span.from_expansion() {
370 return true;
371 }
372 let result = macro_backtrace(expr.span).try_for_each(|macro_call| {
373 if macro_call.expn == assert_expn {
374 ControlFlow::Break(false)
375 } else {
376 match cx.tcx.item_name(macro_call.def_id) {
377 sym::cfg => ControlFlow::Continue(()),
379 _ => ControlFlow::Break(true),
381 }
382 }
383 });
384 match result {
385 ControlFlow::Break(is_assert_arg) => is_assert_arg,
386 ControlFlow::Continue(()) => true,
387 }
388}
389
390#[derive(Default, Clone)]
393pub struct FormatArgsStorage(Arc<OnceLock<FxHashMap<Span, FormatArgs>>>);
394
395impl FormatArgsStorage {
396 pub fn get(&self, cx: &LateContext<'_>, start: &Expr<'_>, expn_id: ExpnId) -> Option<&FormatArgs> {
401 let format_args_expr = for_each_expr_without_closures(start, |expr| {
402 let ctxt = expr.span.ctxt();
403 if ctxt.outer_expn().is_descendant_of(expn_id) {
404 if macro_backtrace(expr.span)
405 .map(|macro_call| cx.tcx.item_name(macro_call.def_id))
406 .any(|name| matches!(name, sym::const_format_args | sym::format_args | sym::format_args_nl))
407 {
408 ControlFlow::Break(expr)
409 } else {
410 ControlFlow::Continue(Descend::Yes)
411 }
412 } else {
413 ControlFlow::Continue(Descend::No)
414 }
415 })?;
416
417 debug_assert!(self.0.get().is_some(), "`FormatArgsStorage` not yet populated");
418
419 self.0.get()?.get(&format_args_expr.span.with_parent(None))
420 }
421
422 pub fn set(&self, format_args: FxHashMap<Span, FormatArgs>) {
424 self.0
425 .set(format_args)
426 .expect("`FormatArgsStorage::set` should only be called once");
427 }
428}
429
430pub fn find_format_arg_expr<'hir>(start: &'hir Expr<'hir>, target: &FormatArgument) -> Option<&'hir Expr<'hir>> {
432 let SpanData {
433 lo,
434 hi,
435 ctxt,
436 parent: _,
437 } = target.expr.span.data();
438
439 for_each_expr_without_closures(start, |expr| {
440 let data = expr.span.data();
443 if data.lo == lo && data.hi == hi && data.ctxt == ctxt {
444 ControlFlow::Break(expr)
445 } else {
446 ControlFlow::Continue(())
447 }
448 })
449}
450
451pub fn format_placeholder_format_span(placeholder: &FormatPlaceholder) -> Option<Span> {
458 let base = placeholder.span?.data();
459
460 Some(Span::new(
463 placeholder.argument.span?.hi(),
464 base.hi - BytePos(1),
465 base.ctxt,
466 base.parent,
467 ))
468}
469
470pub fn format_args_inputs_span(format_args: &FormatArgs) -> Span {
477 match format_args.arguments.explicit_args() {
478 [] => format_args.span,
479 [.., last] => format_args
480 .span
481 .to(hygiene::walk_chain(last.expr.span, format_args.span.ctxt())),
482 }
483}
484
485pub fn format_arg_removal_span(format_args: &FormatArgs, index: usize) -> Option<Span> {
493 let ctxt = format_args.span.ctxt();
494
495 let current = hygiene::walk_chain(format_args.arguments.by_index(index)?.expr.span, ctxt);
496
497 let prev = if index == 0 {
498 format_args.span
499 } else {
500 hygiene::walk_chain(format_args.arguments.by_index(index - 1)?.expr.span, ctxt)
501 };
502
503 Some(current.with_lo(prev.hi()))
504}
505
506#[derive(Debug, Copy, Clone, PartialEq, Eq)]
508pub enum FormatParamUsage {
509 Argument,
511 Width,
513 Precision,
515}
516
517pub trait HirNode {
519 fn hir_id(&self) -> HirId;
520 fn span(&self) -> Span;
521}
522
523macro_rules! impl_hir_node {
524 ($($t:ident),*) => {
525 $(impl HirNode for hir::$t<'_> {
526 fn hir_id(&self) -> HirId {
527 self.hir_id
528 }
529 fn span(&self) -> Span {
530 self.span
531 }
532 })*
533 };
534}
535
536impl_hir_node!(Expr, Pat);
537
538impl HirNode for hir::Item<'_> {
539 fn hir_id(&self) -> HirId {
540 self.hir_id()
541 }
542
543 fn span(&self) -> Span {
544 self.span
545 }
546}