Skip to main content

debtmap/priority/
unified_analysis_utils.rs

1//! Utility operations for UnifiedAnalysis.
2//!
3//! This module provides methods for managing debt items, accessing data flow
4//! information, and performing auxiliary operations on UnifiedAnalysis instances.
5
6use super::{FileDebtItem, UnifiedAnalysis, UnifiedDebtItem};
7use crate::data_flow::{DataFlowGraph, IoOperation, PurityInfo};
8use crate::priority::call_graph::FunctionId;
9use std::cmp::Ordering;
10
11// Pure comparison functions for zero-copy sorting (spec 204)
12
13/// Compare debt items by score (pure function).
14/// Returns descending order (highest scores first).
15fn compare_debt_items_by_score(a: &UnifiedDebtItem, b: &UnifiedDebtItem) -> Ordering {
16    b.unified_score
17        .final_score
18        .partial_cmp(&a.unified_score.final_score)
19        .unwrap_or(Ordering::Equal)
20        .then_with(|| a.location.file.cmp(&b.location.file))
21        .then_with(|| a.location.line.cmp(&b.location.line))
22        .then_with(|| a.location.function.cmp(&b.location.function))
23}
24
25/// Compare file items by score (pure function).
26/// Returns descending order (highest scores first).
27fn compare_file_items_by_score(a: &FileDebtItem, b: &FileDebtItem) -> Ordering {
28    b.score
29        .partial_cmp(&a.score)
30        .unwrap_or(Ordering::Equal)
31        .then_with(|| a.metrics.path.cmp(&b.metrics.path))
32}
33
34/// Extension trait providing utility operations for UnifiedAnalysis
35pub trait UnifiedAnalysisUtils {
36    /// Get timing information for the analysis phases
37    fn timings(&self) -> Option<&crate::builders::parallel_unified_analysis::AnalysisPhaseTimings>;
38
39    /// Add a file-level debt item
40    fn add_file_item(&mut self, item: FileDebtItem);
41
42    /// Add a function-level debt item
43    fn add_item(&mut self, item: UnifiedDebtItem);
44
45    /// Sort all items by priority score
46    fn sort_by_priority(&mut self);
47
48    /// Get a reference to the data flow graph
49    fn data_flow_graph(&self) -> &DataFlowGraph;
50
51    /// Get a mutable reference to the data flow graph
52    fn data_flow_graph_mut(&mut self) -> &mut DataFlowGraph;
53
54    /// Populate the data flow graph with purity analysis data
55    fn populate_purity_analysis(&mut self, metrics: &[crate::core::FunctionMetrics]);
56
57    /// Add an I/O operation to the data flow graph
58    fn add_io_operation(&mut self, func_id: FunctionId, operation: IoOperation);
59
60    /// Add variable dependencies to the data flow graph
61    fn add_variable_dependencies(
62        &mut self,
63        func_id: FunctionId,
64        variables: std::collections::HashSet<String>,
65    );
66
67    /// Apply file context adjustments to all debt item scores (spec 166)
68    ///
69    /// Adjusts scores based on file context (test vs production).
70    /// Test files receive reduced scores to avoid false positives.
71    fn apply_file_context_adjustments(
72        &mut self,
73        file_contexts: &std::collections::HashMap<std::path::PathBuf, crate::analysis::FileContext>,
74    );
75}
76
77impl UnifiedAnalysisUtils for UnifiedAnalysis {
78    fn timings(&self) -> Option<&crate::builders::parallel_unified_analysis::AnalysisPhaseTimings> {
79        self.timings.as_ref()
80    }
81
82    fn add_file_item(&mut self, item: FileDebtItem) {
83        // Get configurable thresholds
84        let min_score = crate::config::get_minimum_debt_score();
85
86        // Filter out items below minimum thresholds
87        // Items with score 0.0 are "non-debt" and should always be excluded
88        if item.score <= 0.0 || item.score < min_score {
89            return;
90        }
91
92        // Check for duplicates before adding
93        let is_duplicate = self
94            .file_items
95            .iter()
96            .any(|existing| existing.metrics.path == item.metrics.path);
97
98        if !is_duplicate {
99            self.file_items.push_back(item);
100        }
101    }
102
103    fn add_item(&mut self, item: UnifiedDebtItem) {
104        use crate::priority::filter_config::ItemFilterConfig;
105        use crate::priority::filter_predicates::*;
106
107        self.stats.total_items_processed += 1;
108
109        // Items with score 0.0 are "non-debt" and should always be filtered out,
110        // regardless of item type (including god objects)
111        if item.unified_score.final_score <= 0.0 {
112            self.stats.filtered_by_score += 1;
113            return;
114        }
115
116        // God objects bypass other filters as they represent critical architectural issues (spec 207)
117        let is_god_object = item
118            .god_object_indicators
119            .as_ref()
120            .is_some_and(|indicators| indicators.is_god_object);
121
122        if !is_god_object {
123            // Get unified filter configuration (spec 243: single-stage filtering)
124            let config = ItemFilterConfig::from_environment();
125
126            // Apply filters using pure predicates
127            if !meets_score_threshold(&item, config.min_score) {
128                self.stats.filtered_by_score += 1;
129                return;
130            }
131
132            if !meets_risk_threshold(&item, config.min_risk) {
133                self.stats.filtered_by_risk += 1;
134                return;
135            }
136
137            if !meets_complexity_thresholds(&item, config.min_cyclomatic, config.min_cognitive) {
138                self.stats.filtered_by_complexity += 1;
139                return;
140            }
141        }
142
143        // Check for duplicates (applies to all items including god objects)
144        if self
145            .items
146            .iter()
147            .any(|existing| is_duplicate_of(&item, existing))
148        {
149            self.stats.filtered_as_duplicate += 1;
150            return;
151        }
152
153        // Item passed all filters
154        self.items.push_back(item);
155        self.stats.items_added += 1;
156    }
157
158    fn sort_by_priority(&mut self) {
159        // Sort function items by score (highest first) - zero-copy with im::Vector (spec 204)
160        self.items.sort_by(compare_debt_items_by_score);
161
162        // Sort file items by score (highest first) - zero-copy with im::Vector (spec 204)
163        self.file_items.sort_by(compare_file_items_by_score);
164    }
165
166    fn data_flow_graph(&self) -> &DataFlowGraph {
167        &self.data_flow_graph
168    }
169
170    fn data_flow_graph_mut(&mut self) -> &mut DataFlowGraph {
171        &mut self.data_flow_graph
172    }
173
174    fn populate_purity_analysis(&mut self, metrics: &[crate::core::FunctionMetrics]) {
175        for metric in metrics {
176            let func_id = FunctionId::new(metric.file.clone(), metric.name.clone(), metric.line);
177
178            let purity_info = PurityInfo {
179                is_pure: metric.is_pure.unwrap_or(false),
180                confidence: metric.purity_confidence.unwrap_or(0.0),
181                impurity_reasons: if !metric.is_pure.unwrap_or(false) {
182                    vec!["Function may have side effects".to_string()]
183                } else {
184                    vec![]
185                },
186            };
187
188            self.data_flow_graph.set_purity_info(func_id, purity_info);
189        }
190    }
191
192    fn add_io_operation(&mut self, func_id: FunctionId, operation: IoOperation) {
193        self.data_flow_graph.add_io_operation(func_id, operation);
194    }
195
196    fn add_variable_dependencies(
197        &mut self,
198        func_id: FunctionId,
199        variables: std::collections::HashSet<String>,
200    ) {
201        self.data_flow_graph
202            .add_variable_dependencies(func_id, variables);
203    }
204
205    fn apply_file_context_adjustments(
206        &mut self,
207        file_contexts: &std::collections::HashMap<std::path::PathBuf, crate::analysis::FileContext>,
208    ) {
209        use crate::priority::scoring::file_context_scoring::apply_context_adjustments;
210
211        // Apply adjustments to all items
212        self.items = self
213            .items
214            .iter()
215            .map(|item| {
216                // Get the file context for this item
217                if let Some(context) = file_contexts.get(&item.location.file) {
218                    // Apply context adjustment to the final score
219                    let adjusted_score =
220                        apply_context_adjustments(item.unified_score.final_score, context);
221
222                    // Create a new item with the adjusted score and file context
223                    let mut adjusted_item = item.clone();
224                    adjusted_item.unified_score.final_score = adjusted_score.max(0.0);
225                    adjusted_item.file_context = Some(context.clone());
226                    adjusted_item
227                } else {
228                    // No context available, keep original item
229                    item.clone()
230                }
231            })
232            .collect();
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239    use crate::organization::{
240        DetectionType, GodObjectAnalysis, GodObjectConfidence, SplitAnalysisMethod,
241    };
242    use crate::priority::call_graph::CallGraph;
243    use crate::priority::{
244        ActionableRecommendation, DebtType, FunctionRole, ImpactMetrics, Location, UnifiedScore,
245    };
246    use std::collections::HashMap;
247    use std::path::PathBuf;
248
249    fn create_god_object_item(score: f64) -> UnifiedDebtItem {
250        UnifiedDebtItem {
251            location: Location {
252                file: PathBuf::from("test.rs"),
253                function: "[file-scope]".to_string(),
254                line: 1,
255            },
256            debt_type: DebtType::GodObject {
257                methods: 50,
258                fields: Some(20),
259                responsibilities: 10,
260                god_object_score: 85.0,
261                lines: 500,
262            },
263            unified_score: UnifiedScore {
264                final_score: score,
265                complexity_factor: 0.0,
266                coverage_factor: 0.0,
267                dependency_factor: 0.0,
268                role_multiplier: 1.0,
269                base_score: None,
270                exponential_factor: None,
271                risk_boost: None,
272                pre_adjustment_score: None,
273                adjustment_applied: None,
274                purity_factor: None,
275                refactorability_factor: None,
276                pattern_factor: None,
277                debt_adjustment: None,
278                pre_normalization_score: None,
279                structural_multiplier: Some(1.0),
280                has_coverage_data: false,
281                contextual_risk_multiplier: None,
282                pre_contextual_score: None,
283                debt_type_multiplier: None,
284            },
285            cyclomatic_complexity: 65,
286            cognitive_complexity: 6,
287            function_role: FunctionRole::PureLogic,
288            recommendation: ActionableRecommendation {
289                primary_action: "Split".to_string(),
290                rationale: "God object".to_string(),
291                implementation_steps: vec![],
292                related_items: vec![],
293                steps: None,
294                estimated_effort_hours: None,
295            },
296            expected_impact: ImpactMetrics {
297                coverage_improvement: 0.0,
298                lines_reduction: 0,
299                complexity_reduction: 0.0,
300                risk_reduction: 0.0,
301            },
302            transitive_coverage: None,
303            upstream_dependencies: 0,
304            downstream_dependencies: 0,
305            upstream_callers: vec![],
306            downstream_callees: vec![],
307            upstream_production_callers: vec![],
308            upstream_test_callers: vec![],
309            production_blast_radius: 0,
310            nesting_depth: 0,
311            function_length: 500,
312            is_pure: None,
313            purity_confidence: None,
314            purity_level: None,
315            god_object_indicators: Some(GodObjectAnalysis {
316                is_god_object: true,
317                method_count: 50,
318                weighted_method_count: None,
319                field_count: 20,
320                responsibility_count: 10,
321                lines_of_code: 500,
322                complexity_sum: 100,
323                god_object_score: 85.0,
324                recommended_splits: vec![],
325                confidence: GodObjectConfidence::Probable,
326                responsibilities: vec!["data".to_string()],
327                responsibility_method_counts: HashMap::new(),
328                purity_distribution: None,
329                module_structure: None,
330                detection_type: DetectionType::GodFile,
331                struct_name: None,
332                struct_line: None,
333                struct_location: None,
334                visibility_breakdown: None,
335                domain_count: 1,
336                domain_diversity: 0.0,
337                struct_ratio: 0.0,
338                analysis_method: SplitAnalysisMethod::None,
339                cross_domain_severity: None,
340                domain_diversity_metrics: None,
341                aggregated_entropy: None,
342                aggregated_error_swallowing_count: None,
343                aggregated_error_swallowing_patterns: None,
344                layering_impact: None,
345                anti_pattern_report: None,
346                complexity_metrics: None,
347                trait_method_summary: None,
348            }),
349            tier: None,
350            function_context: None,
351            context_confidence: None,
352            contextual_recommendation: None,
353            pattern_analysis: None,
354            file_context: None,
355            context_multiplier: None,
356            context_type: None,
357            language_specific: None,
358            detected_pattern: None,
359            contextual_risk: None,
360            file_line_count: None,
361            responsibility_category: None,
362            error_swallowing_count: None,
363            error_swallowing_patterns: None,
364            entropy_analysis: None,
365            context_suggestion: None,
366        }
367    }
368
369    #[test]
370    fn test_god_object_with_zero_score_is_filtered() {
371        // Bug reproduction: god objects with score 0.0 should NOT be added
372        // because score 0.0 means "this is not debt"
373        let call_graph = CallGraph::new();
374        let mut analysis = UnifiedAnalysis::new(call_graph);
375
376        // Create a god object with score 0.0 (marked as "not debt")
377        let zero_score_god_object = create_god_object_item(0.0);
378
379        analysis.add_item(zero_score_god_object);
380
381        // The item should NOT be added because score 0.0 means "not debt"
382        assert_eq!(
383            analysis.items.len(),
384            0,
385            "God object with score 0.0 should be filtered out (score 0.0 = not debt)"
386        );
387    }
388
389    #[test]
390    fn test_god_object_with_positive_score_is_included() {
391        // God objects with positive scores should still be included
392        let call_graph = CallGraph::new();
393        let mut analysis = UnifiedAnalysis::new(call_graph);
394
395        // Create a god object with a positive score
396        let god_object = create_god_object_item(50.0);
397
398        analysis.add_item(god_object);
399
400        // The item SHOULD be added because it has a positive score
401        assert_eq!(
402            analysis.items.len(),
403            1,
404            "God object with positive score should be included"
405        );
406    }
407}