Skip to content

Commit e52bd6f

Browse files
committed
new lint: should_panic_without_expect
1 parent d9e6aac commit e52bd6f

File tree

6 files changed

+112
-1
lines changed

6 files changed

+112
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5329,6 +5329,7 @@ Released 2018-09-13
53295329
[`short_circuit_statement`]: https://rust-lang.github.io/rust-clippy/master/index.html#short_circuit_statement
53305330
[`should_assert_eq`]: https://rust-lang.github.io/rust-clippy/master/index.html#should_assert_eq
53315331
[`should_implement_trait`]: https://rust-lang.github.io/rust-clippy/master/index.html#should_implement_trait
5332+
[`should_panic_without_expect`]: https://rust-lang.github.io/rust-clippy/master/index.html#should_panic_without_expect
53325333
[`significant_drop_in_scrutinee`]: https://rust-lang.github.io/rust-clippy/master/index.html#significant_drop_in_scrutinee
53335334
[`significant_drop_tightening`]: https://rust-lang.github.io/rust-clippy/master/index.html#significant_drop_tightening
53345335
[`similar_names`]: https://rust-lang.github.io/rust-clippy/master/index.html#similar_names

clippy_lints/src/attrs.rs

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ use clippy_utils::macros::{is_panic, macro_backtrace};
66
use clippy_utils::msrvs::{self, Msrv};
77
use clippy_utils::source::{first_line_of_span, is_present_in_source, snippet_opt, without_block_comments};
88
use if_chain::if_chain;
9-
use rustc_ast::{AttrKind, AttrStyle, Attribute, LitKind, MetaItemKind, MetaItemLit, NestedMetaItem};
9+
use rustc_ast::token::{Token, TokenKind};
10+
use rustc_ast::tokenstream::TokenTree;
11+
use rustc_ast::{
12+
AttrArgs, AttrArgsEq, AttrKind, AttrStyle, Attribute, LitKind, MetaItemKind, MetaItemLit, NestedMetaItem,
13+
};
1014
use rustc_errors::Applicability;
1115
use rustc_hir::{
1216
Block, Expr, ExprKind, ImplItem, ImplItemKind, Item, ItemKind, StmtKind, TraitFn, TraitItem, TraitItemKind,
@@ -339,6 +343,41 @@ declare_clippy_lint! {
339343
"ensures that all `allow` and `expect` attributes have a reason"
340344
}
341345

346+
declare_clippy_lint! {
347+
/// ### What it does
348+
/// Checks for `#[should_panic]` attributes without specifying the expected panic message.
349+
///
350+
/// ### Why is this bad?
351+
/// The expected panic message should be specified to ensure that the test is actually
352+
/// panicking with the expected message, and not another unrelated panic.
353+
///
354+
/// ### Example
355+
/// ```rust
356+
/// fn random() -> i32 { 0 }
357+
///
358+
/// #[should_panic]
359+
/// #[test]
360+
/// fn my_test() {
361+
/// let _ = 1 / random();
362+
/// }
363+
/// ```
364+
///
365+
/// Use instead:
366+
/// ```rust
367+
/// fn random() -> i32 { 0 }
368+
///
369+
/// #[should_panic = "attempt to divide by zero"]
370+
/// #[test]
371+
/// fn my_test() {
372+
/// let _ = 1 / random();
373+
/// }
374+
/// ```
375+
#[clippy::version = "1.73.0"]
376+
pub SHOULD_PANIC_WITHOUT_EXPECT,
377+
pedantic,
378+
"ensures that all `should_panic` attributes specify its expected panic message"
379+
}
380+
342381
declare_clippy_lint! {
343382
/// ### What it does
344383
/// Checks for `any` and `all` combinators in `cfg` with only one condition.
@@ -395,6 +434,7 @@ declare_lint_pass!(Attributes => [
395434
DEPRECATED_SEMVER,
396435
USELESS_ATTRIBUTE,
397436
BLANKET_CLIPPY_RESTRICTION_LINTS,
437+
SHOULD_PANIC_WITHOUT_EXPECT,
398438
]);
399439

400440
impl<'tcx> LateLintPass<'tcx> for Attributes {
@@ -442,6 +482,9 @@ impl<'tcx> LateLintPass<'tcx> for Attributes {
442482
}
443483
}
444484
}
485+
if attr.has_name(sym::should_panic) {
486+
check_should_panic_reason(cx, attr);
487+
}
445488
}
446489

