cha_core/plugins/
length.rs1use crate::{AnalysisContext, Finding, Location, Plugin, Severity, SmellCategory};
2
3pub 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; 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 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: ctx.model.total_lines,
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}