1pub mod context;
2pub mod correlation;
3pub mod coverage_gap;
4pub mod coverage_index;
5pub mod delegation;
6pub mod effects;
7pub mod evidence;
8pub mod evidence_calculator;
9pub mod function_name_matching;
10pub mod insights;
11pub mod lcov;
12pub mod path_normalization;
13pub mod priority;
14pub mod roi;
15pub mod strategy;
16pub mod thresholds;
17
18use crate::core::ComplexityMetrics;
19use im::Vector;
20use serde::{Deserialize, Serialize};
21use std::path::PathBuf;
22
23#[derive(Clone, Debug, Serialize, Deserialize)]
24pub struct FunctionRisk {
25 pub file: PathBuf,
26 pub function_name: String,
27 pub line_range: (usize, usize),
28 pub cyclomatic_complexity: u32,
29 pub cognitive_complexity: u32,
30 pub coverage_percentage: Option<f64>,
31 pub risk_score: f64,
32 #[serde(skip_serializing_if = "Option::is_none")]
33 pub contextual_risk: Option<context::ContextualRisk>,
34 pub test_effort: TestEffort,
35 pub risk_category: RiskCategory,
36 pub is_test_function: bool,
37}
38
39#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
40pub enum RiskCategory {
41 Critical, High, Medium, Low, WellTested, }
47
48#[derive(Clone, Debug, Serialize, Deserialize)]
49pub struct TestEffort {
50 pub estimated_difficulty: Difficulty,
51 pub cognitive_load: u32,
52 pub branch_count: u32,
53 pub recommended_test_cases: u32,
54}
55
56#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
57pub enum Difficulty {
58 Trivial, Simple, Moderate, Complex, VeryComplex, }
64
65#[derive(Clone, Debug, Serialize, Deserialize)]
66pub struct RiskInsight {
67 pub top_risks: Vector<FunctionRisk>,
68 pub risk_reduction_opportunities: Vector<TestingRecommendation>,
69 pub codebase_risk_score: f64,
70 pub complexity_coverage_correlation: Option<f64>,
71 pub risk_distribution: RiskDistribution,
72}
73
74#[derive(Clone, Debug, Serialize, Deserialize)]
75pub struct TestingRecommendation {
76 pub function: String,
77 pub file: PathBuf,
78 pub line: usize,
79 pub current_risk: f64,
80 pub potential_risk_reduction: f64,
81 pub test_effort_estimate: TestEffort,
82 pub rationale: String,
83 pub roi: Option<f64>,
84 pub dependencies: Vec<String>,
85 pub dependents: Vec<String>,
86}
87
88#[derive(Clone, Debug, Serialize, Deserialize)]
89pub struct RiskDistribution {
90 pub critical_count: usize,
91 pub high_count: usize,
92 pub medium_count: usize,
93 pub low_count: usize,
94 pub well_tested_count: usize,
95 pub total_functions: usize,
96}
97
98use self::context::{AnalysisTarget, ContextAggregator, ContextualRisk};
99use self::strategy::{EnhancedRiskStrategy, RiskCalculator, RiskContext};
100use std::sync::Arc;
101
102pub struct RiskAnalyzer {
103 strategy: Box<dyn RiskCalculator>,
104 debt_score: Option<f64>,
105 debt_threshold: Option<f64>,
106 context_aggregator: Option<Arc<ContextAggregator>>,
107}
108
109impl Clone for RiskAnalyzer {
110 fn clone(&self) -> Self {
115 Self {
116 strategy: self.strategy.box_clone(),
117 debt_score: self.debt_score,
118 debt_threshold: self.debt_threshold,
119 context_aggregator: self.context_aggregator.clone(), }
121 }
122}
123
124impl Default for RiskAnalyzer {
125 fn default() -> Self {
126 Self {
127 strategy: Box::new(EnhancedRiskStrategy::default()),
128 debt_score: None,
129 debt_threshold: None,
130 context_aggregator: None,
131 }
132 }
133}
134
135impl RiskAnalyzer {
136 pub fn with_debt_context(mut self, debt_score: f64, debt_threshold: f64) -> Self {
137 self.debt_score = Some(debt_score);
138 self.debt_threshold = Some(debt_threshold);
139 self
140 }
141
142 pub fn with_context_aggregator(mut self, aggregator: ContextAggregator) -> Self {
143 self.context_aggregator = Some(Arc::new(aggregator));
144 self
145 }
146
147 pub fn has_context(&self) -> bool {
148 self.context_aggregator.is_some()
149 }
150
151 pub fn analyze_function(
152 &self,
153 file: PathBuf,
154 function_name: String,
155 line_range: (usize, usize),
156 complexity: &ComplexityMetrics,
157 coverage: Option<f64>,
158 is_test: bool,
159 ) -> FunctionRisk {
160 let context = RiskContext {
161 file,
162 function_name,
163 line_range,
164 complexity: complexity.clone(),
165 coverage,
166 debt_score: self.debt_score,
167 debt_threshold: self.debt_threshold,
168 is_test,
169 is_recognized_pattern: false,
170 pattern_type: None,
171 pattern_confidence: 0.0,
172 };
173
174 self.strategy.calculate(&context)
175 }
176
177 #[allow(clippy::too_many_arguments)]
178 pub fn analyze_function_with_context(
179 &self,
180 file: PathBuf,
181 function_name: String,
182 line_range: (usize, usize),
183 complexity: &ComplexityMetrics,
184 coverage: Option<f64>,
185 is_test: bool,
186 root_path: PathBuf,
187 ) -> (FunctionRisk, Option<ContextualRisk>) {
188 let mut base_risk = self.analyze_function(
189 file.clone(),
190 function_name.clone(),
191 line_range,
192 complexity,
193 coverage,
194 is_test,
195 );
196
197 let contextual_risk = if let Some(ref aggregator) = self.context_aggregator {
198 let target = AnalysisTarget {
199 root_path,
200 file_path: file,
201 function_name: function_name.clone(),
202 line_range,
203 };
204
205 let context_map = aggregator.analyze(&target);
206 let ctx_risk = ContextualRisk::new(base_risk.risk_score, &context_map);
207
208 base_risk.contextual_risk = Some(ctx_risk.clone());
210 base_risk.risk_score = ctx_risk.contextual_risk;
211
212 if log::log_enabled!(log::Level::Debug) {
214 log::debug!(
215 "Context analysis for {}::{}: base_risk={:.1}, contextual_risk={:.1}, multiplier={:.2}x",
216 base_risk.file.display(),
217 function_name,
218 ctx_risk.base_risk,
219 ctx_risk.contextual_risk,
220 ctx_risk.contextual_risk / ctx_risk.base_risk.max(0.1)
221 );
222
223 for context in &ctx_risk.contexts {
224 log::debug!(
225 " └─ {}: contribution={:.2}, weight={:.1}, impact=+{:.1}",
226 context.provider,
227 context.contribution,
228 context.weight,
229 context.contribution * context.weight
230 );
231 }
232 }
233
234 Some(ctx_risk)
235 } else {
236 None
237 };
238
239 (base_risk, contextual_risk)
240 }
241
242 pub fn calculate_risk_score(
243 &self,
244 cyclomatic: u32,
245 cognitive: u32,
246 coverage: Option<f64>,
247 ) -> f64 {
248 let context = RiskContext {
249 file: PathBuf::new(),
250 function_name: String::new(),
251 line_range: (0, 0),
252 complexity: ComplexityMetrics {
253 functions: vec![],
254 cyclomatic_complexity: cyclomatic,
255 cognitive_complexity: cognitive,
256 },
257 coverage,
258 debt_score: self.debt_score,
259 debt_threshold: self.debt_threshold,
260 is_test: false,
261 is_recognized_pattern: false,
262 pattern_type: None,
263 pattern_confidence: 0.0,
264 };
265
266 self.strategy.calculate_risk_score(&context)
267 }
268
269 pub fn calculate_risk_reduction(
270 &self,
271 current_risk: f64,
272 complexity: u32,
273 target_coverage: f64,
274 ) -> f64 {
275 self.strategy
276 .calculate_risk_reduction(current_risk, complexity, target_coverage)
277 }
278
279 pub fn analyze_file_context(
293 &self,
294 file_path: PathBuf,
295 base_risk: f64,
296 root_path: PathBuf,
297 ) -> Option<ContextualRisk> {
298 let aggregator = self.context_aggregator.as_ref()?;
299
300 let target = AnalysisTarget {
301 root_path,
302 file_path,
303 function_name: String::new(), line_range: (0, 0), };
306
307 let context_map = aggregator.analyze(&target);
308 Some(ContextualRisk::new(base_risk, &context_map))
309 }
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315
316 #[test]
317 fn test_risk_analyzer_clone_preserves_context() {
318 let aggregator = ContextAggregator::new();
319
320 let analyzer = RiskAnalyzer::default().with_context_aggregator(aggregator);
321
322 let cloned = analyzer.clone();
323
324 assert!(cloned.has_context());
325 }
326
327 #[test]
331 #[ignore] fn test_analyze_many_functions_with_context_no_stack_overflow() {
333 use crate::core::ComplexityMetrics;
334 use crate::priority::call_graph::CallGraph;
335 use crate::risk::context::critical_path::{
336 CriticalPathAnalyzer, CriticalPathProvider, EntryPoint, EntryType,
337 };
338 use crate::risk::context::dependency::{DependencyGraph, DependencyRiskProvider};
339
340 let mut call_graph = CallGraph::new();
342 for i in 0..2000 {
343 let caller = format!("func_{}", i);
344 let callee = format!("func_{}", i + 1);
345 call_graph.add_edge_by_name(caller, callee, PathBuf::from("src/lib.rs"));
346 }
347
348 let mut cp_analyzer = CriticalPathAnalyzer::new();
350 cp_analyzer.call_graph = call_graph;
351 cp_analyzer.entry_points.push_back(EntryPoint {
352 function_name: "func_0".to_string(),
353 file_path: PathBuf::from("src/main.rs"),
354 entry_type: EntryType::Main,
355 is_user_facing: true,
356 });
357
358 let aggregator = ContextAggregator::new()
360 .with_provider(Box::new(CriticalPathProvider::new(cp_analyzer)))
361 .with_provider(Box::new(
362 DependencyRiskProvider::new(DependencyGraph::new()),
363 ));
364
365 let analyzer = RiskAnalyzer::default().with_context_aggregator(aggregator);
367
368 for i in 0..500 {
370 let complexity = ComplexityMetrics {
371 functions: vec![],
372 cyclomatic_complexity: 10,
373 cognitive_complexity: 15,
374 };
375
376 let (_risk, contextual) = analyzer.analyze_function_with_context(
377 PathBuf::from(format!("src/module_{}.rs", i % 50)),
378 format!("func_{}", i),
379 (1, 50),
380 &complexity,
381 Some(0.75),
382 false,
383 PathBuf::from("/project"),
384 );
385
386 if i < 100 {
388 assert!(
390 contextual.is_some(),
391 "Should get contextual risk for function {}",
392 i
393 );
394 }
395 }
396 }
397
398 #[test]
400 fn test_analyze_file_context_many_files_no_stack_overflow() {
401 use crate::risk::context::critical_path::{
402 CriticalPathAnalyzer, CriticalPathProvider, EntryPoint, EntryType,
403 };
404 use crate::risk::context::dependency::{DependencyGraph, DependencyRiskProvider};
405
406 let mut cp_analyzer = CriticalPathAnalyzer::new();
408 cp_analyzer.entry_points.push_back(EntryPoint {
409 function_name: "main".to_string(),
410 file_path: PathBuf::from("src/main.rs"),
411 entry_type: EntryType::Main,
412 is_user_facing: true,
413 });
414
415 let aggregator = ContextAggregator::new()
416 .with_provider(Box::new(CriticalPathProvider::new(cp_analyzer)))
417 .with_provider(Box::new(
418 DependencyRiskProvider::new(DependencyGraph::new()),
419 ));
420
421 let analyzer = RiskAnalyzer::default().with_context_aggregator(aggregator);
422
423 for i in 0..200 {
425 let result = analyzer.analyze_file_context(
426 PathBuf::from(format!("src/large_file_{}.rs", i)),
427 40.0,
428 PathBuf::from("/project"),
429 );
430
431 assert!(result.is_some(), "Should get context for file {}", i);
432 }
433 }
434
435 struct MockProvider {
437 name: &'static str,
438 }
439
440 impl context::ContextProvider for MockProvider {
441 fn name(&self) -> &str {
442 self.name
443 }
444
445 fn gather(&self, _target: &context::AnalysisTarget) -> anyhow::Result<context::Context> {
446 Ok(context::Context {
447 provider: self.name.to_string(),
448 weight: 1.0,
449 contribution: 0.5,
450 details: context::ContextDetails::Historical {
451 change_frequency: 0.1,
452 bug_density: 0.05,
453 age_days: 100,
454 author_count: 3,
455 },
456 })
457 }
458
459 fn weight(&self) -> f64 {
460 1.0
461 }
462
463 fn explain(&self, _context: &context::Context) -> String {
464 "mock".to_string()
465 }
466 }
467
468 #[test]
470 fn test_three_providers_many_iterations() {
471 let aggregator = ContextAggregator::new()
473 .with_provider(Box::new(MockProvider {
474 name: "critical_path",
475 }))
476 .with_provider(Box::new(MockProvider {
477 name: "dependency_risk",
478 }))
479 .with_provider(Box::new(MockProvider {
480 name: "git_history",
481 }));
482
483 let analyzer = RiskAnalyzer::default().with_context_aggregator(aggregator);
484
485 for i in 0..5000 {
487 let result = analyzer.analyze_file_context(
488 PathBuf::from(format!("src/file_{}.rs", i)),
489 40.0,
490 PathBuf::from("/project"),
491 );
492
493 assert!(result.is_some(), "Iteration {} should succeed", i);
494 }
495 }
496
497 #[test]
499 fn test_parallel_context_analysis_with_rayon() {
500 use rayon::prelude::*;
501
502 let aggregator = ContextAggregator::new()
504 .with_provider(Box::new(MockProvider {
505 name: "critical_path",
506 }))
507 .with_provider(Box::new(MockProvider {
508 name: "dependency_risk",
509 }))
510 .with_provider(Box::new(MockProvider {
511 name: "git_history",
512 }));
513
514 let analyzer = RiskAnalyzer::default().with_context_aggregator(aggregator);
515
516 let results: Vec<_> = (0..5000)
518 .into_par_iter()
519 .map(|i| {
520 analyzer.analyze_file_context(
521 PathBuf::from(format!("src/file_{}.rs", i)),
522 40.0,
523 PathBuf::from("/project"),
524 )
525 })
526 .collect();
527
528 assert_eq!(results.len(), 5000);
529 assert!(results.iter().all(|r| r.is_some()));
530 }
531}