1use crate::core::{DebtItem, DebtType, FunctionMetrics, Priority};
2use std::path::{Path, PathBuf};
3
4#[derive(Debug, Clone, PartialEq)]
6pub enum SmellType {
7 LongParameterList,
8 LargeClass,
9 LongMethod,
10 FeatureEnvy,
11 DataClump,
12 DeepNesting,
13 DuplicateCode,
14}
15
16#[derive(Debug, Clone)]
18pub struct CodeSmell {
19 pub smell_type: SmellType,
20 pub location: PathBuf,
21 pub line: usize,
22 pub message: String,
23 pub severity: Priority,
24}
25
26impl CodeSmell {
27 pub fn to_debt_item(&self) -> DebtItem {
29 DebtItem {
30 id: format!(
31 "smell-{:?}-{}-{}",
32 self.smell_type,
33 self.location.display(),
34 self.line
35 ),
36 debt_type: DebtType::CodeSmell,
37 priority: self.severity,
38 file: self.location.clone(),
39 line: self.line,
40 message: self.message.clone(),
41 context: None,
42 }
43 }
44}
45
46pub fn detect_long_parameter_list(func: &FunctionMetrics, param_count: usize) -> Option<CodeSmell> {
48 const THRESHOLD: usize = 5;
49
50 if param_count > THRESHOLD {
51 Some(CodeSmell {
52 smell_type: SmellType::LongParameterList,
53 location: func.file.clone(),
54 line: func.line,
55 message: format!(
56 "Function '{}' has {} parameters (threshold: {})",
57 func.name, param_count, THRESHOLD
58 ),
59 severity: if param_count > THRESHOLD * 2 {
60 Priority::High
61 } else {
62 Priority::Medium
63 },
64 })
65 } else {
66 None
67 }
68}
69
70pub fn detect_large_module(path: &Path, line_count: usize) -> Option<CodeSmell> {
72 const THRESHOLD: usize = 300;
73
74 if line_count > THRESHOLD {
75 Some(CodeSmell {
76 smell_type: SmellType::LargeClass,
77 location: path.to_path_buf(),
78 line: 1,
79 message: format!("Module has {line_count} lines (threshold: {THRESHOLD})"),
80 severity: if line_count > THRESHOLD * 2 {
81 Priority::High
82 } else {
83 Priority::Medium
84 },
85 })
86 } else {
87 None
88 }
89}
90
91pub fn detect_long_method(func: &FunctionMetrics) -> Option<CodeSmell> {
93 const THRESHOLD: usize = 50;
94
95 if func.length > THRESHOLD {
96 Some(CodeSmell {
97 smell_type: SmellType::LongMethod,
98 location: func.file.clone(),
99 line: func.line,
100 message: format!(
101 "Function '{}' has {} lines (threshold: {})",
102 func.name, func.length, THRESHOLD
103 ),
104 severity: if func.length > THRESHOLD * 2 {
105 Priority::High
106 } else {
107 Priority::Medium
108 },
109 })
110 } else {
111 None
112 }
113}
114
115pub fn detect_deep_nesting(func: &FunctionMetrics) -> Option<CodeSmell> {
117 const THRESHOLD: u32 = 4;
118
119 if func.nesting > THRESHOLD {
120 Some(CodeSmell {
121 smell_type: SmellType::DeepNesting,
122 location: func.file.clone(),
123 line: func.line,
124 message: format!(
125 "Function '{}' has nesting depth of {} (threshold: {})",
126 func.name, func.nesting, THRESHOLD
127 ),
128 severity: if func.nesting > THRESHOLD * 2 {
129 Priority::High
130 } else {
131 Priority::Medium
132 },
133 })
134 } else {
135 None
136 }
137}
138
139pub fn analyze_function_smells(func: &FunctionMetrics, param_count: usize) -> Vec<CodeSmell> {
141 let mut smells = Vec::new();
142
143 if let Some(smell) = detect_long_parameter_list(func, param_count) {
144 smells.push(smell);
145 }
146
147 if let Some(smell) = detect_long_method(func) {
148 smells.push(smell);
149 }
150
151 if let Some(smell) = detect_deep_nesting(func) {
152 smells.push(smell);
153 }
154
155 smells
156}
157
158pub fn analyze_module_smells(path: &Path, line_count: usize) -> Vec<CodeSmell> {
160 let mut smells = Vec::new();
161
162 if let Some(smell) = detect_large_module(path, line_count) {
163 smells.push(smell);
164 }
165
166 smells
167}
168
169pub fn detect_feature_envy(content: &str, path: &Path) -> Vec<CodeSmell> {
172 let mut smells = Vec::new();
173
174 for (line_num, line) in content.lines().enumerate() {
176 let other_calls = line.matches('.').count() - line.matches("self.").count();
177 let self_calls = line.matches("self.").count();
178
179 if other_calls > 3 && other_calls > self_calls * 2 {
180 smells.push(CodeSmell {
181 smell_type: SmellType::FeatureEnvy,
182 location: path.to_path_buf(),
183 line: line_num + 1,
184 message: format!(
185 "Line has {other_calls} external method calls vs {self_calls} self calls"
186 ),
187 severity: Priority::Low,
188 });
189 }
190 }
191
192 smells
193}
194
195pub fn detect_data_clumps(functions: &[FunctionMetrics]) -> Vec<CodeSmell> {
197 let mut smells = Vec::new();
198
199 for i in 0..functions.len() {
202 for j in i + 1..functions.len() {
203 if functions[i].file == functions[j].file {
206 if functions[i].length > 30 && functions[j].length > 30 {
208 smells.push(CodeSmell {
209 smell_type: SmellType::DataClump,
210 location: functions[i].file.clone(),
211 line: functions[i].line,
212 message: format!(
213 "Functions '{}' and '{}' may share data clumps",
214 functions[i].name, functions[j].name
215 ),
216 severity: Priority::Low,
217 });
218 break; }
220 }
221 }
222 }
223
224 smells
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230 use crate::core::FunctionMetrics;
231 use std::path::PathBuf;
232
233 #[test]
234 fn test_detect_data_clumps_empty_functions() {
235 let functions = vec![];
236 let smells = detect_data_clumps(&functions);
237 assert_eq!(
238 smells.len(),
239 0,
240 "No smells should be detected for empty input"
241 );
242 }
243
244 #[test]
245 fn test_detect_data_clumps_single_function() {
246 let functions = vec![FunctionMetrics {
247 name: "large_function".to_string(),
248 file: PathBuf::from("src/lib.rs"),
249 line: 10,
250 cyclomatic: 5,
251 cognitive: 10,
252 nesting: 2,
253 length: 35,
254 is_test: false,
255 visibility: None,
256 }];
257 let smells = detect_data_clumps(&functions);
258 assert_eq!(
259 smells.len(),
260 0,
261 "Single function cannot have data clumps with itself"
262 );
263 }
264
265 #[test]
266 fn test_detect_data_clumps_different_files() {
267 let functions = vec![
268 FunctionMetrics {
269 name: "function_a".to_string(),
270 file: PathBuf::from("src/module_a.rs"),
271 line: 10,
272 cyclomatic: 5,
273 cognitive: 10,
274 nesting: 2,
275 length: 35,
276 is_test: false,
277 visibility: None,
278 },
279 FunctionMetrics {
280 name: "function_b".to_string(),
281 file: PathBuf::from("src/module_b.rs"),
282 line: 20,
283 cyclomatic: 5,
284 cognitive: 10,
285 nesting: 2,
286 length: 35,
287 is_test: false,
288 visibility: None,
289 },
290 ];
291 let smells = detect_data_clumps(&functions);
292 assert_eq!(
293 smells.len(),
294 0,
295 "Functions in different files should not be reported as data clumps"
296 );
297 }
298
299 #[test]
300 fn test_detect_data_clumps_same_file_large_functions() {
301 let functions = vec![
302 FunctionMetrics {
303 name: "process_user_data".to_string(),
304 file: PathBuf::from("src/user_handler.rs"),
305 line: 10,
306 cyclomatic: 8,
307 cognitive: 15,
308 nesting: 3,
309 length: 40,
310 is_test: false,
311 visibility: None,
312 },
313 FunctionMetrics {
314 name: "validate_user_data".to_string(),
315 file: PathBuf::from("src/user_handler.rs"),
316 line: 60,
317 cyclomatic: 6,
318 cognitive: 12,
319 nesting: 2,
320 length: 35,
321 is_test: false,
322 visibility: None,
323 },
324 ];
325 let smells = detect_data_clumps(&functions);
326 assert_eq!(
327 smells.len(),
328 1,
329 "Should detect data clump for large functions in same file"
330 );
331
332 let smell = &smells[0];
333 assert_eq!(smell.smell_type, SmellType::DataClump);
334 assert_eq!(smell.location, PathBuf::from("src/user_handler.rs"));
335 assert_eq!(smell.line, 10);
336 assert!(smell.message.contains("process_user_data"));
337 assert!(smell.message.contains("validate_user_data"));
338 assert_eq!(smell.severity, Priority::Low);
339 }
340
341 #[test]
342 fn test_detect_data_clumps_multiple_clumps() {
343 let functions = vec![
344 FunctionMetrics {
345 name: "func_a".to_string(),
346 file: PathBuf::from("src/module.rs"),
347 line: 10,
348 cyclomatic: 5,
349 cognitive: 10,
350 nesting: 2,
351 length: 35,
352 is_test: false,
353 visibility: None,
354 },
355 FunctionMetrics {
356 name: "func_b".to_string(),
357 file: PathBuf::from("src/module.rs"),
358 line: 50,
359 cyclomatic: 5,
360 cognitive: 10,
361 nesting: 2,
362 length: 32,
363 is_test: false,
364 visibility: None,
365 },
366 FunctionMetrics {
367 name: "func_c".to_string(),
368 file: PathBuf::from("src/module.rs"),
369 line: 90,
370 cyclomatic: 5,
371 cognitive: 10,
372 nesting: 2,
373 length: 31,
374 is_test: false,
375 visibility: None,
376 },
377 FunctionMetrics {
378 name: "small_func".to_string(),
379 file: PathBuf::from("src/module.rs"),
380 line: 130,
381 cyclomatic: 2,
382 cognitive: 3,
383 nesting: 1,
384 length: 10,
385 is_test: false,
386 visibility: None,
387 },
388 ];
389 let smells = detect_data_clumps(&functions);
390
391 assert_eq!(smells.len(), 2, "Should detect multiple data clumps");
394
395 assert_eq!(smells[0].line, 10);
397 assert!(smells[0].message.contains("func_a"));
398 assert!(smells[0].message.contains("func_b"));
399
400 assert_eq!(smells[1].line, 50);
402 assert!(smells[1].message.contains("func_b"));
403 assert!(smells[1].message.contains("func_c"));
404 }
405}