use crate::diagnostics::{Category, Diagnostic, DimensionScores, ScoreLabel, Severity};
use std::collections::{HashMap, HashSet};
pub(super) const SCORE_GOOD_THRESHOLD: u32 = 75;
pub(super) const SCORE_OK_THRESHOLD: u32 = 50;
const ERROR_RULE_PENALTY: f64 = 1.5;
const WARNING_RULE_PENALTY: f64 = 0.75;
const INFO_RULE_PENALTY: f64 = 0.25;
const WEIGHT_SECURITY: f64 = 2.0;
const WEIGHT_RELIABILITY: f64 = 1.5;
const WEIGHT_MAINTAINABILITY: f64 = 1.0;
const WEIGHT_PERFORMANCE: f64 = 1.0;
const WEIGHT_DEPENDENCIES: f64 = 1.0;
const fn category_dimension(category: &Category) -> Dimension {
match category {
Category::Security => Dimension::Security,
Category::Correctness | Category::ErrorHandling | Category::Async | Category::Framework => {
Dimension::Reliability
}
Category::Architecture | Category::Style => Dimension::Maintainability,
Category::Performance => Dimension::Performance,
Category::Cargo | Category::Dependencies => Dimension::Dependencies,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum Dimension {
Security,
Reliability,
Maintainability,
Performance,
Dependencies,
}
fn dimension_score(error_count: usize, warning_count: usize, info_count: usize) -> u32 {
let penalty = (info_count as f64).mul_add(
INFO_RULE_PENALTY,
(error_count as f64).mul_add(
ERROR_RULE_PENALTY,
warning_count as f64 * WARNING_RULE_PENALTY,
),
);
(100.0 - penalty).round().clamp(0.0, 100.0) as u32
}
pub fn calculate_score(diagnostics: &[Diagnostic]) -> (u32, ScoreLabel, DimensionScores) {
let mut dim_errors: HashMap<Dimension, HashSet<&str>> = HashMap::new();
let mut dim_warnings: HashMap<Dimension, HashSet<&str>> = HashMap::new();
let mut dim_infos: HashMap<Dimension, HashSet<&str>> = HashMap::new();
for d in diagnostics {
let dim = category_dimension(&d.category);
match d.severity {
Severity::Error => {
dim_errors.entry(dim).or_default().insert(d.rule.as_str());
}
Severity::Warning => {
dim_warnings.entry(dim).or_default().insert(d.rule.as_str());
}
Severity::Info => {
dim_infos.entry(dim).or_default().insert(d.rule.as_str());
}
}
}
let score_for = |dim: Dimension| -> u32 {
dimension_score(
dim_errors.get(&dim).map_or(0, HashSet::len),
dim_warnings.get(&dim).map_or(0, HashSet::len),
dim_infos.get(&dim).map_or(0, HashSet::len),
)
};
let security = score_for(Dimension::Security);
let reliability = score_for(Dimension::Reliability);
let maintainability = score_for(Dimension::Maintainability);
let performance = score_for(Dimension::Performance);
let dependencies = score_for(Dimension::Dependencies);
let dimensions = DimensionScores {
security,
reliability,
maintainability,
performance,
dependencies,
};
let total_weight = WEIGHT_SECURITY
+ WEIGHT_RELIABILITY
+ WEIGHT_MAINTAINABILITY
+ WEIGHT_PERFORMANCE
+ WEIGHT_DEPENDENCIES;
let weighted_sum = f64::from(security).mul_add(
WEIGHT_SECURITY,
f64::from(reliability).mul_add(
WEIGHT_RELIABILITY,
f64::from(maintainability).mul_add(
WEIGHT_MAINTAINABILITY,
f64::from(performance).mul_add(
WEIGHT_PERFORMANCE,
f64::from(dependencies) * WEIGHT_DEPENDENCIES,
),
),
),
);
let score = (weighted_sum / total_weight).round().clamp(0.0, 100.0) as u32;
let label = score_label(score);
(score, label, dimensions)
}
pub(super) const fn score_label(score: u32) -> ScoreLabel {
if score >= SCORE_GOOD_THRESHOLD {
ScoreLabel::Great
} else if score >= SCORE_OK_THRESHOLD {
ScoreLabel::NeedsWork
} else {
ScoreLabel::Critical
}
}