Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add lint for panic!("{}") #78088

Merged
merged 28 commits into from
Nov 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
462ee9c
Mark the panic macros as diagnostic items.
m-ou-se Oct 18, 2020
a466790
Add lint to warn about braces in a panic message.
m-ou-se Oct 18, 2020
5b3b80a
Allow #[rustc_diagnostic_item] on macros.
m-ou-se Oct 18, 2020
da66a50
Specialize panic_fmt lint for the {core,std}::panic!() macros.
m-ou-se Oct 18, 2020
f228efc
Make panic_fmt lint work properly for assert!(expr, msg) too.
m-ou-se Oct 18, 2020
3beb2e9
Expand assert!(expr) to panic() function instead of panic!() macro.
m-ou-se Oct 18, 2020
451c986
Add test for the panic_fmt lint.
m-ou-se Oct 18, 2020
ded269f
Improve panic_fmt message for panic!("{}") with a fmt placeholder.
m-ou-se Oct 18, 2020
b8a8b68
Formatting.
m-ou-se Oct 18, 2020
14dcf0a
Test for formating placeholders in panic_fmt lint test.
m-ou-se Oct 18, 2020
dd262e3
Add cfg(not(bootstrap)) on the new rustc_diagnostic_item attributes.
m-ou-se Oct 18, 2020
9615d27
Don't see `{{}}` as placeholder in panic_fmt lint.
m-ou-se Oct 18, 2020
9a840a3
Fix brace problem in panic message in rustc_expand.
m-ou-se Oct 18, 2020
0a9330c
Ignore panic_fmt lint in macro-comma-behavior-rpass ui test.
m-ou-se Oct 18, 2020
d3b4149
Also apply panic_fmt lint suggestions to debug_assert!().
m-ou-se Oct 18, 2020
f1fcc4d
Ignore panic_fmt lint in format-args-capture ui test.
m-ou-se Oct 18, 2020
dd81c91
Update mir-opt test output for new assert macro implementation.
m-ou-se Oct 19, 2020
9e3b949
Fix braces in panic message in test.
m-ou-se Oct 19, 2020
ff8df0b
Add cfg(not(test)) to std_panic_macro rustc_diagnostic_item.
m-ou-se Oct 19, 2020
0f193d1
Small cleanups in assert!() and panic_fmt lint.
m-ou-se Oct 19, 2020
6b44662
Parse the format string for the panic_fmt lint for better warnings.
m-ou-se Oct 20, 2020
190c3ad
Improve panic_fmt error messages for invalid format strings too.
m-ou-se Oct 20, 2020
1993f1e
Test that panic_fmt lint doesn't trigger for custom panic macro.
m-ou-se Oct 24, 2020
5cefc3c
Mark panic_fmt suggestion as machine applicable.
m-ou-se Oct 28, 2020
9743f67
Improve panic_fmt lint messages.
m-ou-se Oct 29, 2020
a922c6b
Add test for panic_fmt lint with external panic!()-calling macro.
m-ou-se Oct 29, 2020
454eaec
Remove the clippy::panic-params lint.
m-ou-se Nov 19, 2020
a125ef2
Clippy: Match on assert!() expansions without an inner block.
m-ou-se Nov 19, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3831,6 +3831,7 @@ dependencies = [
"rustc_hir",
"rustc_index",
"rustc_middle",
"rustc_parse_format",
"rustc_session",
"rustc_span",
"rustc_target",
Expand Down
52 changes: 30 additions & 22 deletions compiler/rustc_builtin_macros/src/assert.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use rustc_errors::{Applicability, DiagnosticBuilder};

use rustc_ast::ptr::P;
use rustc_ast::token::{self, TokenKind};
use rustc_ast::tokenstream::{DelimSpan, TokenStream, TokenTree};
use rustc_ast::token;
use rustc_ast::tokenstream::{DelimSpan, TokenStream};
use rustc_ast::{self as ast, *};
use rustc_ast_pretty::pprust;
use rustc_expand::base::*;
Expand All @@ -26,31 +26,39 @@ pub fn expand_assert<'cx>(
// `core::panic` and `std::panic` are different macros, so we use call-site
// context to pick up whichever is currently in scope.
let sp = cx.with_call_site_ctxt(sp);
let tokens = custom_message.unwrap_or_else(|| {
TokenStream::from(TokenTree::token(
TokenKind::lit(
token::Str,

let panic_call = if let Some(tokens) = custom_message {
// Pass the custom message to panic!().
cx.expr(
sp,
ExprKind::MacCall(MacCall {
path: Path::from_ident(Ident::new(sym::panic, sp)),
args: P(MacArgs::Delimited(
DelimSpan::from_single(sp),
MacDelimiter::Parenthesis,
tokens,
)),
prior_type_ascription: None,
}),
)
} else {
// Pass our own message directly to $crate::panicking::panic(),
// because it might contain `{` and `}` that should always be
// passed literally.
cx.expr_call_global(
sp,
cx.std_path(&[sym::panicking, sym::panic]),
vec![cx.expr_str(
DUMMY_SP,
Symbol::intern(&format!(
"assertion failed: {}",
pprust::expr_to_string(&cond_expr).escape_debug()
)),
None,
),
DUMMY_SP,
))
});
let args = P(MacArgs::Delimited(DelimSpan::from_single(sp), MacDelimiter::Parenthesis, tokens));
let panic_call = MacCall {
path: Path::from_ident(Ident::new(sym::panic, sp)),
args,
prior_type_ascription: None,
)],
)
};
let if_expr = cx.expr_if(
sp,
cx.expr(sp, ExprKind::Unary(UnOp::Not, cond_expr)),
cx.expr(sp, ExprKind::MacCall(panic_call)),
None,
);
let if_expr =
cx.expr_if(sp, cx.expr(sp, ExprKind::Unary(UnOp::Not, cond_expr)), panic_call, None);
MacEager::expr(if_expr)
}

Expand Down
3 changes: 2 additions & 1 deletion compiler/rustc_expand/src/mbe/macro_rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1173,7 +1173,8 @@ fn quoted_tt_to_string(tt: &mbe::TokenTree) -> String {
mbe::TokenTree::MetaVar(_, name) => format!("${}", name),
mbe::TokenTree::MetaVarDecl(_, name, kind) => format!("${}:{}", name, kind),
_ => panic!(
"unexpected mbe::TokenTree::{{Sequence or Delimited}} \
"{}",
"unexpected mbe::TokenTree::{Sequence or Delimited} \
in follow set checker"
),
m-ou-se marked this conversation as resolved.
Show resolved Hide resolved
}
Expand Down
1 change: 1 addition & 0 deletions compiler/rustc_lint/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ rustc_feature = { path = "../rustc_feature" }
rustc_index = { path = "../rustc_index" }
rustc_session = { path = "../rustc_session" }
rustc_trait_selection = { path = "../rustc_trait_selection" }
rustc_parse_format = { path = "../rustc_parse_format" }
3 changes: 3 additions & 0 deletions compiler/rustc_lint/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ mod levels;
mod methods;
mod non_ascii_idents;
mod nonstandard_style;
mod panic_fmt;
mod passes;
mod redundant_semicolon;
mod traits;
Expand All @@ -80,6 +81,7 @@ use internal::*;
use methods::*;
use non_ascii_idents::*;
use nonstandard_style::*;
use panic_fmt::PanicFmt;
use redundant_semicolon::*;
use traits::*;
use types::*;
Expand Down Expand Up @@ -166,6 +168,7 @@ macro_rules! late_lint_passes {
ClashingExternDeclarations: ClashingExternDeclarations::new(),
DropTraitConstraints: DropTraitConstraints,
TemporaryCStringAsPtr: TemporaryCStringAsPtr,
PanicFmt: PanicFmt,
]
);
};
Expand Down
150 changes: 150 additions & 0 deletions compiler/rustc_lint/src/panic_fmt.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
use crate::{LateContext, LateLintPass, LintContext};
use rustc_ast as ast;
use rustc_errors::{pluralize, Applicability};
use rustc_hir as hir;
use rustc_middle::ty;
use rustc_parse_format::{ParseMode, Parser, Piece};
use rustc_span::{sym, InnerSpan};

