Skip to main content

debtmap/output/
pattern_analysis.rs

1//! Pattern Analysis Data Structures
2//!
3//! This module defines data structures for aggregating and displaying pattern analysis results
4//! from purity analysis (Spec 143), framework pattern detection (Spec 144), and Rust-specific
5//! pattern detection (Spec 146).
6//!
7//! # Architecture
8//!
9//! - **Data structures are pure data** - no formatting logic, only calculations
10//! - **All types implement Serialize/Deserialize** - for potential JSON output
11//! - **Default implementations** - all types have sensible defaults
12//! - **Helper methods for queries** - has_patterns(), repetitive_traits(), etc.
13//!
14//! # Example
15//!
16//! ```ignore
17//! use debtmap::output::pattern_analysis::{PatternAnalysis, PurityMetrics};
18//!
19//! // Create pattern analysis from function analyses
20//! let pattern_analysis = PatternAnalysis::from_functions(&functions);
21//!
22//! // Query the data
23//! if pattern_analysis.purity.purity_percentage() > 0.5 {
24//!     println!("Codebase is more than 50% pure!");
25//! }
26//! ```
27
28use crate::analysis::purity_analysis::PurityViolation;
29use serde::{Deserialize, Serialize};
30use std::fmt;
31
32/// Constants for thresholds (configurable via settings)
33pub const REPETITIVE_TRAIT_THRESHOLD: usize = 5;
34pub const MAX_DISPLAYED_EXAMPLES: usize = 3;
35pub const MAX_ALMOST_PURE_VIOLATIONS: usize = 2;
36
37/// Purity metrics aggregated from function analyses
38#[derive(Debug, Clone, Serialize, Deserialize, Default)]
39pub struct PurityMetrics {
40    pub strictly_pure: usize,
41    pub locally_pure: usize,
42    pub read_only: usize,
43    pub impure: usize,
44    pub almost_pure: Vec<AlmostPureFunction>,
45}
46
47impl PurityMetrics {
48    /// Pure calculation - returns total number of functions analyzed
49    pub fn total_functions(&self) -> usize {
50        self.strictly_pure + self.locally_pure + self.read_only + self.impure
51    }
52
53    /// Returns purity percentage as a value between 0.0 and 1.0
54    pub fn purity_percentage(&self) -> f64 {
55        let total = self.total_functions();
56        if total == 0 {
57            0.0
58        } else {
59            (self.strictly_pure + self.locally_pure) as f64 / total as f64
60        }
61    }
62
63    /// Returns the top N almost-pure functions by refactoring impact
64    pub fn top_refactoring_opportunities(&self, limit: usize) -> &[AlmostPureFunction] {
65        let end = self.almost_pure.len().min(limit);
66        &self.almost_pure[..end]
67    }
68
69    /// Returns true if there are any functions analyzed
70    pub fn has_functions(&self) -> bool {
71        self.total_functions() > 0
72    }
73
74    /// Returns true if there are refactoring opportunities
75    pub fn has_opportunities(&self) -> bool {
76        !self.almost_pure.is_empty()
77    }
78}
79
80/// Function that is "almost pure" - has only 1-2 purity violations
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct AlmostPureFunction {
83    pub name: String,
84    pub violations: Vec<PurityViolation>,
85    pub refactoring_suggestion: String,
86}
87
88/// Framework pattern metrics aggregated from function analyses
89#[derive(Debug, Clone, Serialize, Deserialize, Default)]
90pub struct FrameworkPatternMetrics {
91    pub patterns: Vec<DetectedPattern>,
92}
93
94impl FrameworkPatternMetrics {
95    /// Returns patterns sorted by count (most frequent first)
96    pub fn sorted_by_frequency(&self) -> Vec<&DetectedPattern> {
97        let mut sorted: Vec<_> = self.patterns.iter().collect();
98        sorted.sort_by(|a, b| b.count.cmp(&a.count));
99        sorted
100    }
101
102    /// Returns true if any framework patterns were detected
103    pub fn has_patterns(&self) -> bool {
104        !self.patterns.is_empty()
105    }
106}
107
108/// Detected framework pattern
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct DetectedPattern {
111    pub framework: String,
112    pub pattern_type: String,
113    pub count: usize,
114    pub examples: Vec<String>,
115    pub recommendation: String,
116}
117
118/// Rust-specific pattern metrics
119#[derive(Debug, Clone, Serialize, Deserialize, Default)]
120pub struct RustPatternMetrics {
121    pub trait_impls: Vec<TraitImplementation>,
122    pub async_patterns: AsyncPatternSummary,
123    pub error_handling: ErrorHandlingSummary,
124    pub builder_candidates: Vec<String>,
125}
126
127impl RustPatternMetrics {
128    /// Returns trait implementations that appear repetitively
129    pub fn repetitive_traits(&self) -> Vec<&TraitImplementation> {
130        self.trait_impls
131            .iter()
132            .filter(|t| t.count >= REPETITIVE_TRAIT_THRESHOLD)
133            .collect()
134    }
135
136    /// Returns true if async patterns are present
137    pub fn has_async_patterns(&self) -> bool {
138        self.async_patterns.async_functions > 0
139    }
140
141    /// Returns true if error handling patterns are present
142    pub fn has_error_handling(&self) -> bool {
143        self.error_handling.question_mark_density > 0.0 || self.error_handling.unwrap_count > 0
144    }
145
146    /// Returns true if builder patterns are present
147    pub fn has_builder_candidates(&self) -> bool {
148        !self.builder_candidates.is_empty()
149    }
150
151    /// Returns true if any Rust patterns are present
152    pub fn has_patterns(&self) -> bool {
153        !self.trait_impls.is_empty()
154            || self.has_async_patterns()
155            || self.has_error_handling()
156            || self.has_builder_candidates()
157    }
158}
159
160/// Trait implementation pattern
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct TraitImplementation {
163    pub trait_name: String,
164    pub count: usize,
165    pub types: Vec<String>,
166}
167
168/// Async pattern summary
169#[derive(Debug, Clone, Serialize, Deserialize, Default)]
170pub struct AsyncPatternSummary {
171    pub async_functions: usize,
172    pub spawn_calls: usize,
173    pub channel_usage: bool,
174    pub mutex_usage: bool,
175}
176
177/// Error handling summary
178#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct ErrorHandlingSummary {
180    /// Average ? operators per function
181    pub question_mark_density: f64,
182    pub custom_error_types: Vec<String>,
183    /// Anti-pattern: unwrap() call count
184    pub unwrap_count: usize,
185}
186
187impl Default for ErrorHandlingSummary {
188    fn default() -> Self {
189        Self {
190            question_mark_density: 0.0,
191            custom_error_types: vec![],
192            unwrap_count: 0,
193        }
194    }
195}
196
197/// Container for all pattern analysis results
198/// Attached to recommendations for display
199#[derive(Debug, Clone, Serialize, Deserialize, Default)]
200pub struct PatternAnalysis {
201    pub purity: PurityMetrics,
202    pub frameworks: FrameworkPatternMetrics,
203    pub rust_patterns: RustPatternMetrics,
204}
205
206impl PatternAnalysis {
207    /// Create a new empty pattern analysis
208    pub fn new() -> Self {
209        Self::default()
210    }
211
212    /// Returns true if any patterns were detected
213    pub fn has_patterns(&self) -> bool {
214        self.purity.has_functions()
215            || self.frameworks.has_patterns()
216            || self.rust_patterns.has_patterns()
217    }
218
219    /// Create PatternAnalysis from a collection of function analyses
220    /// This aggregates purity metrics, framework patterns, and Rust patterns
221    pub fn from_functions(functions: &[crate::priority::FunctionAnalysis]) -> Self {
222        Self {
223            purity: aggregate_purity_metrics(functions),
224            frameworks: aggregate_framework_patterns(functions),
225            rust_patterns: aggregate_rust_patterns(functions),
226        }
227    }
228}
229
230/// Aggregate purity metrics from function analyses
231fn aggregate_purity_metrics(functions: &[crate::priority::FunctionAnalysis]) -> PurityMetrics {
232    functions
233        .iter()
234        .map(classify_purity_bucket)
235        .fold(PurityCounts::default(), PurityCounts::with_bucket)
236        .into_metrics()
237}
238
239#[derive(Debug, Clone, Copy, PartialEq, Eq)]
240enum PurityBucket {
241    StrictlyPure,
242    LocallyPure,
243    ReadOnly,
244    Impure,
245}
246
247#[derive(Debug, Clone, Copy, Default)]
248struct PurityCounts {
249    strictly_pure: usize,
250    locally_pure: usize,
251    read_only: usize,
252    impure: usize,
253}
254
255impl PurityCounts {
256    fn with_bucket(self, bucket: PurityBucket) -> Self {
257        match bucket {
258            PurityBucket::StrictlyPure => Self {
259                strictly_pure: self.strictly_pure + 1,
260                ..self
261            },
262            PurityBucket::LocallyPure => Self {
263                locally_pure: self.locally_pure + 1,
264                ..self
265            },
266            PurityBucket::ReadOnly => Self {
267                read_only: self.read_only + 1,
268                ..self
269            },
270            PurityBucket::Impure => Self {
271                impure: self.impure + 1,
272                ..self
273            },
274        }
275    }
276
277    fn into_metrics(self) -> PurityMetrics {
278        PurityMetrics {
279            strictly_pure: self.strictly_pure,
280            locally_pure: self.locally_pure,
281            read_only: self.read_only,
282            impure: self.impure,
283            almost_pure: Vec::new(),
284        }
285    }
286}
287
288fn classify_purity_bucket(func: &crate::priority::FunctionAnalysis) -> PurityBucket {
289    match func.is_pure {
290        Some(true) if has_strict_purity_confidence(func) => PurityBucket::StrictlyPure,
291        Some(true) => PurityBucket::LocallyPure,
292        Some(false) if is_read_only_candidate(func) => PurityBucket::ReadOnly,
293        _ => PurityBucket::Impure,
294    }
295}
296
297fn has_strict_purity_confidence(func: &crate::priority::FunctionAnalysis) -> bool {
298    func.purity_confidence.unwrap_or(0.0) >= 0.9
299}
300
301fn is_read_only_candidate(func: &crate::priority::FunctionAnalysis) -> bool {
302    func.cyclomatic_complexity <= 5 && func.cognitive_complexity <= 7
303}
304
305/// Aggregate framework patterns from function analyses
306fn aggregate_framework_patterns(
307    _functions: &[crate::priority::FunctionAnalysis],
308) -> FrameworkPatternMetrics {
309    // Framework pattern detection would need additional analysis
310    // For now, return empty metrics
311    FrameworkPatternMetrics {
312        patterns: Vec::new(),
313    }
314}
315
316/// Aggregate Rust-specific patterns from function analyses
317fn aggregate_rust_patterns(_functions: &[crate::priority::FunctionAnalysis]) -> RustPatternMetrics {
318    // Rust pattern detection would need additional analysis
319    // For now, return empty metrics
320    RustPatternMetrics::default()
321}
322
323/// Generate purity refactoring suggestions based on violation types
324/// This is a pure function that takes violation information and returns actionable suggestions
325pub fn suggest_purity_refactoring(violations: &[PurityViolation]) -> Vec<String> {
326    let mut suggestions = Vec::new();
327
328    for violation in violations {
329        let suggestion = match violation {
330            PurityViolation::StateMutation { .. } => {
331                "Pass state as function parameter instead of mutating external state"
332            }
333            PurityViolation::IoOperation { .. } => {
334                "Move I/O to function boundaries; separate pure logic from side effects"
335            }
336            PurityViolation::NonDeterministic { .. } => {
337                "Make function deterministic by accepting random seed or current time as parameter"
338            }
339            PurityViolation::ImpureCall { .. } => {
340                "Extract pure logic from impure function calls; isolate side effects"
341            }
342        };
343
344        if !suggestions.contains(&suggestion.to_string()) {
345            suggestions.push(suggestion.to_string());
346        }
347    }
348
349    suggestions
350}
351
352/// Generate framework-specific recommendations based on detected patterns
353/// Pure function that maps framework patterns to actionable advice
354pub fn generate_framework_recommendation(
355    framework: &str,
356    pattern_type: &str,
357    count: usize,
358) -> String {
359    match (framework, pattern_type) {
360        ("React", "hooks") => {
361            format!(
362                "Consider extracting {} hook usages into custom hooks for reusability",
363                count
364            )
365        }
366        ("React", "component") => {
367            format!(
368                "Review {} components for proper memoization and render optimization",
369                count
370            )
371        }
372        ("Tokio", "async") => {
373            format!(
374                "Verify {} async operations use proper error handling and cancellation",
375                count
376            )
377        }
378        ("Actix", "handler") => {
379            format!(
380                "Ensure {} handlers follow async best practices and proper error propagation",
381                count
382            )
383        }
384        ("Django", "view") => {
385            format!(
386                "Review {} views for proper transaction handling and query optimization",
387                count
388            )
389        }
390        ("Flask", "route") => {
391            format!(
392                "Consider adding validation and error handling to {} route handlers",
393                count
394            )
395        }
396        _ => {
397            format!(
398                "Review {} instances of {} {} pattern for consistency and best practices",
399                count, framework, pattern_type
400            )
401        }
402    }
403}
404
405/// Display implementation for PurityViolation (used in formatting)
406impl fmt::Display for PurityViolation {
407    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
408        write!(f, "{}", self.description())
409    }
410}
411
412#[cfg(test)]
413mod tests {
414    use super::*;
415    use crate::analysis::purity_analysis::PurityViolation;
416    use crate::priority::{FunctionAnalysis, FunctionVisibility};
417    use std::path::PathBuf;
418
419    fn function_analysis(
420        is_pure: Option<bool>,
421        purity_confidence: Option<f32>,
422        cyclomatic_complexity: u32,
423        cognitive_complexity: u32,
424    ) -> FunctionAnalysis {
425        FunctionAnalysis {
426            file: PathBuf::from("src/lib.rs"),
427            function: "analyze".to_string(),
428            line: 1,
429            function_length: 10,
430            cyclomatic_complexity,
431            cognitive_complexity,
432            is_pure,
433            purity_confidence,
434            nesting_depth: 0,
435            is_test: false,
436            visibility: FunctionVisibility::Private,
437        }
438    }
439
440    #[test]
441    fn test_purity_metrics_total_functions() {
442        let metrics = PurityMetrics {
443            strictly_pure: 10,
444            locally_pure: 5,
445            read_only: 3,
446            impure: 7,
447            almost_pure: vec![],
448        };
449
450        assert_eq!(metrics.total_functions(), 25);
451    }
452
453    #[test]
454    fn test_purity_metrics_purity_percentage() {
455        let metrics = PurityMetrics {
456            strictly_pure: 15,
457            locally_pure: 10,
458            read_only: 5,
459            impure: 20,
460            almost_pure: vec![],
461        };
462
463        // (15 + 10) / 50 = 0.5 = 50%
464        assert_eq!(metrics.purity_percentage(), 0.5);
465    }
466
467    #[test]
468    fn test_purity_metrics_purity_percentage_zero_functions() {
469        let metrics = PurityMetrics::default();
470        assert_eq!(metrics.purity_percentage(), 0.0);
471    }
472
473    #[test]
474    fn test_purity_metrics_top_refactoring_opportunities() {
475        let func1 = AlmostPureFunction {
476            name: "func1".to_string(),
477            violations: vec![],
478            refactoring_suggestion: "test".to_string(),
479        };
480        let func2 = AlmostPureFunction {
481            name: "func2".to_string(),
482            violations: vec![],
483            refactoring_suggestion: "test".to_string(),
484        };
485        let func3 = AlmostPureFunction {
486            name: "func3".to_string(),
487            violations: vec![],
488            refactoring_suggestion: "test".to_string(),
489        };
490
491        let metrics = PurityMetrics {
492            strictly_pure: 0,
493            locally_pure: 0,
494            read_only: 0,
495            impure: 0,
496            almost_pure: vec![func1, func2, func3],
497        };
498
499        let top = metrics.top_refactoring_opportunities(2);
500        assert_eq!(top.len(), 2);
501        assert_eq!(top[0].name, "func1");
502        assert_eq!(top[1].name, "func2");
503    }
504
505    #[test]
506    fn test_purity_metrics_has_functions() {
507        let empty = PurityMetrics::default();
508        assert!(!empty.has_functions());
509
510        let with_functions = PurityMetrics {
511            strictly_pure: 1,
512            locally_pure: 0,
513            read_only: 0,
514            impure: 0,
515            almost_pure: vec![],
516        };
517        assert!(with_functions.has_functions());
518    }
519
520    #[test]
521    fn test_purity_metrics_has_opportunities() {
522        let empty = PurityMetrics::default();
523        assert!(!empty.has_opportunities());
524
525        let func = AlmostPureFunction {
526            name: "test".to_string(),
527            violations: vec![],
528            refactoring_suggestion: "fix".to_string(),
529        };
530        let with_opportunities = PurityMetrics {
531            strictly_pure: 0,
532            locally_pure: 0,
533            read_only: 0,
534            impure: 0,
535            almost_pure: vec![func],
536        };
537        assert!(with_opportunities.has_opportunities());
538    }
539
540    #[test]
541    fn test_framework_pattern_metrics_sorted_by_frequency() {
542        let pattern1 = DetectedPattern {
543            framework: "React".to_string(),
544            pattern_type: "hooks".to_string(),
545            count: 5,
546            examples: vec![],
547            recommendation: "test".to_string(),
548        };
549        let pattern2 = DetectedPattern {
550            framework: "Vue".to_string(),
551            pattern_type: "components".to_string(),
552            count: 15,
553            examples: vec![],
554            recommendation: "test".to_string(),
555        };
556        let pattern3 = DetectedPattern {
557            framework: "Angular".to_string(),
558            pattern_type: "services".to_string(),
559            count: 10,
560            examples: vec![],
561            recommendation: "test".to_string(),
562        };
563
564        let metrics = FrameworkPatternMetrics {
565            patterns: vec![pattern1, pattern2, pattern3],
566        };
567
568        let sorted = metrics.sorted_by_frequency();
569        assert_eq!(sorted.len(), 3);
570        assert_eq!(sorted[0].count, 15); // Vue (highest)
571        assert_eq!(sorted[1].count, 10); // Angular
572        assert_eq!(sorted[2].count, 5); // React (lowest)
573    }
574
575    #[test]
576    fn test_rust_pattern_metrics_repetitive_traits() {
577        let trait1 = TraitImplementation {
578            trait_name: "Display".to_string(),
579            count: 3,
580            types: vec![],
581        };
582        let trait2 = TraitImplementation {
583            trait_name: "Clone".to_string(),
584            count: 10,
585            types: vec![],
586        };
587
588        let metrics = RustPatternMetrics {
589            trait_impls: vec![trait1, trait2],
590            async_patterns: AsyncPatternSummary::default(),
591            error_handling: ErrorHandlingSummary::default(),
592            builder_candidates: vec![],
593        };
594
595        let repetitive = metrics.repetitive_traits();
596        assert_eq!(repetitive.len(), 1);
597        assert_eq!(repetitive[0].trait_name, "Clone");
598        assert_eq!(repetitive[0].count, 10);
599    }
600
601    #[test]
602    fn test_rust_pattern_metrics_has_patterns() {
603        let empty = RustPatternMetrics::default();
604        assert!(!empty.has_patterns());
605
606        let with_traits = RustPatternMetrics {
607            trait_impls: vec![TraitImplementation {
608                trait_name: "Debug".to_string(),
609                count: 1,
610                types: vec![],
611            }],
612            async_patterns: AsyncPatternSummary::default(),
613            error_handling: ErrorHandlingSummary::default(),
614            builder_candidates: vec![],
615        };
616        assert!(with_traits.has_patterns());
617
618        let with_async = RustPatternMetrics {
619            trait_impls: vec![],
620            async_patterns: AsyncPatternSummary {
621                async_functions: 5,
622                spawn_calls: 0,
623                channel_usage: false,
624                mutex_usage: false,
625            },
626            error_handling: ErrorHandlingSummary::default(),
627            builder_candidates: vec![],
628        };
629        assert!(with_async.has_patterns());
630    }
631
632    #[test]
633    fn test_pattern_analysis_has_patterns() {
634        let empty = PatternAnalysis::default();
635        assert!(!empty.has_patterns());
636
637        let with_purity = PatternAnalysis {
638            purity: PurityMetrics {
639                strictly_pure: 5,
640                locally_pure: 0,
641                read_only: 0,
642                impure: 0,
643                almost_pure: vec![],
644            },
645            frameworks: FrameworkPatternMetrics::default(),
646            rust_patterns: RustPatternMetrics::default(),
647        };
648        assert!(with_purity.has_patterns());
649    }
650
651    #[test]
652    fn test_classify_purity_bucket_strictly_pure() {
653        let function = function_analysis(Some(true), Some(0.9), 1, 1);
654
655        assert_eq!(
656            classify_purity_bucket(&function),
657            PurityBucket::StrictlyPure
658        );
659    }
660
661    #[test]
662    fn test_classify_purity_bucket_locally_pure() {
663        let function = function_analysis(Some(true), Some(0.89), 1, 1);
664
665        assert_eq!(classify_purity_bucket(&function), PurityBucket::LocallyPure);
666    }
667
668    #[test]
669    fn test_classify_purity_bucket_read_only() {
670        let function = function_analysis(Some(false), None, 5, 7);
671
672        assert_eq!(classify_purity_bucket(&function), PurityBucket::ReadOnly);
673    }
674
675    #[test]
676    fn test_classify_purity_bucket_impure_when_complexity_exceeds_read_only_threshold() {
677        let high_cyclomatic = function_analysis(Some(false), None, 6, 7);
678        let high_cognitive = function_analysis(Some(false), None, 5, 8);
679
680        assert_eq!(
681            classify_purity_bucket(&high_cyclomatic),
682            PurityBucket::Impure
683        );
684        assert_eq!(
685            classify_purity_bucket(&high_cognitive),
686            PurityBucket::Impure
687        );
688    }
689
690    #[test]
691    fn test_classify_purity_bucket_unknown_purity_as_impure() {
692        let function = function_analysis(None, None, 1, 1);
693
694        assert_eq!(classify_purity_bucket(&function), PurityBucket::Impure);
695    }
696
697    #[test]
698    fn test_aggregate_purity_metrics_counts_all_buckets() {
699        let functions = vec![
700            function_analysis(Some(true), Some(0.95), 1, 1),
701            function_analysis(Some(true), Some(0.5), 1, 1),
702            function_analysis(Some(false), None, 5, 7),
703            function_analysis(Some(false), None, 9, 9),
704            function_analysis(None, None, 1, 1),
705        ];
706
707        let metrics = aggregate_purity_metrics(&functions);
708
709        assert_eq!(metrics.strictly_pure, 1);
710        assert_eq!(metrics.locally_pure, 1);
711        assert_eq!(metrics.read_only, 1);
712        assert_eq!(metrics.impure, 2);
713        assert!(metrics.almost_pure.is_empty());
714    }
715
716    #[test]
717    fn test_suggest_purity_refactoring() {
718        let violations = vec![
719            PurityViolation::StateMutation {
720                target: "x".to_string(),
721                line: Some(10),
722            },
723            PurityViolation::IoOperation {
724                description: "println".to_string(),
725                line: Some(15),
726            },
727        ];
728
729        let suggestions = suggest_purity_refactoring(&violations);
730        assert_eq!(suggestions.len(), 2);
731        assert!(suggestions[0].contains("state as function parameter"));
732        assert!(suggestions[1].contains("Move I/O to function boundaries"));
733    }
734
735    #[test]
736    fn test_suggest_purity_refactoring_no_duplicates() {
737        let violations = vec![
738            PurityViolation::StateMutation {
739                target: "x".to_string(),
740                line: Some(10),
741            },
742            PurityViolation::StateMutation {
743                target: "y".to_string(),
744                line: Some(20),
745            },
746        ];
747
748        let suggestions = suggest_purity_refactoring(&violations);
749        assert_eq!(suggestions.len(), 1); // Should not duplicate the same suggestion
750    }
751
752    #[test]
753    fn test_generate_framework_recommendation() {
754        let rec = generate_framework_recommendation("React", "hooks", 10);
755        assert!(rec.contains("React") || rec.contains("10") || rec.contains("hook"));
756
757        let rec2 = generate_framework_recommendation("Tokio", "async", 5);
758        assert!(rec2.contains("async") || rec2.contains("5"));
759    }
760}