Skip to content

Commit e45f64b

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

File tree

19 files changed

+1578
-27
lines changed

19 files changed

+1578
-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: 122 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,38 @@ 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+
// Use a fresh cache for each check to avoid cross-category pollution
79+
let mut local_cache = HashMap::new();
80+
81+
if let Some(violation) =
82+
detect_panic_in_mir(ctx.tcx, mir, &mut local_cache, &categories)
83+
{
84+
span_lint_and_help(
85+
ctx,
86+
FUNCTION_LINT::get_by_severity(severity),
87+
self.name().as_str(),
88+
violation.span,
89+
format!("Function may panic: {}", violation.reason),
90+
None,
91+
format!("Remove panic paths to satisfy the {} rule", rule_name),
92+
);
93+
}
94+
}
95+
}
6496
}
6597

6698
fn evaluate_function_match(
@@ -204,6 +236,40 @@ fn evaluate_function_match(
204236
}
205237
false
206238
}
239+
FunctionMatch::IsUnsafe => {
240+
// Check if the function is unsafe by examining the HIR
241+
if let Some(local_def_id) = fn_def_id.as_local() {
242+
let node = ctx.tcx.hir_node_by_def_id(local_def_id);
243+
match node {
244+
rustc_hir::Node::Item(item) => {
245+
if let rustc_hir::ItemKind::Fn { sig, .. } = &item.kind {
246+
return matches!(
247+
sig.header.safety,
248+
rustc_hir::HeaderSafety::Normal(rustc_hir::Safety::Unsafe)
249+
);
250+
}
251+
}
252+
rustc_hir::Node::TraitItem(trait_item) => {
253+
if let rustc_hir::TraitItemKind::Fn(sig, _) = &trait_item.kind {
254+
return matches!(
255+
sig.header.safety,
256+
rustc_hir::HeaderSafety::Normal(rustc_hir::Safety::Unsafe)
257+
);
258+
}
259+
}
260+
rustc_hir::Node::ImplItem(impl_item) => {
261+
if let rustc_hir::ImplItemKind::Fn(sig, _) = &impl_item.kind {
262+
return matches!(
263+
sig.header.safety,
264+
rustc_hir::HeaderSafety::Normal(rustc_hir::Safety::Unsafe)
265+
);
266+
}
267+
}
268+
_ => {}
269+
}
270+
}
271+
false
272+
}
207273
FunctionMatch::AndMatches(left, right) => {
208274
evaluate_function_match(left, ctx, module_path, function_name, fn_def_id)
209275
&& evaluate_function_match(right, ctx, module_path, function_name, fn_def_id)
@@ -381,6 +447,33 @@ impl<'tcx> LateLintPass<'tcx> for FunctionLint {
381447
}
382448
}
383449
}
450+
FunctionRule::NoUnwrap(severity) => {
451+
self.check_panic_category(
452+
ctx,
453+
fn_def_id,
454+
*severity,
455+
PanicCategory::Unwrap,
456+
"NoUnwrap",
457+
);
458+
}
459+
FunctionRule::NoPanic(severity) => {
460+
self.check_panic_category(
461+
ctx,
462+
fn_def_id,
463+
*severity,
464+
PanicCategory::ExplicitPanic,
465+
"NoPanic",
466+
);
467+
}
468+
FunctionRule::NoIndexPanic(severity) => {
469+
self.check_panic_category(
470+
ctx,
471+
fn_def_id,
472+
*severity,
473+
PanicCategory::IndexBounds,
474+
"NoIndexPanic",
475+
);
476+
}
384477
}
385478
}
386479
}
@@ -506,6 +599,33 @@ impl<'tcx> LateLintPass<'tcx> for FunctionLint {
506599
}
507600
}
508601
}
602+
FunctionRule::NoUnwrap(severity) => {
603+
self.check_panic_category(
604+
ctx,
605+
fn_def_id,
606+
*severity,
607+
PanicCategory::Unwrap,
608+
"NoUnwrap",
609+
);
610+
}
611+
FunctionRule::NoPanic(severity) => {
612+
self.check_panic_category(
613+
ctx,
614+
fn_def_id,
615+
*severity,
616+
PanicCategory::ExplicitPanic,
617+
"NoPanic",
618+
);
619+
}
620+
FunctionRule::NoIndexPanic(severity) => {
621+
self.check_panic_category(
622+
ctx,
623+
fn_def_id,
624+
*severity,
625+
PanicCategory::IndexBounds,
626+
"NoIndexPanic",
627+
);
628+
}
509629
}
510630
}
511631
}

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)