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}