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