1use crate::language::Language;
7use crate::signals::{SignalDetector, StyleSignal};
8use crate::style_ir::StyleIr;
9use crate::treesitter::duplication::IntraFileDupDetector;
10use crate::treesitter::engine::ParsedFile;
11
12const ADAPTER_LANGUAGES: &[Language] = &[
14 Language::Rust,
15 Language::Python,
16 Language::JavaScript,
17 Language::TypeScript,
18 Language::Go,
19 Language::Java,
20 Language::Ruby,
21 Language::Swift,
22 Language::Zig,
23 Language::C,
24 Language::Cpp,
25];
26
27pub struct PanicAddictionDetector;
31
32impl PanicAddictionDetector {
33 pub fn new() -> Self {
34 Self
35 }
36}
37
38impl Default for PanicAddictionDetector {
39 fn default() -> Self {
40 Self::new()
41 }
42}
43
44impl SignalDetector for PanicAddictionDetector {
45 fn signal(&self) -> StyleSignal {
46 StyleSignal::PanicAddiction
47 }
48
49 fn supported_languages(&self) -> &'static [Language] {
50 ADAPTER_LANGUAGES
51 }
52
53 fn count_violations(&self, file: &ParsedFile) -> usize {
54 StyleIr::from_parsed(file)
55 .map(|ir| ir.panic_call_count)
56 .unwrap_or(0)
57 }
58
59 fn count_violations_with_ir(&self, ir: &StyleIr, _file: &ParsedFile) -> usize {
60 ir.panic_call_count
61 }
62}
63
64pub struct NamingChaosDetector;
69
70impl NamingChaosDetector {
71 pub fn new() -> Self {
72 Self
73 }
74}
75
76impl Default for NamingChaosDetector {
77 fn default() -> Self {
78 Self::new()
79 }
80}
81
82impl SignalDetector for NamingChaosDetector {
83 fn signal(&self) -> StyleSignal {
84 StyleSignal::NamingChaos
85 }
86
87 fn supported_languages(&self) -> &'static [Language] {
88 ADAPTER_LANGUAGES
89 }
90
91 fn count_violations(&self, file: &ParsedFile) -> usize {
92 StyleIr::from_parsed(file)
93 .map(|ir| ir.naming_violation_count)
94 .unwrap_or(0)
95 }
96
97 fn count_violations_with_ir(&self, ir: &StyleIr, _file: &ParsedFile) -> usize {
98 ir.naming_violation_count
99 }
100}
101
102pub struct NestedHellDetector;
106
107impl NestedHellDetector {
108 pub fn new() -> Self {
109 Self
110 }
111}
112
113impl Default for NestedHellDetector {
114 fn default() -> Self {
115 Self::new()
116 }
117}
118
119impl SignalDetector for NestedHellDetector {
120 fn signal(&self) -> StyleSignal {
121 StyleSignal::NestedHell
122 }
123
124 fn supported_languages(&self) -> &'static [Language] {
125 ADAPTER_LANGUAGES
126 }
127
128 fn count_violations(&self, file: &ParsedFile) -> usize {
129 StyleIr::from_parsed(file)
130 .map(|ir| ir.deeply_nested_block_count + ir.defer_in_loop_count)
131 .unwrap_or(0)
132 }
133
134 fn count_violations_with_ir(&self, ir: &StyleIr, _file: &ParsedFile) -> usize {
135 ir.deeply_nested_block_count + ir.defer_in_loop_count
136 }
137}
138
139pub struct HotfixCultureDetector;
143
144impl HotfixCultureDetector {
145 pub fn new() -> Self {
146 Self
147 }
148}
149
150impl Default for HotfixCultureDetector {
151 fn default() -> Self {
152 Self::new()
153 }
154}
155
156impl SignalDetector for HotfixCultureDetector {
157 fn signal(&self) -> StyleSignal {
158 StyleSignal::HotfixCulture
159 }
160
161 fn supported_languages(&self) -> &'static [Language] {
162 ADAPTER_LANGUAGES
163 }
164
165 fn count_violations(&self, file: &ParsedFile) -> usize {
166 StyleIr::from_parsed(file)
167 .map(|ir| ir.debug_call_count)
168 .unwrap_or(0)
169 }
170
171 fn count_violations_with_ir(&self, ir: &StyleIr, _file: &ParsedFile) -> usize {
172 ir.debug_call_count
173 }
174}
175
176pub struct OverEngineeringDetector;
180
181impl OverEngineeringDetector {
182 pub fn new() -> Self {
183 Self
184 }
185}
186
187impl Default for OverEngineeringDetector {
188 fn default() -> Self {
189 Self::new()
190 }
191}
192
193impl SignalDetector for OverEngineeringDetector {
194 fn signal(&self) -> StyleSignal {
195 StyleSignal::OverEngineering
196 }
197
198 fn supported_languages(&self) -> &'static [Language] {
199 ADAPTER_LANGUAGES
200 }
201
202 fn count_violations(&self, file: &ParsedFile) -> usize {
203 StyleIr::from_parsed(file)
204 .map(|ir| ir.over_engineering_count())
205 .unwrap_or(0)
206 }
207
208 fn count_violations_with_ir(&self, ir: &StyleIr, _file: &ParsedFile) -> usize {
209 ir.over_engineering_count()
210 }
211}
212
213pub struct CodeSmellsDetector;
217
218impl CodeSmellsDetector {
219 pub fn new() -> Self {
220 Self
221 }
222}
223
224impl Default for CodeSmellsDetector {
225 fn default() -> Self {
226 Self::new()
227 }
228}
229
230impl SignalDetector for CodeSmellsDetector {
231 fn signal(&self) -> StyleSignal {
232 StyleSignal::CodeSmells
233 }
234
235 fn supported_languages(&self) -> &'static [Language] {
236 ADAPTER_LANGUAGES
237 }
238
239 fn count_violations(&self, file: &ParsedFile) -> usize {
240 StyleIr::from_parsed(file)
241 .map(|ir| ir.code_smell_count())
242 .unwrap_or(0)
243 }
244
245 fn count_violations_with_ir(&self, ir: &StyleIr, _file: &ParsedFile) -> usize {
246 ir.code_smell_count()
247 }
248}
249
250pub struct DuplicationDetector;
257
258impl DuplicationDetector {
259 pub fn new() -> Self {
260 Self
261 }
262}
263
264impl Default for DuplicationDetector {
265 fn default() -> Self {
266 Self::new()
267 }
268}
269
270impl SignalDetector for DuplicationDetector {
271 fn signal(&self) -> StyleSignal {
272 StyleSignal::Duplication
273 }
274
275 fn supported_languages(&self) -> &'static [Language] {
276 ADAPTER_LANGUAGES
277 }
278
279 fn count_violations(&self, file: &ParsedFile) -> usize {
280 IntraFileDupDetector::check(file).len()
281 }
282}
283
284pub struct LegacyCodeDetector;
288
289impl LegacyCodeDetector {
290 pub fn new() -> Self {
291 Self
292 }
293}
294
295impl Default for LegacyCodeDetector {
296 fn default() -> Self {
297 Self::new()
298 }
299}
300
301impl SignalDetector for LegacyCodeDetector {
302 fn signal(&self) -> StyleSignal {
303 StyleSignal::LegacyCode
304 }
305
306 fn supported_languages(&self) -> &'static [Language] {
307 ADAPTER_LANGUAGES
308 }
309
310 fn skips_test_files(&self) -> bool {
311 false
312 }
313
314 fn count_violations(&self, file: &ParsedFile) -> usize {
315 StyleIr::from_parsed(file)
316 .map(|ir| ir.commented_out_lines)
317 .unwrap_or(0)
318 }
319
320 fn count_violations_with_ir(&self, ir: &StyleIr, _file: &ParsedFile) -> usize {
321 ir.commented_out_lines
322 }
323}
324
325pub struct TodoMountainDetector;
329
330impl TodoMountainDetector {
331 pub fn new() -> Self {
332 Self
333 }
334}
335
336impl Default for TodoMountainDetector {
337 fn default() -> Self {
338 Self::new()
339 }
340}
341
342impl SignalDetector for TodoMountainDetector {
343 fn signal(&self) -> StyleSignal {
344 StyleSignal::TodoMountain
345 }
346
347 fn supported_languages(&self) -> &'static [Language] {
348 ADAPTER_LANGUAGES
349 }
350
351 fn skips_test_files(&self) -> bool {
352 false
353 }
354
355 fn count_violations(&self, file: &ParsedFile) -> usize {
356 StyleIr::from_parsed(file)
357 .map(|ir| ir.todo_count)
358 .unwrap_or(0)
359 }
360
361 fn count_violations_with_ir(&self, ir: &StyleIr, _file: &ParsedFile) -> usize {
362 ir.todo_count
363 }
364}
365
366pub struct LineCountSmellDetector;
370
371impl LineCountSmellDetector {
372 pub fn new() -> Self {
373 Self
374 }
375}
376
377impl Default for LineCountSmellDetector {
378 fn default() -> Self {
379 Self::new()
380 }
381}
382
383impl SignalDetector for LineCountSmellDetector {
384 fn signal(&self) -> StyleSignal {
385 StyleSignal::LineCountSmell
386 }
387
388 fn supported_languages(&self) -> &'static [Language] {
389 ADAPTER_LANGUAGES
390 }
391
392 fn skips_test_files(&self) -> bool {
393 false
394 }
395
396 fn count_violations(&self, file: &ParsedFile) -> usize {
397 let line_count = file.content.lines().count();
398 let is_test = file.path.to_string_lossy().contains("test");
399 let threshold = if is_test { 2000 } else { 1000 };
400 if line_count > threshold {
401 line_count
402 } else {
403 0
404 }
405 }
406}
407
408#[cfg(test)]
409mod tests {
410 use super::*;
411
412 use crate::treesitter::engine::{ParsedFile, TreeSitterEngine};
413
414 fn parse_rust(source: &str) -> ParsedFile {
415 let engine = TreeSitterEngine::new();
416 engine
417 .parse_file(std::path::Path::new("test.rs"), source)
418 .expect("Rust parse should succeed")
419 }
420
421 #[test]
425 fn test_detector_panic_unwrap() {
426 let file = parse_rust("fn main() { let x = val.unwrap(); let y = other.unwrap(); }");
427 let detector = PanicAddictionDetector::new();
428 let count = detector.count_violations(&file);
429 assert_eq!(count, 2, "should find 2 unwrap calls, got {count}");
430 }
431
432 #[test]
434 fn test_detector_panic_expect_allowed() {
435 let file = parse_rust("fn main() { let x = val.expect(\"msg\"); }");
436 let detector = PanicAddictionDetector::new();
437 let count = detector.count_violations(&file);
438 assert_eq!(count, 0, "expect() is allowed, got {count}");
439 }
440
441 #[test]
443 fn test_detector_panic_macro() {
444 let file = parse_rust(
445 r#"
446fn main() {
447 panic!("something went wrong");
448 panic!("another panic");
449}
450"#,
451 );
452 let detector = PanicAddictionDetector::new();
453 let count = detector.count_violations(&file);
454 assert_eq!(count, 2, "should find 2 panic!() calls, got {count}");
455 }
456
457 #[test]
459 fn test_detector_panic_mixed() {
460 let file = parse_rust(
461 r#"
462fn main() {
463 let a = x.unwrap();
464 let b = y.expect("msg");
465 panic!("boom");
466}
467"#,
468 );
469 let detector = PanicAddictionDetector::new();
470 let count = detector.count_violations(&file);
471 assert_eq!(
472 count, 2,
473 "unwrap + panic = 2, expect is allowed, got {count}"
474 );
475 }
476
477 #[test]
481 fn test_detector_naming_single_letter() {
482 let file = parse_rust("fn main() { let a = 1; }");
483 let detector = NamingChaosDetector::new();
484 assert_eq!(detector.count_violations(&file), 1, "single-letter a");
485 }
486
487 #[test]
489 fn test_detector_naming_terrible() {
490 let file = parse_rust("fn main() { let data = 1; }");
491 let detector = NamingChaosDetector::new();
492 assert_eq!(detector.count_violations(&file), 1, "terrible name 'data'");
493 }
494
495 #[test]
497 fn test_detector_naming_clean() {
498 let file = parse_rust("fn main() { let user_name = \"alice\"; }");
499 let detector = NamingChaosDetector::new();
500 assert_eq!(detector.count_violations(&file), 0, "clean naming");
501 }
502
503 #[test]
507 fn test_detector_nested_hell_deep() {
508 let file = parse_rust(
509 r#"
510fn main() {
511 if true {
512 if true {
513 if true {
514 if true {
515 if true {
516 if true {
517 let x = 1;
518 }
519 }
520 }
521 }
522 }
523 }
524}
525"#,
526 );
527 let detector = NestedHellDetector::new();
528 let count = detector.count_violations(&file);
529 assert!(
530 count >= 1,
531 "6-level deep nesting should find at least 1 deeply-nested block, got {count}"
532 );
533 }
534
535 #[test]
537 fn test_detector_nested_hell_flat() {
538 let file = parse_rust(
539 r#"
540fn main() {
541 let x = 1;
542 let y = 2;
543}
544"#,
545 );
546 let detector = NestedHellDetector::new();
547 assert_eq!(
548 detector.count_violations(&file),
549 0,
550 "flat code should have 0 violations"
551 );
552 }
553
554 #[test]
556 fn test_detector_nested_hell_just_under_threshold() {
557 let file = parse_rust(
558 r#"
559fn main() {
560 if true {
561 if true {
562 if true {
563 if true {
564 let x = 1;
565 }
566 }
567 }
568 }
569}
570"#,
571 );
572 let detector = NestedHellDetector::new();
573 assert_eq!(
574 detector.count_violations(&file),
575 0,
576 "4-level nesting should be under threshold (5)"
577 );
578 }
579
580 #[test]
584 fn test_detector_hotfix_println() {
585 let file = parse_rust(
586 r#"
587fn main() {
588 println!("hello");
589 println!("world");
590}
591"#,
592 );
593 let detector = HotfixCultureDetector::new();
594 assert_eq!(detector.count_violations(&file), 2, "2 println! calls");
595 }
596
597 #[test]
599 fn test_detector_hotfix_todo() {
600 let file = parse_rust(
601 r#"
602fn main() {
603 todo!("implement this");
604 unimplemented!();
605}
606"#,
607 );
608 let detector = HotfixCultureDetector::new();
609 assert_eq!(
610 detector.count_violations(&file),
611 2,
612 "todo! + unimplemented! = 2"
613 );
614 }
615
616 #[test]
618 fn test_detector_hotfix_clean() {
619 let file = parse_rust(
620 r#"
621fn add(a: i32, b: i32) -> i32 {
622 a + b
623}
624"#,
625 );
626 let detector = HotfixCultureDetector::new();
627 assert_eq!(detector.count_violations(&file), 0, "no debug calls");
628 }
629
630 #[test]
632 fn test_detector_hotfix_dbg_eprintln() {
633 let file = parse_rust(
634 r#"
635fn main() {
636 dbg!(42);
637 eprintln!("error!");
638 eprint!("warning!");
639}
640"#,
641 );
642 let detector = HotfixCultureDetector::new();
643 assert_eq!(
644 detector.count_violations(&file),
645 3,
646 "dbg! + eprintln! + eprint! = 3"
647 );
648 }
649
650 #[test]
654 fn test_detector_overengineering_god_function() {
655 let file = parse_rust(
656 r#"
657fn main() {
658 let a = 1;
659 let b = 2;
660 let c = 3;
661 let d = 4;
662 let e = 5;
663}
664"#,
665 );
666 let detector = OverEngineeringDetector::new();
667 assert_eq!(
669 detector.count_violations(&file),
670 0,
671 "short function should not count as overengineered"
672 );
673 }
674
675 #[test]
677 fn test_detector_overengineering_excessive_params() {
678 let file = parse_rust(
679 r#"
680fn process(a: i32, b: i32, c: i32, d: i32, e: i32, f: i32) -> i32 {
681 a + b + c + d + e + f
682}
683"#,
684 );
685 let detector = OverEngineeringDetector::new();
686 assert_eq!(
687 detector.count_violations(&file),
688 1,
689 "function with 6 params should count as violation"
690 );
691 }
692
693 #[test]
695 fn test_detector_overengineering_clean() {
696 let file = parse_rust(
697 r#"
698fn add(a: i32, b: i32) -> i32 {
699 a + b
700}
701"#,
702 );
703 let detector = OverEngineeringDetector::new();
704 assert_eq!(detector.count_violations(&file), 0, "clean function");
705 }
706
707 #[test]
711 fn test_detector_code_smells_unsafe() {
712 let file = parse_rust(
713 r#"
714fn main() {
715 unsafe {
716 let p = 42 as *const i32;
717 let _ = *p;
718 }
719}
720"#,
721 );
722 let detector = CodeSmellsDetector::new();
723 let count = detector.count_violations(&file);
724 assert!(
725 count >= 2,
726 "unsafe block (2 points) should be >= 2, got {count}"
727 );
728 }
729
730 #[test]
732 fn test_detector_code_smells_magic() {
733 let file = parse_rust(
734 r#"
735fn main() {
736 let x = 1;
737 foo(42);
738 bar(100);
739}
740"#,
741 );
742 let detector = CodeSmellsDetector::new();
743 assert_eq!(detector.count_violations(&file), 2, "two magic numbers = 2");
744 }
745
746 #[test]
748 fn test_detector_code_smells_const_ok() {
749 let file = parse_rust(
750 r#"
751const MAX: i32 = 100;
752fn main() {
753 let x = MAX;
754}
755"#,
756 );
757 let detector = CodeSmellsDetector::new();
758 assert_eq!(
759 detector.count_violations(&file),
760 0,
761 "const value and no-magic should be 0"
762 );
763 }
764
765 #[test]
767 fn test_detector_code_smells_trivial_numbers_ok() {
768 let file = parse_rust(
769 r#"
770fn main() {
771 let x = 0;
772 let y = x + 1;
773}
774"#,
775 );
776 let detector = CodeSmellsDetector::new();
777 assert_eq!(detector.count_violations(&file), 0, "0 and 1 not magic");
778 }
779
780 #[test]
782 fn test_detector_code_smells_clean() {
783 let file = parse_rust(
784 r#"
785fn add(a: i32, b: i32) -> i32 {
786 a + b
787}
788"#,
789 );
790 let detector = CodeSmellsDetector::new();
791 assert_eq!(detector.count_violations(&file), 0, "clean code = 0");
792 }
793
794 #[test]
798 fn test_detector_duplication_intra_file() {
799 let file = parse_rust(
800 r#"
801fn setup_a() {
802 let x = 1;
803 let y = 2;
804 let z = 3;
805 let w = 4;
806 let v = 5;
807}
808fn setup_b() {
809 let x = 1;
810 let y = 2;
811 let z = 3;
812 let w = 4;
813 let v = 5;
814}
815"#,
816 );
817 let detector = DuplicationDetector::new();
818 let count = detector.count_violations(&file);
819 assert!(count >= 1, "duplicated blocks should be >= 1, got {count}");
820 }
821
822 #[test]
824 fn test_detector_duplication_clean() {
825 let file = parse_rust(
826 r#"
827fn add(a: i32, b: i32) -> i32 { a + b }
828fn sub(a: i32, b: i32) -> i32 { a - b }
829"#,
830 );
831 let detector = DuplicationDetector::new();
832 assert_eq!(detector.count_violations(&file), 0, "no duplication");
833 }
834
835 #[test]
837 fn test_detector_duplication_short_file() {
838 let file = parse_rust("fn main() { let x = 1; }");
839 let detector = DuplicationDetector::new();
840 assert_eq!(detector.count_violations(&file), 0, "short file = 0");
841 }
842
843 #[test]
847 fn test_detector_legacy_code_block() {
848 let file = parse_rust(
849 r#"
850fn main() {
851 // let x = 1;
852 // let y = 2;
853 // let z = 3;
854 // let w = x + y;
855 // foo(w);
856 bar();
857}
858"#,
859 );
860 let detector = LegacyCodeDetector::new();
861 assert!(
862 detector.count_violations(&file) >= 5,
863 "5 consecutive commented-out lines should be >= 5"
864 );
865 }
866
867 #[test]
869 fn test_detector_legacy_code_doc_ok() {
870 let file = parse_rust(
871 r#"
872/// Documented function
873fn documented() -> i32 {
874 // normal comment
875 42
876}
877"#,
878 );
879 let detector = LegacyCodeDetector::new();
880 assert_eq!(
881 detector.count_violations(&file),
882 0,
883 "doc comment + 1 normal = 0"
884 );
885 }
886
887 #[test]
889 fn test_detector_legacy_code_short_ok() {
890 let file = parse_rust(
891 r#"
892// short
893// comments
894// are fine
895fn main() {}
896"#,
897 );
898 let detector = LegacyCodeDetector::new();
899 assert_eq!(detector.count_violations(&file), 0, "short comments = 0");
900 }
901
902 #[test]
904 fn test_detector_legacy_code_empty() {
905 let file = parse_rust("// just a single comment");
906 let detector = LegacyCodeDetector::new();
907 assert_eq!(detector.count_violations(&file), 0);
908 }
909
910 #[test]
912 fn test_detector_legacy_code_four_lines() {
913 let file = parse_rust(
914 r#"
915// fn old() {
916// do_thing();
917// let x = 1;
918// x
919// }
920fn new() {}
921"#,
922 );
923 let detector = LegacyCodeDetector::new();
924 assert!(
925 detector.count_violations(&file) >= 4,
926 "4 consecutive commented lines"
927 );
928 }
929
930 #[test]
934 fn test_detector_todo_basic() {
935 let file = parse_rust(
936 r#"
937// TODO: refactor
938// FIXME: fix this
939fn main() {}
940"#,
941 );
942 let detector = TodoMountainDetector::new();
943 assert_eq!(detector.count_violations(&file), 2);
944 }
945
946 #[test]
948 fn test_detector_todo_bug_hack() {
949 let file = parse_rust(
950 r#"
951// BUG: critical issue here
952// HACK: workaround
953fn main() {}
954"#,
955 );
956 let detector = TodoMountainDetector::new();
957 assert_eq!(detector.count_violations(&file), 2);
958 }
959
960 #[test]
962 fn test_detector_todo_clean() {
963 let file = parse_rust(
964 r#"
965fn main() {
966 let x = 1;
967}
968"#,
969 );
970 let detector = TodoMountainDetector::new();
971 assert_eq!(detector.count_violations(&file), 0);
972 }
973
974 #[test]
976 fn test_detector_todo_case_insensitive() {
977 let file = parse_rust(
978 r#"
979// todo: lowercase
980// fixme: lowercase
981fn main() {}
982"#,
983 );
984 let detector = TodoMountainDetector::new();
985 assert!(
986 detector.count_violations(&file) >= 2,
987 "case-insensitive TODO + FIXME"
988 );
989 }
990
991 #[test]
993 fn test_detector_todo_inline() {
994 let file = parse_rust(
995 r#"
996fn main() {
997 let x = 1; // TODO: use constant
998 let y = 2; // FIXME: off by one
999}
1000"#,
1001 );
1002 let detector = TodoMountainDetector::new();
1003 assert_eq!(detector.count_violations(&file), 2);
1004 }
1005
1006 fn create_rust_file(lines: usize) -> String {
1009 let mut s = String::from("fn main() {\n");
1010 for i in 0..lines.saturating_sub(2) {
1011 s.push_str(&format!(" let x_{} = {};\n", i, i));
1012 }
1013 s.push_str("}\n");
1014 s
1015 }
1016
1017 #[test]
1019 fn test_detector_linecount_over_threshold() {
1020 let code = create_rust_file(1100);
1021 let engine = TreeSitterEngine::new();
1022 let file = engine
1023 .parse_file(std::path::Path::new("lib.rs"), &code)
1024 .expect("parse should work");
1025 let detector = LineCountSmellDetector::new();
1026 assert!(
1027 detector.count_violations(&file) > 0,
1028 "1100-line file should trigger smell"
1029 );
1030 }
1031
1032 #[test]
1034 fn test_detector_linecount_under_threshold() {
1035 let code = create_rust_file(100);
1036 let file = parse_rust(&code);
1037 let detector = LineCountSmellDetector::new();
1038 assert_eq!(detector.count_violations(&file), 0, "100-line file = 0");
1039 }
1040
1041 #[test]
1043 fn test_detector_linecount_test_threshold() {
1044 use std::path::Path;
1045 let code = create_rust_file(1100);
1046 let engine = TreeSitterEngine::new();
1047 let file = engine
1048 .parse_file(Path::new("test_suite.rs"), &code)
1049 .expect("parse should work");
1050 let detector = LineCountSmellDetector::new();
1051 assert_eq!(
1052 detector.count_violations(&file),
1053 0,
1054 "1100-line test file = 0 (test threshold = 2000)"
1055 );
1056 }
1057
1058 #[test]
1060 fn test_detector_linecount_test_over_threshold() {
1061 use std::path::Path;
1062 let code = create_rust_file(2100);
1063 let engine = TreeSitterEngine::new();
1064 let file = engine
1065 .parse_file(Path::new("test_suite.rs"), &code)
1066 .expect("parse should work");
1067 let detector = LineCountSmellDetector::new();
1068 assert!(
1069 detector.count_violations(&file) > 0,
1070 "2100-line test file should trigger smell"
1071 );
1072 }
1073
1074 #[test]
1078 fn test_detector_naming_hungarian() {
1079 let file = parse_rust(
1080 r#"
1081fn main() {
1082 let strName = String::new();
1083 let intCount = 42;
1084}
1085"#,
1086 );
1087 let detector = NamingChaosDetector::new();
1088 assert!(
1089 detector.count_violations(&file) >= 2,
1090 "Hungarian notation vars"
1091 );
1092 }
1093
1094 #[test]
1096 fn test_detector_naming_non_idiomatic_single() {
1097 let file = parse_rust(
1098 r#"
1099fn main() {
1100 let z = 1;
1101 let q = 2;
1102}
1103"#,
1104 );
1105 let detector = NamingChaosDetector::new();
1106 assert_eq!(detector.count_violations(&file), 2, "z + q = 2");
1107 }
1108
1109 #[test]
1111 fn test_detector_naming_idiomatic_single_ok() {
1112 let file = parse_rust(
1113 r#"
1114fn main() {
1115 let i = 0;
1116 let j = 1;
1117 let n = 100;
1118}
1119"#,
1120 );
1121 let detector = NamingChaosDetector::new();
1122 assert_eq!(detector.count_violations(&file), 0, "i, j, n are idiomatic");
1123 }
1124
1125 #[test]
1129 fn test_detector_panic_clean() {
1130 let file = parse_rust(
1131 r#"
1132fn safe() -> Result<i32, String> {
1133 Ok(42)
1134}
1135"#,
1136 );
1137 let detector = PanicAddictionDetector::new();
1138 assert_eq!(detector.count_violations(&file), 0, "safe code = 0");
1139 }
1140
1141 #[test]
1143 fn test_detector_panic_unwrap_in_closure() {
1144 let file = parse_rust(
1145 r#"
1146fn main() {
1147 let result = (0..10).filter(|x| x.unwrap() > 0);
1148}
1149"#,
1150 );
1151 let detector = PanicAddictionDetector::new();
1152 assert_eq!(detector.count_violations(&file), 1, "unwrap in closure");
1153 }
1154
1155 #[test]
1159 fn test_detector_overengineering_god_function_55_lines() {
1160 let mut code = String::from("fn god() {\n");
1161 for i in 0..53 {
1162 code.push_str(&format!(" let var_{} = {};\n", i, i));
1163 }
1164 code.push_str("}\n");
1165 let file = parse_rust(&code);
1166 let detector = OverEngineeringDetector::new();
1167 assert!(
1168 detector.count_violations(&file) >= 1,
1169 "55-line god function should count"
1170 );
1171 }
1172
1173 #[test]
1177 fn test_detector_code_smells_empty_fn() {
1178 let file = parse_rust("fn empty() {}");
1179 let detector = CodeSmellsDetector::new();
1180 assert_eq!(detector.count_violations(&file), 0);
1181 }
1182
1183 #[test]
1185 fn test_detector_code_smells_duplicate_import() {
1186 let file = parse_rust(
1187 r#"
1188use std::collections::HashMap;
1189use std::collections::HashMap;
1190fn main() {}
1191"#,
1192 );
1193 let detector = CodeSmellsDetector::new();
1194 assert!(
1195 detector.count_violations(&file) >= 1,
1196 "duplicate import should be > 0"
1197 );
1198 }
1199
1200 #[test]
1202 fn test_detector_panic_signal_type() {
1203 assert_eq!(
1204 PanicAddictionDetector::new().signal(),
1205 StyleSignal::PanicAddiction
1206 );
1207 }
1208
1209 #[test]
1210 fn test_detector_naming_signal_type() {
1211 assert_eq!(
1212 NamingChaosDetector::new().signal(),
1213 StyleSignal::NamingChaos
1214 );
1215 }
1216
1217 #[test]
1218 fn test_detector_nested_signal_type() {
1219 assert_eq!(NestedHellDetector::new().signal(), StyleSignal::NestedHell);
1220 }
1221
1222 #[test]
1223 fn test_detector_hotfix_signal_type() {
1224 assert_eq!(
1225 HotfixCultureDetector::new().signal(),
1226 StyleSignal::HotfixCulture
1227 );
1228 }
1229
1230 #[test]
1231 fn test_detector_overeng_signal_type() {
1232 assert_eq!(
1233 OverEngineeringDetector::new().signal(),
1234 StyleSignal::OverEngineering
1235 );
1236 }
1237
1238 #[test]
1239 fn test_detector_code_smells_signal_type() {
1240 assert_eq!(CodeSmellsDetector::new().signal(), StyleSignal::CodeSmells);
1241 }
1242
1243 #[test]
1244 fn test_detector_legacy_signal_type() {
1245 assert_eq!(LegacyCodeDetector::new().signal(), StyleSignal::LegacyCode);
1246 }
1247
1248 #[test]
1249 fn test_detector_todo_signal_type() {
1250 assert_eq!(
1251 TodoMountainDetector::new().signal(),
1252 StyleSignal::TodoMountain
1253 );
1254 }
1255
1256 #[test]
1257 fn test_detector_linecount_signal_type() {
1258 assert_eq!(
1259 LineCountSmellDetector::new().signal(),
1260 StyleSignal::LineCountSmell
1261 );
1262 }
1263
1264 #[test]
1265 fn test_detector_duplication_signal_type() {
1266 assert_eq!(
1267 DuplicationDetector::new().signal(),
1268 StyleSignal::Duplication
1269 );
1270 }
1271}