use std::collections::BTreeMap;
use perspt_sdk::ResidualClass;
use super::types::StructuredDiagnostic;
pub const CLUSTER_PROFILE_V1: &str = "perspt-cluster-v1:log-damped";
#[derive(Debug, Clone, PartialEq)]
pub struct DiagnosticCluster {
pub class: ResidualClass,
pub code: String,
pub root: StructuredDiagnostic,
pub members: Vec<StructuredDiagnostic>,
}
impl DiagnosticCluster {
pub fn magnitude(&self) -> f64 {
1.0 + (self.members.len() as f64).ln()
}
}
pub fn cluster(diagnostics: Vec<StructuredDiagnostic>) -> Vec<DiagnosticCluster> {
let mut order: Vec<(ResidualClass, String)> = Vec::new();
let mut buckets: BTreeMap<(String, String), Vec<StructuredDiagnostic>> = BTreeMap::new();
let mut last_primary_key: Option<(ResidualClass, String)> = None;
for diagnostic in diagnostics {
let key = if !diagnostic.primary && diagnostic.code.is_none() {
last_primary_key
.clone()
.unwrap_or((diagnostic.class, format!("{:?}", diagnostic.class)))
} else {
let code = diagnostic
.code
.clone()
.unwrap_or_else(|| format!("{:?}", diagnostic.class));
let key = (diagnostic.class, code);
if diagnostic.primary {
last_primary_key = Some(key.clone());
}
key
};
let map_key = (format!("{:?}", key.0), key.1.clone());
if !buckets.contains_key(&map_key) {
order.push(key.clone());
}
buckets.entry(map_key).or_default().push(diagnostic);
}
order
.into_iter()
.map(|(class, code)| {
let members = buckets
.remove(&(format!("{class:?}"), code.clone()))
.unwrap_or_default();
DiagnosticCluster {
class,
code,
root: members.first().cloned().expect("nonempty cluster"),
members,
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn diag(class: ResidualClass, code: Option<&str>, primary: bool) -> StructuredDiagnostic {
StructuredDiagnostic {
class,
code: code.map(str::to_string),
message: "m".into(),
path: Some("src/lib.rs".into()),
line: Some(1),
column: Some(1),
primary,
suggestion: None,
}
}
#[test]
fn cascades_fold_into_their_root_cluster() {
let mut diagnostics = vec![diag(ResidualClass::ImportGraph, Some("E0432"), true)];
for _ in 0..199 {
diagnostics.push(diag(ResidualClass::ImportGraph, None, false));
}
let clusters = cluster(diagnostics);
assert_eq!(clusters.len(), 1);
assert_eq!(clusters[0].members.len(), 200);
let magnitude = clusters[0].magnitude();
assert!(magnitude < 8.0, "log damping: {magnitude}");
assert!(magnitude > 1.0);
}
#[test]
fn distinct_codes_stay_distinct_and_singletons_score_one() {
let clusters = cluster(vec![
diag(ResidualClass::Type, Some("E0308"), true),
diag(ResidualClass::ImportGraph, Some("E0432"), true),
]);
assert_eq!(clusters.len(), 2);
assert!((clusters[0].magnitude() - 1.0).abs() < 1e-9);
}
}