use crate::finding::{Finding, FindingKind, Tier};
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
const EXPANDED_SENTINEL: &str = "<expanded>";
pub fn dedup_syn_under_proven(findings: &mut Vec<Finding>) {
let proven_keys = collect_proven_keys(findings);
if proven_keys.is_empty() {
return;
}
findings.retain(|f| !is_shadowed(f, &proven_keys));
}
fn collect_proven_keys(findings: &[Finding]) -> BTreeSet<(String, PathBuf)> {
let mut keys = BTreeSet::new();
for f in findings {
if f.tier != Tier::Proven {
continue;
}
if let FindingKind::ResolvedReference {
source_symbol,
target,
} = &f.kind
{
keys.insert((source_symbol.clone(), target.file.clone()));
}
}
keys
}
fn is_shadowed(f: &Finding, keys: &BTreeSet<(String, PathBuf)>) -> bool {
if f.tier == Tier::Proven {
return false;
}
let Some(file) = f.primary_path().map(Path::to_path_buf) else {
return false;
};
match &f.kind {
FindingKind::TestReference {
matched_symbols, ..
} => matched_symbols
.iter()
.any(|s| keys.contains(&(s.clone(), file.clone()))),
FindingKind::TraitImpl { trait_name, .. }
| FindingKind::DerivedTraitImpl { trait_name, .. }
| FindingKind::DynDispatch { trait_name, .. } => keys.contains(&(trait_name.clone(), file)),
_ => false,
}
}
pub fn dedup_expanded_under_raw(findings: &mut Vec<Finding>) {
let raw_test_names = collect_raw_test_names(findings);
if raw_test_names.is_empty() {
return;
}
findings.retain(|f| !is_expanded_test_shadowed(f, &raw_test_names));
}
fn collect_raw_test_names(findings: &[Finding]) -> BTreeSet<String> {
let mut names = BTreeSet::new();
for f in findings {
if let FindingKind::TestReference { test, .. } = &f.kind
&& test.file != Path::new(EXPANDED_SENTINEL)
{
names.insert(test.symbol.clone());
}
}
names
}
fn is_expanded_test_shadowed(f: &Finding, raw_test_names: &BTreeSet<String>) -> bool {
let FindingKind::TestReference { test, .. } = &f.kind else {
return false;
};
test.file == Path::new(EXPANDED_SENTINEL) && raw_test_names.contains(&test.symbol)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::finding::{Finding, FindingKind, Location, Tier};
use std::path::PathBuf;
fn proven_ref(target_file: &str, source_symbol: &str) -> Finding {
let file = PathBuf::from(target_file);
Finding::new(
"",
Tier::Proven,
0.98,
FindingKind::ResolvedReference {
source_symbol: source_symbol.into(),
target: Location {
file: file.clone(),
symbol: format!("{}:1", file.display()),
},
},
"ra ref",
)
}
fn test_ref(file: &str, test_symbol: &str, matched: &[&str]) -> Finding {
Finding::new(
"",
Tier::Likely,
0.85,
FindingKind::TestReference {
test: Location {
file: PathBuf::from(file),
symbol: test_symbol.into(),
},
matched_symbols: matched.iter().map(|s| (*s).to_string()).collect(),
},
"syn test",
)
}
fn trait_impl(file: &str, trait_name: &str, impl_for: &str) -> Finding {
Finding::new(
"",
Tier::Likely,
0.80,
FindingKind::TraitImpl {
trait_name: trait_name.into(),
impl_for: impl_for.into(),
impl_site: Location {
file: PathBuf::from(file),
symbol: format!("impl {trait_name} for {impl_for}"),
},
},
"syn impl",
)
}
fn dyn_dispatch(file: &str, trait_name: &str) -> Finding {
Finding::new(
"",
Tier::Likely,
0.75,
FindingKind::DynDispatch {
trait_name: trait_name.into(),
site: Location {
file: PathBuf::from(file),
symbol: format!("dyn {trait_name}"),
},
},
"syn dyn",
)
}
#[test]
fn empty_set_leaves_findings_untouched() {
let mut findings = vec![test_ref("tests/a.rs", "t1", &["login"])];
dedup_syn_under_proven(&mut findings);
assert_eq!(findings.len(), 1);
}
#[test]
fn proven_ref_shadows_test_reference_at_same_file_and_symbol() {
let mut findings = vec![
proven_ref("tests/auth.rs", "login"),
test_ref("tests/auth.rs", "t_login", &["login"]),
];
dedup_syn_under_proven(&mut findings);
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].tier, Tier::Proven);
}
#[test]
fn proven_ref_at_different_file_does_not_shadow() {
let mut findings = vec![
proven_ref("src/other.rs", "login"),
test_ref("tests/auth.rs", "t_login", &["login"]),
];
dedup_syn_under_proven(&mut findings);
assert_eq!(findings.len(), 2);
}
#[test]
fn proven_ref_for_different_symbol_does_not_shadow() {
let mut findings = vec![
proven_ref("tests/auth.rs", "logout"),
test_ref("tests/auth.rs", "t_login", &["login"]),
];
dedup_syn_under_proven(&mut findings);
assert_eq!(findings.len(), 2);
}
#[test]
fn test_reference_shadowed_by_any_matched_symbol() {
let mut findings = vec![
proven_ref("tests/auth.rs", "login"),
test_ref("tests/auth.rs", "t_auth", &["logout", "login", "refresh"]),
];
dedup_syn_under_proven(&mut findings);
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].tier, Tier::Proven);
}
#[test]
fn trait_impl_shadowed_by_proven_ref_on_trait_name() {
let mut findings = vec![
proven_ref("src/widget.rs", "Greeter"),
trait_impl("src/widget.rs", "Greeter", "Widget"),
];
dedup_syn_under_proven(&mut findings);
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].tier, Tier::Proven);
}
#[test]
fn dyn_dispatch_shadowed_by_proven_ref_on_trait_name() {
let mut findings = vec![
proven_ref("src/bus.rs", "Handler"),
dyn_dispatch("src/bus.rs", "Handler"),
];
dedup_syn_under_proven(&mut findings);
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].tier, Tier::Proven);
}
#[test]
fn semver_finding_without_path_is_never_shadowed() {
let semver = Finding::new(
"",
Tier::Likely,
0.9,
FindingKind::SemverCheck {
level: "breaking".into(),
details: "foo removed".into(),
},
"semver",
);
let mut findings = vec![proven_ref("src/lib.rs", "foo"), semver];
dedup_syn_under_proven(&mut findings);
assert_eq!(findings.len(), 2);
}
#[test]
fn multiple_proven_refs_deduplicate_multiple_syn_findings() {
let mut findings = vec![
proven_ref("tests/a.rs", "login"),
proven_ref("tests/b.rs", "logout"),
test_ref("tests/a.rs", "t1", &["login"]),
test_ref("tests/b.rs", "t2", &["logout"]),
test_ref("tests/c.rs", "t3", &["other"]),
];
dedup_syn_under_proven(&mut findings);
assert_eq!(findings.len(), 3);
assert_eq!(
findings.iter().filter(|f| f.tier == Tier::Proven).count(),
2
);
assert_eq!(
findings.iter().filter(|f| f.tier == Tier::Likely).count(),
1
);
}
fn expanded_test_ref(test_symbol: &str, matched: &[&str]) -> Finding {
Finding::new(
"",
Tier::Likely,
0.75,
FindingKind::TestReference {
test: Location {
file: PathBuf::from(EXPANDED_SENTINEL),
symbol: test_symbol.into(),
},
matched_symbols: matched.iter().map(|s| (*s).to_string()).collect(),
},
"expanded test",
)
}
#[test]
fn expanded_test_shadowed_by_raw_with_same_name() {
let mut findings = vec![
test_ref("tests/auth.rs", "t_login", &["login"]),
expanded_test_ref("t_login", &["login"]),
];
dedup_expanded_under_raw(&mut findings);
assert_eq!(findings.len(), 1);
assert_eq!(
findings[0].primary_path().unwrap(),
Path::new("tests/auth.rs")
);
}
#[test]
fn expanded_test_with_no_raw_counterpart_survives() {
let mut findings = vec![expanded_test_ref("t_query", &["User"])];
dedup_expanded_under_raw(&mut findings);
assert_eq!(findings.len(), 1);
}
#[test]
fn expanded_trait_impls_are_never_shadowed_by_raw_test_refs() {
let expanded_impl = Finding::new(
"",
Tier::Likely,
0.75,
FindingKind::TraitImpl {
trait_name: "Serialize".into(),
impl_for: "User".into(),
impl_site: Location {
file: PathBuf::from(EXPANDED_SENTINEL),
symbol: "impl Serialize for User".into(),
},
},
"expanded impl",
);
let mut findings = vec![
test_ref("tests/auth.rs", "t_login", &["login"]),
expanded_impl,
];
dedup_expanded_under_raw(&mut findings);
assert_eq!(findings.len(), 2);
}
#[test]
fn empty_findings_is_noop() {
let mut findings: Vec<Finding> = Vec::new();
dedup_expanded_under_raw(&mut findings);
assert!(findings.is_empty());
}
#[test]
fn expanded_with_different_test_name_survives() {
let mut findings = vec![
test_ref("tests/auth.rs", "t_login", &["login"]),
expanded_test_ref("t_query", &["User"]),
];
dedup_expanded_under_raw(&mut findings);
assert_eq!(findings.len(), 2);
}
}