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        for f in &ctx.model.functions {
43            if f.line_count > self.max_function_lines {
44                findings.push(Finding {
45                    smell_name: "long_method".into(),
46                    category: SmellCategory::Bloaters,
47                    severity: severity_for_ratio(f.line_count, self.max_function_lines),
48                    location: Location {
49                        path: ctx.file.path.clone(),
50                        start_line: f.start_line,
51                        end_line: f.end_line,
52                        name: Some(f.name.clone()),
53                    },
54                    message: format!(
55                        "Function `{}` is {} lines (threshold: {})",
56                        f.name, f.line_count, self.max_function_lines
57                    ),
58                    suggested_refactorings: vec!["Extract Method".into()],
59                    actual_value: Some(f.line_count as f64),
60                    threshold: Some(self.max_function_lines as f64),
61                });
62            }
63        }
64    }
65
66    fn check_classes(&self, ctx: &AnalysisContext, findings: &mut Vec<Finding>) {
67        for c in &ctx.model.classes {
68            if let Some(f) = self.check_single_class(ctx, c) {
69                findings.push(f);
70            }
71        }
72    }
73
74    /// Build a finding for a single class if it exceeds size thresholds.
75    fn check_single_class(&self, ctx: &AnalysisContext, c: &crate::ClassInfo) -> Option<Finding> {
76        let over_methods = c.method_count > self.max_class_methods;
77        let over_lines = c.line_count > self.max_class_lines;
78        if !over_methods && !over_lines {
79            return None;
80        }
81        let mut reasons = Vec::new();
82        if over_methods {
83            reasons.push(format!("{} methods", c.method_count));
84        }
85        if over_lines {
86            reasons.push(format!("{} lines", c.line_count));
87        }
88        Some(Finding {
89            smell_name: "large_class".into(),
90            category: SmellCategory::Bloaters,
91            severity: if over_methods && over_lines {
92                Severity::Error
93            } else {
94                Severity::Warning
95            },
96            location: Location {
97                path: ctx.file.path.clone(),
98                start_line: c.start_line,
99                end_line: c.end_line,
100                name: Some(c.name.clone()),
101            },
102            message: format!("Class `{}` is too large ({})", c.name, reasons.join(", ")),
103            suggested_refactorings: vec!["Extract Class".into()],
104            actual_value: Some(c.line_count as f64),
105            threshold: Some(self.max_class_lines as f64),
106        })
107    }
108
109    fn check_file(&self, ctx: &AnalysisContext, findings: &mut Vec<Finding>) {
110        if ctx.model.total_lines > self.max_file_lines {
111            findings.push(Finding {
112                smell_name: "large_file".into(),
113                category: SmellCategory::Bloaters,
114                severity: severity_for_ratio(ctx.model.total_lines, self.max_file_lines),
115                location: Location {
116                    path: ctx.file.path.clone(),
117                    start_line: 1,
118                    end_line: ctx.model.total_lines,
119                    name: None,
120                },
121                message: format!(
122                    "File is {} lines (threshold: {})",
123                    ctx.model.total_lines, self.max_file_lines
124                ),
125                suggested_refactorings: vec!["Extract Class".into(), "Move Method".into()],
126                actual_value: Some(ctx.model.total_lines as f64),
127                threshold: Some(self.max_file_lines as f64),
128            });
129        }
130    }
131}
132
133fn severity_for_ratio(actual: usize, threshold: usize) -> Severity {
134    let ratio = actual as f64 / threshold as f64;
135    if ratio > 2.0 {
136        Severity::Error
137    } else {
138        Severity::Warning
139    }
140}