use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::scan::{Immunity, ScanReport};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum WitnessStatus {
Resolved {
location: PathBuf,
witness_kind: WitnessKind,
},
External {
tool_hint: String,
},
Ambiguous {
candidates: Vec<PathBuf>,
},
NotFound {
reason: String,
},
Missing,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum WitnessKind {
Test,
IgnoredTest,
Proptest,
Function,
PhantomType {
proof_type: String,
type_params: Vec<String>,
constructor: Option<String>,
},
SubstrateWitness {
kind: antigen_attestation::RatificationKind,
},
CrossCrateWitness,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "snake_case")]
pub enum WitnessTier {
None = 0,
Reachability = 1,
Execution = 2,
FormalProof = 4,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum AuditHint {
NoneApplicable,
FunctionResolves,
TestAttributePresentNotInvoked,
TestAttributePresentIgnoreSkipped,
ProptestPresentNotInvoked,
ExternalToolPrefixRecognized,
ExternalToolInvoked,
PhantomTypeShapeRecognized,
PhantomTypeConstructionValidated,
AmbiguousResolution,
FabricatedPathPrefix,
InheritedPresentationNotReAttested,
DisciplineSidecarMissing,
DisciplineSidecarSchemaInvalid,
DisciplinePredicateFailed,
DisciplineSubstrateStale,
DisciplineSubstrateDeltaChainNearCap,
DisciplinePredicatePassedViaDeltaChain,
DisciplinePredicatePassedSubstrateCurrent,
ToleranceVibesGrade,
ToleranceSidecarMissing,
TolerancePredicateFailed,
TolerancePredicatePassedSubstrateCurrent,
DisciplineSidecarKindMismatchExpectedImmunityGotTolerance,
ToleranceSidecarKindMismatchExpectedToleranceGotImmunity,
DisciplineImmunityToleranceContradiction,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImmunityAudit {
pub immunity: Immunity,
pub witness_status: WitnessStatus,
pub witness_tier: WitnessTier,
pub audit_hint: AuditHint,
#[serde(default = "default_evidence_kind")]
pub evidence_kind: antigen_attestation::EvidenceKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signature_strength: Option<antigen_attestation::SignatureStrength>,
#[serde(default)]
pub compound_evidence: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub evaluated_predicate: Option<String>,
}
const fn default_evidence_kind() -> antigen_attestation::EvidenceKind {
antigen_attestation::EvidenceKind::None
}
impl ImmunityAudit {
#[must_use]
pub const fn has_witness(&self) -> bool {
!matches!(self.witness_tier, WitnessTier::None)
}
#[must_use]
pub fn meets_tier(&self, minimum: WitnessTier) -> bool {
self.witness_tier >= minimum
}
#[must_use]
pub fn is_well_formed(&self) -> bool {
self.meets_tier(WitnessTier::Execution)
}
}
impl WitnessTier {
#[must_use]
pub const fn from_status(status: &WitnessStatus) -> Self {
match status {
WitnessStatus::Missing
| WitnessStatus::NotFound { .. }
| WitnessStatus::Ambiguous { .. } => Self::None,
WitnessStatus::External { .. } => Self::Reachability,
WitnessStatus::Resolved { witness_kind, .. } => match witness_kind {
WitnessKind::Test
| WitnessKind::IgnoredTest
| WitnessKind::Proptest
| WitnessKind::Function => Self::Reachability,
WitnessKind::PhantomType { .. } => Self::FormalProof,
WitnessKind::SubstrateWitness { .. } | WitnessKind::CrossCrateWitness => {
Self::Reachability
}
},
}
}
}
#[must_use]
pub const fn evidence_kind_from_status(
status: &WitnessStatus,
) -> antigen_attestation::EvidenceKind {
match status {
WitnessStatus::Missing
| WitnessStatus::NotFound { .. }
| WitnessStatus::Ambiguous { .. } => antigen_attestation::EvidenceKind::None,
WitnessStatus::External { .. } => antigen_attestation::EvidenceKind::Behavioral,
WitnessStatus::Resolved { witness_kind, .. } => match witness_kind {
WitnessKind::Test
| WitnessKind::IgnoredTest
| WitnessKind::Proptest
| WitnessKind::Function => antigen_attestation::EvidenceKind::Behavioral,
WitnessKind::PhantomType { .. } => antigen_attestation::EvidenceKind::TypeSystemProof,
WitnessKind::SubstrateWitness { .. } | WitnessKind::CrossCrateWitness => {
antigen_attestation::EvidenceKind::SubstrateState
}
},
}
}
impl AuditHint {
#[must_use]
pub const fn from_status(status: &WitnessStatus) -> Self {
match status {
WitnessStatus::Missing | WitnessStatus::NotFound { .. } => Self::NoneApplicable,
WitnessStatus::Ambiguous { .. } => Self::AmbiguousResolution,
WitnessStatus::External { .. } => Self::ExternalToolPrefixRecognized,
WitnessStatus::Resolved { witness_kind, .. } => match witness_kind {
WitnessKind::Test => Self::TestAttributePresentNotInvoked,
WitnessKind::IgnoredTest => Self::TestAttributePresentIgnoreSkipped,
WitnessKind::Proptest => Self::ProptestPresentNotInvoked,
WitnessKind::Function => Self::FunctionResolves,
WitnessKind::PhantomType { .. } => Self::PhantomTypeShapeRecognized,
WitnessKind::SubstrateWitness { .. } | WitnessKind::CrossCrateWitness => {
Self::ExternalToolPrefixRecognized
}
},
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InheritedUnaddressed {
pub presentation: crate::scan::Presentation,
pub audit_hint: AuditHint,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AuditReport {
pub audits: Vec<ImmunityAudit>,
pub resolved_count: usize,
pub external_count: usize,
pub ambiguous_count: usize,
pub broken_count: usize,
pub missing_count: usize,
#[serde(default)]
pub inherited_unaddressed: Vec<InheritedUnaddressed>,
}
impl AuditReport {
#[must_use]
pub fn all_valid(&self) -> bool {
self.audits.iter().all(ImmunityAudit::is_well_formed)
}
#[must_use]
pub fn all_meet_tier(&self, minimum: WitnessTier) -> bool {
self.audits.iter().all(|a| a.meets_tier(minimum))
}
#[must_use]
pub fn problematic_audits(&self) -> Vec<&ImmunityAudit> {
self.audits.iter().filter(|a| !a.is_well_formed()).collect()
}
}
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()
}
}
fn load_sidecar(
immunity_file: &Path,
antigen_type: &str,
) -> Option<antigen_attestation::Ratification> {
let dir = immunity_file.parent()?;
let stem = antigen_type.rsplit("::").next().unwrap_or(antigen_type);
let sidecar_path = dir.join(".attest").join(format!("{stem}.json"));
let content = std::fs::read_to_string(&sidecar_path).ok()?;
serde_json::from_str(&content).ok()
}
#[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(
|| {
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);
ImmunityAudit {
immunity: immunity.clone(),
witness_status: status,
witness_tier,
audit_hint,
evidence_kind,
signature_strength: None,
compound_evidence: false,
evaluated_predicate: None,
}
},
|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
}
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 Some(sidecar) = load_sidecar(&immunity.file, &immunity.antigen_type) else {
let result = antigen_attestation::EvaluatedPredicate::sidecar_missing();
return immunity_audit_from_evaluated(
immunity,
result,
predicate_json.to_string(),
antigen_attestation::RatificationKind::Immunity,
);
};
let Some(item) = sidecar.items.first() 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 = &item.current_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),
}
}
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::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>>;
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 super::*;
#[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 { .. }),
));
}
}