Skip to main content

cha_core/plugins/
switch_statement.rs

1use crate::{AnalysisContext, Finding, Location, Plugin, Severity, SmellCategory};
2
3/// Detect functions with excessive switch/match arms.
4pub 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
65/// Scan the function body for the first `switch`/`match` keyword and return
66/// `(line, start_col, end_col)` of the keyword token. Returns None if not
67/// found (fallback to function name location).
68fn 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
85/// Find `keyword` in `line` but only where it stands as a word (whitespace /
86/// line start before, non-alphanumeric after). Skips occurrences inside
87/// comments starting at line start (`//`, `#`, `/*`).
88fn 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}