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