use antigen_fingerprint::Fingerprint;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ToleranceVerdict {
Spared,
BindsCleanItem {
clean_index: usize,
},
}
impl ToleranceVerdict {
#[must_use]
pub const fn is_safe(&self) -> bool {
matches!(self, Self::Spared)
}
}
#[must_use]
pub fn evaluate(draft: &Fingerprint, clean_corpus: &[syn::Item]) -> ToleranceVerdict {
for (i, item) in clean_corpus.iter().enumerate() {
if draft.matches(item) {
return ToleranceVerdict::BindsCleanItem { clean_index: i };
}
}
ToleranceVerdict::Spared
}
#[must_use]
pub fn spare_clean(draft: &Fingerprint, clean_corpus: &[syn::Item]) -> bool {
evaluate(draft, clean_corpus).is_safe()
}
#[must_use]
pub fn promote_if_safe(draft: Fingerprint, clean_corpus: &[syn::Item]) -> Option<Fingerprint> {
if clean_corpus.is_empty() {
return None;
}
if spare_clean(&draft, clean_corpus) {
Some(draft)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
fn drop_family() -> Vec<syn::Item> {
let src = r#"
pub struct GuardA;
impl Drop for GuardA {
fn drop(&mut self) { let _ = flush().unwrap(); }
}
pub struct GuardB;
impl Drop for GuardB {
fn drop(&mut self) { let _ = flush().expect("must"); }
}
pub struct CleanGuard;
impl Drop for CleanGuard {
fn drop(&mut self) { let _ = flush().ok(); }
}
"#;
syn::parse_file(src).expect("parses").items
}
fn impl_drop_for(items: &[syn::Item], ty: &str) -> syn::Item {
items
.iter()
.find(|it| {
let syn::Item::Impl(i) = it else { return false };
let Some((_, tp, _)) = &i.trait_ else {
return false;
};
let is_drop = tp.segments.last().is_some_and(|s| s.ident == "Drop");
let syn::Type::Path(p) = &*i.self_ty else {
return false;
};
let on = p.path.segments.last().is_some_and(|s| s.ident == ty);
is_drop && on
})
.expect("found")
.clone()
}
fn naive_draft() -> Fingerprint {
Fingerprint::parse(r#"all_of([item = impl, impl_of_trait("Drop")])"#).unwrap()
}
fn disjunction_draft() -> Fingerprint {
Fingerprint::parse(
r#"all_of([item = impl, impl_of_trait("Drop"), any_of([body_calls("unwrap"), body_calls("expect")])])"#,
)
.unwrap()
}
#[test]
fn rejects_the_naive_autoimmune_draft() {
let items = drop_family();
let clean = vec![impl_drop_for(&items, "CleanGuard")];
let v = evaluate(&naive_draft(), &clean);
assert_eq!(v, ToleranceVerdict::BindsCleanItem { clean_index: 0 });
assert!(!v.is_safe());
assert!(!spare_clean(&naive_draft(), &clean));
assert!(promote_if_safe(naive_draft(), &clean).is_none());
}
#[test]
fn accepts_the_disjunction_draft() {
let items = drop_family();
let clean = vec![impl_drop_for(&items, "CleanGuard")];
let v = evaluate(&disjunction_draft(), &clean);
assert_eq!(v, ToleranceVerdict::Spared);
assert!(v.is_safe());
assert!(spare_clean(&disjunction_draft(), &clean));
assert!(promote_if_safe(disjunction_draft(), &clean).is_some());
}
#[test]
fn empty_corpus_spare_clean_predicate_is_vacuously_true() {
assert!(spare_clean(&naive_draft(), &[]));
}
#[test]
fn promote_if_safe_refuses_an_empty_corpus_the_gate_g_hazard() {
assert!(promote_if_safe(naive_draft(), &[]).is_none());
assert!(promote_if_safe(disjunction_draft(), &[]).is_none());
}
#[test]
fn rejects_when_any_clean_item_binds_not_just_the_first() {
let items = drop_family();
let nonbinding: syn::Item = syn::parse_quote! { pub struct NotEvenADrop; };
let clean = vec![nonbinding, impl_drop_for(&items, "CleanGuard")];
assert_eq!(
evaluate(&naive_draft(), &clean),
ToleranceVerdict::BindsCleanItem { clean_index: 1 }
);
}
}