#![cfg_attr(coverage_nightly, coverage(off))]
use crate::models::defect_report::{Defect, DefectCategory};
use anyhow::Result;
use async_trait::async_trait;
use std::path::Path;
pub trait AnalyzerConfig: Default + Clone + Send + Sync {}
#[async_trait]
pub trait DefectAnalyzer: Send + Sync {
type Config: AnalyzerConfig;
async fn analyze(&self, project: &Path, config: Self::Config) -> Result<Vec<Defect>>;
fn category(&self) -> DefectCategory;
fn supports_incremental(&self) -> bool {
false
}
fn name(&self) -> &'static str {
match self.category() {
DefectCategory::Complexity => "Complexity Analyzer",
DefectCategory::TechnicalDebt => "Technical Debt Analyzer",
DefectCategory::DeadCode => "Dead Code Analyzer",
DefectCategory::Duplication => "Duplication Analyzer",
DefectCategory::Performance => "Performance Analyzer",
DefectCategory::Architecture => "Architecture Analyzer",
DefectCategory::TestCoverage => "Test Coverage Analyzer",
}
}
}
pub struct FileRankingEngine {
scorer: Box<dyn FileScorer + Send + Sync>,
cache: std::sync::Arc<dashmap::DashMap<std::path::PathBuf, f64>>,
}
pub trait FileScorer: Send + Sync {
fn compute_score(&self, defects: &[Defect]) -> f64;
}
pub struct SimpleScorer;
impl FileScorer for SimpleScorer {
fn compute_score(&self, defects: &[Defect]) -> f64 {
defects
.iter()
.map(super::super::models::defect_report::Defect::severity_weight)
.sum()
}
}
#[derive(Debug, Clone)]
pub struct RankedFile {
pub rank: usize,
pub score: f64,
pub path: std::path::PathBuf,
pub defects: Vec<Defect>,
}
impl FileRankingEngine {
#[must_use]
pub fn new(scorer: Box<dyn FileScorer + Send + Sync>) -> Self {
Self {
scorer,
cache: std::sync::Arc::new(dashmap::DashMap::new()),
}
}
#[must_use]
pub fn rank_files(&self, defects: Vec<Defect>, limit: usize) -> Vec<RankedFile> {
use rayon::prelude::*;
use std::cmp::Ordering;
use std::collections::BTreeMap;
let mut defects_by_file: BTreeMap<std::path::PathBuf, Vec<Defect>> = BTreeMap::new();
for defect in defects {
defects_by_file
.entry(defect.file_path.clone())
.or_default()
.push(defect);
}
let mut scored: Vec<_> = defects_by_file
.into_par_iter()
.map(|(path, file_defects)| {
let score = self.cache.get(&path).map(|s| *s).unwrap_or_else(|| {
let s = self.scorer.compute_score(&file_defects);
self.cache.insert(path.clone(), s);
s
});
RankedFile {
rank: 0, score,
path,
defects: file_defects,
}
})
.collect();
scored.par_sort_by(|a, b| {
b.score
.partial_cmp(&a.score)
.unwrap_or(Ordering::Equal)
.then_with(|| a.path.cmp(&b.path)) });
let take_count = if limit == 0 {
scored.len()
} else {
limit.min(scored.len())
};
scored.truncate(take_count);
for (i, file) in scored.iter_mut().enumerate() {
file.rank = i + 1;
}
scored
}
}
#[derive(Debug, Clone)]
pub struct AnalysisResult {
pub file_path: std::path::PathBuf,
pub absolute_path: std::path::PathBuf,
pub line_range: LineRange,
pub metrics: std::collections::BTreeMap<String, MetricValue>,
pub context: AnalysisContext,
}
#[derive(Debug, Clone)]
pub struct LineRange {
pub start: LineInfo,
pub end: Option<LineInfo>,
}
#[derive(Debug, Clone)]
pub struct LineInfo {
pub line: u32,
pub column: u32,
pub byte_offset: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub enum MetricValue {
Integer(i64),
Float(f64),
String(String),
Boolean(bool),
}
impl std::fmt::Display for MetricValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MetricValue::Integer(i) => write!(f, "{i}"),
MetricValue::Float(fl) => write!(f, "{fl:.2}"),
MetricValue::String(s) => write!(f, "{s}"),
MetricValue::Boolean(b) => write!(f, "{b}"),
}
}
}
#[derive(Debug, Clone)]
pub struct AnalysisContext {
pub description: String,
pub entity_name: Option<String>,
pub entity_type: Option<String>,
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
use crate::models::defect_report::{DefectCategory, Severity};
use std::collections::HashMap;
use std::path::PathBuf;
#[test]
fn test_simple_scorer() {
let scorer = SimpleScorer;
let defects = vec![
Defect {
id: "TEST-001".to_string(),
severity: Severity::Critical,
category: DefectCategory::Complexity,
file_path: PathBuf::from("test.rs"),
line_start: 1,
line_end: None,
column_start: None,
column_end: None,
message: "Test".to_string(),
rule_id: "test".to_string(),
fix_suggestion: None,
metrics: HashMap::new(),
},
Defect {
id: "TEST-002".to_string(),
severity: Severity::High,
category: DefectCategory::Complexity,
file_path: PathBuf::from("test.rs"),
line_start: 10,
line_end: None,
column_start: None,
column_end: None,
message: "Test 2".to_string(),
rule_id: "test".to_string(),
fix_suggestion: None,
metrics: HashMap::new(),
},
];
assert_eq!(scorer.compute_score(&defects), 15.0); }
#[test]
fn test_file_ranking_engine() {
let engine = FileRankingEngine::new(Box::new(SimpleScorer));
let defects = vec![
Defect {
id: "TEST-001".to_string(),
severity: Severity::Critical,
category: DefectCategory::Complexity,
file_path: PathBuf::from("file1.rs"),
line_start: 1,
line_end: None,
column_start: None,
column_end: None,
message: "Test".to_string(),
rule_id: "test".to_string(),
fix_suggestion: None,
metrics: HashMap::new(),
},
Defect {
id: "TEST-002".to_string(),
severity: Severity::Low,
category: DefectCategory::Complexity,
file_path: PathBuf::from("file2.rs"),
line_start: 1,
line_end: None,
column_start: None,
column_end: None,
message: "Test".to_string(),
rule_id: "test".to_string(),
fix_suggestion: None,
metrics: HashMap::new(),
},
];
let ranked = engine.rank_files(defects, 0);
assert_eq!(ranked.len(), 2);
assert_eq!(ranked[0].path, PathBuf::from("file1.rs"));
assert_eq!(ranked[0].rank, 1);
assert_eq!(ranked[0].score, 10.0);
assert_eq!(ranked[1].path, PathBuf::from("file2.rs"));
assert_eq!(ranked[1].rank, 2);
assert_eq!(ranked[1].score, 1.0);
}
#[test]
fn test_file_ranking_with_limit() {
let engine = FileRankingEngine::new(Box::new(SimpleScorer));
let defects = vec![
Defect {
id: "TEST-001".to_string(),
severity: Severity::High,
category: DefectCategory::Complexity,
file_path: PathBuf::from("file1.rs"),
line_start: 1,
line_end: None,
column_start: None,
column_end: None,
message: "Test".to_string(),
rule_id: "test".to_string(),
fix_suggestion: None,
metrics: HashMap::new(),
},
Defect {
id: "TEST-002".to_string(),
severity: Severity::Medium,
category: DefectCategory::Complexity,
file_path: PathBuf::from("file2.rs"),
line_start: 1,
line_end: None,
column_start: None,
column_end: None,
message: "Test".to_string(),
rule_id: "test".to_string(),
fix_suggestion: None,
metrics: HashMap::new(),
},
Defect {
id: "TEST-003".to_string(),
severity: Severity::Low,
category: DefectCategory::Complexity,
file_path: PathBuf::from("file3.rs"),
line_start: 1,
line_end: None,
column_start: None,
column_end: None,
message: "Test".to_string(),
rule_id: "test".to_string(),
fix_suggestion: None,
metrics: HashMap::new(),
},
];
let ranked = engine.rank_files(defects, 2);
assert_eq!(ranked.len(), 2);
assert_eq!(ranked[0].path, PathBuf::from("file1.rs"));
assert_eq!(ranked[1].path, PathBuf::from("file2.rs"));
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}