Skip to content

Commit 5483c2a

Browse files
committed
feat: add panic detection rules and IsUnsafe function matcher
1 parent 49ad38e commit 5483c2a

File tree

19 files changed

+1583
-27
lines changed

19 files changed

+1583
-27
lines changed

cargo_pup_lint_config/src/function_lint/builder.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,37 @@ impl<'a> FunctionConstraintBuilder<'a> {
138138
self
139139
}
140140

141+
/// Forbid unwrap/expect calls on Option and Result
142+
/// This detects: Option::unwrap, Option::expect, Result::unwrap, Result::expect,
143+
/// Result::unwrap_err, Result::expect_err
144+
pub fn no_unwrap(mut self) -> Self {
145+
self.add_rule_internal(FunctionRule::NoUnwrap(self.current_severity));
146+
self
147+
}
148+
149+
/// Forbid all panic-family macros: panic!(), unreachable!(), unimplemented!(), todo!(), assert!()
150+
/// Note: MIR-level analysis cannot distinguish between these macros as they all
151+
/// compile to similar underlying panic functions.
152+
pub fn no_panic(mut self) -> Self {
153+
self.add_rule_internal(FunctionRule::NoPanic(self.current_severity));
154+
self
155+
}
156+
157+
/// Forbid index bounds panics
158+
pub fn no_index_panic(mut self) -> Self {
159+
self.add_rule_internal(FunctionRule::NoIndexPanic(self.current_severity));
160+
self
161+
}
162+
163+
/// Convenience method: forbid ALL panic sources
164+
/// This adds all panic-related rules: NoUnwrap, NoPanic, and NoIndexPanic
165+
pub fn no_panics(mut self) -> Self {
166+
self.add_rule_internal(FunctionRule::NoUnwrap(self.current_severity));
167+
self.add_rule_internal(FunctionRule::NoPanic(self.current_severity));
168+
self.add_rule_internal(FunctionRule::NoIndexPanic(self.current_severity));
169+
self
170+
}
171+
141172
/// Create a new MaxLength rule with the current severity
142173
pub fn create_max_length_rule(&self, length: usize) -> FunctionRule {
143174
FunctionRule::MaxLength(length, self.current_severity)

cargo_pup_lint_config/src/function_lint/matcher.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ impl FunctionMatcher {
8080
pub fn is_async(&self) -> FunctionMatchNode {
8181
FunctionMatchNode::Leaf(FunctionMatch::IsAsync)
8282
}
83+
84+
/// Matches unsafe functions
85+
pub fn is_unsafe(&self) -> FunctionMatchNode {
86+
FunctionMatchNode::Leaf(FunctionMatch::IsUnsafe)
87+
}
8388
}
8489

8590
/// Node in the matcher expression tree

cargo_pup_lint_config/src/function_lint/types.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ pub enum FunctionMatch {
3737
ReturnsType(ReturnTypePattern),
3838
/// Match async functions
3939
IsAsync,
40+
/// Match unsafe functions
41+
IsUnsafe,
4042
/// Logical AND - both patterns must match
4143
AndMatches(Box<FunctionMatch>, Box<FunctionMatch>),
4244
/// Logical OR - either pattern must match
@@ -64,6 +66,15 @@ pub enum FunctionRule {
6466
MustNotExist(Severity),
6567
/// Enforces that a function must not perform heap allocations
6668
NoAllocation(Severity),
69+
/// Enforces that a function must not call unwrap/expect on Option or Result
70+
NoUnwrap(Severity),
71+
/// Enforces that a function must not use any panic-family macros.
72+
/// This includes: panic!(), unreachable!(), unimplemented!(), todo!(), and assert!().
73+
/// Note: MIR-level analysis cannot distinguish between these macros as they all
74+
/// compile to similar underlying panic functions.
75+
NoPanic(Severity),
76+
/// Enforces that a function must not trigger index bounds panics
77+
NoIndexPanic(Severity),
6778
}
6879

6980
// Helper methods for FunctionRule

cargo_pup_lint_impl/src/lints/function_lint/lint.rs

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ use rustc_lint::{LateContext, LateLintPass, LintStore};
1111
use rustc_middle::ty::TyKind;
1212
use rustc_session::impl_lint_pass;
1313
use rustc_span::BytePos;
14-
use std::collections::HashMap;
14+
use std::collections::{HashMap, HashSet};
1515
use std::sync::Mutex;
1616

1717
use super::no_allocation::detect_allocation_in_mir;
18+
use super::no_panic::{PanicCategory, detect_panic_in_mir};
1819

1920
// Helper: retrieve the concrete Self type of the impl the method belongs to, if any
2021
fn get_self_type<'tcx>(
@@ -60,7 +61,33 @@ impl FunctionLint {
6061
evaluate_function_match(&self.matches, ctx, module_path, function_name, fn_def_id)
6162
}
6263

63-
// Evaluates the complex matcher structure to determine if a function matches
64+
/// Helper method to check a single panic category and emit a lint if found
65+
fn check_panic_category(
66+
&self,
67+
ctx: &LateContext<'_>,
68+
fn_def_id: rustc_hir::def_id::DefId,
69+
severity: cargo_pup_lint_config::Severity,
70+
category: PanicCategory,
71+
rule_name: &str,
72+
) {
73+
if ctx.tcx.is_mir_available(fn_def_id) {
74+
let mir = ctx.tcx.optimized_mir(fn_def_id);
75+
let mut categories = HashSet::new();
76+
categories.insert(category);
77+
78+
if let Some(violation) = detect_panic_in_mir(ctx.tcx, mir, &categories) {
79+
span_lint_and_help(
80+
ctx,
81+
FUNCTION_LINT::get_by_severity(severity),
82+
self.name().as_str(),
83+
violation.span,
84+
format!("Function may panic: {}", violation.reason),
85+
None,
86+
format!("Remove panic paths to satisfy the {} rule", rule_name),
87+
);
88+
}
89+
}
90+
}
6491
}
6592

6693
fn evaluate_function_match(
@@ -204,6 +231,40 @@ fn evaluate_function_match(
204231
}
205232
false
206233
}
234+
FunctionMatch::IsUnsafe => {
235+
// Check if the function is unsafe by examining the HIR
236+
if let Some(local_def_id) = fn_def_id.as_local() {
237+
let node = ctx.tcx.hir_node_by_def_id(local_def_id);
238+
match node {
239+
rustc_hir::Node::Item(item) => {
240+
if let rustc_hir::ItemKind::Fn { sig, .. } = &item.kind {
241+
return matches!(
242+
sig.header.safety,
243+
rustc_hir::HeaderSafety::Normal(rustc_hir::Safety::Unsafe)
244+
);
245+
}
246+
}
247+
rustc_hir::Node::TraitItem(trait_item) => {
248+
if let rustc_hir::TraitItemKind::Fn(sig, _) = &trait_item.kind {
249+
return matches!(
250+
sig.header.safety,
251+
rustc_hir::HeaderSafety::Normal(rustc_hir::Safety::Unsafe)
252+
);
253+
}
254+
}
255+
rustc_hir::Node::ImplItem(impl_item) => {
256+
if let rustc_hir::ImplItemKind::Fn(sig, _) = &impl_item.kind {
257+
return matches!(
258+
sig.header.safety,
259+
rustc_hir::HeaderSafety::Normal(rustc_hir::Safety::Unsafe)
260+
);
261+
}
262+
}
263+
_ => {}
264+
}
265+
}
266+
false
267+
}
207268
FunctionMatch::AndMatches(left, right) => {
208269
evaluate_function_match(left, ctx, module_path, function_name, fn_def_id)
209270
&& evaluate_function_match(right, ctx, module_path, function_name, fn_def_id)
@@ -381,6 +442,33 @@ impl<'tcx> LateLintPass<'tcx> for FunctionLint {
381442
}
382443
}
383444
}
445+
FunctionRule::NoUnwrap(severity) => {
446+
self.check_panic_category(
447+
ctx,
448+
fn_def_id,
449+
*severity,
450+
PanicCategory::Unwrap,
451+
"NoUnwrap",
452+
);
453+
}
454+
FunctionRule::NoPanic(severity) => {
455+
self.check_panic_category(
456+
ctx,
457+
fn_def_id,
458+
*severity,
459+
PanicCategory::ExplicitPanic,
460+
"NoPanic",
461+
);
462+
}
463+
FunctionRule::NoIndexPanic(severity) => {
464+
self.check_panic_category(
465+
ctx,
466+
fn_def_id,
467+
*severity,
468+
PanicCategory::IndexBounds,
469+
"NoIndexPanic",
470+
);
471+
}
384472
}
385473
}
386474
}
@@ -506,6 +594,33 @@ impl<'tcx> LateLintPass<'tcx> for FunctionLint {
506594
}
507595
}
508596
}
597+
FunctionRule::NoUnwrap(severity) => {
598+
self.check_panic_category(
599+
ctx,
600+
fn_def_id,
601+
*severity,
602+
PanicCategory::Unwrap,
603+
"NoUnwrap",
604+
);
605+
}
606+
FunctionRule::NoPanic(severity) => {
607+
self.check_panic_category(
608+
ctx,
609+
fn_def_id,
610+
*severity,
611+
PanicCategory::ExplicitPanic,
612+
"NoPanic",
613+
);
614+
}
615+
FunctionRule::NoIndexPanic(severity) => {
616+
self.check_panic_category(
617+
ctx,
618+
fn_def_id,
619+
*severity,
620+
PanicCategory::IndexBounds,
621+
"NoIndexPanic",
622+
);
623+
}
509624
}
510625
}
511626
}

cargo_pup_lint_impl/src/lints/function_lint/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22

33
mod lint;
44
mod no_allocation;
5+
mod no_panic;
56

67
pub use lint::FunctionLint;

0 commit comments

Comments
 (0)