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