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