Skip to content

Commit 132f55c

Browse files
authored
Rollup merge of rust-lang#81645 - m-ou-se:panic-lint, r=estebank
Add lint for `panic!(123)` which is not accepted in Rust 2021. This extends the `panic_fmt` lint to warn for all cases where the first argument cannot be interpreted as a format string, as will happen in Rust 2021. It suggests to add `"{}",` to format the message as a string. In the case of `std::panic!()`, it also suggests the recently stabilized `std::panic::panic_any()` function as an alternative. It renames the lint to `non_fmt_panic` to match the lint naming guidelines. ![image](https://user-images.githubusercontent.com/783247/106520928-675ea680-64d5-11eb-81f7-d8fa48b93a0b.png) This is part of rust-lang#80162. r? `@estebank`
2 parents 2a41f20 + 717355d commit 132f55c

24 files changed

+365
-227
lines changed

compiler/rustc_ast/src/mut_visit.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ pub trait ExpectOne<A: Array> {
2828

2929
impl<A: Array> ExpectOne<A> for SmallVec<A> {
3030
fn expect_one(self, err: &'static str) -> A::Item {
31-
assert!(self.len() == 1, err);
31+
assert!(self.len() == 1, "{}", err);
3232
self.into_iter().next().unwrap()
3333
}
3434
}

compiler/rustc_errors/src/lib.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -901,7 +901,7 @@ impl HandlerInner {
901901

902902
fn span_bug(&mut self, sp: impl Into<MultiSpan>, msg: &str) -> ! {
903903
self.emit_diag_at_span(Diagnostic::new(Bug, msg), sp);
904-
panic!(ExplicitBug);
904+
panic::panic_any(ExplicitBug);
905905
}
906906

907907
fn emit_diag_at_span(&mut self, mut diag: Diagnostic, sp: impl Into<MultiSpan>) {
@@ -955,7 +955,7 @@ impl HandlerInner {
955955

956956
fn bug(&mut self, msg: &str) -> ! {
957957
self.emit_diagnostic(&Diagnostic::new(Bug, msg));
958-
panic!(ExplicitBug);
958+
panic::panic_any(ExplicitBug);
959959
}
960960

961961
fn delay_as_bug(&mut self, diagnostic: Diagnostic) {

compiler/rustc_lint/src/lib.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ mod late;
5454
mod levels;
5555
mod methods;
5656
mod non_ascii_idents;
57+
mod non_fmt_panic;
5758
mod nonstandard_style;
58-
mod panic_fmt;
5959
mod passes;
6060
mod redundant_semicolon;
6161
mod traits;
@@ -80,8 +80,8 @@ use builtin::*;
8080
use internal::*;
8181
use methods::*;
8282
use non_ascii_idents::*;
83+
use non_fmt_panic::NonPanicFmt;
8384
use nonstandard_style::*;
84-
use panic_fmt::PanicFmt;
8585
use redundant_semicolon::*;
8686
use traits::*;
8787
use types::*;
@@ -168,7 +168,7 @@ macro_rules! late_lint_passes {
168168
ClashingExternDeclarations: ClashingExternDeclarations::new(),
169169
DropTraitConstraints: DropTraitConstraints,
170170
TemporaryCStringAsPtr: TemporaryCStringAsPtr,
171-
PanicFmt: PanicFmt,
171+
NonPanicFmt: NonPanicFmt,
172172
]
173173
);
174174
};
+197
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
use crate::{LateContext, LateLintPass, LintContext};
2+
use rustc_ast as ast;
3+
use rustc_errors::{pluralize, Applicability};
4+
use rustc_hir as hir;
5+
use rustc_middle::ty;
6+
use rustc_parse_format::{ParseMode, Parser, Piece};
7+
use rustc_span::{sym, symbol::kw, InnerSpan, Span, Symbol};
8+
9+
declare_lint! {
10+
/// The `non_fmt_panic` lint detects `panic!(..)` invocations where the first
11+
/// argument is not a formatting string.
12+
///
13+
/// ### Example
14+
///
15+
/// ```rust,no_run
16+
/// panic!("{}");
17+
/// panic!(123);
18+
/// ```
19+
///
20+
/// {{produces}}
21+
///
22+
/// ### Explanation
23+
///
24+
/// In Rust 2018 and earlier, `panic!(x)` directly uses `x` as the message.
25+
/// That means that `panic!("{}")` panics with the message `"{}"` instead
26+
/// of using it as a formatting string, and `panic!(123)` will panic with
27+
/// an `i32` as message.
28+
///
29+
/// Rust 2021 always interprets the first argument as format string.
30+
NON_FMT_PANIC,
31+
Warn,
32+
"detect single-argument panic!() invocations in which the argument is not a format string",
33+
report_in_external_macro
34+
}
35+
36+
declare_lint_pass!(NonPanicFmt => [NON_FMT_PANIC]);
37+
38+
impl<'tcx> LateLintPass<'tcx> for NonPanicFmt {
39+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'tcx>) {
40+
if let hir::ExprKind::Call(f, [arg]) = &expr.kind {
41+
if let &ty::FnDef(def_id, _) = cx.typeck_results().expr_ty(f).kind() {
42+
if Some(def_id) == cx.tcx.lang_items().begin_panic_fn()
43+
|| Some(def_id) == cx.tcx.lang_items().panic_fn()
44+
|| Some(def_id) == cx.tcx.lang_items().panic_str()
45+
{
46+
if let Some(id) = f.span.ctxt().outer_expn_data().macro_def_id {
47+
if cx.tcx.is_diagnostic_item(sym::std_panic_2015_macro, id)
48+
|| cx.tcx.is_diagnostic_item(sym::core_panic_2015_macro, id)
49+
{
50+
check_panic(cx, f, arg);
51+
}
52+
}
53+
}
54+
}
55+
}
56+
}
57+
}
58+
59+
fn check_panic<'tcx>(cx: &LateContext<'tcx>, f: &'tcx hir::Expr<'tcx>, arg: &'tcx hir::Expr<'tcx>) {
60+
if let hir::ExprKind::Lit(lit) = &arg.kind {
61+
if let ast::LitKind::Str(sym, _) = lit.node {
62+
// The argument is a string literal.
63+
check_panic_str(cx, f, arg, &sym.as_str());
64+
return;
65+
}
66+
}
67+
68+
// The argument is *not* a string literal.
69+
70+
let (span, panic) = panic_call(cx, f);
71+
72+
cx.struct_span_lint(NON_FMT_PANIC, arg.span, |lint| {
73+
let mut l = lint.build("panic message is not a string literal");
74+
l.note("this is no longer accepted in Rust 2021");
75+
if span.contains(arg.span) {
76+
l.span_suggestion_verbose(
77+
arg.span.shrink_to_lo(),
78+
"add a \"{}\" format string to Display the message",
79+
"\"{}\", ".into(),
80+
Applicability::MaybeIncorrect,
81+
);
82+
if panic == sym::std_panic_macro {
83+
l.span_suggestion_verbose(
84+
span.until(arg.span),
85+
"or use std::panic::panic_any instead",
86+
"std::panic::panic_any(".into(),
87+
Applicability::MachineApplicable,
88+
);
89+
}
90+
}
91+
l.emit();
92+
});
93+
}
94+
95+
fn check_panic_str<'tcx>(
96+
cx: &LateContext<'tcx>,
97+
f: &'tcx hir::Expr<'tcx>,
98+
arg: &'tcx hir::Expr<'tcx>,
99+
fmt: &str,
100+
) {
101+
if !fmt.contains(&['{', '}'][..]) {
102+
// No brace, no problem.
103+
return;
104+
}
105+
106+
let fmt_span = arg.span.source_callsite();
107+
108+
let (snippet, style) = match cx.sess().parse_sess.source_map().span_to_snippet(fmt_span) {
109+
Ok(snippet) => {
110+
// Count the number of `#`s between the `r` and `"`.
111+
let style = snippet.strip_prefix('r').and_then(|s| s.find('"'));
112+
(Some(snippet), style)
113+
}
114+
Err(_) => (None, None),
115+
};
116+
117+
let mut fmt_parser =
118+
Parser::new(fmt.as_ref(), style, snippet.clone(), false, ParseMode::Format);
119+
let n_arguments = (&mut fmt_parser).filter(|a| matches!(a, Piece::NextArgument(_))).count();
120+
121+
let (span, _) = panic_call(cx, f);
122+
123+
if n_arguments > 0 && fmt_parser.errors.is_empty() {
124+
let arg_spans: Vec<_> = match &fmt_parser.arg_places[..] {
125+
[] => vec![fmt_span],
126+
v => v.iter().map(|span| fmt_span.from_inner(*span)).collect(),
127+
};
128+
cx.struct_span_lint(NON_FMT_PANIC, arg_spans, |lint| {
129+
let mut l = lint.build(match n_arguments {
130+
1 => "panic message contains an unused formatting placeholder",
131+
_ => "panic message contains unused formatting placeholders",
132+
});
133+
l.note("this message is not used as a format string when given without arguments, but will be in Rust 2021");
134+
if span.contains(arg.span) {
135+
l.span_suggestion(
136+
arg.span.shrink_to_hi(),
137+
&format!("add the missing argument{}", pluralize!(n_arguments)),
138+
", ...".into(),
139+
Applicability::HasPlaceholders,
140+
);
141+
l.span_suggestion(
142+
arg.span.shrink_to_lo(),
143+
"or add a \"{}\" format string to use the message literally",
144+
"\"{}\", ".into(),
145+
Applicability::MachineApplicable,
146+
);
147+
}
148+
l.emit();
149+
});
150+
} else {
151+
let brace_spans: Option<Vec<_>> =
152+
snippet.filter(|s| s.starts_with('"') || s.starts_with("r#")).map(|s| {
153+
s.char_indices()
154+
.filter(|&(_, c)| c == '{' || c == '}')
155+
.map(|(i, _)| fmt_span.from_inner(InnerSpan { start: i, end: i + 1 }))
156+
.collect()
157+
});
158+
let msg = match &brace_spans {
159+
Some(v) if v.len() == 1 => "panic message contains a brace",
160+
_ => "panic message contains braces",
161+
};
162+
cx.struct_span_lint(NON_FMT_PANIC, brace_spans.unwrap_or(vec![span]), |lint| {
163+
let mut l = lint.build(msg);
164+
l.note("this message is not used as a format string, but will be in Rust 2021");
165+
if span.contains(arg.span) {
166+
l.span_suggestion(
167+
arg.span.shrink_to_lo(),
168+
"add a \"{}\" format string to use the message literally",
169+
"\"{}\", ".into(),
170+
Applicability::MachineApplicable,
171+
);
172+
}
173+
l.emit();
174+
});
175+
}
176+
}
177+
178+
fn panic_call<'tcx>(cx: &LateContext<'tcx>, f: &'tcx hir::Expr<'tcx>) -> (Span, Symbol) {
179+
let mut expn = f.span.ctxt().outer_expn_data();
180+
181+
let mut panic_macro = kw::Empty;
182+
183+
// Unwrap more levels of macro expansion, as panic_2015!()
184+
// was likely expanded from panic!() and possibly from
185+
// [debug_]assert!().
186+
for &i in
187+
&[sym::std_panic_macro, sym::core_panic_macro, sym::assert_macro, sym::debug_assert_macro]
188+
{
189+
let parent = expn.call_site.ctxt().outer_expn_data();
190+
if parent.macro_def_id.map_or(false, |id| cx.tcx.is_diagnostic_item(i, id)) {
191+
expn = parent;
192+
panic_macro = i;
193+
}
194+
}
195+
196+
(expn.call_site, panic_macro)
197+
}

0 commit comments

Comments
 (0)