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