declare_lint! {
/// The `panic_fmt` lint detects `panic!("..")` with `{` or `}` in the string literal.
///
/// ### Example
///
/// ```rust,no_run
/// panic!("{}");
/// ```
///
/// {{produces}}
///
/// ### Explanation
///
/// `panic!("{}")` panics with the message `"{}"`, as a `panic!()` invocation
/// with a single argument does not use `format_args!()`.
/// A future edition of Rust will interpret this string as format string,
/// which would break this.
PANIC_FMT,
Warn,
"detect braces in single-argument panic!() invocations",
report_in_external_macro
}

declare_lint_pass!(PanicFmt => [PANIC_FMT]);

impl<'tcx> LateLintPass<'tcx> for PanicFmt {
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'tcx>) {
if let hir::ExprKind::Call(f, [arg]) = &expr.kind {
if let &ty::FnDef(def_id, _) = cx.typeck_results().expr_ty(f).kind() {
if Some(def_id) == cx.tcx.lang_items().begin_panic_fn()
|| Some(def_id) == cx.tcx.lang_items().panic_fn()
{
check_panic(cx, f, arg);
}
}
}
}
}

fn check_panic<'tcx>(cx: &LateContext<'tcx>, f: &'tcx hir::Expr<'tcx>, arg: &'tcx hir::Expr<'tcx>) {
if let hir::ExprKind::Lit(lit) = &arg.kind {
if let ast::LitKind::Str(sym, _) = lit.node {
let mut expn = f.span.ctxt().outer_expn_data();
if let Some(id) = expn.macro_def_id {
if cx.tcx.is_diagnostic_item(sym::std_panic_macro, id)
|| cx.tcx.is_diagnostic_item(sym::core_panic_macro, id)
{
Comment on lines +49 to +55
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A common approach I take to avoid deeply nested fns is to use early returns like

let lit = match &arg.kind {
    hir::ExprKind::Lit(lit) => lit,
    _ => return,
};
}

Not necessary to do that now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The let_chains feature would be nice. But looks like that one still needs more work before it's usable.

let fmt = sym.as_str();
if !fmt.contains(&['{', '}'][..]) {
return;
}

let fmt_span = arg.span.source_callsite();

let (snippet, style) =
match cx.sess().parse_sess.source_map().span_to_snippet(fmt_span) {
Ok(snippet) => {
// Count the number of `#`s between the `r` and `"`.
let style = snippet.strip_prefix('r').and_then(|s| s.find('"'));
(Some(snippet), style)
}
Err(_) => (None, None),
};

let mut fmt_parser =
Parser::new(fmt.as_ref(), style, snippet.clone(), false, ParseMode::Format);
let n_arguments =
(&mut fmt_parser).filter(|a| matches!(a, Piece::NextArgument(_))).count();

// Unwrap another level of macro expansion if this panic!()
// was expanded from assert!() or debug_assert!().
Comment on lines +78 to +79
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would happen if a crate I depend on has a fancy_panic!() macro that calls panic!() itself? Would that cause problematic output?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That shows the warning, but not any suggestions (because it doesn't know if extra arguments can be added to fancy_panic!(..):

warning: panic message contains an unused formatting placeholder
 --> src/main.rs:8:19
  |
8 |     fancy_panic!("{}");
  |                   ^^
  |
  = note: `#[warn(panic_fmt)]` on by default
  = note: this message is not used as a format string when given without arguments, but will be in a future Rust version

for &assert in &[sym::assert_macro, sym::debug_assert_macro] {
let parent = expn.call_site.ctxt().outer_expn_data();
if parent
.macro_def_id
.map_or(false, |id| cx.tcx.is_diagnostic_item(assert, id))
{
expn = parent;
}
}

if n_arguments > 0 && fmt_parser.errors.is_empty() {
let arg_spans: Vec<_> = match &fmt_parser.arg_places[..] {
[] => vec![fmt_span],
v => v.iter().map(|span| fmt_span.from_inner(*span)).collect(),
};
cx.struct_span_lint(PANIC_FMT, arg_spans, |lint| {
let mut l = lint.build(match n_arguments {
1 => "panic message contains an unused formatting placeholder",
_ => "panic message contains unused formatting placeholders",
});
l.note("this message is not used as a format string when given without arguments, but will be in a future Rust edition");
if expn.call_site.contains(arg.span) {
l.span_suggestion(
arg.span.shrink_to_hi(),
&format!("add the missing argument{}", pluralize!(n_arguments)),
", ...".into(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use ", _".repeat(n_arguments), (or some other placeholder other than _) here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's tricky to get right. For example: "{0} {0}" would only need one argument. This gets even trickier with implicit arguments: "{} {x}" would work with either one or two additional arguments (e.g. arg0 or arg0, x = arg1) if x is in scope and implements Display.

Applicability::HasPlaceholders,
);
l.span_suggestion(
arg.span.shrink_to_lo(),
"or add a \"{}\" format string to use the message literally",
"\"{}\", ".into(),
Applicability::MachineApplicable,
);
}
l.emit();
});
} else {
let brace_spans: Option<Vec<_>> = snippet
.filter(|s| s.starts_with('"') || s.starts_with("r#"))
.map(|s| {
s.char_indices()
.filter(|&(_, c)| c == '{' || c == '}')
.map(|(i, _)| {
fmt_span.from_inner(InnerSpan { start: i, end: i + 1 })
})
.collect()
});
let msg = match &brace_spans {
Some(v) if v.len() == 1 => "panic message contains a brace",
_ => "panic message contains braces",
};
cx.struct_span_lint(PANIC_FMT, brace_spans.unwrap_or(vec![expn.call_site]), |lint| {
let mut l = lint.build(msg);
l.note("this message is not used as a format string, but will be in a future Rust edition");
if expn.call_site.contains(arg.span) {
l.span_suggestion(
arg.span.shrink_to_lo(),
"add a \"{}\" format string to use the message literally",
"\"{}\", ".into(),
Applicability::MachineApplicable,
);
}
Comment on lines +135 to +142
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing this is relevant when the panic is being called inside of a separate macro. Could we maybe account for the cases of that being in a local macro to suggest modifying the original macro in this way and if it is from an external macro to ask the crate writer to do so (or maybe just to mention that a newer version of the crate might have solved it)? For that later case we might just want not to emit this lint at all (at least at first).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, that might just not be worth the effort. Let's see how much breaks with the deny(panic_fmt) crater run, and how much happens through macros.

l.emit();
});
}
}
}
}
}
}
4 changes: 4 additions & 0 deletions compiler/rustc_passes/src/diagnostic_items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ fn collect<'tcx>(tcx: TyCtxt<'tcx>) -> FxHashMap<Symbol, DefId> {
}
}

