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 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
62fn 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}