]>
Commit | Line | Data |
---|---|---|
5869c6ff XL |
1 | use crate::{LateContext, LateLintPass, LintContext}; |
2 | use rustc_ast as ast; | |
064997fb | 3 | use rustc_errors::{fluent, Applicability}; |
5869c6ff | 4 | use rustc_hir as hir; |
94222f64 XL |
5 | use rustc_infer::infer::TyCtxtInferExt; |
6 | use rustc_middle::lint::in_external_macro; | |
5869c6ff XL |
7 | use rustc_middle::ty; |
8 | use rustc_parse_format::{ParseMode, Parser, Piece}; | |
136023e0 XL |
9 | use rustc_session::lint::FutureIncompatibilityReason; |
10 | use rustc_span::edition::Edition; | |
a2a8927a | 11 | use rustc_span::{hygiene, sym, symbol::kw, InnerSpan, Span, Symbol}; |
94222f64 | 12 | use rustc_trait_selection::infer::InferCtxtExt; |
5869c6ff XL |
13 | |
14 | declare_lint! { | |
136023e0 | 15 | /// The `non_fmt_panics` lint detects `panic!(..)` invocations where the first |
5869c6ff XL |
16 | /// argument is not a formatting string. |
17 | /// | |
18 | /// ### Example | |
19 | /// | |
c295e0f8 | 20 | /// ```rust,no_run,edition2018 |
5869c6ff XL |
21 | /// panic!("{}"); |
22 | /// panic!(123); | |
23 | /// ``` | |
24 | /// | |
25 | /// {{produces}} | |
26 | /// | |
27 | /// ### Explanation | |
28 | /// | |
29 | /// In Rust 2018 and earlier, `panic!(x)` directly uses `x` as the message. | |
30 | /// That means that `panic!("{}")` panics with the message `"{}"` instead | |
31 | /// of using it as a formatting string, and `panic!(123)` will panic with | |
32 | /// an `i32` as message. | |
33 | /// | |
34 | /// Rust 2021 always interprets the first argument as format string. | |
136023e0 | 35 | NON_FMT_PANICS, |
5869c6ff XL |
36 | Warn, |
37 | "detect single-argument panic!() invocations in which the argument is not a format string", | |
136023e0 XL |
38 | @future_incompatible = FutureIncompatibleInfo { |
39 | reason: FutureIncompatibilityReason::EditionSemanticsChange(Edition::Edition2021), | |
40 | explain_reason: false, | |
41 | }; | |
5869c6ff XL |
42 | report_in_external_macro |
43 | } | |
44 | ||
136023e0 | 45 | declare_lint_pass!(NonPanicFmt => [NON_FMT_PANICS]); |
5869c6ff XL |
46 | |
47 | impl<'tcx> LateLintPass<'tcx> for NonPanicFmt { | |
48 | fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'tcx>) { | |
49 | if let hir::ExprKind::Call(f, [arg]) = &expr.kind { | |
50 | if let &ty::FnDef(def_id, _) = cx.typeck_results().expr_ty(f).kind() { | |
a2a8927a XL |
51 | let f_diagnostic_name = cx.tcx.get_diagnostic_name(def_id); |
52 | ||
5869c6ff XL |
53 | if Some(def_id) == cx.tcx.lang_items().begin_panic_fn() |
54 | || Some(def_id) == cx.tcx.lang_items().panic_fn() | |
a2a8927a | 55 | || f_diagnostic_name == Some(sym::panic_str) |
5869c6ff XL |
56 | { |
57 | if let Some(id) = f.span.ctxt().outer_expn_data().macro_def_id { | |
c295e0f8 XL |
58 | if matches!( |
59 | cx.tcx.get_diagnostic_name(id), | |
60 | Some(sym::core_panic_2015_macro | sym::std_panic_2015_macro) | |
61 | ) { | |
5869c6ff XL |
62 | check_panic(cx, f, arg); |
63 | } | |
64 | } | |
a2a8927a XL |
65 | } else if f_diagnostic_name == Some(sym::unreachable_display) { |
66 | if let Some(id) = f.span.ctxt().outer_expn_data().macro_def_id { | |
67 | if cx.tcx.is_diagnostic_item(sym::unreachable_2015_macro, id) { | |
68 | check_panic( | |
69 | cx, | |
70 | f, | |
71 | // This is safe because we checked above that the callee is indeed | |
72 | // unreachable_display | |
73 | match &arg.kind { | |
74 | // Get the borrowed arg not the borrow | |
75 | hir::ExprKind::AddrOf(ast::BorrowKind::Ref, _, arg) => arg, | |
76 | _ => bug!("call to unreachable_display without borrow"), | |
77 | }, | |
78 | ); | |
79 | } | |
80 | } | |
5869c6ff XL |
81 | } |
82 | } | |
83 | } | |
84 | } | |
85 | } | |
86 | ||
87 | fn check_panic<'tcx>(cx: &LateContext<'tcx>, f: &'tcx hir::Expr<'tcx>, arg: &'tcx hir::Expr<'tcx>) { | |
88 | if let hir::ExprKind::Lit(lit) = &arg.kind { | |
89 | if let ast::LitKind::Str(sym, _) = lit.node { | |
90 | // The argument is a string literal. | |
a2a8927a | 91 | check_panic_str(cx, f, arg, sym.as_str()); |
5869c6ff XL |
92 | return; |
93 | } | |
94 | } | |
95 | ||
96 | // The argument is *not* a string literal. | |
97 | ||
a2a8927a | 98 | let (span, panic, symbol) = panic_call(cx, f); |
5869c6ff | 99 | |
94222f64 XL |
100 | if in_external_macro(cx.sess(), span) { |
101 | // Nothing that can be done about it in the current crate. | |
102 | return; | |
103 | } | |
104 | ||
a2a8927a XL |
105 | // Find the span of the argument to `panic!()` or `unreachable!`, before expansion in the |
106 | // case of `panic!(some_macro!())` or `unreachable!(some_macro!())`. | |
6a06907d XL |
107 | // We don't use source_callsite(), because this `panic!(..)` might itself |
108 | // be expanded from another macro, in which case we want to stop at that | |
109 | // expansion. | |
110 | let mut arg_span = arg.span; | |
111 | let mut arg_macro = None; | |
112 | while !span.contains(arg_span) { | |
113 | let expn = arg_span.ctxt().outer_expn_data(); | |
114 | if expn.is_root() { | |
115 | break; | |
116 | } | |
117 | arg_macro = expn.macro_def_id; | |
118 | arg_span = expn.call_site; | |
119 | } | |
120 | ||
2b03887a FG |
121 | cx.struct_span_lint(NON_FMT_PANICS, arg_span, fluent::lint_non_fmt_panic, |lint| { |
122 | lint.set_arg("name", symbol); | |
123 | lint.note(fluent::note); | |
124 | lint.note(fluent::more_info_note); | |
94222f64 | 125 | if !is_arg_inside_call(arg_span, span) { |
6a06907d | 126 | // No clue where this argument is coming from. |
2b03887a | 127 | return lint; |
6a06907d XL |
128 | } |
129 | if arg_macro.map_or(false, |id| cx.tcx.is_diagnostic_item(sym::format_macro, id)) { | |
130 | // A case of `panic!(format!(..))`. | |
2b03887a | 131 | lint.note(fluent::supports_fmt_note); |
6a06907d | 132 | if let Some((open, close, _)) = find_delimiters(cx, arg_span) { |
2b03887a FG |
133 | lint.multipart_suggestion( |
134 | fluent::supports_fmt_suggestion, | |
6a06907d XL |
135 | vec![ |
136 | (arg_span.until(open.shrink_to_hi()), "".into()), | |
137 | (close.until(arg_span.shrink_to_hi()), "".into()), | |
138 | ], | |
139 | Applicability::MachineApplicable, | |
140 | ); | |
141 | } | |
142 | } else { | |
94222f64 XL |
143 | let ty = cx.typeck_results().expr_ty(arg); |
144 | // If this is a &str or String, we can confidently give the `"{}", ` suggestion. | |
145 | let is_str = matches!( | |
146 | ty.kind(), | |
147 | ty::Ref(_, r, _) if *r.kind() == ty::Str, | |
148 | ) || matches!( | |
149 | ty.ty_adt_def(), | |
487cf647 | 150 | Some(ty_def) if Some(ty_def.did()) == cx.tcx.lang_items().string(), |
5869c6ff | 151 | ); |
94222f64 | 152 | |
2b03887a FG |
153 | let infcx = cx.tcx.infer_ctxt().build(); |
154 | let suggest_display = is_str | |
487cf647 FG |
155 | || cx |
156 | .tcx | |
157 | .get_diagnostic_item(sym::Display) | |
158 | .map(|t| infcx.type_implements_trait(t, [ty], cx.param_env).may_apply()) | |
159 | == Some(true); | |
2b03887a | 160 | let suggest_debug = !suggest_display |
487cf647 FG |
161 | && cx |
162 | .tcx | |
163 | .get_diagnostic_item(sym::Debug) | |
164 | .map(|t| infcx.type_implements_trait(t, [ty], cx.param_env).may_apply()) | |
165 | == Some(true); | |
94222f64 XL |
166 | |
167 | let suggest_panic_any = !is_str && panic == sym::std_panic_macro; | |
168 | ||
169 | let fmt_applicability = if suggest_panic_any { | |
170 | // If we can use panic_any, use that as the MachineApplicable suggestion. | |
171 | Applicability::MaybeIncorrect | |
172 | } else { | |
173 | // If we don't suggest panic_any, using a format string is our best bet. | |
174 | Applicability::MachineApplicable | |
175 | }; | |
176 | ||
177 | if suggest_display { | |
2b03887a | 178 | lint.span_suggestion_verbose( |
94222f64 | 179 | arg_span.shrink_to_lo(), |
2b03887a | 180 | fluent::display_suggestion, |
04454e1e | 181 | "\"{}\", ", |
94222f64 XL |
182 | fmt_applicability, |
183 | ); | |
184 | } else if suggest_debug { | |
2b03887a FG |
185 | lint.set_arg("ty", ty); |
186 | lint.span_suggestion_verbose( | |
94222f64 | 187 | arg_span.shrink_to_lo(), |
2b03887a | 188 | fluent::debug_suggestion, |
04454e1e | 189 | "\"{:?}\", ", |
94222f64 XL |
190 | fmt_applicability, |
191 | ); | |
192 | } | |
193 | ||
194 | if suggest_panic_any { | |
6a06907d | 195 | if let Some((open, close, del)) = find_delimiters(cx, span) { |
2b03887a FG |
196 | lint.set_arg("already_suggested", suggest_display || suggest_debug); |
197 | lint.multipart_suggestion( | |
198 | fluent::panic_suggestion, | |
6a06907d XL |
199 | if del == '(' { |
200 | vec![(span.until(open), "std::panic::panic_any".into())] | |
201 | } else { | |
202 | vec![ | |
203 | (span.until(open.shrink_to_hi()), "std::panic::panic_any(".into()), | |
204 | (close, ")".into()), | |
205 | ] | |
206 | }, | |
207 | Applicability::MachineApplicable, | |
208 | ); | |
209 | } | |
5869c6ff XL |
210 | } |
211 | } | |
2b03887a | 212 | lint |
5869c6ff XL |
213 | }); |
214 | } | |
215 | ||
216 | fn check_panic_str<'tcx>( | |
217 | cx: &LateContext<'tcx>, | |
218 | f: &'tcx hir::Expr<'tcx>, | |
219 | arg: &'tcx hir::Expr<'tcx>, | |
220 | fmt: &str, | |
221 | ) { | |
a2a8927a | 222 | if !fmt.contains(&['{', '}']) { |
5869c6ff XL |
223 | // No brace, no problem. |
224 | return; | |
225 | } | |
226 | ||
94222f64 XL |
227 | let (span, _, _) = panic_call(cx, f); |
228 | ||
229 | if in_external_macro(cx.sess(), span) && in_external_macro(cx.sess(), arg.span) { | |
230 | // Nothing that can be done about it in the current crate. | |
231 | return; | |
232 | } | |
233 | ||
5869c6ff XL |
234 | let fmt_span = arg.span.source_callsite(); |
235 | ||
236 | let (snippet, style) = match cx.sess().parse_sess.source_map().span_to_snippet(fmt_span) { | |
237 | Ok(snippet) => { | |
238 | // Count the number of `#`s between the `r` and `"`. | |
239 | let style = snippet.strip_prefix('r').and_then(|s| s.find('"')); | |
240 | (Some(snippet), style) | |
241 | } | |
242 | Err(_) => (None, None), | |
243 | }; | |
244 | ||
c295e0f8 | 245 | let mut fmt_parser = Parser::new(fmt, style, snippet.clone(), false, ParseMode::Format); |
5869c6ff XL |
246 | let n_arguments = (&mut fmt_parser).filter(|a| matches!(a, Piece::NextArgument(_))).count(); |
247 | ||
5869c6ff XL |
248 | if n_arguments > 0 && fmt_parser.errors.is_empty() { |
249 | let arg_spans: Vec<_> = match &fmt_parser.arg_places[..] { | |
250 | [] => vec![fmt_span], | |
04454e1e FG |
251 | v => v |
252 | .iter() | |
253 | .map(|span| fmt_span.from_inner(InnerSpan::new(span.start, span.end))) | |
254 | .collect(), | |
5869c6ff | 255 | }; |
2b03887a FG |
256 | cx.struct_span_lint(NON_FMT_PANICS, arg_spans, fluent::lint_non_fmt_panic_unused, |lint| { |
257 | lint.set_arg("count", n_arguments); | |
258 | lint.note(fluent::note); | |
94222f64 | 259 | if is_arg_inside_call(arg.span, span) { |
2b03887a | 260 | lint.span_suggestion( |
5869c6ff | 261 | arg.span.shrink_to_hi(), |
2b03887a | 262 | fluent::add_args_suggestion, |
04454e1e | 263 | ", ...", |
5869c6ff XL |
264 | Applicability::HasPlaceholders, |
265 | ); | |
2b03887a | 266 | lint.span_suggestion( |
5869c6ff | 267 | arg.span.shrink_to_lo(), |
2b03887a | 268 | fluent::add_fmt_suggestion, |
04454e1e | 269 | "\"{}\", ", |
5869c6ff XL |
270 | Applicability::MachineApplicable, |
271 | ); | |
272 | } | |
2b03887a | 273 | lint |
5869c6ff XL |
274 | }); |
275 | } else { | |
276 | let brace_spans: Option<Vec<_>> = | |
277 | snippet.filter(|s| s.starts_with('"') || s.starts_with("r#")).map(|s| { | |
278 | s.char_indices() | |
279 | .filter(|&(_, c)| c == '{' || c == '}') | |
280 | .map(|(i, _)| fmt_span.from_inner(InnerSpan { start: i, end: i + 1 })) | |
281 | .collect() | |
282 | }); | |
064997fb | 283 | let count = brace_spans.as_ref().map(|v| v.len()).unwrap_or(/* any number >1 */ 2); |
2b03887a FG |
284 | cx.struct_span_lint( |
285 | NON_FMT_PANICS, | |
286 | brace_spans.unwrap_or_else(|| vec![span]), | |
287 | fluent::lint_non_fmt_panic_braces, | |
288 | |lint| { | |
289 | lint.set_arg("count", count); | |
290 | lint.note(fluent::note); | |
291 | if is_arg_inside_call(arg.span, span) { | |
292 | lint.span_suggestion( | |
293 | arg.span.shrink_to_lo(), | |
294 | fluent::suggestion, | |
295 | "\"{}\", ", | |
296 | Applicability::MachineApplicable, | |
297 | ); | |
298 | } | |
299 | lint | |
300 | }, | |
301 | ); | |
5869c6ff XL |
302 | } |
303 | } | |
304 | ||
6a06907d XL |
305 | /// Given the span of `some_macro!(args);`, gives the span of `(` and `)`, |
306 | /// and the type of (opening) delimiter used. | |
307 | fn find_delimiters<'tcx>(cx: &LateContext<'tcx>, span: Span) -> Option<(Span, Span, char)> { | |
308 | let snippet = cx.sess().parse_sess.source_map().span_to_snippet(span).ok()?; | |
309 | let (open, open_ch) = snippet.char_indices().find(|&(_, c)| "([{".contains(c))?; | |
310 | let close = snippet.rfind(|c| ")]}".contains(c))?; | |
311 | Some(( | |
312 | span.from_inner(InnerSpan { start: open, end: open + 1 }), | |
313 | span.from_inner(InnerSpan { start: close, end: close + 1 }), | |
314 | open_ch, | |
315 | )) | |
316 | } | |
317 | ||
a2a8927a | 318 | fn panic_call<'tcx>(cx: &LateContext<'tcx>, f: &'tcx hir::Expr<'tcx>) -> (Span, Symbol, Symbol) { |
5869c6ff XL |
319 | let mut expn = f.span.ctxt().outer_expn_data(); |
320 | ||
321 | let mut panic_macro = kw::Empty; | |
322 | ||
323 | // Unwrap more levels of macro expansion, as panic_2015!() | |
324 | // was likely expanded from panic!() and possibly from | |
325 | // [debug_]assert!(). | |
a2a8927a | 326 | loop { |
5869c6ff | 327 | let parent = expn.call_site.ctxt().outer_expn_data(); |
a2a8927a XL |
328 | let Some(id) = parent.macro_def_id else { break }; |
329 | let Some(name) = cx.tcx.get_diagnostic_name(id) else { break }; | |
330 | if !matches!( | |
331 | name, | |
332 | sym::core_panic_macro | |
333 | | sym::std_panic_macro | |
334 | | sym::assert_macro | |
335 | | sym::debug_assert_macro | |
336 | | sym::unreachable_macro | |
337 | ) { | |
338 | break; | |
5869c6ff | 339 | } |
a2a8927a XL |
340 | expn = parent; |
341 | panic_macro = name; | |
5869c6ff XL |
342 | } |
343 | ||
17df50a5 | 344 | let macro_symbol = |
136023e0 | 345 | if let hygiene::ExpnKind::Macro(_, symbol) = expn.kind { symbol } else { sym::panic }; |
a2a8927a | 346 | (expn.call_site, panic_macro, macro_symbol) |
5869c6ff | 347 | } |
94222f64 XL |
348 | |
349 | fn is_arg_inside_call(arg: Span, call: Span) -> bool { | |
350 | // We only add suggestions if the argument we're looking at appears inside the | |
351 | // panic call in the source file, to avoid invalid suggestions when macros are involved. | |
352 | // We specifically check for the spans to not be identical, as that happens sometimes when | |
353 | // proc_macros lie about spans and apply the same span to all the tokens they produce. | |
5099ac24 | 354 | call.contains(arg) && !call.source_equal(arg) |
94222f64 | 355 | } |