Skip to main content

garbage_code_hunter/
detectors.rs

1//! Direct signal detectors for the StyleSignal system.
2//!
3//! Each detector implements `SignalDetector` and produces scores
4//! directly from parsed AST files, bypassing the Rule → Issue pipeline.
5
6use crate::language::Language;
7use crate::signals::{SignalDetector, StyleSignal};
8use crate::style_ir::StyleIr;
9use crate::treesitter::duplication::IntraFileDupDetector;
10use crate::treesitter::engine::ParsedFile;
11
12/// All languages that have a LanguageAdapter implementation.
13const 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
27// ── PanicAddiction Detector ───────────────────────────────────────
28
29/// Detects PanicAddiction signal: .unwrap(), .expect(), panic!() calls.
30pub 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
64// ── NamingChaos Detector ─────────────────────────────────────────
65
66/// Detects NamingChaos signal: single-letter vars, terrible/meaningless names,
67/// Hungarian notation, and abbreviation abuse.
68pub 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
102// ── NestedHell Detector ──────────────────────────────────────────
103
104/// Detects NestedHell signal: deeply-nested block scopes (≥5 levels).
105pub 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
139// ── HotfixCulture Detector ────────────────────────────────────────
140
141/// Detects HotfixCulture signal: println!, dbg!, todo!, unimplemented! calls.
142pub 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
176// ── OverEngineering Detector ─────────────────────────────────────
177
178/// Detects OverEngineering signal: god functions (>50 lines) and excessive params (>5).
179pub 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
213// ── CodeSmells Detector ────────────────────────────────────────────
214
215/// Detects CodeSmells signal: unsafe blocks, magic numbers, unnecessary clone, etc.
216pub 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
250// ── Duplication Detector ───────────────────────────────────────────
251
252/// Detects Duplication signal: intra-file duplicated code blocks.
253///
254/// Cross-file duplication detection is stateful (accumulates fingerprints
255/// across all files) and is handled separately in the analysis pipeline.
256pub 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
284// ── LegacyCode Detector ─────────────────────────────────────────────
285
286/// Detects LegacyCode signal: blocks of commented-out code left in source.
287pub 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
325// ── TodoMountain Detector ────────────────────────────────────────────
326
327/// Detects TodoMountain signal: TODO/FIXME/BUG/HACK markers in comments.
328pub 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
366// ── LineCountSmell Detector ──────────────────────────────────────────
367
368/// Detects LineCountSmell signal: files exceeding reasonable line thresholds.
369pub 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    // ── SignalDetector — PanicAddictionDetector ────────────────────
422
423    /// Objective: Verify PanicAddictionDetector finds .unwrap() calls.
424    #[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    /// Objective: Verify PanicAddictionDetector does NOT flag .expect() calls.
433    #[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    /// Objective: Verify PanicAddictionDetector finds panic!() macro calls.
442    #[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    /// Objective: Verify detector finds unwrap + panic but not expect.
458    #[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    // ── SignalDetector — NamingChaosDetector ─────────────────────
478
479    /// Objective: Verify NamingChaosDetector catches single-letter var.
480    #[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    /// Objective: Verify NamingChaosDetector catches terrible naming.
488    #[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    /// Objective: Verify NamingChaosDetector returns 0 for clean naming.
496    #[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    // ── SignalDetector — NestedHellDetector ──────────────────────
504
505    /// Objective: Verify NestedHellDetector finds deeply-nested blocks.
506    #[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    /// Objective: Verify NestedHellDetector returns 0 for flat code.
536    #[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    /// Objective: Verify NestedHellDetector counts only blocks at depth >= 5.
555    #[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    // ── SignalDetector — HotfixCultureDetector ─────────────────────
581
582    /// Objective: Verify HotfixCultureDetector counts println! calls.
583    #[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    /// Objective: Verify HotfixCultureDetector counts todo! and unimplemented!.
598    #[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    /// Objective: Verify HotfixCultureDetector returns 0 for clean code.
617    #[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    /// Objective: Verify HotfixCultureDetector counts dbg! and eprintln! too.
631    #[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    // ── SignalDetector — OverEngineeringDetector ──────────────────
651
652    /// Objective: Verify OverEngineeringDetector counts god functions (>50 lines).
653    #[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        // The main function is short (< 50 lines), no god functions
668        assert_eq!(
669            detector.count_violations(&file),
670            0,
671            "short function should not count as overengineered"
672        );
673    }
674
675    /// Objective: Verify OverEngineeringDetector counts excessive params (>5).
676    #[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    /// Objective: Verify OverEngineeringDetector is 0 for clean functions.
694    #[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    // ── SignalDetector — CodeSmellsDetector ──────────────────────
708
709    /// Objective: Verify CodeSmellsDetector finds unsafe blocks.
710    #[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    /// Objective: Verify CodeSmellsDetector counts magic numbers in expressions.
731    #[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    /// Objective: Verify CodeSmellsDetector skips numbers in const/let declarations.
747    #[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    /// Objective: Verify CodeSmellsDetector skips 0 and 1 in trivial expressions.
766    #[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    /// Objective: Verify CodeSmellsDetector returns 0 for clean code.
781    #[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    // ── SignalDetector — DuplicationDetector ─────────────────────
795
796    /// Objective: Verify DuplicationDetector finds intra-file duplication.
797    #[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    /// Objective: Verify DuplicationDetector returns 0 for clean code.
823    #[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    /// Objective: Verify DuplicationDetector returns 0 for short files (<10 lines).
836    #[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    // ── SignalDetector — LegacyCodeDetector ─────────────────────
844
845    /// Objective: Verify LegacyCodeDetector finds consecutive commented-out code lines.
846    #[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    /// Objective: Verify LegacyCodeDetector ignores doc comments.
868    #[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    /// Objective: Verify LegacyCodeDetector returns 0 for short comments.
888    #[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    /// Objective: Verify LegacyCodeDetector is 0 for empty code.
903    #[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    /// Objective: Verify LegacyCodeDetector catches 4+ consecutive commented lines.
911    #[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    // ── SignalDetector — TodoMountainDetector ────────────────────
931
932    /// Objective: Verify TodoMountainDetector counts TODO markers.
933    #[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    /// Objective: Verify TodoMountainDetector counts BUG and HACK too.
947    #[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    /// Objective: Verify TodoMountainDetector returns 0 for clean code.
961    #[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    /// Objective: Verify TodoMountainDetector is case-insensitive.
975    #[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    /// Objective: Verify TodoMountainDetector counts inline markers.
992    #[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    // ── SignalDetector — LineCountSmellDetector ──────────────────
1007
1008    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    /// Objective: Verify LineCountSmellDetector signals for large files.
1018    #[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    /// Objective: Verify LineCountSmellDetector does NOT signal small files.
1033    #[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    /// Objective: Verify LineCountSmellDetector uses higher threshold for test files.
1042    #[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    /// Objective: Verify LineCountSmellDetector crosses test threshold.
1059    #[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    // ── SignalDetector — NamingChaosDetector additional ──────────
1075
1076    /// Objective: Verify NamingChaosDetector catches Hungarian notation.
1077    #[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    /// Objective: Verify NamingChaosDetector catches non-idiomatic single-letter.
1095    #[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    /// Objective: Verify NamingChaosDetector exempts idiomatic single-letter vars.
1110    #[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    // ── SignalDetector — PanicAddictionDetector additional ───────
1126
1127    /// Objective: Verify PanicAddictionDetector returns 0 for safe code.
1128    #[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    /// Objective: Verify PanicAddictionDetector finds .unwrap() in closures.
1142    #[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    // ── SignalDetector — OverEngineeringDetector additional ──────
1156
1157    /// Objective: Verify OverEngineeringDetector counts god function with >50 lines.
1158    #[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    // ── SignalDetector — CodeSmellsDetector additional ───────────
1174
1175    /// Objective: Verify CodeSmellsDetector handles empty functions.
1176    #[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    /// Objective: Verify CodeSmellsDetector counts multiply-imported items.
1184    #[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    /// Objective: Verify signal() returns the correct StyleSignal variant.
1201    #[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}