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 switch_nodes = collect_switch_nodes(ctx);
29        ctx.model
30            .functions
31            .iter()
32            .filter(|f| f.switch_arms > self.max_arms)
33            .map(|f| {
34                let loc = first_switch_in_range(&switch_nodes, f.start_line, f.end_line)
35                    .unwrap_or((f.start_line, f.name_col, f.name_end_col));
36                Finding {
37                    smell_name: "switch_statement".into(),
38                    category: SmellCategory::OoAbusers,
39                    severity: Severity::Warning,
40                    location: Location {
41                        path: ctx.file.path.clone(),
42                        start_line: loc.0,
43                        start_col: loc.1,
44                        end_line: loc.0,
45                        end_col: loc.2,
46                        name: Some(f.name.clone()),
47                    },
48                    message: format!(
49                        "Function `{}` has {} switch/match arms (threshold: {})",
50                        f.name, f.switch_arms, self.max_arms
51                    ),
52                    suggested_refactorings: vec!["Replace Conditional with Polymorphism".into()],
53                    actual_value: Some(f.switch_arms as f64),
54                    threshold: Some(self.max_arms as f64),
55                    risk_score: None,
56                }
57            })
58            .collect()
59    }
60}
61
62/// Returns `(line, start_col, end_col)` per switch/match, sorted by file order.
63fn collect_switch_nodes(ctx: &AnalysisContext) -> Vec<(usize, usize, usize)> {
64    let (Some(tree), Some(lang)) = (ctx.tree, ctx.ts_language) else {
65        return Vec::new();
66    };
67    let source = ctx.file.content.as_bytes();
68    let patterns: &[&str] = match ctx.model.language.as_str() {
69        "rust" => &["(match_expression) @s"],
70        "typescript" => &["(switch_statement) @s"],
71        "python" => &["(match_statement) @s"],
72        "go" => &[
73            "(expression_switch_statement) @s",
74            "(type_switch_statement) @s",
75        ],
76        "c" | "cpp" => &["(switch_statement) @s"],
77        _ => return Vec::new(),
78    };
79    let mut out = Vec::new();
80    for pat in patterns {
81        for matches in crate::query::run_query(tree, lang, source, pat) {
82            for cap in matches {
83                let len = match cap.node_kind.as_str() {
84                    "match_expression" | "match_statement" => "match".len(),
85                    _ => "switch".len(),
86                };
87                out.push((
88                    cap.start_line as usize,
89                    cap.start_col as usize,
90                    cap.start_col as usize + len,
91                ));
92            }
93        }
94    }
95    out.sort();
96    out
97}
98
99fn first_switch_in_range(
100    switches: &[(usize, usize, usize)],
101    start: usize,
102    end: usize,
103) -> Option<(usize, usize, usize)> {
104    switches
105        .iter()
106        .find(|(line, _, _)| *line >= start && *line <= end)
107        .copied()
108}