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 sites = collect_unwrap_sites(&lines, f.start_line, f.end_line);
49 if sites.len() > self.max_unwraps_per_function {
50 for site in &sites {
51 findings.push(build_unwrap_finding(
52 ctx,
53 f,
54 site,
55 sites.len(),
56 self.max_unwraps_per_function,
57 ));
58 }
59 }
60 }
61
62 detect_empty_catch(&lines, ctx, &mut findings);
63 findings
64 }
65}
66
67struct UnwrapSite {
69 line: usize,
71 start_col: usize,
73 end_col: usize,
75 matched: &'static str,
77}
78
79fn collect_unwrap_sites(lines: &[&str], start: usize, end: usize) -> Vec<UnwrapSite> {
80 let mut sites = Vec::new();
81 for (idx, line) in lines
82 .iter()
83 .enumerate()
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 push_matches(&mut sites, idx + 1, line, ".unwrap()");
92 push_matches(&mut sites, idx + 1, line, ".expect(");
93 }
94 sites
95}
96
97fn push_matches(sites: &mut Vec<UnwrapSite>, line: usize, text: &str, needle: &'static str) {
98 let mut search_from = 0;
99 while let Some(pos) = text[search_from..].find(needle) {
100 let abs = search_from + pos;
101 sites.push(UnwrapSite {
102 line,
103 start_col: abs,
104 end_col: abs + needle.len(),
105 matched: needle,
106 });
107 search_from = abs + needle.len();
108 }
109}
110
111fn build_unwrap_finding(
112 ctx: &AnalysisContext,
113 f: &crate::FunctionInfo,
114 site: &UnwrapSite,
115 total: usize,
116 threshold: usize,
117) -> Finding {
118 Finding {
119 smell_name: "unwrap_abuse".into(),
120 category: SmellCategory::Security,
121 severity: Severity::Warning,
122 location: Location {
123 path: ctx.file.path.clone(),
124 start_line: site.line,
125 start_col: site.start_col,
126 end_line: site.line,
127 end_col: site.end_col,
128 name: Some(f.name.clone()),
129 },
130 message: format!(
131 "`{}` in `{}` (function has {total} unwrap/expect calls, threshold: {threshold})",
132 site.matched, f.name
133 ),
134 suggested_refactorings: vec!["Use ? operator".into(), "Handle errors explicitly".into()],
135 ..Default::default()
136 }
137}
138
139fn detect_empty_catch(lines: &[&str], ctx: &AnalysisContext, findings: &mut Vec<Finding>) {
140 for (i, line) in lines.iter().enumerate() {
141 let trimmed = line.trim();
142 let is_catch = trimmed.starts_with("catch")
143 || trimmed.starts_with("except")
144 || trimmed.starts_with("} catch")
145 || trimmed.starts_with("rescue");
146 if !is_catch {
147 continue;
148 }
149 if let Some(next) = lines.iter().skip(i + 1).find(|l| !l.trim().is_empty()) {
150 let next_trimmed = next.trim();
151 if next_trimmed == "}" || next_trimmed == "pass" || next_trimmed.is_empty() {
152 let col = line.find(trimmed).unwrap_or(0);
153 findings.push(Finding {
154 smell_name: "empty_catch".into(),
155 category: SmellCategory::Security,
156 severity: Severity::Warning,
157 location: Location {
158 path: ctx.file.path.clone(),
159 start_line: i + 1,
160 start_col: col,
161 end_line: i + 2,
162 name: None,
163 ..Default::default()
164 },
165 message: "Empty catch/except block — errors are silently swallowed".into(),
166 suggested_refactorings: vec![
167 "Log the error".into(),
168 "Re-throw or handle explicitly".into(),
169 ],
170 ..Default::default()
171 });
172 }
173 }
174 }
175}