use std::path::{Path, PathBuf};
use antigen_macros::{dread, presents};
use super::{
AuditHint, AuditReport, ImmuneVerdict, ImmunityAudit, InheritedUnaddressed,
PresentationVerdict, WitnessKind, WitnessStatus, WitnessTier, evidence_kind_from_status,
};
use crate::scan::{Immunity, ScanReport};
pub struct FilesystemAuditContext;
impl antigen_attestation::EvaluationContext for FilesystemAuditContext {
fn today(&self) -> chrono::NaiveDate {
chrono::Local::now().date_naive()
}
fn read_doc(&self, path: &std::path::Path) -> Option<String> {
std::fs::read_to_string(path).ok()
}
fn read_oracle(&self, path: &std::path::Path) -> Option<String> {
std::fs::read_to_string(path).ok()
}
fn read_git_trailers(
&self,
_item_source_file: &std::path::Path,
_item_path: &str,
) -> Vec<String> {
Vec::new()
}
}
pub enum SidecarLoad {
Missing,
SchemaInvalid,
Ok(antigen_attestation::Ratification),
}
pub fn load_sidecar(immunity_file: &Path, antigen_type: &str) -> SidecarLoad {
let Some(dir) = immunity_file.parent() else {
return SidecarLoad::Missing;
};
let stem = antigen_type.rsplit("::").next().unwrap_or(antigen_type);
let sidecar_path = dir.join(".attest").join(format!("{stem}.json"));
let Ok(content) = std::fs::read_to_string(&sidecar_path) else {
return SidecarLoad::Missing;
};
let Ok(ratification) = serde_json::from_str::<antigen_attestation::Ratification>(&content)
else {
return SidecarLoad::SchemaInvalid;
};
if ratification
.validate(
antigen_attestation::schema::DEFAULT_DELTA_CHAIN_CAP,
antigen_attestation::schema::DEFAULT_DELTA_RATIONALE_MIN_CHARS,
)
.is_err()
{
return SidecarLoad::SchemaInvalid;
}
SidecarLoad::Ok(ratification)
}
fn audit_code_witness(
immunity: &Immunity,
workspace_functions: &FunctionIndex,
report: &ScanReport,
) -> ImmunityAudit {
let status = validate_witness(&immunity.witness, workspace_functions);
let witness_tier = WitnessTier::from_status(&status);
let audit_hint = AuditHint::from_status(&status);
let evidence_kind = evidence_kind_from_status(&status);
let has_companion_requires = report.immunities.iter().any(|other| {
other.requires_predicate.is_some()
&& other.antigen_type == immunity.antigen_type
&& other.item_target == immunity.item_target
&& other.file == immunity.file
});
let code_witness_sidecar_ignored = !has_companion_requires
&& matches!(
load_sidecar(&immunity.file, &immunity.antigen_type),
SidecarLoad::Ok(_)
);
ImmunityAudit {
immunity: immunity.clone(),
witness_status: status,
witness_tier,
audit_hint,
evidence_kind,
signature_strength: None,
compound_evidence: false,
evaluated_predicate: None,
code_witness_sidecar_ignored,
leaf_outcomes: Vec::new(),
}
}
#[must_use]
pub fn audit(report: &ScanReport, workspace_root: &Path) -> AuditReport {
let workspace_functions = collect_function_index(workspace_root);
let mut audits = Vec::new();
for immunity in &report.immunities {
let immunity_audit = immunity.requires_predicate.as_ref().map_or_else(
|| audit_code_witness(immunity, &workspace_functions, report),
|predicate_json| audit_substrate_witness(immunity, predicate_json),
);
audits.push(immunity_audit);
}
let mut audit_report = AuditReport {
audits,
..AuditReport::default()
};
for a in &audit_report.audits {
match &a.witness_status {
WitnessStatus::Resolved { .. } => audit_report.resolved_count += 1,
WitnessStatus::External { .. } => audit_report.external_count += 1,
WitnessStatus::Ambiguous { .. } => audit_report.ambiguous_count += 1,
WitnessStatus::NotFound { .. } => audit_report.broken_count += 1,
WitnessStatus::Missing => audit_report.missing_count += 1,
}
}
for u in report.unaddressed_presentations() {
if u.presentation.inherited_from.is_some() {
audit_report
.inherited_unaddressed
.push(InheritedUnaddressed {
presentation: u.presentation,
audit_hint: AuditHint::InheritedPresentationNotReAttested,
});
}
}
audit_report.presentation_verdicts =
compute_presentation_verdicts(report, &audit_report.audits);
audit_report
}
fn compute_presentation_verdicts(
report: &ScanReport,
immunity_audits: &[ImmunityAudit],
) -> Vec<PresentationVerdict> {
let mut verdicts = Vec::new();
for p in &report.presentations {
if p.match_kind != crate::scan::MatchKind::ExplicitMarker {
continue;
}
let code_witnesses: Vec<&crate::scan::Defense> = report
.defenses
.iter()
.filter(|d| {
crate::scan::defense_addresses(d, p)
&& (d.item_kind == "fn" || d.item_kind == "impl_fn")
})
.collect();
let immune_audit: Option<&ImmunityAudit> = immunity_audits.iter().find(|a| {
a.immunity.antigen_type == p.antigen_type
&& a.immunity.file == p.file
&& a.immunity.item_target == p.item_target
});
let immune_any_substrate_gap = immunity_audits.iter().any(|a| {
a.immunity.antigen_type == p.antigen_type
&& a.immunity.file == p.file
&& a.immunity.item_target == p.item_target
&& immune_audit_is_substrate_gap(a)
});
let code_tier = if code_witnesses.is_empty() {
None
} else {
Some(WitnessTier::Reachability)
};
let immune_tier = immune_audit
.map(|a| a.witness_tier)
.filter(|t| *t != WitnessTier::None);
let site_requires_eval = p.requires_predicate.as_ref().map(|json| {
let adapter = Immunity {
antigen_type: p.antigen_type.clone(),
witness: String::new(),
requires_predicate: Some(json.clone()),
file: p.file.clone(),
line: p.line,
item_kind: p.item_kind.clone(),
item_target: p.item_target.clone(),
canonical_path: p.canonical_path.clone(),
structural_fingerprint: p.structural_fingerprint.clone(),
};
audit_substrate_witness(&adapter, json).witness_tier
});
let site_requires_tier = site_requires_eval.filter(|t| *t != WitnessTier::None);
let site_proof_tier = p
.proof
.as_deref()
.filter(|s| !s.trim().is_empty())
.map(|_| WitnessTier::FormalProof);
let best_tier = [code_tier, immune_tier, site_requires_tier, site_proof_tier]
.into_iter()
.flatten()
.max_by_key(|t| *t as u8);
let requires_present_and_failed =
site_requires_eval == Some(WitnessTier::None) || immune_any_substrate_gap;
let verdict = if requires_present_and_failed {
ImmuneVerdict::SubstrateGap
} else {
match best_tier {
Some(tier) => ImmuneVerdict::Defended { tier },
None if site_requires_eval.is_some() || immune_any_substrate_gap => {
ImmuneVerdict::SubstrateGap
},
None => ImmuneVerdict::Undefended,
}
};
let defended_by = code_witnesses
.iter()
.map(|d| format!("{}:{}", d.file.display(), d.line))
.collect();
verdicts.push(PresentationVerdict {
presentation: p.clone(),
antigen_type: p.antigen_type.clone(),
verdict,
defended_by,
});
}
verdicts
}
fn immune_audit_is_substrate_gap(a: &ImmunityAudit) -> bool {
a.evaluated_predicate.is_some()
&& a.witness_tier == WitnessTier::None
&& a.audit_hint != AuditHint::DisciplinePredicateDeferred
}
#[presents(AuditFingerprintSelfReferential)]
pub fn audit_substrate_witness(immunity: &Immunity, predicate_json: &str) -> ImmunityAudit {
use antigen_attestation::evaluate::evaluate_predicate_with_kind;
let Ok(predicate) = serde_json::from_str::<antigen_attestation::Predicate>(predicate_json)
else {
let result = antigen_attestation::EvaluatedPredicate::sidecar_schema_invalid();
return immunity_audit_from_evaluated(
immunity,
result,
predicate_json.to_string(),
antigen_attestation::RatificationKind::Immunity,
);
};
let sidecar = match load_sidecar(&immunity.file, &immunity.antigen_type) {
SidecarLoad::Missing => {
let result = antigen_attestation::EvaluatedPredicate::sidecar_missing();
return immunity_audit_from_evaluated(
immunity,
result,
predicate_json.to_string(),
antigen_attestation::RatificationKind::Immunity,
);
},
SidecarLoad::SchemaInvalid => {
let result = antigen_attestation::EvaluatedPredicate::sidecar_schema_invalid();
return immunity_audit_from_evaluated(
immunity,
result,
predicate_json.to_string(),
antigen_attestation::RatificationKind::Immunity,
);
},
SidecarLoad::Ok(r) => r,
};
let immunity_label = immunity.item_target.label();
let Some(item) = sidecar
.items
.iter()
.find(|item| item.item_path == immunity_label)
else {
let result = antigen_attestation::EvaluatedPredicate::sidecar_missing();
return immunity_audit_from_evaluated(
immunity,
result,
predicate_json.to_string(),
sidecar.kind,
);
};
let ctx = FilesystemAuditContext;
let current_fingerprint: &str = if immunity.structural_fingerprint.is_empty() {
&item.current_fingerprint
} else {
&immunity.structural_fingerprint
};
let result = evaluate_predicate_with_kind(
&predicate,
item,
current_fingerprint,
&immunity.file,
sidecar.kind,
&ctx,
)
.unwrap_or_else(|_| antigen_attestation::EvaluatedPredicate::sidecar_schema_invalid());
immunity_audit_from_evaluated(immunity, result, predicate_json.to_string(), sidecar.kind)
}
fn immunity_audit_from_evaluated(
immunity: &Immunity,
result: antigen_attestation::EvaluatedPredicate,
predicate_json: String,
sidecar_kind: antigen_attestation::RatificationKind,
) -> ImmunityAudit {
let status = WitnessStatus::Resolved {
location: immunity.file.clone(),
witness_kind: WitnessKind::SubstrateWitness { kind: sidecar_kind },
};
ImmunityAudit {
immunity: immunity.clone(),
witness_status: status,
witness_tier: map_attestation_tier(result.witness_tier),
audit_hint: map_attestation_audit_hint(result.audit_hint),
evidence_kind: result.evidence_kind,
signature_strength: result.signature_strength,
compound_evidence: false,
evaluated_predicate: Some(predicate_json),
code_witness_sidecar_ignored: false,
leaf_outcomes: result.leaf_outcomes,
}
}
const fn map_attestation_tier(tier: antigen_attestation::WitnessTier) -> WitnessTier {
match tier {
antigen_attestation::WitnessTier::None => WitnessTier::None,
antigen_attestation::WitnessTier::Reachability => WitnessTier::Reachability,
antigen_attestation::WitnessTier::Execution => WitnessTier::Execution,
antigen_attestation::WitnessTier::FormalProof => WitnessTier::FormalProof,
}
}
const fn map_attestation_audit_hint(hint: antigen_attestation::AuditHint) -> AuditHint {
use antigen_attestation::AuditHint as AH;
match hint {
AH::DisciplineSidecarMissing => AuditHint::DisciplineSidecarMissing,
AH::DisciplineSidecarSchemaInvalid => AuditHint::DisciplineSidecarSchemaInvalid,
AH::DisciplinePredicateFailed => AuditHint::DisciplinePredicateFailed,
AH::DisciplinePredicateDeferred => AuditHint::DisciplinePredicateDeferred,
AH::DisciplineSubstrateStale => AuditHint::DisciplineSubstrateStale,
AH::DisciplineSubstrateDeltaChainNearCap => AuditHint::DisciplineSubstrateDeltaChainNearCap,
AH::DisciplinePredicatePassedViaDeltaChain => {
AuditHint::DisciplinePredicatePassedViaDeltaChain
},
AH::DisciplinePredicatePassedSubstrateCurrent => {
AuditHint::DisciplinePredicatePassedSubstrateCurrent
},
AH::ToleranceVibesGrade => AuditHint::ToleranceVibesGrade,
AH::ToleranceSidecarMissing => AuditHint::ToleranceSidecarMissing,
AH::TolerancePredicateFailed => AuditHint::TolerancePredicateFailed,
AH::TolerancePredicatePassedSubstrateCurrent => {
AuditHint::TolerancePredicatePassedSubstrateCurrent
},
AH::DisciplineSidecarKindMismatchExpectedImmunityGotTolerance => {
AuditHint::DisciplineSidecarKindMismatchExpectedImmunityGotTolerance
},
AH::ToleranceSidecarKindMismatchExpectedToleranceGotImmunity => {
AuditHint::ToleranceSidecarKindMismatchExpectedToleranceGotImmunity
},
AH::DisciplineImmunityToleranceContradiction => {
AuditHint::DisciplineImmunityToleranceContradiction
},
}
}
#[derive(Debug, Clone)]
struct FunctionEntry {
location: PathBuf,
kind: WitnessKind,
}
type FunctionIndex = std::collections::HashMap<String, Vec<FunctionEntry>>;
#[dread(
trigger = "collect_function_index silently `continue`s past a file it cannot read \
and skips one it cannot parse, so the witness-function index is built \
over an INCOMPLETE corpus; a downstream 'witness function not found' \
cannot be told apart from 'the file was unreadable/unparseable'."
)]
fn collect_function_index(root: &Path) -> FunctionIndex {
use syn::visit::Visit;
use walkdir::WalkDir;
let exclusions = ["target", ".git", "node_modules"];
let mut index = FunctionIndex::new();
for entry in WalkDir::new(root)
.follow_links(false)
.into_iter()
.filter_entry(|e| {
if e.file_type().is_dir() {
let name = e.file_name().to_string_lossy();
!exclusions.iter().any(|x| *x == name)
} else {
true
}
})
{
let Ok(entry) = entry else { continue };
if !entry.file_type().is_file() {
continue;
}
if entry.path().extension().and_then(|e| e.to_str()) != Some("rs") {
continue;
}
let Ok(content) = std::fs::read_to_string(entry.path()) else {
continue;
};
if let Ok(file) = syn::parse_file(&content) {
let mut visitor = FunctionIndexVisitor {
file_path: entry.path().to_path_buf(),
source: &content,
index: &mut index,
};
visitor.visit_file(&file);
}
}
index
}
struct FunctionIndexVisitor<'a> {
file_path: PathBuf,
#[allow(
dead_code,
reason = "reserved for span-anchored diagnostic work \
that mirrors scan::ScanVisitor::source"
)]
source: &'a str,
index: &'a mut FunctionIndex,
}
impl FunctionIndexVisitor<'_> {
fn detect_kind(attrs: &[syn::Attribute]) -> WitnessKind {
let has_test = attrs.iter().any(|a| a.path().is_ident("test"));
let has_ignore = attrs.iter().any(|a| a.path().is_ident("ignore"));
match (has_test, has_ignore) {
(true, true) => WitnessKind::IgnoredTest,
(true, false) => WitnessKind::Test,
(false, _) => WitnessKind::Function,
}
}
}
fn extract_proptest_fn_names(tokens: &proc_macro2::TokenStream) -> Vec<String> {
use proc_macro2::TokenTree;
let mut names = Vec::new();
let mut iter = tokens.clone().into_iter();
while let Some(tt) = iter.next() {
if let TokenTree::Ident(i) = &tt {
if i == "fn" {
if let Some(TokenTree::Ident(name)) = iter.next() {
names.push(name.to_string());
}
}
}
}
names
}
fn macro_path_last_is(path: &syn::Path, name: &str) -> bool {
path.segments.last().is_some_and(|s| s.ident == name)
}
impl FunctionIndexVisitor<'_> {
fn push(&mut self, name: String, kind: WitnessKind) {
self.index.entry(name).or_default().push(FunctionEntry {
location: self.file_path.clone(),
kind,
});
}
}
impl<'ast> syn::visit::Visit<'ast> for FunctionIndexVisitor<'_> {
fn visit_item_fn(&mut self, item: &'ast syn::ItemFn) {
let name = item.sig.ident.to_string();
let kind = Self::detect_kind(&item.attrs);
self.push(name, kind);
syn::visit::visit_item_fn(self, item);
}
fn visit_impl_item_fn(&mut self, item: &'ast syn::ImplItemFn) {
let name = item.sig.ident.to_string();
let kind = Self::detect_kind(&item.attrs);
self.push(name, kind);
syn::visit::visit_impl_item_fn(self, item);
}
fn visit_macro(&mut self, mac: &'ast syn::Macro) {
if macro_path_last_is(&mac.path, "proptest") {
for name in extract_proptest_fn_names(&mac.tokens) {
self.push(name, WitnessKind::Proptest);
}
}
syn::visit::visit_macro(self, mac);
}
}
fn validate_witness(witness: &str, index: &FunctionIndex) -> WitnessStatus {
let normalized_owned: String = {
let collapsed = witness.split_whitespace().collect::<Vec<_>>().join(" ");
collapsed
.replace(" :: ", "::")
.replace(":: ", "::")
.replace(" ::", "::")
.replace("< ", "<")
.replace(" >", ">")
};
let trimmed = normalized_owned.trim();
if trimmed.is_empty() {
return WitnessStatus::Missing;
}
if let Some(tool) = detect_external_tool(trimmed) {
return WitnessStatus::External {
tool_hint: tool.to_string(),
};
}
if let Some(phantom) = detect_phantom_type_witness(trimmed) {
return WitnessStatus::Resolved {
location: PathBuf::new(),
witness_kind: phantom,
};
}
let function_name = trimmed
.rsplit("::")
.next()
.unwrap_or(trimmed)
.trim_end_matches("()")
.trim();
let candidates = index.get(function_name);
let Some(candidates) = candidates else {
return WitnessStatus::NotFound {
reason: format!(
"no function named `{function_name}` found in any .rs file under the scan root"
),
};
};
match candidates.as_slice() {
[] => WitnessStatus::NotFound {
reason: format!(
"no function named `{function_name}` found in any .rs file under the scan root"
),
},
[only] => WitnessStatus::Resolved {
location: only.location.clone(),
witness_kind: only.kind.clone(),
},
many => WitnessStatus::Ambiguous {
candidates: many.iter().map(|e| e.location.clone()).collect(),
},
}
}
fn detect_phantom_type_witness(witness: &str) -> Option<WitnessKind> {
let trimmed = witness.trim().trim_end_matches("()").trim();
let has_turbofish = trimmed.contains("::<");
if !has_turbofish {
return None;
}
let (before, after) = trimmed.split_once("::<")?;
let (params_raw, ctor_part) = after.split_once('>')?;
let open_count = params_raw.chars().filter(|&c| c == '<').count();
if open_count > 0 {
return None;
}
let proof_type = before.trim().to_string();
let type_params: Vec<String> = params_raw
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
let constructor = ctor_part
.trim_start_matches(['>', ':'])
.trim()
.trim_end_matches("()")
.trim();
let constructor = if constructor.is_empty() {
None
} else {
Some(constructor.to_string())
};
Some(WitnessKind::PhantomType {
proof_type,
type_params,
constructor,
})
}
fn detect_external_tool(witness: &str) -> Option<&'static str> {
let lower = witness.to_ascii_lowercase();
if lower.starts_with("clippy::") || lower.contains("clippy_") {
Some("clippy")
} else if lower.starts_with("kani::") || lower.contains("kani_proof") {
Some("kani")
} else if lower.starts_with("prusti::") {
Some("prusti")
} else if lower.starts_with("creusot::") {
Some("creusot")
} else if lower.starts_with("verus::") {
Some("verus")
} else if lower.starts_with("mutants::") {
Some("cargo-mutants")
} else {
None
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::{
FunctionEntry, FunctionIndex, FunctionIndexVisitor, detect_external_tool,
detect_phantom_type_witness, extract_proptest_fn_names, macro_path_last_is,
validate_witness,
};
use crate::audit::{WitnessKind, WitnessStatus};
#[test]
fn detect_clippy_external_tool() {
assert_eq!(
detect_external_tool("clippy::no_panic_in_drop"),
Some("clippy")
);
}
#[test]
fn detect_kani_external_tool() {
assert_eq!(
detect_external_tool("kani::proof_drop_safety"),
Some("kani")
);
}
#[test]
fn detect_no_tool_for_local_function() {
assert_eq!(detect_external_tool("safe_type_drop_no_panic_test"), None);
}
#[test]
fn validate_witness_strips_path_prefix() {
let mut idx = FunctionIndex::new();
idx.insert(
"my_test".to_string(),
vec![FunctionEntry {
location: PathBuf::from("src/lib.rs"),
kind: WitnessKind::Test,
}],
);
let status = validate_witness("module::path::my_test", &idx);
assert!(matches!(status, WitnessStatus::Resolved { .. }));
}
#[test]
fn validate_witness_reports_missing_when_empty() {
let idx = FunctionIndex::new();
let status = validate_witness("", &idx);
assert_eq!(status, WitnessStatus::Missing);
}
#[test]
fn validate_witness_reports_not_found_for_unknown() {
let idx = FunctionIndex::new();
let status = validate_witness("nonexistent_test", &idx);
assert!(matches!(status, WitnessStatus::NotFound { .. }));
}
fn index_from_str(source: &str) -> FunctionIndex {
use syn::visit::Visit;
let file = syn::parse_file(source).expect("source must parse");
let mut index = FunctionIndex::new();
let mut visitor = FunctionIndexVisitor {
file_path: PathBuf::from("<test>.rs"),
source,
index: &mut index,
};
visitor.visit_file(&file);
index
}
fn unique_kind(idx: &FunctionIndex, name: &str) -> WitnessKind {
let entries = idx.get(name).unwrap_or_else(|| panic!("{name} indexed"));
assert_eq!(
entries.len(),
1,
"expected single index entry for {name}, got {entries:?}",
);
entries[0].kind.clone()
}
#[test]
fn w5_proptest_inner_fns_are_classified_proptest() {
let src = r"
proptest! {
#[test]
fn first_proptest(x in 0u32..100) {
assert!(x < 100);
}
#[test]
fn second_proptest(x in 0u32..100, y in 0u32..100) {
assert!(x + y < 200);
}
}
";
let idx = index_from_str(src);
assert_eq!(unique_kind(&idx, "first_proptest"), WitnessKind::Proptest);
assert_eq!(unique_kind(&idx, "second_proptest"), WitnessKind::Proptest);
}
#[test]
fn w5_proptest_path_qualified_macro_is_recognized() {
let src = r"
proptest::proptest! {
#[test]
fn qualified_form_proptest(x in 0u32..100) {
assert!(x < 100);
}
}
";
let idx = index_from_str(src);
assert_eq!(
unique_kind(&idx, "qualified_form_proptest"),
WitnessKind::Proptest,
);
}
#[test]
fn w5_test_function_outside_proptest_is_classified_test() {
let src = r"
// Doc-style comment mentioning proptest! for explanation purposes.
// Pre-W5 this string in the source was sufficient to flag every
// function in the file as Proptest. W5 must not regress to that.
#[test]
fn plain_test() {
assert_eq!(2 + 2, 4);
}
proptest! {
#[test]
fn proptest_one(x in 0u32..10) {
assert!(x < 10);
}
}
";
let idx = index_from_str(src);
assert_eq!(
unique_kind(&idx, "plain_test"),
WitnessKind::Test,
"plain_test outside proptest! must be Test, not Proptest, even when \
the same file contains a proptest! invocation",
);
assert_eq!(unique_kind(&idx, "proptest_one"), WitnessKind::Proptest);
}
#[test]
fn w5_doc_comment_mentioning_proptest_does_not_over_classify() {
let src = r"
/// This function has nothing to do with proptest! — the macro
/// is named here only for documentation.
#[test]
fn doc_comment_only_test() {
assert!(true);
}
";
let idx = index_from_str(src);
assert_eq!(
unique_kind(&idx, "doc_comment_only_test"),
WitnessKind::Test,
"doc-comment mention must not trigger Proptest",
);
}
#[test]
fn w5_plain_function_is_classified_function() {
let src = r"
fn no_attribute_function() {}
";
let idx = index_from_str(src);
assert_eq!(
unique_kind(&idx, "no_attribute_function"),
WitnessKind::Function,
);
}
#[test]
fn w5_extract_proptest_fn_names_skips_nested() {
use proc_macro2::TokenStream;
let tokens: TokenStream = r"
#[test]
fn outer(x in 0u32..10) {
fn nested_helper() {}
assert!(x < 10);
}
"
.parse()
.unwrap();
let names = extract_proptest_fn_names(&tokens);
assert_eq!(names, vec!["outer".to_string()]);
}
#[test]
fn w5_macro_path_last_is_handles_qualified_paths() {
let bare: syn::Path = syn::parse_str("proptest").unwrap();
let qualified: syn::Path = syn::parse_str("proptest::proptest").unwrap();
let unrelated: syn::Path = syn::parse_str("other_crate::other_macro").unwrap();
assert!(macro_path_last_is(&bare, "proptest"));
assert!(macro_path_last_is(&qualified, "proptest"));
assert!(!macro_path_last_is(&unrelated, "proptest"));
}
#[test]
fn detect_phantom_nested_generic_returns_none() {
assert_eq!(
detect_phantom_type_witness("Witnessed::<Option<MyType>, MyWitness>::try_new"),
None,
);
assert!(matches!(
detect_phantom_type_witness("PolarityProof::<FrameTranslation>::verified"),
Some(WitnessKind::PhantomType { .. }),
));
}
}