Multi-Pass Analysis
Multi-pass analysis is an advanced feature that performs two separate complexity analyses on your code to distinguish between genuine logical complexity and complexity artifacts introduced by code formatting. By comparing raw and normalized versions of your code, debtmap can attribute complexity to specific sources and provide actionable insights for refactoring.
Overview
Traditional complexity analysis treats all code as-is, which means formatting choices like multiline expressions, whitespace, and indentation can artificially inflate complexity metrics. Multi-pass analysis solves this problem by:
- Raw Analysis - Measures complexity of code exactly as written
- Normalized Analysis - Measures complexity after removing formatting artifacts
- Attribution - Compares the two analyses to identify complexity sources
The difference between raw and normalized complexity reveals how much “complexity” comes from formatting versus genuine logical complexity from control flow, branching, and nesting.
How It Works
Two-Pass Analysis Process
┌─────────────┐
│ Raw Code │
└──────┬──────┘
│
├─────────────────────┐
│ │
▼ ▼
┌──────────────┐ ┌────────────────────┐
│ Raw Analysis │ │ Normalize Formatting│
└──────┬───────┘ └─────────┬──────────┘
│ │
│ ▼
│ ┌──────────────────────┐
│ │ Normalized Analysis │
│ └─────────┬────────────┘
│ │
└──────────┬───────────┘
▼
┌──────────────────┐
│ Attribution │
│ Engine │
└─────────┬────────┘
│
┌─────────┴──────────┐
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Insights │ │ Recommendations │
└─────────────────┘ └─────────────────┘
Raw Analysis examines your code as-is, capturing all complexity including:
- Logical control flow (if, loops, match, try/catch)
- Function calls and closures
- Formatting artifacts (multiline expressions, whitespace, indentation)
Normalized Analysis processes semantically equivalent code with standardized formatting:
- Removes excessive whitespace
- Normalizes multiline expressions to single lines where appropriate
- Standardizes indentation
- Preserves logical structure
Attribution Engine compares the results to categorize complexity sources:
- Logical Complexity - From control flow and branching (normalized result)
- Formatting Artifacts - From code formatting choices (difference between raw and normalized)
- Pattern Complexity - From recognized code patterns (error handling, validation, etc.)
CLI Usage
Enable multi-pass analysis with the --multi-pass flag:
# Basic multi-pass analysis
debtmap analyze . --multi-pass
# Multi-pass with detailed attribution breakdown
debtmap analyze . --multi-pass --attribution
# Control detail level
debtmap analyze . --multi-pass --attribution --detail-level comprehensive
# Output as JSON for tooling integration
debtmap analyze . --multi-pass --attribution --json
Available Flags
| Flag | Description |
|---|---|
--multi-pass | Enable two-pass analysis (raw + normalized) |
--attribution | Show detailed complexity attribution breakdown |
--detail-level <level> | Set output detail: summary, standard, comprehensive, debug |
--json | Output results in JSON format |
Attribution Engine
The attribution engine breaks down complexity into three main categories, each with detailed tracking and suggestions.
Logical Complexity
Represents inherent complexity from your code’s control flow and structure:
- Function complexity - Cyclomatic and cognitive complexity per function
- Control flow - If statements, loops, match expressions
- Error handling - Try/catch blocks, Result/Option handling
- Closures and callbacks - Anonymous functions and callbacks
- Nesting levels - Depth of nested control structures
Each logical complexity component includes:
- Contribution - Complexity points from this construct
- Location - File, line, column, and span information
- Suggestions - Specific refactoring recommendations
Example:
#![allow(unused)] fn main() { // Function with high logical complexity fn process_data(items: Vec<Item>) -> Result<Vec<Output>> { let mut results = Vec::new(); for item in items { // +1 (loop) if item.is_valid() { // +1 (if) match item.category { // +1 (match) Category::A => { if item.value > 100 { // +2 (nested if) results.push(transform_a(&item)?); } } Category::B => { results.push(transform_b(&item)?); } _ => continue, // +1 (match arm) } } } Ok(results) } // Logical complexity: ~7 points }
Formatting Artifacts
Identifies complexity introduced by code formatting choices:
- Multiline expressions - Long expressions split across multiple lines
- Excessive whitespace - Blank lines within code blocks
- Inconsistent indentation - Mixed tabs/spaces or irregular indentation
- Line breaks in chains - Method chains split across many lines
Formatting artifacts are categorized by severity:
- Low - Minor formatting inconsistencies (<10% impact)
- Medium - Noticeable formatting impact (10-25% impact)
- High - Significant complexity inflation (>25% impact)
Example:
#![allow(unused)] fn main() { // Same function with formatting that inflates complexity fn process_data( items: Vec<Item> ) -> Result<Vec<Output>> { let mut results = Vec::new(); for item in items { if item .is_valid() { match item .category { Category::A => { if item .value > 100 { results .push( transform_a( &item )? ); } } Category::B => { results .push( transform_b( &item )? ); } _ => continue, } } } Ok(results) } // Raw complexity: ~12 points (formatting adds ~5 points) // Normalized complexity: ~7 points (true logical complexity) }
Pattern Complexity
Recognizes common code patterns and their complexity characteristics:
- Error handling patterns - Result/Option propagation, error conversion
- Validation patterns - Input validation, constraint checking
- Data transformation - Map/filter/fold chains, data conversions
- Builder patterns - Fluent interfaces and builders
- State machines - Explicit state management
Each pattern includes:
- Confidence score (0.0-1.0) - How certain the pattern recognition is
- Opportunities - Suggestions for pattern extraction or improvement
Example:
#![allow(unused)] fn main() { // Error handling pattern (confidence: 0.85) fn load_config(path: &Path) -> Result<Config> { let contents = fs::read_to_string(path) .context("Failed to read config file")?; let config: Config = serde_json::from_str(&contents) .context("Failed to parse config JSON")?; config.validate() .context("Config validation failed")?; Ok(config) } // Pattern complexity: moderate error handling overhead // Suggestion: Consider error enum for better type safety }
Understanding Attribution Output
When you run with --attribution, you’ll see a detailed breakdown:
$ debtmap analyze src/main.rs --multi-pass --attribution --detail-level comprehensive
Sample Output
Multi-Pass Analysis Results
============================
File: src/main.rs
Raw Complexity: 45
Normalized Complexity: 32
Formatting Impact: 28.9%
Attribution Breakdown
---------------------
Logical Complexity: 32 points
├─ Function 'main' (line 10): 8 points
│ ├─ Control flow: 5 points (2 if, 1 match, 2 loops)
│ ├─ Nesting: 3 points (max depth: 3)
│ └─ Suggestions:
│ - Break down into smaller functions
│ - Extract complex conditions into named variables
│
├─ Function 'process_request' (line 45): 12 points
│ ├─ Control flow: 8 points (4 if, 1 match, 3 early returns)
│ ├─ Nesting: 4 points (max depth: 4)
│ └─ Suggestions:
│ - Consider using early returns to reduce nesting
│ - Extract validation logic into separate function
│
└─ Function 'handle_error' (line 89): 12 points
├─ Control flow: 9 points (5 match arms, 4 if conditions)
├─ Pattern: Error handling (confidence: 0.90)
└─ Suggestions:
- Consider error enum instead of multiple match arms
Formatting Artifacts: 13 points (28.9% of raw complexity)
├─ Multiline expressions: 8 points (Medium severity)
│ └─ Locations: lines 23, 45, 67, 89
├─ Excessive whitespace: 3 points (Low severity)
│ └─ Locations: lines 12-14, 56-58
└─ Inconsistent indentation: 2 points (Low severity)
└─ Locations: lines 34, 78
Pattern Complexity: 3 recognized patterns
├─ Error handling (confidence: 0.85): 8 occurrences
│ └─ Opportunity: Consider centralizing error handling
├─ Validation (confidence: 0.72): 5 occurrences
│ └─ Opportunity: Extract validation to separate module
└─ Data transformation (confidence: 0.68): 3 occurrences
└─ Opportunity: Review for functional composition
Interpreting the Results
Logical Complexity Breakdown
- Each function is listed with its complexity contribution
- Control flow elements are itemized (if, loops, match, etc.)
- Nesting depth shows how deeply structures are nested
- Suggestions are specific to that function’s complexity patterns
Formatting Artifacts
- Shows percentage of “false” complexity from formatting
- Severity indicates impact on metrics
- Locations help you find the formatting issues
- High formatting impact (>25%) suggests inconsistent style
Pattern Analysis
- Confidence score shows pattern recognition certainty
- High confidence (>0.7) means reliable pattern detection
- Low confidence (<0.5) suggests unique code structure
- Opportunities highlight potential refactoring
Insights and Recommendations
Multi-pass analysis automatically generates insights and recommendations based on the attribution results.
Insight Types
FormattingImpact
- Triggered when formatting contributes >20% of measured complexity
- Suggests using automated formatting tools
- Recommends standardizing team coding style
PatternOpportunity
- Triggered when pattern confidence is low (<0.5)
- Suggests extracting common patterns
- Recommends reviewing for code duplication
RefactoringCandidate
- Triggered when logical complexity exceeds threshold (>20)
- Identifies functions needing breakdown
- Provides specific refactoring strategies
ComplexityHotspot
- Identifies areas of concentrated complexity
- Highlights files or modules needing attention
- Suggests architectural improvements
Recommendation Structure
Each recommendation includes:
- Priority: Low, Medium, High
- Category: Refactoring, Pattern, Formatting, General
- Title: Brief description of the issue
- Description: Detailed explanation
- Estimated Impact: Expected complexity reduction (in points)
- Suggested Actions: Specific steps to take
Example Recommendations
{
"recommendations": [
{
"priority": "High",
"category": "Refactoring",
"title": "Simplify control flow in 'process_request'",
"description": "This function contributes 12 complexity points with deeply nested conditions",
"estimated_impact": 6,
"suggested_actions": [
"Extract validation logic into separate function",
"Use early returns to reduce nesting depth",
"Consider state pattern for complex branching"
]
},
{
"priority": "Medium",
"category": "Formatting",
"title": "Formatting contributes 29% of measured complexity",
"description": "Code formatting choices are inflating complexity metrics",
"estimated_impact": 13,
"suggested_actions": [
"Use automated formatting tools (rustfmt, prettier)",
"Standardize code formatting across the team",
"Configure editor to format on save"
]
},
{
"priority": "Low",
"category": "Pattern",
"title": "Low pattern recognition suggests unique code structure",
"description": "Pattern confidence score of 0.45 indicates non-standard patterns",
"estimated_impact": 3,
"suggested_actions": [
"Consider extracting common patterns into utilities",
"Review for code duplication opportunities",
"Document unique patterns for team understanding"
]
}
]
}
Performance Considerations
Multi-pass analysis adds overhead compared to single-pass analysis, but debtmap monitors and limits this overhead.
Performance Metrics
When performance tracking is enabled, you’ll see:
Performance Metrics
-------------------
Raw analysis: 145ms
Normalized analysis: 132ms
Attribution: 45ms
Total time: 322ms
Memory used: 12.3 MB
Overhead: 121.7% vs single-pass (145ms baseline)
⚠️ Warning: Overhead exceeds 25% target
Tracked Metrics:
- Raw analysis time - Time to analyze original code
- Normalized analysis time - Time to analyze normalized code
- Attribution time - Time to compute attribution breakdown
- Total time - Complete multi-pass analysis duration
- Memory used - Additional memory for two-pass analysis
Performance Overhead
Target Overhead: ≤25% compared to single-pass analysis
Multi-pass analysis aims to add no more than 25% overhead versus standard single-pass analysis. If overhead exceeds this threshold, a warning is issued.
Typical Overhead:
- Attribution adds ~10-15% on average
- Normalization adds ~5-10% on average
- Total overhead usually 15-25%
Factors Affecting Performance:
- File size - Larger files take proportionally longer
- Complexity - More complex code requires more analysis time
- Language - Some languages (TypeScript) are slower to parse
- Parallel processing - Overhead is per-file, parallel reduces impact
Optimization Tips
Enable Caching
# Use shared cache for repeated analyses
debtmap analyze . --multi-pass --cache-location shared
Disable Performance Tracking in Production
#![allow(unused)] fn main() { MultiPassOptions { performance_tracking: false, // Reduces overhead slightly ..Default::default() } }
Use Parallel Processing
# Parallel analysis amortizes overhead across cores
debtmap analyze . --multi-pass --jobs 8
Target Specific Files
# Analyze only files that need detailed attribution
debtmap analyze src/complex_module.rs --multi-pass --attribution
Comparative Analysis
Multi-pass analysis supports comparing code changes to validate refactoring efforts.
Basic Comparison
#![allow(unused)] fn main() { use debtmap::analysis::multi_pass::compare_complexity; use debtmap::core::Language; let before_code = r#" fn process(items: Vec<i32>) -> i32 { let mut sum = 0; for item in items { if item > 0 { if item % 2 == 0 { sum += item * 2; } else { sum += item; } } } sum } "#; let after_code = r#" fn process(items: Vec<i32>) -> i32 { items .into_iter() .filter(|&item| item > 0) .map(|item| if item % 2 == 0 { item * 2 } else { item }) .sum() } "#; let comparison = compare_complexity(before_code, after_code, Language::Rust)?; println!("Complexity change: {}", comparison.complexity_change); println!("Cognitive complexity change: {}", comparison.cognitive_change); println!("Formatting impact change: {}", comparison.formatting_impact_change); }
Comparison Results
The ComparativeAnalysis struct includes:
#![allow(unused)] fn main() { pub struct ComparativeAnalysis { pub before: MultiPassResult, pub after: MultiPassResult, pub complexity_change: i32, // Negative = improvement pub cognitive_change: i32, // Negative = improvement pub formatting_impact_change: f32, // Negative = less formatting noise pub improvements: Vec<String>, pub regressions: Vec<String>, } }
Interpreting Changes:
- Negative complexity change - Refactoring reduced complexity ✓
- Positive complexity change - Refactoring increased complexity ✗
- Improvements - List of detected improvements (reduced nesting, extracted functions, etc.)
- Regressions - List of detected regressions (increased complexity, new anti-patterns, etc.)
Example Output
Comparative Analysis
====================
Complexity Changes:
├─ Cyclomatic: 8 → 4 (-4, -50%)
├─ Cognitive: 12 → 5 (-7, -58.3%)
└─ Formatting Impact: 25% → 10% (-15%, -60%)
Improvements Detected:
✓ Reduced nesting depth (3 → 1)
✓ Eliminated mutable state
✓ Replaced imperative loop with functional chain
✓ Improved formatting consistency
No regressions detected.
Verdict: Refactoring reduced complexity by 50% and improved code clarity.
Configuration Options
Configure multi-pass analysis programmatically:
#![allow(unused)] fn main() { use debtmap::analysis::multi_pass::{MultiPassAnalyzer, MultiPassOptions}; use debtmap::analysis::diagnostics::{DetailLevel, OutputFormat}; use debtmap::core::Language; let options = MultiPassOptions { language: Language::Rust, detail_level: DetailLevel::Comprehensive, enable_recommendations: true, track_source_locations: true, generate_insights: true, output_format: OutputFormat::Json, performance_tracking: true, }; let analyzer = MultiPassAnalyzer::new(options); }
Configuration Fields
| Field | Type | Default | Description |
|---|---|---|---|
language | Language | Rust | Target programming language |
detail_level | DetailLevel | Standard | Output detail: Summary, Standard, Comprehensive, Debug |
enable_recommendations | bool | true | Generate actionable recommendations |
track_source_locations | bool | true | Include file/line/column in attribution |
generate_insights | bool | true | Automatically generate insights |
output_format | OutputFormat | Json | Output format: Json, Markdown, Terminal |
performance_tracking | bool | false | Track and report performance metrics |
Use Cases
When to Use Multi-Pass Analysis
Refactoring Validation
- Compare before/after complexity to validate refactoring
- Ensure complexity actually decreased
- Identify unintended complexity increases
Formatting Impact Assessment
- Determine how much formatting affects your metrics
- Justify automated formatting tool adoption
- Identify formatting inconsistencies
Targeted Refactoring
- Use attribution to find highest-impact refactoring targets
- Focus on logical complexity, not formatting artifacts
- Prioritize functions with actionable suggestions
Code Review
- Provide objective complexity data in pull requests
- Identify genuine complexity increases vs formatting changes
- Guide refactoring discussions with data
Codebase Health Monitoring
- Track logical complexity trends over time
- Separate signal (logic) from noise (formatting)
- Identify complexity hotspots for architectural review
When to Use Standard Analysis
Quick Feedback
- Fast complexity checks during development
- CI/CD gates that need speed
- Large codebases where overhead matters
Sufficient Metrics
- When overall complexity trends are enough
- No need for detailed attribution
- Formatting is already standardized
Future Enhancements
Spec 84: Detailed AST-Based Source Mapping
The current implementation uses estimated complexity locations based on function metrics. Spec 84 will enhance attribution with precise AST-based source mapping:
Planned Improvements:
- Exact AST node locations - Precise line, column, and span for each complexity point
- 100% accurate mapping - No estimation, direct AST-to-source mapping
- IDE integration - Jump from complexity reports directly to source code
- Inline visualization - Show complexity heat maps in your editor
- Statement-level tracking - Complexity attribution at statement granularity
Current vs Future:
Current (estimated):
#![allow(unused)] fn main() { ComplexityComponent { location: CodeLocation { line: 45, // Function start line column: 0, // Estimated span: None, // Not available }, description: "Function: process_request", } }
Future (precise):
#![allow(unused)] fn main() { ComplexityComponent { location: SourceLocation { line: 47, // Exact if statement line column: 8, // Exact column span: Some(47, 52), // Exact span of construct ast_path: "fn::process_request::body::if[0]", }, description: "If condition: item.is_valid()", } }
This will enable:
- Click-to-navigate from reports to exact code locations
- Visual Studio Code / IntelliJ integration for inline complexity display
- More precise refactoring suggestions
- Better complexity trend tracking at fine granularity
Summary
Multi-pass analysis provides deep insights into your code’s complexity by:
- Separating signal from noise - Distinguishing logical complexity from formatting artifacts
- Attributing complexity sources - Identifying what contributes to complexity and why
- Generating actionable insights - Providing specific refactoring recommendations
- Validating refactoring - Comparing before/after to prove complexity reduction
- Monitoring performance - Ensuring overhead stays within acceptable bounds
Use --multi-pass --attribution when you need detailed complexity analysis and targeted refactoring guidance. The overhead (typically 15-25%) is worthwhile when you need to understand why code is complex and how to improve it.
For quick complexity checks and CI/CD integration, standard single-pass analysis is usually sufficient. Save multi-pass analysis for deep dives, refactoring validation, and complexity investigations.
See Also:
- Analysis Guide - General analysis capabilities
- Scoring Strategies - How complexity affects debt scores
- Coverage Integration - Combining complexity with coverage
- Examples - Real-world multi-pass analysis examples