Skip to main content

cha_core/plugins/
length.rs

1use crate::{AnalysisContext, Finding, Location, Plugin, Severity, SmellCategory};
2
3/// Configurable thresholds for length checks.
4pub struct LengthAnalyzer {
5    pub max_function_lines: usize,
6    pub max_class_methods: usize,
7    pub max_class_lines: usize,
8    pub max_file_lines: usize,
9    pub complexity_factor_threshold: f64,
10}
11
12impl Default for LengthAnalyzer {
13    fn default() -> Self {
14        Self {
15            max_function_lines: 50,
16            max_class_methods: 10,
17            max_class_lines: 200,
18            max_file_lines: 500,
19            complexity_factor_threshold: 10.0,
20        }
21    }
22}
23
24impl Plugin for LengthAnalyzer {
25    fn name(&self) -> &str {
26        "length"
27    }
28
29    fn smells(&self) -> Vec<String> {
30        vec![
31            "long_method".into(),
32            "large_class".into(),
33            "large_file".into(),
34        ]
35    }
36
37    fn description(&self) -> &str {
38        "Long method, large class, or large file"
39    }
40
41    fn analyze(&self, ctx: &AnalysisContext) -> Vec<Finding> {
42        let mut findings = Vec::new();
43        self.check_functions(ctx, &mut findings);
44        self.check_classes(ctx, &mut findings);
45        self.check_file(ctx, &mut findings);
46        findings
47    }
48}
49
50impl LengthAnalyzer {
51    fn check_functions(&self, ctx: &AnalysisContext, findings: &mut Vec<Finding>) {
52        for f in &ctx.model.functions {
53            let line_ratio = f.line_count as f64 / self.max_function_lines as f64;
54            let complexity_factor =
55                (f.complexity as f64 / self.complexity_factor_threshold).max(1.0);
56            let risk = line_ratio * complexity_factor;
57            if risk < 1.0 {
58                continue;
59            }
60            let severity = risk_severity(risk);
61            findings.push(Finding {
62                smell_name: "long_method".into(),
63                category: SmellCategory::Bloaters,
64                severity,
65                location: Location {
66                    path: ctx.file.path.clone(),
67                    start_line: f.start_line,
68                    start_col: f.name_col,
69                    end_line: f.start_line,
70                    end_col: f.name_end_col,
71                    name: Some(f.name.clone()),
72                },
73                message: format!(
74                    "Function `{}` is {} lines (threshold: {}, risk: {:.1})",
75                    f.name, f.line_count, self.max_function_lines, risk
76                ),
77                suggested_refactorings: vec!["Extract Method".into()],
78                actual_value: Some(risk),
79                threshold: Some(1.0),
80                risk_score: None,
81            });
82        }
83    }
84
85    fn check_classes(&self, ctx: &AnalysisContext, findings: &mut Vec<Finding>) {
86        for c in &ctx.model.classes {
87            if let Some(f) = self.check_single_class(ctx, c) {
88                findings.push(f);
89            }
90        }
91    }
92
93    /// Build a finding for a single class if it exceeds size thresholds.
94    fn check_single_class(&self, ctx: &AnalysisContext, c: &crate::ClassInfo) -> Option<Finding> {
95        let over_methods = c.method_count > self.max_class_methods;
96        let over_lines = c.line_count > self.max_class_lines;
97        if !over_methods && !over_lines {
98            return None;
99        }
100        let is_error = over_methods && over_lines;
101        let reasons: Vec<_> = [
102            over_methods.then(|| format!("{} methods", c.method_count)),
103            over_lines.then(|| format!("{} lines", c.line_count)),
104        ]
105        .into_iter()
106        .flatten()
107        .collect();
108        let severity = if is_error {
109            Severity::Error
110        } else {
111            Severity::Warning
112        };
113        Some(Finding {
114            smell_name: "large_class".into(),
115            category: SmellCategory::Bloaters,
116            severity,
117            location: Location {
118                path: ctx.file.path.clone(),
119                start_line: c.start_line,
120                start_col: c.name_col,
121                end_line: c.start_line,
122                end_col: c.name_end_col,
123                name: Some(c.name.clone()),
124            },
125            message: format!("Class `{}` is too large ({})", c.name, reasons.join(", ")),
126            suggested_refactorings: vec!["Extract Class".into()],
127            actual_value: Some(c.line_count as f64),
128            threshold: Some(self.max_class_lines as f64),
129            risk_score: None,
130        })
131    }
132
133    fn check_file(&self, ctx: &AnalysisContext, findings: &mut Vec<Finding>) {
134        if ctx.model.total_lines > self.max_file_lines {
135            findings.push(Finding {
136                smell_name: "large_file".into(),
137                category: SmellCategory::Bloaters,
138                severity: severity_for_ratio(ctx.model.total_lines, self.max_file_lines),
139                location: Location {
140                    path: ctx.file.path.clone(),
141                    start_line: 1,
142                    end_line: 1,
143                    name: None,
144                    ..Default::default()
145                },
146                message: format!(
147                    "File is {} lines (threshold: {})",
148                    ctx.model.total_lines, self.max_file_lines
149                ),
150                suggested_refactorings: vec!["Extract Class".into(), "Move Method".into()],
151                actual_value: Some(ctx.model.total_lines as f64),
152                threshold: Some(self.max_file_lines as f64),
153                risk_score: None,
154            });
155        }
156    }
157}
158
159fn severity_for_ratio(actual: usize, threshold: usize) -> Severity {
160    let ratio = actual as f64 / threshold as f64;
161    if ratio > 2.0 {
162        Severity::Error
163    } else {
164        Severity::Warning
165    }
166}
167
168fn risk_severity(risk: f64) -> Severity {
169    if risk >= 4.0 {
170        Severity::Error
171    } else if risk >= 2.0 {
172        Severity::Warning
173    } else {
174        Severity::Hint
175    }
176}