cha_core/plugins/
error_handling.rs1use crate::{AnalysisContext, Finding, Location, Plugin, Severity, SmellCategory};
2
3pub struct ErrorHandlingAnalyzer {
19 pub max_unwraps_per_function: usize,
20}
21
22impl Default for ErrorHandlingAnalyzer {
23 fn default() -> Self {
24 Self {
25 max_unwraps_per_function: 3,
26 }
27 }
28}
29
30impl Plugin for ErrorHandlingAnalyzer {
31 fn name(&self) -> &str {
32 "error_handling"
33 }
34
35 fn description(&self) -> &str {
36 "Empty catch blocks, unwrap/expect abuse"
37 }
38
39 fn analyze(&self, ctx: &AnalysisContext) -> Vec<Finding> {
40 let mut findings = Vec::new();
41 let lines: Vec<&str> = ctx.file.content.lines().collect();
42
43 for f in &ctx.model.functions {
44 let unwrap_count = count_unwraps(&lines, f.start_line, f.end_line);
45 if unwrap_count > self.max_unwraps_per_function {
46 findings.push(Finding {
47 smell_name: "unwrap_abuse".into(),
48 category: SmellCategory::Security,
49 severity: Severity::Warning,
50 location: Location {
51 path: ctx.file.path.clone(),
52 start_line: f.start_line,
53 start_col: f.name_col,
54 end_line: f.start_line,
55 end_col: f.name_end_col,
56 name: Some(f.name.clone()),
57 },
58 message: format!(
59 "Function `{}` has {} unwrap/expect calls (threshold: {})",
60 f.name, unwrap_count, self.max_unwraps_per_function
61 ),
62 suggested_refactorings: vec![
63 "Use ? operator".into(),
64 "Handle errors explicitly".into(),
65 ],
66 ..Default::default()
67 });
68 }
69 }
70
71 detect_empty_catch(&lines, ctx, &mut findings);
72 findings
73 }
74}
75
76fn count_unwraps(lines: &[&str], start: usize, end: usize) -> usize {
77 let mut count = 0;
78 for line in lines
79 .iter()
80 .take(end.min(lines.len()))
81 .skip(start.saturating_sub(1))
82 {
83 let trimmed = line.trim();
84 if trimmed.starts_with("//") || trimmed.starts_with('#') {
85 continue;
86 }
87 count += line.matches(".unwrap()").count();
88 count += line.matches(".expect(").count();
89 }
90 count
91}
92
93fn detect_empty_catch(lines: &[&str], ctx: &AnalysisContext, findings: &mut Vec<Finding>) {
94 for (i, line) in lines.iter().enumerate() {
95 let trimmed = line.trim();
96 let is_catch = trimmed.starts_with("catch")
97 || trimmed.starts_with("except")
98 || trimmed.starts_with("} catch")
99 || trimmed.starts_with("rescue");
100 if !is_catch {
101 continue;
102 }
103 if let Some(next) = lines.iter().skip(i + 1).find(|l| !l.trim().is_empty()) {
104 let next_trimmed = next.trim();
105 if next_trimmed == "}" || next_trimmed == "pass" || next_trimmed.is_empty() {
106 let col = line.find(trimmed).unwrap_or(0);
107 findings.push(Finding {
108 smell_name: "empty_catch".into(),
109 category: SmellCategory::Security,
110 severity: Severity::Warning,
111 location: Location {
112 path: ctx.file.path.clone(),
113 start_line: i + 1,
114 start_col: col,
115 end_line: i + 2,
116 name: None,
117 ..Default::default()
118 },
119 message: "Empty catch/except block — errors are silently swallowed".into(),
120 suggested_refactorings: vec![
121 "Log the error".into(),
122 "Re-throw or handle explicitly".into(),
123 ],
124 ..Default::default()
125 });
126 }
127 }
128 }
129}