Skip to main content

debtmap/analysis/attribution/
mod.rs

1//! Complexity attribution analysis.
2//!
3//! This module provides tools for attributing complexity metrics to specific
4//! source code constructs, distinguishing between logical complexity,
5//! formatting artifacts, and recognized patterns.
6
7use crate::core::FunctionMetrics;
8use serde::{Deserialize, Serialize};
9
10/// Tracks changes in complexity between analysis runs.
11///
12/// Compares current analysis results with previous runs to identify
13/// complexity trends and categorize changes.
14pub mod change_tracker;
15/// Pattern detection and complexity adjustment.
16///
17/// Identifies recognized patterns (error handling, validation, data transformation)
18/// and adjusts complexity scores based on pattern familiarity.
19pub mod pattern_tracker;
20/// Complexity source tracking and classification.
21///
22/// Tracks the origin of complexity contributions, distinguishing logical
23/// structure from formatting artifacts.
24pub mod source_tracker;
25
26use self::pattern_tracker::PatternTracker;
27use self::source_tracker::{ComplexitySourceType, SourceTracker};
28
29/// Estimated location information for complexity mapping
30#[derive(Debug, Clone)]
31struct EstimatedComplexityLocation {
32    line: u32,
33    column: u32,
34    span: Option<(u32, u32)>,
35    construct_type: String,
36    context: String,
37}
38
39/// Core attribution engine for complexity source analysis
40pub struct AttributionEngine {
41    #[allow(dead_code)]
42    source_trackers: Vec<Box<dyn SourceTracker>>,
43    pattern_tracker: PatternTracker,
44}
45
46impl AttributionEngine {
47    /// Creates a new attribution engine with default source trackers.
48    ///
49    /// Initializes the engine with trackers for logical structure complexity
50    /// and formatting artifacts, plus a pattern tracker for recognized patterns.
51    pub fn new() -> Self {
52        Self {
53            source_trackers: vec![
54                Box::new(source_tracker::LogicalStructureTracker::new()),
55                Box::new(source_tracker::FormattingArtifactTracker::new()),
56            ],
57            pattern_tracker: PatternTracker::new(),
58        }
59    }
60
61    /// Attributes complexity between logical structure, formatting, and patterns.
62    ///
63    /// Compares raw and normalized complexity results to identify how much
64    /// complexity comes from true logical structure versus formatting artifacts
65    /// or recognized patterns. Returns a complete attribution analysis.
66    pub fn attribute(
67        &self,
68        raw_result: &super::multi_pass::ComplexityResult,
69        normalized_result: &super::multi_pass::ComplexityResult,
70    ) -> ComplexityAttribution {
71        // Calculate logical complexity from normalized result
72        let logical_complexity = self.calculate_logical_complexity(normalized_result);
73
74        // Calculate formatting artifacts as difference between raw and normalized
75        let formatting_artifacts =
76            self.calculate_formatting_artifacts(raw_result, normalized_result);
77
78        // Analyze patterns in the code
79        let pattern_complexity = self
80            .pattern_tracker
81            .analyze_patterns(&normalized_result.functions);
82
83        // Generate source mappings
84        let source_mappings = self.generate_source_mappings(&raw_result.functions);
85
86        ComplexityAttribution {
87            logical_complexity,
88            formatting_artifacts,
89            pattern_complexity,
90            source_mappings,
91        }
92    }
93
94    fn calculate_logical_complexity(
95        &self,
96        normalized_result: &super::multi_pass::ComplexityResult,
97    ) -> AttributedComplexity {
98        let mut total = 0u32;
99        let mut breakdown = Vec::new();
100
101        for func in &normalized_result.functions {
102            total += func.cyclomatic;
103
104            breakdown.push(ComplexityComponent {
105                source_type: ComplexitySourceType::LogicalStructure {
106                    construct_type: LogicalConstruct::Function,
107                    nesting_level: func.nesting,
108                },
109                contribution: func.cyclomatic,
110                location: CodeLocation {
111                    file: func.file.to_string_lossy().to_string(),
112                    line: func.line as u32,
113                    column: 0,
114                    span: None,
115                },
116                description: format!("Function: {}", func.name),
117                suggestions: if func.cyclomatic > 10 {
118                    vec![
119                        "Consider breaking down this function".to_string(),
120                        "Extract complex conditions into helper functions".to_string(),
121                    ]
122                } else {
123                    vec![]
124                },
125            });
126        }
127
128        AttributedComplexity {
129            total,
130            breakdown,
131            confidence: 0.9, // High confidence for logical complexity
132        }
133    }
134
135    fn calculate_formatting_artifacts(
136        &self,
137        raw_result: &super::multi_pass::ComplexityResult,
138        normalized_result: &super::multi_pass::ComplexityResult,
139    ) -> AttributedComplexity {
140        let raw_total = raw_result.total_complexity;
141        let normalized_total = normalized_result.total_complexity;
142
143        let artifact_total = raw_total.saturating_sub(normalized_total);
144
145        let mut breakdown = Vec::new();
146
147        // Compare function-by-function to identify formatting artifacts
148        for (raw_func, norm_func) in raw_result
149            .functions
150            .iter()
151            .zip(normalized_result.functions.iter())
152        {
153            let diff = raw_func.cyclomatic.saturating_sub(norm_func.cyclomatic);
154
155            if diff > 0 {
156                breakdown.push(ComplexityComponent {
157                    source_type: ComplexitySourceType::FormattingArtifact {
158                        artifact_type: FormattingArtifact::MultilineExpression,
159                        severity: ArtifactSeverity::Medium,
160                    },
161                    contribution: diff,
162                    location: CodeLocation {
163                        file: raw_func.file.to_string_lossy().to_string(),
164                        line: raw_func.line as u32,
165                        column: 0,
166                        span: None,
167                    },
168                    description: format!("Formatting in function: {}", raw_func.name),
169                    suggestions: vec![
170                        "Use consistent formatting".to_string(),
171                        "Consider automated formatting tools".to_string(),
172                    ],
173                });
174            }
175        }
176
177        AttributedComplexity {
178            total: artifact_total,
179            breakdown,
180            confidence: 0.75, // Medium-high confidence for formatting artifacts
181        }
182    }
183
184    fn generate_source_mappings(&self, functions: &[FunctionMetrics]) -> Vec<SourceMapping> {
185        let mut mappings = Vec::new();
186
187        for func in functions {
188            // Create a base mapping for the function
189            mappings.push(SourceMapping {
190                complexity_point: 1,
191                location: CodeLocation {
192                    file: func.file.to_string_lossy().to_string(),
193                    line: func.line as u32,
194                    column: 0,
195                    span: Some((func.line as u32, (func.line + func.length) as u32)),
196                },
197                ast_path: vec![
198                    "module".to_string(),
199                    "function".to_string(),
200                    func.name.clone(),
201                ],
202                context: format!("Function definition: {}", func.name),
203            });
204
205            // Generate estimated mappings for complexity points
206            // In a full implementation, this would use AST analysis
207            let estimated_complexity_points = self.estimate_complexity_locations(func);
208
209            for (point, location_info) in estimated_complexity_points.into_iter().enumerate() {
210                if point > 0 {
211                    // Skip first point since it's already added above
212                    mappings.push(SourceMapping {
213                        complexity_point: (point + 1) as u32,
214                        location: CodeLocation {
215                            file: func.file.to_string_lossy().to_string(),
216                            line: location_info.line,
217                            column: location_info.column,
218                            span: location_info.span,
219                        },
220                        ast_path: vec![
221                            "module".to_string(),
222                            "function".to_string(),
223                            func.name.clone(),
224                            location_info.construct_type,
225                        ],
226                        context: location_info.context,
227                    });
228                }
229            }
230        }
231
232        mappings
233    }
234
235    /// Estimate complexity locations within a function
236    /// In a full implementation, this would parse the AST to find actual control flow constructs
237    fn estimate_complexity_locations(
238        &self,
239        func: &FunctionMetrics,
240    ) -> Vec<EstimatedComplexityLocation> {
241        let mut locations = Vec::new();
242
243        // Create estimated locations based on function properties
244        let _complexity_per_line = if func.length > 0 {
245            func.cyclomatic as f32 / func.length as f32
246        } else {
247            1.0
248        };
249
250        let mut current_line = func.line;
251        let line_step = if func.cyclomatic > 1 && func.length > 1 {
252            func.length / (func.cyclomatic as usize).max(1)
253        } else {
254            1
255        };
256
257        for i in 0..func.cyclomatic {
258            locations.push(EstimatedComplexityLocation {
259                line: current_line as u32,
260                column: if i == 0 { 0 } else { 4 }, // Estimate indentation
261                span: Some((current_line as u32, current_line as u32)),
262                construct_type: if i == 0 {
263                    "function_signature".to_string()
264                } else {
265                    format!("control_flow_{}", i)
266                },
267                context: if i == 0 {
268                    format!("Function signature for {}", func.name)
269                } else {
270                    format!("Control flow construct #{} in {}", i, func.name)
271                },
272            });
273            current_line += line_step;
274        }
275
276        locations
277    }
278}
279
280impl Default for AttributionEngine {
281    fn default() -> Self {
282        Self::new()
283    }
284}
285
286/// Complete complexity attribution analysis.
287///
288/// Breaks down total complexity into distinct categories: logical structure,
289/// formatting artifacts, and recognized patterns. Includes source mappings
290/// for precise code location tracking.
291#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct ComplexityAttribution {
293    /// Complexity from true logical control flow and structure.
294    pub logical_complexity: AttributedComplexity,
295    /// Complexity from formatting choices that inflate raw metrics.
296    pub formatting_artifacts: AttributedComplexity,
297    /// Complexity from recognized patterns with adjusted scores.
298    pub pattern_complexity: AttributedComplexity,
299    /// Mappings from complexity points back to source code locations.
300    pub source_mappings: Vec<SourceMapping>,
301}
302
303/// Attributed complexity with breakdown.
304///
305/// Represents a category of complexity (logical, formatting, or pattern-based)
306/// with detailed breakdown of individual components and confidence level.
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct AttributedComplexity {
309    /// Total complexity score for this category.
310    pub total: u32,
311    /// Individual components contributing to the total.
312    pub breakdown: Vec<ComplexityComponent>,
313    /// Confidence level in the attribution (0.0 to 1.0).
314    pub confidence: f32,
315}
316
317/// Individual complexity component with source attribution.
318///
319/// Represents a single contributor to complexity, tracking its source type,
320/// contribution amount, code location, and any improvement suggestions.
321#[derive(Debug, Clone, Serialize, Deserialize)]
322pub struct ComplexityComponent {
323    /// The type of complexity source (logical structure, formatting, or pattern).
324    pub source_type: ComplexitySourceType,
325    /// Numeric contribution to the total complexity score.
326    pub contribution: u32,
327    /// Source code location where this complexity originates.
328    pub location: CodeLocation,
329    /// Human-readable description of the complexity source.
330    pub description: String,
331    /// Suggested improvements to reduce this complexity.
332    pub suggestions: Vec<String>,
333}
334
335/// Mapping from a complexity point back to source code.
336///
337/// Provides precise traceability from computed complexity metrics
338/// to their originating source code constructs via AST path.
339#[derive(Debug, Clone, Serialize, Deserialize)]
340pub struct SourceMapping {
341    /// The complexity point index being mapped.
342    pub complexity_point: u32,
343    /// Source code location for this complexity point.
344    pub location: CodeLocation,
345    /// Path through the AST to reach this construct (e.g., `["module", "function", "if"]`).
346    pub ast_path: Vec<String>,
347    /// Human-readable context describing what this mapping represents.
348    pub context: String,
349}
350
351/// Source code location information.
352///
353/// Identifies a specific position in a source file, optionally with a span
354/// for multi-line constructs.
355#[derive(Debug, Clone, Serialize, Deserialize)]
356pub struct CodeLocation {
357    /// Path to the source file.
358    pub file: String,
359    /// Line number (1-indexed).
360    pub line: u32,
361    /// Column number (0-indexed).
362    pub column: u32,
363    /// Optional span as (start_line, end_line) for multi-line constructs.
364    pub span: Option<(u32, u32)>,
365}
366
367/// Types of logical constructs that contribute to complexity.
368///
369/// These are control flow and structural elements that represent
370/// genuine logical complexity in the code.
371#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
372pub enum LogicalConstruct {
373    /// Function or method definition.
374    Function,
375    /// Conditional branch (if/else).
376    If,
377    /// Iteration construct (for, while, loop).
378    Loop,
379    /// Pattern matching expression.
380    Match,
381    /// Error handling block (try/catch in other languages, `?` in Rust).
382    Try,
383    /// Closure or lambda expression.
384    Closure,
385}
386
387/// Types of formatting artifacts that inflate raw complexity metrics.
388///
389/// These are stylistic choices that don't represent actual logical
390/// complexity but may affect raw line-based metrics.
391#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
392pub enum FormattingArtifact {
393    /// Expression split across multiple lines.
394    MultilineExpression,
395    /// Unnecessary blank lines or spacing.
396    ExcessiveWhitespace,
397    /// Mixed or inconsistent indentation style.
398    InconsistentIndentation,
399    /// Redundant parentheses around expressions.
400    UnnecessaryParentheses,
401    /// Unusual line break placement.
402    LineBreakPattern,
403}
404
405/// Severity level of formatting artifacts.
406///
407/// Indicates how much the artifact inflates complexity metrics.
408#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
409pub enum ArtifactSeverity {
410    /// Minor impact on metrics (1-2 points).
411    Low,
412    /// Moderate impact on metrics (3-5 points).
413    Medium,
414    /// Significant impact on metrics (6+ points).
415    High,
416}
417
418/// Recognized code patterns that have adjusted complexity scoring.
419///
420/// When these patterns are detected, complexity scores may be adjusted
421/// since the pattern is well-understood and familiar to developers.
422#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
423pub enum RecognizedPattern {
424    /// Result/Option handling chains.
425    ErrorHandling,
426    /// Input validation and sanitization.
427    Validation,
428    /// Data mapping and transformation pipelines.
429    DataTransformation,
430    /// Mutable state management.
431    StateManagement,
432    /// Iterator and collection operations.
433    Iterator,
434    /// Builder pattern for object construction.
435    Builder,
436    /// Factory pattern for object creation.
437    Factory,
438    /// Observer/listener pattern for event handling.
439    Observer,
440}
441
442/// Language-specific features that affect complexity analysis.
443///
444/// These features require special handling during analysis since
445/// they have different complexity characteristics per language.
446#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
447pub enum LanguageFeature {
448    /// Async/await syntax for asynchronous code.
449    AsyncAwait,
450    /// Pattern matching (Rust `match`, Python `match`).
451    PatternMatching,
452    /// Generic type parameters and constraints.
453    Generics,
454    /// Macro invocations (Rust macros, C preprocessor).
455    Macros,
456    /// Decorators/attributes (Python decorators, Rust attributes).
457    Decorators,
458    /// List/dict comprehensions (Python).
459    Comprehensions,
460}
461
462#[cfg(test)]
463mod tests {
464    use super::*;
465
466    #[test]
467    fn test_attribution_engine_new() {
468        let engine = AttributionEngine::new();
469        assert!(!engine.source_trackers.is_empty());
470    }
471
472    #[test]
473    fn test_attributed_complexity() {
474        let complexity = AttributedComplexity {
475            total: 10,
476            breakdown: vec![],
477            confidence: 0.8,
478        };
479
480        assert_eq!(complexity.total, 10);
481        assert_eq!(complexity.confidence, 0.8);
482    }
483
484    #[test]
485    fn test_code_location() {
486        let location = CodeLocation {
487            file: "test.rs".to_string(),
488            line: 42,
489            column: 5,
490            span: Some((42, 50)),
491        };
492
493        assert_eq!(location.file, "test.rs");
494        assert_eq!(location.line, 42);
495        assert_eq!(location.span, Some((42, 50)));
496    }
497
498    #[test]
499    fn test_source_mapping() {
500        let mapping = SourceMapping {
501            complexity_point: 3,
502            location: CodeLocation {
503                file: "main.rs".to_string(),
504                line: 10,
505                column: 0,
506                span: None,
507            },
508            ast_path: vec!["module".to_string(), "function".to_string()],
509            context: "Test context".to_string(),
510        };
511
512        assert_eq!(mapping.complexity_point, 3);
513        assert_eq!(mapping.ast_path.len(), 2);
514    }
515}