447490
fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'_>) {
@@ -550,6 +593,35 @@ fn extract_clippy_lint(lint: &NestedMetaItem) -> Option<Symbol> {
550593
None
551594
}
552595

596+
fn check_should_panic_reason(cx: &LateContext<'_>, attr: &Attribute) {
597+
if let AttrKind::Normal(normal_attr) = &attr.kind {
598+
if let AttrArgs::Eq(_, AttrArgsEq::Hir(_)) = &normal_attr.item.args {
599+
// `#[should_panic = ".."]` found, good
600+
return;
601+
}
602+
603+
if let AttrArgs::Delimited(args) = &normal_attr.item.args
604+
&& let mut tt_iter = args.tokens.trees()
605+
&& let Some(TokenTree::Token(Token { kind: TokenKind::Ident(sym::expected, _), .. }, _)) = tt_iter.next()
606+
&& let Some(TokenTree::Token(Token { kind: TokenKind::Eq, .. }, _)) = tt_iter.next()
607+
&& let Some(TokenTree::Token(Token { kind: TokenKind::Literal(_), .. }, _)) = tt_iter.next()
608+
{
609+
// `#[should_panic(expected = "..")]` found, good
610+
return;
611+
}
612+
613+
span_lint_and_sugg(
614+
cx,
615+
SHOULD_PANIC_WITHOUT_EXPECT,
616+
attr.span,
617+
"#[should_panic] attribute without a reason",
618+
"consider specifying the expected panic",
619+
r#"#[should_panic(expected = /* panic message */)]"#.into(),
620+
Applicability::HasPlaceholders,
621+
);
622+
}
623+
}
624+
553625
fn check_clippy_lint_names(cx: &LateContext<'_>, name: Symbol, items: &[NestedMetaItem]) {
554626
for lint in items {
555627
if let Some(lint_name) = extract_clippy_lint(lint) {

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
5858
crate::attrs::MAYBE_MISUSED_CFG_INFO,
5959
crate::attrs::MISMATCHED_TARGET_OS_INFO,
6060
crate::attrs::NON_MINIMAL_CFG_INFO,
61+
crate::attrs::SHOULD_PANIC_WITHOUT_EXPECT_INFO,
6162
crate::attrs::USELESS_ATTRIBUTE_INFO,
6263
crate::await_holding_invalid::AWAIT_HOLDING_INVALID_TYPE_INFO,
6364
crate::await_holding_invalid::AWAIT_HOLDING_LOCK_INFO,

clippy_utils/src/paths.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,5 @@ pub const DEBUG_STRUCT: [&str; 4] = ["core", "fmt", "builders", "DebugStruct"];
166166
pub const ORD_CMP: [&str; 4] = ["core", "cmp", "Ord", "cmp"];
167167
#[expect(clippy::invalid_paths)] // not sure why it thinks this, it works so
168168
pub const BOOL_THEN: [&str; 4] = ["core", "bool", "<impl bool>", "then"];
169+
#[allow(clippy::invalid_paths, reason = "internal lints do not always know about ::test")]
170+
pub const TEST_DESC_AND_FN: [&str; 3] = ["test", "types", "TestDescAndFn"];
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//@no-rustfix
2+
#![deny(clippy::should_panic_without_expect)]
3+
4+
#[test]
5+
#[should_panic]
6+
fn no_message() {}
7+
8+
#[test]
9+
#[should_panic]
10+
#[cfg(not(test))]
11+
fn no_message_cfg_false() {}
12+
13+
#[test]
14+
#[should_panic = "message"]
15+
fn metastr() {}
16+
17+
#[test]
18+
#[should_panic(expected = "message")]
19+
fn metalist() {}
20+
21+
fn main() {}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
error: #[should_panic] attribute without a reason
2+
--> $DIR/should_panic_without_expect.rs:5:1
3+
|
4+
LL | #[should_panic]
5+
| ^^^^^^^^^^^^^^^ help: consider specifying the expected panic: `#[should_panic(expected = /* panic message */)]`
6+
|
7+
note: the lint level is defined here
8+
--> $DIR/should_panic_without_expect.rs:2:9
9+
|
10+
LL | #![deny(clippy::should_panic_without_expect)]
11+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
12+
13+
error: aborting due to previous error
14+

0 commit comments

Comments
 (0)