1 use crate::{LateContext, LateLintPass, LintContext}
;
3 use rustc_errors
::{pluralize, Applicability}
;
5 use rustc_infer
::infer
::TyCtxtInferExt
;
6 use rustc_middle
::lint
::in_external_macro
;
8 use rustc_middle
::ty
::subst
::InternalSubsts
;
9 use rustc_parse_format
::{ParseMode, Parser, Piece}
;
10 use rustc_session
::lint
::FutureIncompatibilityReason
;
11 use rustc_span
::edition
::Edition
;
12 use rustc_span
::{hygiene, sym, symbol::kw, InnerSpan, Span, Symbol}
;
13 use rustc_trait_selection
::infer
::InferCtxtExt
;
16 /// The `non_fmt_panics` lint detects `panic!(..)` invocations where the first
17 /// argument is not a formatting string.
21 /// ```rust,no_run,edition2018
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.
35 /// Rust 2021 always interprets the first argument as format string.
38 "detect single-argument panic!() invocations in which the argument is not a format string",
39 @future_incompatible
= FutureIncompatibleInfo
{
40 reason
: FutureIncompatibilityReason
::EditionSemanticsChange(Edition
::Edition2021
),
41 explain_reason
: false,
43 report_in_external_macro
46 declare_lint_pass
!(NonPanicFmt
=> [NON_FMT_PANICS
]);
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() {
52 let f_diagnostic_name
= cx
.tcx
.get_diagnostic_name(def_id
);
54 if Some(def_id
) == cx
.tcx
.lang_items().begin_panic_fn()
55 || Some(def_id
) == cx
.tcx
.lang_items().panic_fn()
56 || f_diagnostic_name
== Some(sym
::panic_str
)
58 if let Some(id
) = f
.span
.ctxt().outer_expn_data().macro_def_id
{
60 cx
.tcx
.get_diagnostic_name(id
),
61 Some(sym
::core_panic_2015_macro
| sym
::std_panic_2015_macro
)
63 check_panic(cx
, f
, arg
);
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
) {
72 // This is safe because we checked above that the callee is indeed
73 // unreachable_display
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"),
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.
92 check_panic_str(cx
, f
, arg
, sym
.as_str());
97 // The argument is *not* a string literal.
99 let (span
, panic
, symbol
) = panic_call(cx
, f
);
101 if in_external_macro(cx
.sess(), span
) {
102 // Nothing that can be done about it in the current crate.
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!())`.
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
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();
118 arg_macro
= expn
.macro_def_id
;
119 arg_span
= expn
.call_site
;
122 cx
.struct_span_lint(NON_FMT_PANICS
, arg_span
, |lint
| {
123 let mut l
= lint
.build("panic message is not a string literal");
124 l
.note(&format
!("this usage of {}!() is deprecated; it will be a hard error in Rust 2021", symbol
));
125 l
.note("for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2021/panic-macro-consistency.html>");
126 if !is_arg_inside_call(arg_span
, span
) {
127 // No clue where this argument is coming from.
131 if arg_macro
.map_or(false, |id
| cx
.tcx
.is_diagnostic_item(sym
::format_macro
, id
)) {
132 // A case of `panic!(format!(..))`.
133 l
.note(format
!("the {}!() macro supports formatting, so there's no need for the format!() macro here", symbol
).as_str());
134 if let Some((open
, close
, _
)) = find_delimiters(cx
, arg_span
) {
135 l
.multipart_suggestion(
136 "remove the `format!(..)` macro call",
138 (arg_span
.until(open
.shrink_to_hi()), "".into()),
139 (close
.until(arg_span
.shrink_to_hi()), "".into()),
141 Applicability
::MachineApplicable
,
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
!(
149 ty
::Ref(_
, r
, _
) if *r
.kind() == ty
::Str
,
152 Some(ty_def
) if cx
.tcx
.is_diagnostic_item(sym
::String
, ty_def
.did()),
155 let (suggest_display
, suggest_debug
) = cx
.tcx
.infer_ctxt().enter(|infcx
| {
156 let display
= is_str
|| cx
.tcx
.get_diagnostic_item(sym
::Display
).map(|t
| {
157 infcx
.type_implements_trait(t
, ty
, InternalSubsts
::empty(), cx
.param_env
).may_apply()
159 let debug
= !display
&& cx
.tcx
.get_diagnostic_item(sym
::Debug
).map(|t
| {
160 infcx
.type_implements_trait(t
, ty
, InternalSubsts
::empty(), cx
.param_env
).may_apply()
165 let suggest_panic_any
= !is_str
&& panic
== sym
::std_panic_macro
;
167 let fmt_applicability
= if suggest_panic_any
{
168 // If we can use panic_any, use that as the MachineApplicable suggestion.
169 Applicability
::MaybeIncorrect
171 // If we don't suggest panic_any, using a format string is our best bet.
172 Applicability
::MachineApplicable
176 l
.span_suggestion_verbose(
177 arg_span
.shrink_to_lo(),
178 "add a \"{}\" format string to Display the message",
182 } else if suggest_debug
{
183 l
.span_suggestion_verbose(
184 arg_span
.shrink_to_lo(),
186 "add a \"{{:?}}\" format string to use the Debug implementation of `{}`",
194 if suggest_panic_any
{
195 if let Some((open
, close
, del
)) = find_delimiters(cx
, span
) {
196 l
.multipart_suggestion(
198 "{}use std::panic::panic_any instead",
199 if suggest_display
|| suggest_debug
{
206 vec
![(span
.until(open
), "std::panic::panic_any".into())]
209 (span
.until(open
.shrink_to_hi()), "std::panic::panic_any(".into()),
213 Applicability
::MachineApplicable
,
222 fn check_panic_str
<'tcx
>(
223 cx
: &LateContext
<'tcx
>,
224 f
: &'tcx hir
::Expr
<'tcx
>,
225 arg
: &'tcx hir
::Expr
<'tcx
>,
228 if !fmt
.contains(&['{', '}'
]) {
229 // No brace, no problem.
233 let (span
, _
, _
) = panic_call(cx
, f
);
235 if in_external_macro(cx
.sess(), span
) && in_external_macro(cx
.sess(), arg
.span
) {
236 // Nothing that can be done about it in the current crate.
240 let fmt_span
= arg
.span
.source_callsite();
242 let (snippet
, style
) = match cx
.sess().parse_sess
.source_map().span_to_snippet(fmt_span
) {
244 // Count the number of `#`s between the `r` and `"`.
245 let style
= snippet
.strip_prefix('r'
).and_then(|s
| s
.find('
"'));
246 (Some(snippet), style)
248 Err(_) => (None, None),
251 let mut fmt_parser = Parser::new(fmt, style, snippet.clone(), false, ParseMode::Format);
252 let n_arguments = (&mut fmt_parser).filter(|a| matches!(a, Piece::NextArgument(_))).count();
254 if n_arguments > 0 && fmt_parser.errors.is_empty() {
255 let arg_spans: Vec<_> = match &fmt_parser.arg_places[..] {
256 [] => vec![fmt_span],
259 .map(|span| fmt_span.from_inner(InnerSpan::new(span.start, span.end)))
262 cx.struct_span_lint(NON_FMT_PANICS, arg_spans, |lint| {
263 let mut l = lint.build(match n_arguments {
264 1 => "panic message contains an unused formatting placeholder
",
265 _ => "panic message contains unused formatting placeholders
",
267 l.note("this message is not used
as a format string when given without arguments
, but will be
in Rust
2021");
268 if is_arg_inside_call(arg.span, span) {
270 arg.span.shrink_to_hi(),
271 &format!("add the missing argument{}
", pluralize!(n_arguments)),
273 Applicability::HasPlaceholders,
276 arg.span.shrink_to_lo(),
277 "or add a
\"{}
\" format string to
use the message literally
",
279 Applicability::MachineApplicable,
285 let brace_spans: Option<Vec<_>> =
286 snippet.filter(|s| s.starts_with('"'
) || s
.starts_with("r#")).map(|s
| {
288 .filter(|&(_
, c
)| c
== '{' || c == '}'
)
289 .map(|(i
, _
)| fmt_span
.from_inner(InnerSpan { start: i, end: i + 1 }
))
292 let msg
= match &brace_spans
{
293 Some(v
) if v
.len() == 1 => "panic message contains a brace",
294 _
=> "panic message contains braces",
296 cx
.struct_span_lint(NON_FMT_PANICS
, brace_spans
.unwrap_or_else(|| vec
![span
]), |lint
| {
297 let mut l
= lint
.build(msg
);
298 l
.note("this message is not used as a format string, but will be in Rust 2021");
299 if is_arg_inside_call(arg
.span
, span
) {
301 arg
.span
.shrink_to_lo(),
302 "add a \"{}\" format string to use the message literally",
304 Applicability
::MachineApplicable
,
312 /// Given the span of `some_macro!(args);`, gives the span of `(` and `)`,
313 /// and the type of (opening) delimiter used.
314 fn find_delimiters
<'tcx
>(cx
: &LateContext
<'tcx
>, span
: Span
) -> Option
<(Span
, Span
, char)> {
315 let snippet
= cx
.sess().parse_sess
.source_map().span_to_snippet(span
).ok()?
;
316 let (open
, open_ch
) = snippet
.char_indices().find(|&(_
, c
)| "([{".contains(c
))?
;
317 let close
= snippet
.rfind(|c
| ")]}".contains(c
))?
;
319 span
.from_inner(InnerSpan { start: open, end: open + 1 }
),
320 span
.from_inner(InnerSpan { start: close, end: close + 1 }
),
325 fn panic_call
<'tcx
>(cx
: &LateContext
<'tcx
>, f
: &'tcx hir
::Expr
<'tcx
>) -> (Span
, Symbol
, Symbol
) {
326 let mut expn
= f
.span
.ctxt().outer_expn_data();
328 let mut panic_macro
= kw
::Empty
;
330 // Unwrap more levels of macro expansion, as panic_2015!()
331 // was likely expanded from panic!() and possibly from
332 // [debug_]assert!().
334 let parent
= expn
.call_site
.ctxt().outer_expn_data();
335 let Some(id
) = parent
.macro_def_id
else { break }
;
336 let Some(name
) = cx
.tcx
.get_diagnostic_name(id
) else { break }
;
339 sym
::core_panic_macro
340 | sym
::std_panic_macro
342 | sym
::debug_assert_macro
343 | sym
::unreachable_macro
352 if let hygiene
::ExpnKind
::Macro(_
, symbol
) = expn
.kind { symbol }
else { sym::panic }
;
353 (expn
.call_site
, panic_macro
, macro_symbol
)
356 fn is_arg_inside_call(arg
: Span
, call
: Span
) -> bool
{
357 // We only add suggestions if the argument we're looking at appears inside the
358 // panic call in the source file, to avoid invalid suggestions when macros are involved.
359 // We specifically check for the spans to not be identical, as that happens sometimes when
360 // proc_macros lie about spans and apply the same span to all the tokens they produce.
361 call
.contains(arg
) && !call
.source_equal(arg
)