Skip to content

Commit 4bb7fdc

Browse files
committed
Add undocumented_may_panic_call lint
Checks for calls to functions marked with `#[clippy::may_panic]` or configured in `may-panic-functions` that lack a `// Panic:` comment documenting why the panic is acceptable. changelog: new_lint: [`undocumented_may_panic_call`]
1 parent 0226fa9 commit 4bb7fdc

File tree

14 files changed

+342
-0
lines changed

14 files changed

+342
-0
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6867,6 +6867,7 @@ Released 2018-09-13
68676867
[`unchecked_duration_subtraction`]: https://rust-lang.github.io/rust-clippy/master/index.html#unchecked_duration_subtraction
68686868
[`unchecked_time_subtraction`]: https://rust-lang.github.io/rust-clippy/master/index.html#unchecked_time_subtraction
68696869
[`unconditional_recursion`]: https://rust-lang.github.io/rust-clippy/master/index.html#unconditional_recursion
6870+
[`undocumented_may_panic_call`]: https://rust-lang.github.io/rust-clippy/master/index.html#undocumented_may_panic_call
68706871
[`undocumented_unsafe_blocks`]: https://rust-lang.github.io/rust-clippy/master/index.html#undocumented_unsafe_blocks
68716872
[`undropped_manually_drops`]: https://rust-lang.github.io/rust-clippy/master/index.html#undropped_manually_drops
68726873
[`unicode_not_nfc`]: https://rust-lang.github.io/rust-clippy/master/index.html#unicode_not_nfc
@@ -7047,6 +7048,7 @@ Released 2018-09-13
70477048
[`max-struct-bools`]: https://doc.rust-lang.org/clippy/lint_configuration.html#max-struct-bools
70487049
[`max-suggested-slice-pattern-length`]: https://doc.rust-lang.org/clippy/lint_configuration.html#max-suggested-slice-pattern-length
70497050
[`max-trait-bounds`]: https://doc.rust-lang.org/clippy/lint_configuration.html#max-trait-bounds
7051+
[`may-panic-functions`]: https://doc.rust-lang.org/clippy/lint_configuration.html#may-panic-functions
70507052
[`min-ident-chars-threshold`]: https://doc.rust-lang.org/clippy/lint_configuration.html#min-ident-chars-threshold
70517053
[`missing-docs-allow-unused`]: https://doc.rust-lang.org/clippy/lint_configuration.html#missing-docs-allow-unused
70527054
[`missing-docs-in-crate-items`]: https://doc.rust-lang.org/clippy/lint_configuration.html#missing-docs-in-crate-items

