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 column: None,
41 message: self.message.clone(),
42 context: None,
43 }
44 }
45}
46
47pub fn detect_long_parameter_list(func: &FunctionMetrics, param_count: usize) -> Option<CodeSmell> {
49 const THRESHOLD: usize = 5;
50
51 if param_count > THRESHOLD {
52 Some(CodeSmell {
53 smell_type: SmellType::LongParameterList,
54 location: func.file.clone(),
55 line: func.line,
56 message: format!(
57 "Function '{}' has {} parameters (threshold: {})",
58 func.name, param_count, THRESHOLD
59 ),
60 severity: if param_count > THRESHOLD * 2 {
61 Priority::High
62 } else {
63 Priority::Medium
64 },
65 })
66 } else {
67 None
68 }
69}
70
71pub fn detect_large_module(path: &Path, line_count: usize) -> Option<CodeSmell> {
73 const THRESHOLD: usize = 300;
74
75 if line_count > THRESHOLD {
76 Some(CodeSmell {
77 smell_type: SmellType::LargeClass,
78 location: path.to_path_buf(),
79 line: 1,
80 message: format!("Module has {line_count} lines (threshold: {THRESHOLD})"),
81 severity: if line_count > THRESHOLD * 2 {
82 Priority::High
83 } else {
84 Priority::Medium
85 },
86 })
87 } else {
88 None
89 }
90}
91
92pub fn detect_long_method(func: &FunctionMetrics) -> Option<CodeSmell> {
94 const THRESHOLD: usize = 50;
95
96 if func.length > THRESHOLD {
97 Some(CodeSmell {
98 smell_type: SmellType::LongMethod,
99 location: func.file.clone(),
100 line: func.line,
101 message: format!(
102 "Function '{}' has {} lines (threshold: {})",
103 func.name, func.length, THRESHOLD
104 ),
105 severity: if func.length > THRESHOLD * 2 {
106 Priority::High
107 } else {
108 Priority::Medium
109 },
110 })
111 } else {
112 None
113 }
114}
115
116pub fn detect_deep_nesting(func: &FunctionMetrics) -> Option<CodeSmell> {
118 const THRESHOLD: u32 = 4;
119
120 if func.nesting > THRESHOLD {
121 Some(CodeSmell {
122 smell_type: SmellType::DeepNesting,
123 location: func.file.clone(),
124 line: func.line,
125 message: format!(
126 "Function '{}' has nesting depth of {} (threshold: {})",
127 func.name, func.nesting, THRESHOLD
128 ),
129 severity: if func.nesting > THRESHOLD * 2 {
130 Priority::High
131 } else {
132 Priority::Medium
133 },
134 })
135 } else {
136 None
137 }
138}
139
140pub fn analyze_function_smells(func: &FunctionMetrics, param_count: usize) -> Vec<CodeSmell> {
142 let mut smells = Vec::new();
143
144 if let Some(smell) = detect_long_parameter_list(func, param_count) {
145 smells.push(smell);
146 }
147
148 if let Some(smell) = detect_long_method(func) {
149 smells.push(smell);
150 }
151
152 if let Some(smell) = detect_deep_nesting(func) {
153 smells.push(smell);
154 }
155
156 smells
157}
158
159pub fn analyze_module_smells(path: &Path, line_count: usize) -> Vec<CodeSmell> {
161 let mut smells = Vec::new();
162
163 if let Some(smell) = detect_large_module(path, line_count) {
164 smells.push(smell);
165 }
166
167 smells
168}
169
170pub fn detect_feature_envy(content: &str, path: &Path) -> Vec<CodeSmell> {
173 let mut smells = Vec::new();
174 let mut object_calls: std::collections::HashMap<String, usize> =
175 std::collections::HashMap::new();
176 let mut self_calls = 0;
177
178 for line in content.lines() {
180 self_calls += line.matches("self.").count();
182
183 let trimmed = line.trim();
186 if let Some(dot_pos) = trimmed.find('.') {
187 if dot_pos > 0 {
188 let before_dot = &trimmed[..dot_pos];
189 let object_name = before_dot.split_whitespace().last().unwrap_or("");
190
191 if !object_name.is_empty()
193 && object_name != "self"
194 && object_name
195 .chars()
196 .next()
197 .is_some_and(|c| c.is_alphabetic() || c == '_')
198 && !object_name.contains('(')
199 && !object_name.contains('"')
200 && !object_name.contains('\'')
201 {
202 *object_calls.entry(object_name.to_string()).or_insert(0) += 1;
203 }
204 }
205 }
206 }
207
208 for (object, count) in object_calls {
210 if count >= 3 && count > self_calls {
211 smells.push(CodeSmell {
212 smell_type: SmellType::FeatureEnvy,
213 location: path.to_path_buf(),
214 line: 1, message: format!(
216 "Possible feature envy: {} calls to '{}' vs {} self calls",
217 count, object, self_calls
218 ),
219 severity: if count > 5 {
220 Priority::Medium
221 } else {
222 Priority::Low
223 },
224 });
225 }
226 }
227
228 smells
229}
230
231pub fn detect_data_clumps(functions: &[FunctionMetrics]) -> Vec<CodeSmell> {
233 let mut smells = Vec::new();
234
235 for i in 0..functions.len() {
238 for j in i + 1..functions.len() {
239 if functions[i].file == functions[j].file {
242 if functions[i].length > 30 && functions[j].length > 30 {
244 smells.push(CodeSmell {
245 smell_type: SmellType::DataClump,
246 location: functions[i].file.clone(),
247 line: functions[i].line,
248 message: format!(
249 "Functions '{}' and '{}' may share data clumps",
250 functions[i].name, functions[j].name
251 ),
252 severity: Priority::Low,
253 });
254 break; }
256 }
257 }
258 }
259
260 smells
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266 use crate::core::FunctionMetrics;
267 use std::path::PathBuf;
268
269 #[test]
270 fn test_detect_data_clumps_empty_functions() {
271 let functions = vec![];
272 let smells = detect_data_clumps(&functions);
273 assert_eq!(
274 smells.len(),
275 0,
276 "No smells should be detected for empty input"
277 );
278 }
279
280 #[test]
281 fn test_detect_data_clumps_single_function() {
282 let functions = vec![FunctionMetrics {
283 name: "large_function".to_string(),
284 file: PathBuf::from("src/lib.rs"),
285 line: 10,
286 cyclomatic: 5,
287 cognitive: 10,
288 nesting: 2,
289 length: 35,
290 is_test: false,
291 visibility: None,
292 is_trait_method: false,
293 in_test_module: false,
294 entropy_score: None,
295 is_pure: None,
296 purity_confidence: None,
297 }];
298 let smells = detect_data_clumps(&functions);
299 assert_eq!(
300 smells.len(),
301 0,
302 "Single function cannot have data clumps with itself"
303 );
304 }
305
306 #[test]
307 fn test_detect_data_clumps_different_files() {
308 let functions = vec![
309 FunctionMetrics {
310 name: "function_a".to_string(),
311 file: PathBuf::from("src/module_a.rs"),
312 line: 10,
313 cyclomatic: 5,
314 cognitive: 10,
315 nesting: 2,
316 length: 35,
317 is_test: false,
318 visibility: None,
319 is_trait_method: false,
320 in_test_module: false,
321 entropy_score: None,
322 is_pure: None,
323 purity_confidence: None,
324 },
325 FunctionMetrics {
326 name: "function_b".to_string(),
327 file: PathBuf::from("src/module_b.rs"),
328 line: 20,
329 cyclomatic: 5,
330 cognitive: 10,
331 nesting: 2,
332 length: 35,
333 is_test: false,
334 visibility: None,
335 is_trait_method: false,
336 in_test_module: false,
337 entropy_score: None,
338 is_pure: None,
339 purity_confidence: None,
340 },
341 ];
342 let smells = detect_data_clumps(&functions);
343 assert_eq!(
344 smells.len(),
345 0,
346 "Functions in different files should not be reported as data clumps"
347 );
348 }
349
350 #[test]
351 fn test_detect_data_clumps_same_file_large_functions() {
352 let functions = vec![
353 FunctionMetrics {
354 name: "process_user_data".to_string(),
355 file: PathBuf::from("src/user_handler.rs"),
356 line: 10,
357 cyclomatic: 8,
358 cognitive: 15,
359 nesting: 3,
360 length: 40,
361 is_test: false,
362 visibility: None,
363 is_trait_method: false,
364 in_test_module: false,
365 entropy_score: None,
366 is_pure: None,
367 purity_confidence: None,
368 },
369 FunctionMetrics {
370 name: "validate_user_data".to_string(),
371 file: PathBuf::from("src/user_handler.rs"),
372 line: 60,
373 cyclomatic: 6,
374 cognitive: 12,
375 nesting: 2,
376 length: 35,
377 is_test: false,
378 visibility: None,
379 is_trait_method: false,
380 in_test_module: false,
381 entropy_score: None,
382 is_pure: None,
383 purity_confidence: None,
384 },
385 ];
386 let smells = detect_data_clumps(&functions);
387 assert_eq!(
388 smells.len(),
389 1,
390 "Should detect data clump for large functions in same file"
391 );
392
393 let smell = &smells[0];
394 assert_eq!(smell.smell_type, SmellType::DataClump);
395 assert_eq!(smell.location, PathBuf::from("src/user_handler.rs"));
396 assert_eq!(smell.line, 10);
397 assert!(smell.message.contains("process_user_data"));
398 assert!(smell.message.contains("validate_user_data"));
399 assert_eq!(smell.severity, Priority::Low);
400 }
401
402 #[test]
403 fn test_detect_data_clumps_multiple_clumps() {
404 let functions = vec![
405 FunctionMetrics {
406 name: "func_a".to_string(),
407 file: PathBuf::from("src/module.rs"),
408 line: 10,
409 cyclomatic: 5,
410 cognitive: 10,
411 nesting: 2,
412 length: 35,
413 is_test: false,
414 visibility: None,
415 is_trait_method: false,
416 in_test_module: false,
417 entropy_score: None,
418 is_pure: None,
419 purity_confidence: None,
420 },
421 FunctionMetrics {
422 name: "func_b".to_string(),
423 file: PathBuf::from("src/module.rs"),
424 line: 50,
425 cyclomatic: 5,
426 cognitive: 10,
427 nesting: 2,
428 length: 32,
429 is_test: false,
430 visibility: None,
431 is_trait_method: false,
432 in_test_module: false,
433 entropy_score: None,
434 is_pure: None,
435 purity_confidence: None,
436 },
437 FunctionMetrics {
438 name: "func_c".to_string(),
439 file: PathBuf::from("src/module.rs"),
440 line: 90,
441 cyclomatic: 5,
442 cognitive: 10,
443 nesting: 2,
444 length: 31,
445 is_test: false,
446 visibility: None,
447 is_trait_method: false,
448 in_test_module: false,
449 entropy_score: None,
450 is_pure: None,
451 purity_confidence: None,
452 },
453 FunctionMetrics {
454 name: "small_func".to_string(),
455 file: PathBuf::from("src/module.rs"),
456 line: 130,
457 cyclomatic: 2,
458 cognitive: 3,
459 nesting: 1,
460 length: 10,
461 is_test: false,
462 visibility: None,
463 is_trait_method: false,
464 in_test_module: false,
465 entropy_score: None,
466 is_pure: None,
467 purity_confidence: None,
468 },
469 ];
470 let smells = detect_data_clumps(&functions);
471
472 assert_eq!(smells.len(), 2, "Should detect multiple data clumps");
475
476 assert_eq!(smells[0].line, 10);
478 assert!(smells[0].message.contains("func_a"));
479 assert!(smells[0].message.contains("func_b"));
480
481 assert_eq!(smells[1].line, 50);
483 assert!(smells[1].message.contains("func_b"));
484 assert!(smells[1].message.contains("func_c"));
485 }
486
487 #[test]
488 fn test_detect_long_parameter_list() {
489 let func = FunctionMetrics {
490 name: "test_func".to_string(),
491 file: PathBuf::from("src/test.rs"),
492 line: 10,
493 cyclomatic: 5,
494 cognitive: 10,
495 nesting: 2,
496 length: 20,
497 is_test: false,
498 visibility: None,
499 is_trait_method: false,
500 in_test_module: false,
501 entropy_score: None,
502 is_pure: None,
503 purity_confidence: None,
504 };
505
506 let smell = detect_long_parameter_list(&func, 3);
508 assert!(smell.is_none(), "Should not detect smell for 3 parameters");
509
510 let smell = detect_long_parameter_list(&func, 5);
512 assert!(smell.is_none(), "Should not detect smell at threshold");
513
514 let smell = detect_long_parameter_list(&func, 6);
516 assert!(smell.is_some(), "Should detect smell for 6 parameters");
517 let smell = smell.unwrap();
518 assert_eq!(smell.smell_type, SmellType::LongParameterList);
519 assert_eq!(smell.severity, Priority::Medium);
520 assert!(smell.message.contains("6 parameters"));
521
522 let smell = detect_long_parameter_list(&func, 12);
524 assert!(smell.is_some(), "Should detect smell for 12 parameters");
525 let smell = smell.unwrap();
526 assert_eq!(smell.severity, Priority::High);
527 }
528
529 #[test]
530 fn test_detect_large_module() {
531 let path = PathBuf::from("src/large_module.rs");
532
533 let smell = detect_large_module(&path, 250);
535 assert!(smell.is_none(), "Should not detect smell for 250 lines");
536
537 let smell = detect_large_module(&path, 300);
539 assert!(smell.is_none(), "Should not detect smell at threshold");
540
541 let smell = detect_large_module(&path, 350);
543 assert!(smell.is_some(), "Should detect smell for 350 lines");
544 let smell = smell.unwrap();
545 assert_eq!(smell.smell_type, SmellType::LargeClass);
546 assert_eq!(smell.severity, Priority::Medium);
547 assert!(smell.message.contains("350 lines"));
548
549 let smell = detect_large_module(&path, 700);
551 assert!(smell.is_some(), "Should detect smell for 700 lines");
552 let smell = smell.unwrap();
553 assert_eq!(smell.severity, Priority::High);
554 }
555
556 #[test]
557 fn test_detect_long_method() {
558 let func = FunctionMetrics {
559 name: "long_func".to_string(),
560 file: PathBuf::from("src/test.rs"),
561 line: 10,
562 cyclomatic: 5,
563 cognitive: 10,
564 nesting: 2,
565 length: 40,
566 is_test: false,
567 visibility: None,
568 is_trait_method: false,
569 in_test_module: false,
570 entropy_score: None,
571 is_pure: None,
572 purity_confidence: None,
573 };
574
575 let smell = detect_long_method(&func);
577 assert!(smell.is_none(), "Should not detect smell for 40 lines");
578
579 let mut long_func = func.clone();
581 long_func.length = 60;
582 let smell = detect_long_method(&long_func);
583 assert!(smell.is_some(), "Should detect smell for 60 lines");
584 let smell = smell.unwrap();
585 assert_eq!(smell.smell_type, SmellType::LongMethod);
586 assert_eq!(smell.severity, Priority::Medium);
587 assert!(smell.message.contains("60 lines"));
588
589 long_func.length = 120;
591 let smell = detect_long_method(&long_func);
592 assert!(smell.is_some(), "Should detect smell for 120 lines");
593 let smell = smell.unwrap();
594 assert_eq!(smell.severity, Priority::High);
595 }
596
597 #[test]
598 fn test_detect_deep_nesting() {
599 let func = FunctionMetrics {
600 name: "nested_func".to_string(),
601 file: PathBuf::from("src/test.rs"),
602 line: 10,
603 cyclomatic: 5,
604 cognitive: 10,
605 nesting: 3,
606 length: 30,
607 is_test: false,
608 visibility: None,
609 is_trait_method: false,
610 in_test_module: false,
611 entropy_score: None,
612 is_pure: None,
613 purity_confidence: None,
614 };
615
616 let smell = detect_deep_nesting(&func);
618 assert!(
619 smell.is_none(),
620 "Should not detect smell for nesting depth 3"
621 );
622
623 let mut nested_func = func.clone();
625 nested_func.nesting = 4;
626 let smell = detect_deep_nesting(&nested_func);
627 assert!(smell.is_none(), "Should not detect smell at threshold");
628
629 nested_func.nesting = 5;
631 let smell = detect_deep_nesting(&nested_func);
632 assert!(smell.is_some(), "Should detect smell for nesting depth 5");
633 let smell = smell.unwrap();
634 assert_eq!(smell.smell_type, SmellType::DeepNesting);
635 assert_eq!(smell.severity, Priority::Medium);
636 assert!(smell.message.contains("nesting depth of 5"));
637
638 nested_func.nesting = 10;
640 let smell = detect_deep_nesting(&nested_func);
641 assert!(smell.is_some(), "Should detect smell for nesting depth 10");
642 let smell = smell.unwrap();
643 assert_eq!(smell.severity, Priority::High);
644 }
645
646 #[test]
647 fn test_analyze_function_smells() {
648 let func = FunctionMetrics {
649 name: "complex_func".to_string(),
650 file: PathBuf::from("src/test.rs"),
651 line: 10,
652 cyclomatic: 5,
653 cognitive: 10,
654 nesting: 5,
655 length: 60,
656 is_test: false,
657 visibility: None,
658 is_trait_method: false,
659 in_test_module: false,
660 entropy_score: None,
661 is_pure: None,
662 purity_confidence: None,
663 };
664
665 let smells = analyze_function_smells(&func, 7);
667 assert_eq!(smells.len(), 3, "Should detect 3 smells");
668
669 let smell_types: Vec<SmellType> = smells.iter().map(|s| s.smell_type.clone()).collect();
671 assert!(smell_types.contains(&SmellType::LongParameterList));
672 assert!(smell_types.contains(&SmellType::LongMethod));
673 assert!(smell_types.contains(&SmellType::DeepNesting));
674
675 let clean_func = FunctionMetrics {
677 name: "clean_func".to_string(),
678 file: PathBuf::from("src/test.rs"),
679 line: 10,
680 cyclomatic: 3,
681 cognitive: 5,
682 nesting: 2,
683 length: 20,
684 is_test: false,
685 visibility: None,
686 is_trait_method: false,
687 in_test_module: false,
688 entropy_score: None,
689 is_pure: None,
690 purity_confidence: None,
691 };
692
693 let smells = analyze_function_smells(&clean_func, 3);
694 assert_eq!(smells.len(), 0, "Clean function should have no smells");
695 }
696
697 #[test]
698 fn test_analyze_module_smells() {
699 let path = PathBuf::from("src/module.rs");
700
701 let smells = analyze_module_smells(&path, 200);
703 assert_eq!(smells.len(), 0, "Small module should have no smells");
704
705 let smells = analyze_module_smells(&path, 400);
707 assert_eq!(smells.len(), 1, "Large module should have 1 smell");
708 assert_eq!(smells[0].smell_type, SmellType::LargeClass);
709
710 let smells = analyze_module_smells(&path, 300);
712 assert_eq!(smells.len(), 0, "Module at threshold should have no smells");
713 }
714
715 #[test]
716 fn test_detect_feature_envy() {
717 let path = PathBuf::from("src/test.rs");
718
719 let content = r#"
721 fn process_data(&self) {
722 self.validate();
723 self.transform();
724 self.save();
725 }
726 "#;
727 let smells = detect_feature_envy(content, &path);
728 assert_eq!(smells.len(), 0, "Should not detect feature envy");
729
730 let content = r#"
732 fn process_order(&self, customer: &Customer) {
733 customer.validate_address();
734 customer.check_credit();
735 customer.update_status();
736 customer.send_notification();
737 customer.log_activity();
738 self.save();
739 }
740 "#;
741 let smells = detect_feature_envy(content, &path);
742 assert!(!smells.is_empty(), "Should detect feature envy");
743 assert_eq!(smells[0].smell_type, SmellType::FeatureEnvy);
744 assert!(smells[0].message.contains("customer"));
745
746 let content = r#"
748 fn coordinate(&self, order: &Order, payment: &Payment) {
749 order.validate();
750 order.calculate_total();
751 order.apply_discount();
752 payment.process();
753 payment.verify();
754 payment.record();
755 }
756 "#;
757 let smells = detect_feature_envy(content, &path);
758 assert_eq!(
759 smells.len(),
760 2,
761 "Should detect feature envy for both objects"
762 );
763 }
764
765 #[test]
766 fn test_code_smell_to_debt_item() {
767 let smell = CodeSmell {
768 smell_type: SmellType::LongMethod,
769 location: PathBuf::from("src/test.rs"),
770 line: 42,
771 message: "Test message".to_string(),
772 severity: Priority::High,
773 };
774
775 let debt_item = smell.to_debt_item();
776 assert_eq!(debt_item.debt_type, DebtType::CodeSmell);
777 assert_eq!(debt_item.file, PathBuf::from("src/test.rs"));
778 assert_eq!(debt_item.line, 42);
779 assert_eq!(debt_item.message, "Test message");
780 assert_eq!(debt_item.priority, Priority::High);
781 }
782}