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