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