1use 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 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 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}