cha_core/plugins/
message_chain.rs1use crate::{AnalysisContext, Finding, Location, Plugin, Severity, SmellCategory};
2
3pub struct MessageChainAnalyzer {
5 pub max_depth: usize,
6}
7
8impl Default for MessageChainAnalyzer {
9 fn default() -> Self {
10 Self { max_depth: 3 }
11 }
12}
13
14impl Plugin for MessageChainAnalyzer {
15 fn name(&self) -> &str {
16 "message_chain"
17 }
18
19 fn smells(&self) -> Vec<String> {
20 vec!["message_chain".into()]
21 }
22
23 fn description(&self) -> &str {
24 "Deep field access chains (a.b.c.d)"
25 }
26
27 fn analyze(&self, ctx: &AnalysisContext) -> Vec<Finding> {
28 let chains = collect_chains(ctx);
29 ctx.model
30 .functions
31 .iter()
32 .filter(|f| f.chain_depth > self.max_depth)
33 .map(|f| {
34 let loc = first_chain_in_range(&chains, 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: "message_chain".into(),
41 category: SmellCategory::Couplers,
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 chain depth {} (threshold: {})",
53 f.name, f.chain_depth, self.max_depth
54 ),
55 suggested_refactorings: vec!["Hide Delegate".into()],
56 actual_value: Some(f.chain_depth as f64),
57 threshold: Some(self.max_depth as f64),
58 risk_score: None,
59 }
60 })
61 .collect()
62 }
63}
64
65fn collect_chains(ctx: &AnalysisContext) -> Vec<(usize, usize, usize)> {
67 let (Some(tree), Some(lang)) = (ctx.tree, ctx.ts_language) else {
68 return Vec::new();
69 };
70 let source = ctx.file.content.as_bytes();
71 let patterns: &[&str] = match ctx.model.language.as_str() {
72 "rust" => &["(field_expression) @c"],
73 "typescript" | "javascript" => &["(member_expression) @c"],
74 "python" => &["(attribute) @c"],
75 "go" => &["(selector_expression) @c"],
76 "c" | "cpp" => &["(field_expression) @c"],
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 out.push((
84 cap.start_line as usize,
85 cap.start_col as usize,
86 cap.end_col as usize,
87 ));
88 }
89 }
90 }
91 out.sort();
92 out
93}
94
95fn first_chain_in_range(
96 chains: &[(usize, usize, usize)],
97 start: usize,
98 end: usize,
99) -> Option<(usize, usize, usize)> {
100 chains
101 .iter()
102 .find(|(line, _, _)| *line >= start && *line <= end)
103 .copied()
104}