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