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}
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; 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 });
79 }
80 }
81
82 fn check_classes(&self, ctx: &AnalysisContext, findings: &mut Vec<Finding>) {
83 for c in &ctx.model.classes {
84 if let Some(f) = self.check_single_class(ctx, c) {
85 findings.push(f);
86 }
87 }
88 }
89
90 fn check_single_class(&self, ctx: &AnalysisContext, c: &crate::ClassInfo) -> Option<Finding> {
92 let over_methods = c.method_count > self.max_class_methods;
93 let over_lines = c.line_count > self.max_class_lines;
94 if !over_methods && !over_lines {
95 return None;
96 }
97 let is_error = over_methods && over_lines;
98 let reasons: Vec<_> = [
99 over_methods.then(|| format!("{} methods", c.method_count)),
100 over_lines.then(|| format!("{} lines", c.line_count)),
101 ]
102 .into_iter()
103 .flatten()
104 .collect();
105 let severity = if is_error {
106 Severity::Error
107 } else {
108 Severity::Warning
109 };
110 Some(Finding {
111 smell_name: "large_class".into(),
112 category: SmellCategory::Bloaters,
113 severity,
114 location: Location {
115 path: ctx.file.path.clone(),
116 start_line: c.start_line,
117 start_col: c.name_col,
118 end_line: c.start_line,
119 end_col: c.name_end_col,
120 name: Some(c.name.clone()),
121 },
122 message: format!("Class `{}` is too large ({})", c.name, reasons.join(", ")),
123 suggested_refactorings: vec!["Extract Class".into()],
124 actual_value: Some(c.line_count as f64),
125 threshold: Some(self.max_class_lines as f64),
126 })
127 }
128
129 fn check_file(&self, ctx: &AnalysisContext, findings: &mut Vec<Finding>) {
130 if ctx.model.total_lines > self.max_file_lines {
131 findings.push(Finding {
132 smell_name: "large_file".into(),
133 category: SmellCategory::Bloaters,
134 severity: severity_for_ratio(ctx.model.total_lines, self.max_file_lines),
135 location: Location {
136 path: ctx.file.path.clone(),
137 start_line: 1,
138 end_line: 1,
139 name: None,
140 ..Default::default()
141 },
142 message: format!(
143 "File is {} lines (threshold: {})",
144 ctx.model.total_lines, self.max_file_lines
145 ),
146 suggested_refactorings: vec!["Extract Class".into(), "Move Method".into()],
147 actual_value: Some(ctx.model.total_lines as f64),
148 threshold: Some(self.max_file_lines as f64),
149 });
150 }
151 }
152}
153
154fn severity_for_ratio(actual: usize, threshold: usize) -> Severity {
155 let ratio = actual as f64 / threshold as f64;
156 if ratio > 2.0 {
157 Severity::Error
158 } else {
159 Severity::Warning
160 }
161}
162
163fn risk_severity(risk: f64) -> Severity {
164 if risk >= 4.0 {
165 Severity::Error
166 } else if risk >= 2.0 {
167 Severity::Warning
168 } else {
169 Severity::Hint
170 }
171}