Skip to main content

cha_core/plugins/
message_chain.rs

1use crate::{AnalysisContext, Finding, Location, Plugin, Severity, SmellCategory};
2
3/// Detect deep method chain calls (e.g. a.b().c().d()).
4pub 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
65/// Returns `(line, start_col, end_col)` per dotted-access node, sorted by file order.
66fn 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}