for m in tcx.hir().krate().exported_macros {
collector.observe_item(m.attrs, m.hir_id);
}

collector.items
}

Expand Down
5 changes: 5 additions & 0 deletions compiler/rustc_span/src/symbol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ symbols! {
asm,
assert,
assert_inhabited,
assert_macro,
assert_receiver_is_total_eq,
assert_uninit_valid,
assert_zero_valid,
Expand Down Expand Up @@ -393,6 +394,7 @@ symbols! {
copysignf64,
core,
core_intrinsics,
core_panic_macro,
cosf32,
cosf64,
crate_id,
Expand All @@ -416,6 +418,7 @@ symbols! {
dead_code,
dealloc,
debug,
debug_assert_macro,
debug_assertions,
debug_struct,
debug_trait,
Expand Down Expand Up @@ -789,6 +792,7 @@ symbols! {
panic_runtime,
panic_str,
panic_unwind,
panicking,
param_attrs,
parent_trait,
partial_cmp,
Expand Down Expand Up @@ -1064,6 +1068,7 @@ symbols! {
staticlib,
std,
std_inject,
std_panic_macro,
stmt,
stmt_expr_attributes,
stop_after_dataflow,
Expand Down
4 changes: 4 additions & 0 deletions library/core/src/macros/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#[macro_export]
#[allow_internal_unstable(core_panic, const_caller_location)]
#[stable(feature = "core", since = "1.6.0")]
#[cfg_attr(not(bootstrap), rustc_diagnostic_item = "core_panic_macro")]
macro_rules! panic {
() => (
$crate::panic!("explicit panic")
Expand Down Expand Up @@ -162,6 +163,7 @@ macro_rules! assert_ne {
/// ```
#[macro_export]
#[stable(feature = "rust1", since = "1.0.0")]
#[cfg_attr(not(bootstrap), rustc_diagnostic_item = "debug_assert_macro")]
macro_rules! debug_assert {
($($arg:tt)*) => (if $crate::cfg!(debug_assertions) { $crate::assert!($($arg)*); })
}
Expand Down Expand Up @@ -1215,6 +1217,8 @@ pub(crate) mod builtin {
#[stable(feature = "rust1", since = "1.0.0")]
#[rustc_builtin_macro]
#[macro_export]
#[cfg_attr(not(bootstrap), rustc_diagnostic_item = "assert_macro")]
#[allow_internal_unstable(core_panic)]
macro_rules! assert {
($cond:expr $(,)?) => {{ /* compiler built-in */ }};
($cond:expr, $($arg:tt)+) => {{ /* compiler built-in */ }};
Expand Down
2 changes: 1 addition & 1 deletion library/core/tests/nonzero.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ fn test_match_option_string() {
let five = "Five".to_string();
match Some(five) {
Some(s) => assert_eq!(s, "Five"),
None => panic!("unexpected None while matching on Some(String { ... })"),
None => panic!("{}", "unexpected None while matching on Some(String { ... })"),
}
}

Expand Down
1 change: 1 addition & 0 deletions library/std/src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#[macro_export]
#[stable(feature = "rust1", since = "1.0.0")]
#[allow_internal_unstable(libstd_sys_internals)]
#[cfg_attr(not(any(bootstrap, test)), rustc_diagnostic_item = "std_panic_macro")]
macro_rules! panic {
() => ({ $crate::panic!("explicit panic") });
($msg:expr $(,)?) => ({ $crate::rt::begin_panic($msg) });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
let mut _8: bool; // in scope 0 at $DIR/inst_combine_deref.rs:60:5: 60:23
let mut _9: bool; // in scope 0 at $DIR/inst_combine_deref.rs:60:13: 60:21
let mut _10: i32; // in scope 0 at $DIR/inst_combine_deref.rs:60:13: 60:15
let mut _11: !; // in scope 0 at $SRC_DIR/std/src/macros.rs:LL:COL
let mut _11: !; // in scope 0 at $DIR/inst_combine_deref.rs:60:5: 60:23
scope 1 {
debug x => _1; // in scope 1 at $DIR/inst_combine_deref.rs:55:9: 55:10
let _2: i32; // in scope 1 at $DIR/inst_combine_deref.rs:56:9: 56:10
Expand Down Expand Up @@ -69,11 +69,11 @@
}

bb2: {
StorageLive(_11); // scope 4 at $SRC_DIR/std/src/macros.rs:LL:COL
begin_panic::<&str>(const "assertion failed: *y == 99"); // scope 4 at $SRC_DIR/std/src/macros.rs:LL:COL
StorageLive(_11); // scope 4 at $DIR/inst_combine_deref.rs:60:5: 60:23
core::panicking::panic(const "assertion failed: *y == 99"); // scope 4 at $DIR/inst_combine_deref.rs:60:5: 60:23
// mir::Constant
// + span: $SRC_DIR/std/src/macros.rs:LL:COL
// + literal: Const { ty: fn(&str) -> ! {std::rt::begin_panic::<&str>}, val: Value(Scalar(<ZST>)) }
// + span: $DIR/inst_combine_deref.rs:60:5: 60:23
// + literal: Const { ty: fn(&'static str) -> ! {core::panicking::panic}, val: Value(Scalar(<ZST>)) }
// ty::Const
// + ty: &str
// + val: Value(Slice { data: Allocation { bytes: [97, 115, 115, 101, 114, 116, 105, 111, 110, 32, 102, 97, 105, 108, 101, 100, 58, 32, 42, 121, 32, 61, 61, 32, 57, 57], relocations: Relocations(SortedMap { data: [] }), init_mask: InitMask { blocks: [67108863], len: Size { raw: 26 } }, size: Size { raw: 26 }, align: Align { pow2: 0 }, mutability: Not, extra: () }, start: 0, end: 26 })
Expand Down
6 changes: 6 additions & 0 deletions src/test/ui/auxiliary/fancy-panic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#[macro_export]
macro_rules! fancy_panic {
($msg:expr) => {
panic!($msg)
};
}
Loading