cha_core/plugins/
switch_statement.rs1use crate::{AnalysisContext, Finding, Location, Plugin, Severity, SmellCategory};
2
3pub struct SwitchStatementAnalyzer {
5 pub max_arms: usize,
6}
7
8impl Default for SwitchStatementAnalyzer {
9 fn default() -> Self {
10 Self { max_arms: 8 }
11 }
12}
13
14impl Plugin for SwitchStatementAnalyzer {
15 fn name(&self) -> &str {
16 "switch_statement"
17 }
18
19 fn smells(&self) -> Vec<String> {
20 vec!["switch_statement".into()]
21 }
22
23 fn description(&self) -> &str {
24 "Excessive switch/match arms"
25 }
26
27 fn analyze(&self, ctx: &AnalysisContext) -> Vec<Finding> {
28 let lines: Vec<&str> = ctx.file.content.lines().collect();
29 ctx.model
30 .functions
31 .iter()
32 .filter(|f| f.switch_arms > self.max_arms)
33 .map(|f| {
34 let loc = find_switch_keyword(&lines, f.start_line, f.end_line).unwrap_or((
35 f.start_line,
36 f.name_col,
37 f.name_end_col,
38 ));
39 Finding {
40 smell_name: "switch_statement".into(),
41 category: SmellCategory::OoAbusers,
42 severity: Severity::Warning,
43 location: Location {
44 path: ctx.file.path.clone(),
45 start_line: loc.0,
46 start_col: loc.1,
47 end_line: loc.0,
48 end_col: loc.2,
49 name: Some(f.name.clone()),
50 },
51 message: format!(
52 "Function `{}` has {} switch/match arms (threshold: {})",
53 f.name, f.switch_arms, self.max_arms
54 ),
55 suggested_refactorings: vec!["Replace Conditional with Polymorphism".into()],
56 actual_value: Some(f.switch_arms as f64),
57 threshold: Some(self.max_arms as f64),
58 risk_score: None,
59 }
60 })
61 .collect()
62 }
63}
64
65fn find_switch_keyword(lines: &[&str], start: usize, end: usize) -> Option<(usize, usize, usize)> {
69 let keywords = ["switch", "match"];
70 for (idx, line) in lines
71 .iter()
72 .enumerate()
73 .take(end.min(lines.len()))
74 .skip(start.saturating_sub(1))
75 {
76 for kw in &keywords {
77 if let Some(col) = find_keyword(line, kw) {
78 return Some((idx + 1, col, col + kw.len()));
79 }
80 }
81 }
82 None
83}
84
85fn find_keyword(line: &str, keyword: &str) -> Option<usize> {
89 let trimmed = line.trim_start();
90 if trimmed.starts_with("//") || trimmed.starts_with('#') || trimmed.starts_with("/*") {
91 return None;
92 }
93 let bytes = line.as_bytes();
94 let klen = keyword.len();
95 let mut i = 0;
96 while i + klen <= bytes.len() {
97 if &bytes[i..i + klen] == keyword.as_bytes() {
98 let before_ok = i == 0 || !is_ident_byte(bytes[i - 1]);
99 let after_ok = i + klen == bytes.len() || !is_ident_byte(bytes[i + klen]);
100 if before_ok && after_ok {
101 return Some(i);
102 }
103 }
104 i += 1;
105 }
106 None
107}
108
109fn is_ident_byte(b: u8) -> bool {
110 b.is_ascii_alphanumeric() || b == b'_'
111}