book/src/lint_configuration.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,17 @@ The maximum number of bounds a trait can have to be linted
776776
* [`type_repetition_in_bounds`](https://rust-lang.github.io/rust-clippy/master/index.html#type_repetition_in_bounds)
777777

778778

779+
## `may-panic-functions`
780+
List of function/method paths that may panic and should be documented with a `// Panic:` comment
781+
at call sites.
782+
783+
**Default Value:** `[]`
784+
785+
---
786+
**Affected lints:**
787+
* [`undocumented_may_panic_call`](https://rust-lang.github.io/rust-clippy/master/index.html#undocumented_may_panic_call)
788+
789+
779790
## `min-ident-chars-threshold`
780791
Minimum chars an ident can have, anything below or equal to this will be linted.
781792

clippy_config/src/conf.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,10 @@ define_Conf! {
703703
/// The maximum number of bounds a trait can have to be linted
704704
#[lints(type_repetition_in_bounds)]
705705
max_trait_bounds: u64 = 3,
706+
/// List of function/method paths that may panic and should be documented with a `// Panic:` comment
707+
/// at call sites.
708+
#[lints(undocumented_may_panic_call)]
709+
may_panic_functions: Vec<String> = Vec::new(),
706710
/// Minimum chars an ident can have, anything below or equal to this will be linted.
707711
#[lints(min_ident_chars)]
708712
min_ident_chars_threshold: u64 = 1,

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -739,6 +739,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
739739
crate::types::TYPE_COMPLEXITY_INFO,
740740
crate::types::VEC_BOX_INFO,
741741
crate::unconditional_recursion::UNCONDITIONAL_RECURSION_INFO,
742+
crate::undocumented_may_panic_call::UNDOCUMENTED_MAY_PANIC_CALL_INFO,
742743
crate::undocumented_unsafe_blocks::UNDOCUMENTED_UNSAFE_BLOCKS_INFO,
743744
crate::undocumented_unsafe_blocks::UNNECESSARY_SAFETY_COMMENT_INFO,
744745
crate::unicode::INVISIBLE_CHARACTERS_INFO,

clippy_lints/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,7 @@ mod transmute;
358358
mod tuple_array_conversions;
359359
mod types;
360360
mod unconditional_recursion;
361+
mod undocumented_may_panic_call;
361362
mod undocumented_unsafe_blocks;
362363
mod unicode;
363364
mod uninhabited_references;
@@ -825,5 +826,7 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co
825826
store.register_late_pass(|_| Box::new(toplevel_ref_arg::ToplevelRefArg));
826827
store.register_late_pass(|_| Box::new(volatile_composites::VolatileComposites));
827828
store.register_late_pass(|_| Box::new(replace_box::ReplaceBox));
829+
store
830+
.register_late_pass(move |tcx| Box::new(undocumented_may_panic_call::UndocumentedMayPanicCall::new(tcx, conf)));
828831
// add lints here, do not remove this comment, it's used in `new_lint`
829832
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
use clippy_config::Conf;
2+
use clippy_utils::diagnostics::span_lint;
3+
use clippy_utils::paths::{PathNS, lookup_path_str};
4+
use clippy_utils::{get_unique_attr, sym};
5+
use rustc_data_structures::fx::FxHashSet;
6+
use rustc_hir as hir;
7+
use rustc_hir::def_id::DefId;
8+
use rustc_lint::{LateContext, LateLintPass, LintContext};
9+
use rustc_middle::ty::TyCtxt;
10+
use rustc_session::impl_lint_pass;
11+
use rustc_span::Pos;
12+
13+
declare_clippy_lint! {
14+
/// ### What it does
15+
/// Checks for calls to functions marked with `#[clippy::may_panic]` or configured in
16+
/// `may-panic-functions` that don't have a `// Panic:` comment on the line above.
17+
///
18+
/// ### Why is this bad?
19+
/// Functions that may panic should be documented at their call sites to explain why the
20+
/// panic is acceptable or impossible in that context.
21+
///
22+
/// ### Example
23+
/// ```rust,ignore
24+
/// #[clippy::may_panic]
25+
/// fn my_panicable_func(n: u32) {
26+
/// if n % 2 == 0 {
27+
/// panic!("even number not allowed")
28+
/// }
29+
/// }
30+
///
31+
/// fn main() {
32+
/// // Missing documentation - will lint
33+
/// my_panicable_func(1);
34+
/// }
35+
/// ```
36+
/// Use instead:
37+
/// ```rust,ignore
38+
/// #[clippy::may_panic]
39+
/// fn my_panicable_func(n: u32) {
40+
/// if n % 2 == 0 {
41+
/// panic!("even number not allowed")
42+
/// }
43+
/// }
44+
///
45+
/// fn main() {
46+
/// // Panic: This is safe, it's an odd number
47+
/// my_panicable_func(1);
48+
/// }
49+
/// ```
50+
///
51+
/// ### Configuration
52+
/// This lint can be configured to check calls to external functions that may panic:
53+
/// ```toml
54+
/// # clippy.toml
55+
/// may-panic-functions = [
56+
/// "alloc::vec::Vec::push", # Can panic on allocation failure
57+
/// "std::fs::File::open", # Can panic in some configurations
58+
/// ]
59+
/// ```
60+
#[clippy::version = "1.92.0"]
61+
pub UNDOCUMENTED_MAY_PANIC_CALL,
62+
pedantic,
63+
"missing `// Panic:` documentation on calls to functions that may panic"
64+
}
65+
66+
pub struct UndocumentedMayPanicCall {
67+
may_panic_def_ids: FxHashSet<DefId>,
68+
}
69+
70+
impl_lint_pass!(UndocumentedMayPanicCall => [UNDOCUMENTED_MAY_PANIC_CALL]);
71+
72+
impl UndocumentedMayPanicCall {
73+
pub fn new(tcx: TyCtxt<'_>, conf: &'static Conf) -> Self {
74+
let may_panic_def_ids = conf
75+
.may_panic_functions
76+
.iter()
77+
.flat_map(|path| lookup_path_str(tcx, PathNS::Value, path))
78+
.collect();
79+
80+
Self { may_panic_def_ids }
81+
}
82+
83+
// A function is a may_panic_function if it has the may_panic attribute
84+
// or is in the may-panic-functions configuration
85+
fn is_may_panic_function(&self, cx: &LateContext<'_>, def_id: DefId) -> bool {
86+
if get_unique_attr(cx.sess(), cx.tcx.get_all_attrs(def_id), sym::may_panic).is_some() {
87+
return true;
88+
}
89+
90+
self.may_panic_def_ids.contains(&def_id)
91+
}
92+
}
93+
94+
impl LateLintPass<'_> for UndocumentedMayPanicCall {
95+
fn check_expr(&mut self, cx: &LateContext<'_>, expr: &'_ rustc_hir::Expr<'_>) {
96+
let def_id = match &expr.kind {
97+
hir::ExprKind::Call(func, _args) => {
98+
if let hir::ExprKind::Path(qpath) = &func.kind {
99+
cx.qpath_res(qpath, func.hir_id).opt_def_id()
100+
} else {
101+
None
102+
}
103+
},
104+
hir::ExprKind::MethodCall(_path, _receiver, _args, _span) => {
105+
cx.typeck_results().type_dependent_def_id(expr.hir_id)
106+
},
107+
_ => None,
108+
};
109+
110+
if let Some(def_id) = def_id
111+
&& self.is_may_panic_function(cx, def_id)
112+
&& !has_panic_comment_above(cx, expr.span)
113+
{
114+
span_lint(
115+
cx,
116+
UNDOCUMENTED_MAY_PANIC_CALL,
117+
expr.span,
118+
"call to a function that may panic is not documented with a `// Panic:` comment",
119+
);
120+
}
121+
}
122+
}
123+
124+
/// Checks if the lines immediately preceding the call contain a "Panic:" comment.
125+
fn has_panic_comment_above(cx: &LateContext<'_>, call_span: rustc_span::Span) -> bool {
126+
let source_map = cx.sess().source_map();
127+
128+
if let Ok(call_line) = source_map.lookup_line(call_span.lo())
129+
&& call_line.line > 0
130+
&& let Some(src) = call_line.sf.src.as_deref()
131+
{
132+
let lines = call_line.sf.lines();
133+
let line_starts = &lines[..=call_line.line];
134+
135+
has_panic_comment_in_text(src, line_starts)
136+
} else {
137+
false
138+
}
139+
}
140+
141+
fn has_panic_comment_in_text(src: &str, line_starts: &[rustc_span::RelativeBytePos]) -> bool {
142+
let mut lines = line_starts
143+
.array_windows::<2>()
144+
.rev()
145+
.map_while(|[start, end]| {
146+
let start = start.to_usize();
147+
let end = end.to_usize();
148+
let text = src.get(start..end)?;
149+
let trimmed = text.trim_start();
150+
Some((trimmed, text.len() - trimmed.len()))
151+
})
152+
.filter(|(text, _)| !text.is_empty());
153+
154+
let Some((line, _)) = lines.next() else {
155+
return false;
156+
};
157+
158+
if line.starts_with("//") {
159+
let mut current_line = line;
160+
loop {
161+
if current_line.to_ascii_uppercase().contains("PANIC:") {
162+
return true;
163+
}
164+
match lines.next() {
165+
Some((text, _)) if text.starts_with("//") => current_line = text,
166+
_ => return false,
167+
}
168+
}
169+
}
170+
171+
false
172+
}

clippy_utils/src/attrs.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ pub const BUILTIN_ATTRIBUTES: &[(Symbol, DeprecationStatus)] = &[
3333
// See book/src/attribs.md
3434
(sym::has_significant_drop, DeprecationStatus::None),
3535
(sym::format_args, DeprecationStatus::None),
36+
(sym::may_panic, DeprecationStatus::None),
3637
];
3738

3839
pub struct LimitStack {

clippy_utils/src/sym.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ generate! {
227227
max_by_key,
228228
max_value,
229229
maximum,
230+
may_panic,
230231
min,
231232
min_by,
232233
min_by_key,

tests/ui-toml/toml_unknown_key/conf_unknown_key.stderr

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ error: error reading Clippy's configuration file: unknown field `foobar`, expect
5959
max-struct-bools
6060
max-suggested-slice-pattern-length
6161
max-trait-bounds
62+
may-panic-functions
6263
min-ident-chars-threshold
6364
missing-docs-allow-unused
6465
missing-docs-in-crate-items
@@ -156,6 +157,7 @@ error: error reading Clippy's configuration file: unknown field `barfoo`, expect
156157
max-struct-bools
157158
max-suggested-slice-pattern-length
158159
max-trait-bounds
160+
may-panic-functions
159161
min-ident-chars-threshold
160162
missing-docs-allow-unused
161163
missing-docs-in-crate-items
@@ -253,6 +255,7 @@ error: error reading Clippy's configuration file: unknown field `allow_mixed_uni
253255
max-struct-bools
254256
max-suggested-slice-pattern-length
255257
max-trait-bounds
258+
may-panic-functions
256259
min-ident-chars-threshold
257260
missing-docs-allow-unused
258261
missing-docs-in-crate-items
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
may-panic-functions = ["alloc::vec::Vec::push"]

0 commit comments

Comments
 (0)