Skip to main content

debtmap/debt/
predicates.rs

1//! Composable predicate system for debt detection rules.
2//!
3//! This module provides declarative predicates for detecting technical debt patterns.
4//! Predicates are built using stillwater's predicate combinators, enabling:
5//!
6//! - **Composition**: Combine predicates with `and()`, `or()`, `not()`
7//! - **Testability**: Each predicate is independently testable
8//! - **Configurability**: Thresholds are parameterized via config
9//! - **Self-documentation**: Each predicate describes what it detects
10//!
11//! # Example
12//!
13//! ```rust,ignore
14//! use debtmap::debt::predicates::*;
15//!
16//! // Create predicates with thresholds
17//! let high_complexity = HighCyclomatic::new(21);
18//! let low_coverage = LowCoverage::new(0.5);
19//!
20//! // Compose for high-risk detection
21//! let high_risk = high_complexity.and(low_coverage);
22//!
23//! // Check against function metrics
24//! if high_risk.check(&metrics) {
25//!     println!("High risk function detected!");
26//! }
27//! ```
28//!
29//! # Available Predicates
30//!
31//! ## Complexity Predicates
32//! - [`HighCyclomatic`]: Cyclomatic complexity exceeds threshold
33//! - [`HighCognitive`]: Cognitive complexity exceeds threshold
34//! - [`CriticalComplexity`]: Both cyclomatic AND cognitive exceed thresholds
35//!
36//! ## Structure Predicates
37//! - [`DeepNesting`]: Nesting depth exceeds threshold
38//! - [`LongMethod`]: Line count exceeds threshold
39//! - [`TooManyParameters`]: Parameter count exceeds threshold (placeholder)
40//!
41//! ## Risk Predicates (Composed)
42//! - [`HighRisk`]: High complexity AND low coverage
43//! - [`CriticalRisk`]: Critical complexity AND no coverage
44//! - [`ModerateRisk`]: High complexity OR low coverage
45//!
46//! ## Coverage Predicates
47//! - [`LowCoverage`]: Coverage below threshold
48//! - [`NoCoverage`]: Coverage is zero or absent
49//! - [`PartialCoverage`]: Coverage between 0% and threshold
50
51use crate::core::FunctionMetrics;
52use crate::effects::validation::ValidationRuleSet;
53use stillwater::predicate::Predicate;
54
55// =============================================================================
56// Complexity Predicates
57// =============================================================================
58
59/// Predicate for high cyclomatic complexity.
60///
61/// Cyclomatic complexity measures the number of linearly independent paths
62/// through a function. High values indicate functions that are difficult to test.
63#[derive(Debug, Clone, Copy)]
64pub struct HighCyclomatic {
65    threshold: u32,
66}
67
68impl HighCyclomatic {
69    /// Create a new predicate with the given threshold.
70    pub fn new(threshold: u32) -> Self {
71        Self { threshold }
72    }
73
74    /// Create from a ValidationRuleSet using the warning threshold.
75    pub fn from_rules(rules: &ValidationRuleSet) -> Self {
76        Self::new(rules.complexity_warning)
77    }
78
79    /// Get the threshold value.
80    pub fn threshold(&self) -> u32 {
81        self.threshold
82    }
83
84    /// Get a description of what this predicate checks.
85    pub fn description(&self) -> String {
86        format!("cyclomatic complexity > {}", self.threshold)
87    }
88}
89
90impl Predicate<FunctionMetrics> for HighCyclomatic {
91    fn check(&self, metrics: &FunctionMetrics) -> bool {
92        metrics.cyclomatic > self.threshold
93    }
94}
95
96impl Predicate<u32> for HighCyclomatic {
97    fn check(&self, value: &u32) -> bool {
98        *value > self.threshold
99    }
100}
101
102/// Predicate for high cognitive complexity.
103///
104/// Cognitive complexity measures how difficult code is to understand.
105/// High values suggest the code should be refactored for readability.
106#[derive(Debug, Clone, Copy)]
107pub struct HighCognitive {
108    threshold: u32,
109}
110
111impl HighCognitive {
112    /// Create a new predicate with the given threshold.
113    pub fn new(threshold: u32) -> Self {
114        Self { threshold }
115    }
116
117    /// Create from a ValidationRuleSet using the warning threshold.
118    pub fn from_rules(rules: &ValidationRuleSet) -> Self {
119        Self::new(rules.complexity_warning)
120    }
121
122    /// Get the threshold value.
123    pub fn threshold(&self) -> u32 {
124        self.threshold
125    }
126
127    /// Get a description of what this predicate checks.
128    pub fn description(&self) -> String {
129        format!("cognitive complexity > {}", self.threshold)
130    }
131}
132
133impl Predicate<FunctionMetrics> for HighCognitive {
134    fn check(&self, metrics: &FunctionMetrics) -> bool {
135        metrics.cognitive > self.threshold
136    }
137}
138
139impl Predicate<u32> for HighCognitive {
140    fn check(&self, value: &u32) -> bool {
141        *value > self.threshold
142    }
143}
144
145/// Predicate for critical complexity (both cyclomatic AND cognitive exceed thresholds).
146///
147/// This represents functions that are both hard to test AND hard to understand,
148/// indicating the highest priority for refactoring.
149#[derive(Debug, Clone, Copy)]
150pub struct CriticalComplexity {
151    cyclomatic_threshold: u32,
152    cognitive_threshold: u32,
153}
154
155impl CriticalComplexity {
156    /// Create a new predicate with the given thresholds.
157    pub fn new(cyclomatic_threshold: u32, cognitive_threshold: u32) -> Self {
158        Self {
159            cyclomatic_threshold,
160            cognitive_threshold,
161        }
162    }
163
164    /// Create from a ValidationRuleSet using the critical threshold.
165    pub fn from_rules(rules: &ValidationRuleSet) -> Self {
166        Self::new(rules.complexity_critical, rules.complexity_critical)
167    }
168
169    /// Get a description of what this predicate checks.
170    pub fn description(&self) -> String {
171        format!(
172            "cyclomatic > {} AND cognitive > {}",
173            self.cyclomatic_threshold, self.cognitive_threshold
174        )
175    }
176}
177
178impl Predicate<FunctionMetrics> for CriticalComplexity {
179    fn check(&self, metrics: &FunctionMetrics) -> bool {
180        metrics.cyclomatic > self.cyclomatic_threshold
181            && metrics.cognitive > self.cognitive_threshold
182    }
183}
184
185// =============================================================================
186// Structure Predicates
187// =============================================================================
188
189/// Predicate for deep nesting depth.
190///
191/// Deeply nested code is harder to understand and maintain.
192/// High nesting often indicates opportunities for extraction or early returns.
193#[derive(Debug, Clone, Copy)]
194pub struct DeepNesting {
195    threshold: u32,
196}
197
198impl DeepNesting {
199    /// Create a new predicate with the given threshold.
200    pub fn new(threshold: u32) -> Self {
201        Self { threshold }
202    }
203
204    /// Create from a ValidationRuleSet.
205    pub fn from_rules(rules: &ValidationRuleSet) -> Self {
206        Self::new(rules.max_nesting_depth)
207    }
208
209    /// Get the threshold value.
210    pub fn threshold(&self) -> u32 {
211        self.threshold
212    }
213
214    /// Get a description of what this predicate checks.
215    pub fn description(&self) -> String {
216        format!("nesting depth > {}", self.threshold)
217    }
218}
219
220impl Predicate<FunctionMetrics> for DeepNesting {
221    fn check(&self, metrics: &FunctionMetrics) -> bool {
222        metrics.nesting > self.threshold
223    }
224}
225
226impl Predicate<u32> for DeepNesting {
227    fn check(&self, value: &u32) -> bool {
228        *value > self.threshold
229    }
230}
231
232/// Predicate for long methods.
233///
234/// Long functions are harder to understand and test.
235/// They often indicate multiple responsibilities that should be split.
236#[derive(Debug, Clone, Copy)]
237pub struct LongMethod {
238    threshold: usize,
239}
240
241impl LongMethod {
242    /// Create a new predicate with the given threshold.
243    pub fn new(threshold: usize) -> Self {
244        Self { threshold }
245    }
246
247    /// Create from a ValidationRuleSet.
248    pub fn from_rules(rules: &ValidationRuleSet) -> Self {
249        Self::new(rules.max_function_length)
250    }
251
252    /// Get the threshold value.
253    pub fn threshold(&self) -> usize {
254        self.threshold
255    }
256
257    /// Get a description of what this predicate checks.
258    pub fn description(&self) -> String {
259        format!("line count > {}", self.threshold)
260    }
261}
262
263impl Predicate<FunctionMetrics> for LongMethod {
264    fn check(&self, metrics: &FunctionMetrics) -> bool {
265        metrics.length > self.threshold
266    }
267}
268
269impl Predicate<usize> for LongMethod {
270    fn check(&self, value: &usize) -> bool {
271        *value > self.threshold
272    }
273}
274
275/// Predicate for too many parameters.
276///
277/// Functions with many parameters are harder to call correctly and test.
278/// Note: This is a placeholder as FunctionMetrics doesn't currently track parameter count.
279#[derive(Debug, Clone, Copy)]
280pub struct TooManyParameters {
281    threshold: usize,
282}
283
284impl TooManyParameters {
285    /// Create a new predicate with the given threshold.
286    pub fn new(threshold: usize) -> Self {
287        Self { threshold }
288    }
289
290    /// Get the threshold value.
291    pub fn threshold(&self) -> usize {
292        self.threshold
293    }
294
295    /// Get a description of what this predicate checks.
296    pub fn description(&self) -> String {
297        format!("parameter count > {}", self.threshold)
298    }
299}
300
301impl Predicate<usize> for TooManyParameters {
302    fn check(&self, value: &usize) -> bool {
303        *value > self.threshold
304    }
305}
306
307// =============================================================================
308// Coverage Predicates
309// =============================================================================
310
311/// Predicate for low test coverage.
312///
313/// Functions with low coverage are riskier to modify because bugs may not be caught.
314#[derive(Debug, Clone, Copy)]
315pub struct LowCoverage {
316    /// Threshold as a fraction (0.0 to 1.0)
317    threshold: f64,
318}
319
320impl LowCoverage {
321    /// Create a new predicate with the given threshold (0.0 to 1.0).
322    pub fn new(threshold: f64) -> Self {
323        Self {
324            threshold: threshold.clamp(0.0, 1.0),
325        }
326    }
327
328    /// Create with a percentage threshold (0 to 100).
329    pub fn from_percentage(percentage: u32) -> Self {
330        Self::new(f64::from(percentage) / 100.0)
331    }
332
333    /// Get the threshold value.
334    pub fn threshold(&self) -> f64 {
335        self.threshold
336    }
337
338    /// Get a description of what this predicate checks.
339    pub fn description(&self) -> String {
340        format!("coverage < {}%", (self.threshold * 100.0) as u32)
341    }
342}
343
344impl Predicate<f64> for LowCoverage {
345    fn check(&self, coverage: &f64) -> bool {
346        *coverage < self.threshold
347    }
348}
349
350impl Predicate<Option<f64>> for LowCoverage {
351    fn check(&self, coverage: &Option<f64>) -> bool {
352        coverage.is_none_or(|c| c < self.threshold)
353    }
354}
355
356/// Predicate for zero (or absent) test coverage.
357///
358/// Functions with no coverage are highest risk for untested changes.
359#[derive(Debug, Clone, Copy, Default)]
360pub struct NoCoverage;
361
362impl NoCoverage {
363    /// Create a new NoCoverage predicate.
364    pub fn new() -> Self {
365        Self
366    }
367
368    /// Get a description of what this predicate checks.
369    pub fn description(&self) -> String {
370        "coverage = 0% or absent".to_string()
371    }
372}
373
374impl Predicate<f64> for NoCoverage {
375    fn check(&self, coverage: &f64) -> bool {
376        *coverage == 0.0
377    }
378}
379
380impl Predicate<Option<f64>> for NoCoverage {
381    fn check(&self, coverage: &Option<f64>) -> bool {
382        coverage.is_none_or(|c| c == 0.0)
383    }
384}
385
386/// Predicate for partial coverage (between 0% and threshold).
387///
388/// These functions have some tests but not enough to be confident.
389#[derive(Debug, Clone, Copy)]
390pub struct PartialCoverage {
391    threshold: f64,
392}
393
394impl PartialCoverage {
395    /// Create a new predicate with the given threshold (0.0 to 1.0).
396    pub fn new(threshold: f64) -> Self {
397        Self {
398            threshold: threshold.clamp(0.0, 1.0),
399        }
400    }
401
402    /// Get the threshold value.
403    pub fn threshold(&self) -> f64 {
404        self.threshold
405    }
406
407    /// Get a description of what this predicate checks.
408    pub fn description(&self) -> String {
409        format!("0% < coverage < {}%", (self.threshold * 100.0) as u32)
410    }
411}
412
413impl Predicate<f64> for PartialCoverage {
414    fn check(&self, coverage: &f64) -> bool {
415        *coverage > 0.0 && *coverage < self.threshold
416    }
417}
418
419impl Predicate<Option<f64>> for PartialCoverage {
420    fn check(&self, coverage: &Option<f64>) -> bool {
421        coverage.is_some_and(|c| c > 0.0 && c < self.threshold)
422    }
423}
424
425// =============================================================================
426// Composed Predicates (Logical Combinations)
427// =============================================================================
428
429/// High-risk predicate: High complexity AND low coverage.
430///
431/// Functions that are both complex and poorly tested represent the highest
432/// maintenance risk.
433#[derive(Debug, Clone)]
434pub struct HighRisk {
435    high_complexity: HighCyclomatic,
436    low_coverage: LowCoverage,
437}
438
439impl HighRisk {
440    /// Create a new high-risk predicate.
441    pub fn new(complexity_threshold: u32, coverage_threshold: f64) -> Self {
442        Self {
443            high_complexity: HighCyclomatic::new(complexity_threshold),
444            low_coverage: LowCoverage::new(coverage_threshold),
445        }
446    }
447
448    /// Create from a ValidationRuleSet.
449    pub fn from_rules(rules: &ValidationRuleSet) -> Self {
450        Self::new(rules.complexity_warning, 0.5) // 50% default coverage threshold
451    }
452
453    /// Get a description of what this predicate checks.
454    pub fn description(&self) -> String {
455        format!(
456            "{} AND {}",
457            self.high_complexity.description(),
458            self.low_coverage.description()
459        )
460    }
461}
462
463/// Metrics with coverage for risk evaluation.
464pub struct FunctionWithCoverage<'a> {
465    pub metrics: &'a FunctionMetrics,
466    pub coverage: Option<f64>,
467}
468
469impl Predicate<FunctionWithCoverage<'_>> for HighRisk {
470    fn check(&self, value: &FunctionWithCoverage<'_>) -> bool {
471        self.high_complexity.check(value.metrics) && self.low_coverage.check(&value.coverage)
472    }
473}
474
475/// Critical-risk predicate: Critical complexity AND no coverage.
476///
477/// The most dangerous functions: extremely complex with zero tests.
478#[derive(Debug, Clone)]
479pub struct CriticalRisk {
480    critical_complexity: CriticalComplexity,
481    no_coverage: NoCoverage,
482}
483
484impl CriticalRisk {
485    /// Create a new critical-risk predicate.
486    pub fn new(cyclomatic_threshold: u32, cognitive_threshold: u32) -> Self {
487        Self {
488            critical_complexity: CriticalComplexity::new(cyclomatic_threshold, cognitive_threshold),
489            no_coverage: NoCoverage::new(),
490        }
491    }
492
493    /// Create from a ValidationRuleSet.
494    pub fn from_rules(rules: &ValidationRuleSet) -> Self {
495        Self::new(rules.complexity_critical, rules.complexity_critical)
496    }
497
498    /// Get a description of what this predicate checks.
499    pub fn description(&self) -> String {
500        format!(
501            "{} AND {}",
502            self.critical_complexity.description(),
503            self.no_coverage.description()
504        )
505    }
506}
507
508impl Predicate<FunctionWithCoverage<'_>> for CriticalRisk {
509    fn check(&self, value: &FunctionWithCoverage<'_>) -> bool {
510        self.critical_complexity.check(value.metrics) && self.no_coverage.check(&value.coverage)
511    }
512}
513
514/// Moderate-risk predicate: High complexity OR low coverage.
515///
516/// Functions that have at least one risk factor present.
517#[derive(Debug, Clone)]
518pub struct ModerateRisk {
519    high_complexity: HighCyclomatic,
520    low_coverage: LowCoverage,
521}
522
523impl ModerateRisk {
524    /// Create a new moderate-risk predicate.
525    pub fn new(complexity_threshold: u32, coverage_threshold: f64) -> Self {
526        Self {
527            high_complexity: HighCyclomatic::new(complexity_threshold),
528            low_coverage: LowCoverage::new(coverage_threshold),
529        }
530    }
531
532    /// Create from a ValidationRuleSet.
533    pub fn from_rules(rules: &ValidationRuleSet) -> Self {
534        Self::new(rules.complexity_warning, 0.5)
535    }
536
537    /// Get a description of what this predicate checks.
538    pub fn description(&self) -> String {
539        format!(
540            "{} OR {}",
541            self.high_complexity.description(),
542            self.low_coverage.description()
543        )
544    }
545}
546
547impl Predicate<FunctionWithCoverage<'_>> for ModerateRisk {
548    fn check(&self, value: &FunctionWithCoverage<'_>) -> bool {
549        self.high_complexity.check(value.metrics) || self.low_coverage.check(&value.coverage)
550    }
551}
552
553// =============================================================================
554// Predicate Factory
555// =============================================================================
556
557/// Factory for creating configured debt detection predicates.
558///
559/// This struct provides convenient access to all standard predicates,
560/// configured from a single validation rule set.
561#[derive(Debug, Clone)]
562pub struct DebtPredicates {
563    /// High cyclomatic complexity predicate.
564    pub high_cyclomatic: HighCyclomatic,
565    /// High cognitive complexity predicate.
566    pub high_cognitive: HighCognitive,
567    /// Critical complexity predicate (both thresholds exceeded).
568    pub critical_complexity: CriticalComplexity,
569    /// Deep nesting predicate.
570    pub deep_nesting: DeepNesting,
571    /// Long method predicate.
572    pub long_method: LongMethod,
573    /// Low coverage predicate.
574    pub low_coverage: LowCoverage,
575    /// No coverage predicate.
576    pub no_coverage: NoCoverage,
577}
578
579impl DebtPredicates {
580    /// Create predicates from a ValidationRuleSet.
581    pub fn from_rules(rules: &ValidationRuleSet) -> Self {
582        Self {
583            high_cyclomatic: HighCyclomatic::from_rules(rules),
584            high_cognitive: HighCognitive::from_rules(rules),
585            critical_complexity: CriticalComplexity::from_rules(rules),
586            deep_nesting: DeepNesting::from_rules(rules),
587            long_method: LongMethod::from_rules(rules),
588            low_coverage: LowCoverage::new(0.5), // 50% default
589            no_coverage: NoCoverage::new(),
590        }
591    }
592
593    /// Create with custom coverage threshold.
594    pub fn with_coverage_threshold(mut self, threshold: f64) -> Self {
595        self.low_coverage = LowCoverage::new(threshold);
596        self
597    }
598
599    /// Create a high-risk predicate from these settings.
600    pub fn high_risk(&self) -> HighRisk {
601        HighRisk {
602            high_complexity: self.high_cyclomatic,
603            low_coverage: self.low_coverage,
604        }
605    }
606
607    /// Create a critical-risk predicate from these settings.
608    pub fn critical_risk(&self) -> CriticalRisk {
609        CriticalRisk {
610            critical_complexity: self.critical_complexity,
611            no_coverage: self.no_coverage,
612        }
613    }
614
615    /// Create a moderate-risk predicate from these settings.
616    pub fn moderate_risk(&self) -> ModerateRisk {
617        ModerateRisk {
618            high_complexity: self.high_cyclomatic,
619            low_coverage: self.low_coverage,
620        }
621    }
622
623    /// Check if a function has any complexity issues.
624    pub fn has_complexity_issues(&self, metrics: &FunctionMetrics) -> bool {
625        self.high_cyclomatic.check(metrics)
626            || self.high_cognitive.check(metrics)
627            || self.critical_complexity.check(metrics)
628    }
629
630    /// Check if a function has any structural issues.
631    pub fn has_structural_issues(&self, metrics: &FunctionMetrics) -> bool {
632        self.deep_nesting.check(metrics) || self.long_method.check(metrics)
633    }
634
635    /// Check if a function has any issues at all.
636    pub fn has_any_issues(&self, metrics: &FunctionMetrics) -> bool {
637        self.has_complexity_issues(metrics) || self.has_structural_issues(metrics)
638    }
639}
640
641impl Default for DebtPredicates {
642    fn default() -> Self {
643        Self::from_rules(&ValidationRuleSet::default())
644    }
645}
646
647// =============================================================================
648// Predicate Result Types
649// =============================================================================
650
651/// Result of predicate evaluation with context.
652#[derive(Debug, Clone)]
653pub struct PredicateResult {
654    /// Whether the predicate matched.
655    pub matched: bool,
656    /// Name of the predicate that was evaluated.
657    pub predicate_name: String,
658    /// Human-readable description of the predicate.
659    pub description: String,
660    /// Optional additional details about the match.
661    pub details: Option<String>,
662}
663
664impl PredicateResult {
665    /// Create a new matched result.
666    pub fn matched(name: impl Into<String>, description: impl Into<String>) -> Self {
667        Self {
668            matched: true,
669            predicate_name: name.into(),
670            description: description.into(),
671            details: None,
672        }
673    }
674
675    /// Create a new unmatched result.
676    pub fn not_matched(name: impl Into<String>, description: impl Into<String>) -> Self {
677        Self {
678            matched: false,
679            predicate_name: name.into(),
680            description: description.into(),
681            details: None,
682        }
683    }
684
685    /// Add details to this result.
686    pub fn with_details(mut self, details: impl Into<String>) -> Self {
687        self.details = Some(details.into());
688        self
689    }
690}
691
692/// Collection of predicate evaluation results for a function.
693#[derive(Debug, Clone)]
694pub struct DebtFindings {
695    /// The function that was evaluated.
696    pub function_name: String,
697    /// All predicate results (both matched and not).
698    pub results: Vec<PredicateResult>,
699}
700
701impl DebtFindings {
702    /// Create new findings for a function.
703    pub fn new(function_name: impl Into<String>) -> Self {
704        Self {
705            function_name: function_name.into(),
706            results: Vec::new(),
707        }
708    }
709
710    /// Add a predicate result.
711    pub fn add_result(&mut self, result: PredicateResult) {
712        self.results.push(result);
713    }
714
715    /// Get only the matched predicates.
716    pub fn matched_predicates(&self) -> impl Iterator<Item = &PredicateResult> {
717        self.results.iter().filter(|r| r.matched)
718    }
719
720    /// Check if any predicates matched.
721    pub fn has_issues(&self) -> bool {
722        self.results.iter().any(|r| r.matched)
723    }
724
725    /// Count of matched predicates.
726    pub fn issue_count(&self) -> usize {
727        self.results.iter().filter(|r| r.matched).count()
728    }
729}
730
731// =============================================================================
732// Tests
733// =============================================================================
734
735#[cfg(test)]
736mod tests {
737    use super::*;
738    use std::path::PathBuf;
739
740    fn create_test_metrics(
741        name: &str,
742        cyclomatic: u32,
743        cognitive: u32,
744        nesting: u32,
745        length: usize,
746    ) -> FunctionMetrics {
747        FunctionMetrics {
748            name: name.to_string(),
749            file: PathBuf::from("test.rs"),
750            line: 1,
751            cyclomatic,
752            cognitive,
753            nesting,
754            length,
755            is_test: false,
756            visibility: None,
757            is_trait_method: false,
758            in_test_module: false,
759            entropy_score: None,
760            is_pure: None,
761            purity_confidence: None,
762            purity_reason: None,
763            call_dependencies: None,
764            detected_patterns: None,
765            upstream_callers: None,
766            downstream_callees: None,
767            mapping_pattern_result: None,
768            adjusted_complexity: None,
769            composition_metrics: None,
770            language_specific: None,
771            purity_level: None,
772            error_swallowing_count: None,
773            error_swallowing_patterns: None,
774            entropy_analysis: None,
775        }
776    }
777
778    // =========================================================================
779    // Complexity Predicate Tests
780    // =========================================================================
781
782    mod complexity_predicates {
783        use super::*;
784
785        #[test]
786        fn high_cyclomatic_above_threshold() {
787            let pred = HighCyclomatic::new(20);
788            let metrics = create_test_metrics("complex_fn", 25, 10, 2, 30);
789            assert!(pred.check(&metrics));
790        }
791
792        #[test]
793        fn high_cyclomatic_at_threshold() {
794            let pred = HighCyclomatic::new(20);
795            let metrics = create_test_metrics("boundary_fn", 20, 10, 2, 30);
796            assert!(!pred.check(&metrics)); // At threshold, not above
797        }
798
799        #[test]
800        fn high_cyclomatic_below_threshold() {
801            let pred = HighCyclomatic::new(20);
802            let metrics = create_test_metrics("simple_fn", 10, 5, 1, 15);
803            assert!(!pred.check(&metrics));
804        }
805
806        #[test]
807        fn high_cyclomatic_direct_value() {
808            let pred = HighCyclomatic::new(20);
809            assert!(pred.check(&25_u32));
810            assert!(!pred.check(&20_u32));
811            assert!(!pred.check(&15_u32));
812        }
813
814        #[test]
815        fn high_cognitive_above_threshold() {
816            let pred = HighCognitive::new(15);
817            let metrics = create_test_metrics("confusing_fn", 10, 20, 2, 30);
818            assert!(pred.check(&metrics));
819        }
820
821        #[test]
822        fn high_cognitive_below_threshold() {
823            let pred = HighCognitive::new(15);
824            let metrics = create_test_metrics("clear_fn", 10, 10, 2, 30);
825            assert!(!pred.check(&metrics));
826        }
827
828        #[test]
829        fn critical_complexity_both_exceeded() {
830            let pred = CriticalComplexity::new(50, 40);
831            let metrics = create_test_metrics("nightmare_fn", 60, 50, 5, 100);
832            assert!(pred.check(&metrics));
833        }
834
835        #[test]
836        fn critical_complexity_only_one_exceeded() {
837            let pred = CriticalComplexity::new(50, 40);
838
839            // Only cyclomatic exceeded
840            let metrics1 = create_test_metrics("high_cyclomatic", 60, 30, 2, 50);
841            assert!(!pred.check(&metrics1));
842
843            // Only cognitive exceeded
844            let metrics2 = create_test_metrics("high_cognitive", 30, 50, 2, 50);
845            assert!(!pred.check(&metrics2));
846        }
847
848        #[test]
849        fn critical_complexity_neither_exceeded() {
850            let pred = CriticalComplexity::new(50, 40);
851            let metrics = create_test_metrics("good_fn", 30, 25, 2, 30);
852            assert!(!pred.check(&metrics));
853        }
854    }
855
856    // =========================================================================
857    // Structure Predicate Tests
858    // =========================================================================
859
860    mod structure_predicates {
861        use super::*;
862
863        #[test]
864        fn deep_nesting_above_threshold() {
865            let pred = DeepNesting::new(4);
866            let metrics = create_test_metrics("nested_fn", 10, 8, 6, 30);
867            assert!(pred.check(&metrics));
868        }
869
870        #[test]
871        fn deep_nesting_at_threshold() {
872            let pred = DeepNesting::new(4);
873            let metrics = create_test_metrics("boundary_fn", 10, 8, 4, 30);
874            assert!(!pred.check(&metrics));
875        }
876
877        #[test]
878        fn deep_nesting_below_threshold() {
879            let pred = DeepNesting::new(4);
880            let metrics = create_test_metrics("flat_fn", 10, 8, 2, 30);
881            assert!(!pred.check(&metrics));
882        }
883
884        #[test]
885        fn long_method_above_threshold() {
886            let pred = LongMethod::new(50);
887            let metrics = create_test_metrics("huge_fn", 10, 8, 2, 100);
888            assert!(pred.check(&metrics));
889        }
890
891        #[test]
892        fn long_method_at_threshold() {
893            let pred = LongMethod::new(50);
894            let metrics = create_test_metrics("boundary_fn", 10, 8, 2, 50);
895            assert!(!pred.check(&metrics));
896        }
897
898        #[test]
899        fn long_method_below_threshold() {
900            let pred = LongMethod::new(50);
901            let metrics = create_test_metrics("short_fn", 10, 8, 2, 25);
902            assert!(!pred.check(&metrics));
903        }
904
905        #[test]
906        fn too_many_parameters_direct_value() {
907            let pred = TooManyParameters::new(4);
908            assert!(pred.check(&6_usize));
909            assert!(!pred.check(&4_usize));
910            assert!(!pred.check(&2_usize));
911        }
912    }
913
914    // =========================================================================
915    // Coverage Predicate Tests
916    // =========================================================================
917
918    mod coverage_predicates {
919        use super::*;
920
921        #[test]
922        fn low_coverage_below_threshold() {
923            let pred = LowCoverage::new(0.5);
924            assert!(pred.check(&0.3_f64));
925            assert!(pred.check(&Some(0.3_f64)));
926        }
927
928        #[test]
929        fn low_coverage_at_threshold() {
930            let pred = LowCoverage::new(0.5);
931            assert!(!pred.check(&0.5_f64));
932            assert!(!pred.check(&Some(0.5_f64)));
933        }
934
935        #[test]
936        fn low_coverage_above_threshold() {
937            let pred = LowCoverage::new(0.5);
938            assert!(!pred.check(&0.7_f64));
939            assert!(!pred.check(&Some(0.7_f64)));
940        }
941
942        #[test]
943        fn low_coverage_missing() {
944            let pred = LowCoverage::new(0.5);
945            assert!(pred.check(&None::<f64>)); // Missing coverage treated as low
946        }
947
948        #[test]
949        fn low_coverage_from_percentage() {
950            let pred = LowCoverage::from_percentage(50);
951            assert!(pred.check(&0.3_f64));
952            assert!(!pred.check(&0.5_f64));
953            assert!(!pred.check(&0.7_f64));
954        }
955
956        #[test]
957        fn no_coverage_zero() {
958            let pred = NoCoverage::new();
959            assert!(pred.check(&0.0_f64));
960            assert!(pred.check(&Some(0.0_f64)));
961        }
962
963        #[test]
964        fn no_coverage_missing() {
965            let pred = NoCoverage::new();
966            assert!(pred.check(&None::<f64>));
967        }
968
969        #[test]
970        fn no_coverage_present() {
971            let pred = NoCoverage::new();
972            assert!(!pred.check(&0.5_f64));
973            assert!(!pred.check(&Some(0.5_f64)));
974        }
975
976        #[test]
977        fn partial_coverage_in_range() {
978            let pred = PartialCoverage::new(0.5);
979            assert!(pred.check(&0.25_f64));
980            assert!(pred.check(&Some(0.25_f64)));
981        }
982
983        #[test]
984        fn partial_coverage_at_zero() {
985            let pred = PartialCoverage::new(0.5);
986            assert!(!pred.check(&0.0_f64)); // Zero is excluded
987            assert!(!pred.check(&Some(0.0_f64)));
988        }
989
990        #[test]
991        fn partial_coverage_at_threshold() {
992            let pred = PartialCoverage::new(0.5);
993            assert!(!pred.check(&0.5_f64)); // At threshold is excluded
994            assert!(!pred.check(&Some(0.5_f64)));
995        }
996
997        #[test]
998        fn partial_coverage_missing() {
999            let pred = PartialCoverage::new(0.5);
1000            assert!(!pred.check(&None::<f64>)); // Missing is not partial
1001        }
1002    }
1003
1004    // =========================================================================
1005    // Composed Predicate Tests
1006    // =========================================================================
1007
1008    mod composed_predicates {
1009        use super::*;
1010
1011        #[test]
1012        fn high_risk_both_conditions() {
1013            let pred = HighRisk::new(20, 0.5);
1014            let metrics = create_test_metrics("risky_fn", 30, 10, 2, 30);
1015            let with_coverage = FunctionWithCoverage {
1016                metrics: &metrics,
1017                coverage: Some(0.3),
1018            };
1019            assert!(pred.check(&with_coverage));
1020        }
1021
1022        #[test]
1023        fn high_risk_only_complexity() {
1024            let pred = HighRisk::new(20, 0.5);
1025            let metrics = create_test_metrics("complex_but_tested", 30, 10, 2, 30);
1026            let with_coverage = FunctionWithCoverage {
1027                metrics: &metrics,
1028                coverage: Some(0.8),
1029            };
1030            assert!(!pred.check(&with_coverage));
1031        }
1032
1033        #[test]
1034        fn high_risk_only_low_coverage() {
1035            let pred = HighRisk::new(20, 0.5);
1036            let metrics = create_test_metrics("simple_untested", 10, 5, 1, 15);
1037            let with_coverage = FunctionWithCoverage {
1038                metrics: &metrics,
1039                coverage: Some(0.2),
1040            };
1041            assert!(!pred.check(&with_coverage));
1042        }
1043
1044        #[test]
1045        fn critical_risk_critical_and_zero() {
1046            let pred = CriticalRisk::new(50, 50);
1047            let metrics = create_test_metrics("disaster_fn", 100, 80, 8, 200);
1048            let with_coverage = FunctionWithCoverage {
1049                metrics: &metrics,
1050                coverage: Some(0.0),
1051            };
1052            assert!(pred.check(&with_coverage));
1053        }
1054
1055        #[test]
1056        fn critical_risk_critical_but_some_coverage() {
1057            let pred = CriticalRisk::new(50, 50);
1058            let metrics = create_test_metrics("complex_tested", 100, 80, 8, 200);
1059            let with_coverage = FunctionWithCoverage {
1060                metrics: &metrics,
1061                coverage: Some(0.5),
1062            };
1063            assert!(!pred.check(&with_coverage));
1064        }
1065
1066        #[test]
1067        fn moderate_risk_either_condition() {
1068            let pred = ModerateRisk::new(20, 0.5);
1069            let metrics = create_test_metrics("complex_fn", 30, 10, 2, 30);
1070
1071            // High complexity only
1072            let with_good_coverage = FunctionWithCoverage {
1073                metrics: &metrics,
1074                coverage: Some(0.8),
1075            };
1076            assert!(pred.check(&with_good_coverage));
1077
1078            // Low coverage only
1079            let simple_metrics = create_test_metrics("simple_fn", 10, 5, 1, 15);
1080            let with_low_coverage = FunctionWithCoverage {
1081                metrics: &simple_metrics,
1082                coverage: Some(0.2),
1083            };
1084            assert!(pred.check(&with_low_coverage));
1085        }
1086
1087        #[test]
1088        fn moderate_risk_neither_condition() {
1089            let pred = ModerateRisk::new(20, 0.5);
1090            let metrics = create_test_metrics("good_fn", 10, 5, 1, 15);
1091            let with_coverage = FunctionWithCoverage {
1092                metrics: &metrics,
1093                coverage: Some(0.8),
1094            };
1095            assert!(!pred.check(&with_coverage));
1096        }
1097    }
1098
1099    // =========================================================================
1100    // Factory Tests
1101    // =========================================================================
1102
1103    mod factory_tests {
1104        use super::*;
1105
1106        #[test]
1107        fn debt_predicates_default() {
1108            let predicates = DebtPredicates::default();
1109
1110            // Check default thresholds from ValidationRuleSet::default()
1111            assert_eq!(predicates.high_cyclomatic.threshold(), 21);
1112            assert_eq!(predicates.deep_nesting.threshold(), 4);
1113            assert_eq!(predicates.long_method.threshold(), 50);
1114        }
1115
1116        #[test]
1117        fn debt_predicates_from_strict_rules() {
1118            let rules = ValidationRuleSet::strict();
1119            let predicates = DebtPredicates::from_rules(&rules);
1120
1121            assert_eq!(predicates.high_cyclomatic.threshold(), 10);
1122            assert_eq!(predicates.deep_nesting.threshold(), 2);
1123            assert_eq!(predicates.long_method.threshold(), 20);
1124        }
1125
1126        #[test]
1127        fn debt_predicates_from_lenient_rules() {
1128            let rules = ValidationRuleSet::lenient();
1129            let predicates = DebtPredicates::from_rules(&rules);
1130
1131            assert_eq!(predicates.high_cyclomatic.threshold(), 30);
1132            assert_eq!(predicates.deep_nesting.threshold(), 6);
1133            assert_eq!(predicates.long_method.threshold(), 100);
1134        }
1135
1136        #[test]
1137        fn debt_predicates_with_custom_coverage() {
1138            let predicates = DebtPredicates::default().with_coverage_threshold(0.8);
1139            assert!((predicates.low_coverage.threshold() - 0.8).abs() < 0.001);
1140        }
1141
1142        #[test]
1143        fn debt_predicates_has_complexity_issues() {
1144            let predicates = DebtPredicates::default();
1145
1146            let complex = create_test_metrics("complex_fn", 50, 10, 2, 30);
1147            assert!(predicates.has_complexity_issues(&complex));
1148
1149            let simple = create_test_metrics("simple_fn", 5, 3, 1, 10);
1150            assert!(!predicates.has_complexity_issues(&simple));
1151        }
1152
1153        #[test]
1154        fn debt_predicates_has_structural_issues() {
1155            let predicates = DebtPredicates::default();
1156
1157            let deeply_nested = create_test_metrics("nested_fn", 10, 8, 10, 30);
1158            assert!(predicates.has_structural_issues(&deeply_nested));
1159
1160            let long_fn = create_test_metrics("long_fn", 10, 8, 2, 100);
1161            assert!(predicates.has_structural_issues(&long_fn));
1162
1163            let good_fn = create_test_metrics("good_fn", 10, 8, 2, 30);
1164            assert!(!predicates.has_structural_issues(&good_fn));
1165        }
1166
1167        #[test]
1168        fn debt_predicates_has_any_issues() {
1169            let predicates = DebtPredicates::default();
1170
1171            let problematic = create_test_metrics("bad_fn", 50, 40, 8, 100);
1172            assert!(predicates.has_any_issues(&problematic));
1173
1174            let good = create_test_metrics("good_fn", 5, 3, 1, 10);
1175            assert!(!predicates.has_any_issues(&good));
1176        }
1177    }
1178
1179    // =========================================================================
1180    // Predicate Description Tests
1181    // =========================================================================
1182
1183    mod description_tests {
1184        use super::*;
1185
1186        #[test]
1187        fn high_cyclomatic_description() {
1188            let pred = HighCyclomatic::new(25);
1189            assert_eq!(pred.description(), "cyclomatic complexity > 25");
1190        }
1191
1192        #[test]
1193        fn high_cognitive_description() {
1194            let pred = HighCognitive::new(15);
1195            assert_eq!(pred.description(), "cognitive complexity > 15");
1196        }
1197
1198        #[test]
1199        fn critical_complexity_description() {
1200            let pred = CriticalComplexity::new(50, 40);
1201            assert_eq!(pred.description(), "cyclomatic > 50 AND cognitive > 40");
1202        }
1203
1204        #[test]
1205        fn deep_nesting_description() {
1206            let pred = DeepNesting::new(4);
1207            assert_eq!(pred.description(), "nesting depth > 4");
1208        }
1209
1210        #[test]
1211        fn long_method_description() {
1212            let pred = LongMethod::new(50);
1213            assert_eq!(pred.description(), "line count > 50");
1214        }
1215
1216        #[test]
1217        fn low_coverage_description() {
1218            let pred = LowCoverage::new(0.5);
1219            assert_eq!(pred.description(), "coverage < 50%");
1220        }
1221
1222        #[test]
1223        fn no_coverage_description() {
1224            let pred = NoCoverage::new();
1225            assert_eq!(pred.description(), "coverage = 0% or absent");
1226        }
1227
1228        #[test]
1229        fn high_risk_description() {
1230            let pred = HighRisk::new(20, 0.5);
1231            assert_eq!(
1232                pred.description(),
1233                "cyclomatic complexity > 20 AND coverage < 50%"
1234            );
1235        }
1236    }
1237
1238    // =========================================================================
1239    // DebtFindings Tests
1240    // =========================================================================
1241
1242    mod findings_tests {
1243        use super::*;
1244
1245        #[test]
1246        fn debt_findings_creation() {
1247            let findings = DebtFindings::new("test_function");
1248            assert_eq!(findings.function_name, "test_function");
1249            assert!(findings.results.is_empty());
1250            assert!(!findings.has_issues());
1251            assert_eq!(findings.issue_count(), 0);
1252        }
1253
1254        #[test]
1255        fn debt_findings_with_results() {
1256            let mut findings = DebtFindings::new("problematic_fn");
1257            findings.add_result(PredicateResult::matched(
1258                "high_complexity",
1259                "cyclomatic > 20",
1260            ));
1261            findings.add_result(PredicateResult::not_matched("deep_nesting", "nesting > 4"));
1262            findings.add_result(PredicateResult::matched("long_method", "lines > 50"));
1263
1264            assert!(findings.has_issues());
1265            assert_eq!(findings.issue_count(), 2);
1266            assert_eq!(findings.matched_predicates().count(), 2);
1267        }
1268
1269        #[test]
1270        fn predicate_result_with_details() {
1271            let result = PredicateResult::matched("high_complexity", "cyclomatic > 20")
1272                .with_details("Actual value: 45");
1273
1274            assert!(result.matched);
1275            assert_eq!(result.details, Some("Actual value: 45".to_string()));
1276        }
1277    }
1278}