use crate::cli::proof_annotation_helpers::{
collect_and_filter_annotations, format_as_full, format_as_json, format_as_markdown,
format_as_sarif, format_as_summary, incomplete_analysis_note, setup_proof_annotator,
ProofAnnotationFilter,
};
use crate::cli::{ProofAnnotationOutputFormat, PropertyTypeFilter, VerificationMethodFilter};
use crate::models::unified_ast::{Location, ProofAnnotation};
use crate::services::proof_annotator::ProofAnnotator;
use anyhow::Result;
use std::path::{Path, PathBuf};
use std::time::Instant;
#[allow(clippy::too_many_arguments)]
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub async fn handle_analyze_proof_annotations(
project_path: PathBuf,
format: ProofAnnotationOutputFormat,
high_confidence_only: bool,
include_evidence: bool,
property_type: Option<PropertyTypeFilter>,
verification_method: Option<VerificationMethodFilter>,
output: Option<PathBuf>,
_perf: bool,
clear_cache: bool,
top_files: usize,
) -> Result<()> {
crate::cli::ensure_analysis_path_exists(&project_path)?;
crate::status_eprintln!("🔍 Collecting proof annotations from project...");
let start = Instant::now();
let annotator = setup_proof_annotator(clear_cache);
let filter = ProofAnnotationFilter {
high_confidence_only,
property_type,
verification_method,
};
let annotations = collect_and_filter_annotations(&annotator, &project_path, &filter).await;
let method_note = measure_method_filter(&annotator, &project_path, &filter, &annotations).await;
let elapsed = start.elapsed();
crate::status_eprintln!(
"âś… Found {} matching proof annotations in {} ms (verified at {})",
annotations.len(),
elapsed.as_millis(),
chrono::Utc::now().to_rfc3339()
);
if let Some(note) = incomplete_analysis_note(annotator.collection_errors()) {
eprint!("⚠️{note}");
}
if let Some(ref outcome) = method_note {
eprint!("⚠️{}", outcome.note());
}
let content = format_proof_annotations(
format,
&annotations,
elapsed,
&annotator,
&project_path,
include_evidence,
top_files,
method_note.as_ref(),
)?;
if let Some(output_path) = output {
tokio::fs::write(&output_path, &content).await?;
crate::status_eprintln!("âś… Proof annotations written to: {}", output_path.display());
} else {
println!("{content}");
}
Ok(())
}
struct MethodFilterOutcome {
requested: String,
collected_ignoring_method: usize,
methods_present: Vec<String>,
}
impl MethodFilterOutcome {
fn note(&self) -> String {
format!(
" UNMEASURED: --verification-method {} matched 0 of the {} annotation(s) this \
project yields. The methods actually present are: {}. pmat collects proof \
annotations from its Rust static analyser only, so an empty result under \
--verification-method {} means no collector in this build produces that method — \
NOT that the property was verified and found absent.\n",
self.requested,
self.collected_ignoring_method,
self.methods_present.join(", "),
self.requested,
)
}
}
async fn measure_method_filter(
annotator: &ProofAnnotator,
project_path: &Path,
filter: &ProofAnnotationFilter,
matched: &[(Location, ProofAnnotation)],
) -> Option<MethodFilterOutcome> {
let requested = match filter.verification_method.as_ref() {
None | Some(VerificationMethodFilter::All) => return None,
Some(method) => method.to_string(),
};
if !matched.is_empty() {
return None;
}
let without_method = ProofAnnotationFilter {
high_confidence_only: filter.high_confidence_only,
property_type: filter.property_type.clone(),
verification_method: None,
};
let all = collect_and_filter_annotations(annotator, project_path, &without_method).await;
if all.is_empty() {
return None;
}
let methods_present: Vec<String> = all
.iter()
.map(|(_, annotation)| format!("{:?}", annotation.method))
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.collect();
Some(MethodFilterOutcome {
requested,
collected_ignoring_method: all.len(),
methods_present,
})
}
fn attach_method_filter_to_json(content: &str, outcome: &MethodFilterOutcome) -> Result<String> {
let mut doc: serde_json::Value = serde_json::from_str(content)?;
if let Some(summary) = doc.get_mut("summary").and_then(|s| s.as_object_mut()) {
summary.insert(
"verification_method_filter".to_string(),
serde_json::json!({
"requested": outcome.requested,
"annotations_ignoring_method_filter": outcome.collected_ignoring_method,
"methods_present": outcome.methods_present,
"reason": "no registered proof source produces this verification method",
}),
);
}
Ok(serde_json::to_string_pretty(&doc)?)
}
#[allow(clippy::too_many_arguments)]
fn format_proof_annotations(
format: ProofAnnotationOutputFormat,
annotations: &[(Location, ProofAnnotation)],
elapsed: std::time::Duration,
annotator: &ProofAnnotator,
project_path: &Path,
include_evidence: bool,
top_files: usize,
method_note: Option<&MethodFilterOutcome>,
) -> Result<String> {
let mut content = match format {
ProofAnnotationOutputFormat::Json => format_as_json(annotations, elapsed, annotator)?,
ProofAnnotationOutputFormat::Summary => format_as_summary(annotations, elapsed, top_files)?,
ProofAnnotationOutputFormat::Full => {
format_as_full(annotations, project_path, include_evidence)?
}
ProofAnnotationOutputFormat::Markdown => {
format_as_markdown(annotations, project_path, include_evidence)?
}
ProofAnnotationOutputFormat::Sarif => format_as_sarif(annotations, project_path)?,
};
if !matches!(
format,
ProofAnnotationOutputFormat::Json | ProofAnnotationOutputFormat::Sarif
) {
if let Some(note) = incomplete_analysis_note(annotator.collection_errors()) {
content.push_str(¬e);
}
}
if let Some(outcome) = method_note {
match format {
ProofAnnotationOutputFormat::Json => {
content = attach_method_filter_to_json(&content, outcome)?;
}
ProofAnnotationOutputFormat::Sarif => {}
_ => content.push_str(&outcome.note()),
}
}
Ok(content)
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod active_tests {
use super::*;
use tempfile::TempDir;
#[tokio::test]
async fn test_handle_analyze_proof_annotations_empty_dir() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let result = handle_analyze_proof_annotations(
temp_dir.path().to_path_buf(),
ProofAnnotationOutputFormat::Summary,
false,
false,
None,
None,
None,
false,
false,
10,
)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_unparseable_files_are_disclosed_in_the_report() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
std::fs::write(
temp_dir.path().join("good.rs"),
"pub fn good(x: &str) -> usize { x.len() }\n",
)
.expect("write");
std::fs::write(temp_dir.path().join("broken.rs"), "fn ((( <<< not rust\n").expect("write");
let summary_out = temp_dir.path().join("summary.txt");
handle_analyze_proof_annotations(
temp_dir.path().to_path_buf(),
ProofAnnotationOutputFormat::Summary,
false,
false,
None,
None,
Some(summary_out.clone()),
false,
true,
10,
)
.await
.expect("summary run");
let summary = std::fs::read_to_string(&summary_out).expect("read summary");
assert!(
summary.contains("INCOMPLETE"),
"the summary must disclose the file it could not parse, got:\n{summary}"
);
let json_out = temp_dir.path().join("out.json");
handle_analyze_proof_annotations(
temp_dir.path().to_path_buf(),
ProofAnnotationOutputFormat::Json,
false,
false,
None,
None,
Some(json_out.clone()),
false,
true,
10,
)
.await
.expect("json run");
let doc: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&json_out).expect("read json"))
.expect("valid json");
assert_eq!(
doc["summary"]["files_not_analyzed"].as_u64(),
Some(1),
"the JSON summary must report the skipped file, got: {doc}"
);
}
#[tokio::test]
async fn test_complete_analysis_carries_no_incomplete_note() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
std::fs::write(
temp_dir.path().join("good.rs"),
"pub fn good(x: &str) -> usize { x.len() }\n",
)
.expect("write");
let out = temp_dir.path().join("summary.txt");
handle_analyze_proof_annotations(
temp_dir.path().to_path_buf(),
ProofAnnotationOutputFormat::Summary,
false,
false,
None,
None,
Some(out.clone()),
false,
true,
10,
)
.await
.expect("summary run");
let summary = std::fs::read_to_string(&out).expect("read summary");
assert!(!summary.contains("INCOMPLETE"), "got:\n{summary}");
}
#[tokio::test]
async fn an_uncollectable_verification_method_is_disclosed_not_rendered_as_zero() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
std::fs::write(
temp_dir.path().join("lib.rs"),
"pub fn safe_add(a: i32, b: i32) -> i32 { a + b }\n",
)
.expect("write");
let summary_out = temp_dir.path().join("summary.txt");
handle_analyze_proof_annotations(
temp_dir.path().to_path_buf(),
ProofAnnotationOutputFormat::Summary,
false,
false,
None,
Some(VerificationMethodFilter::FormalProof),
Some(summary_out.clone()),
false,
true,
10,
)
.await
.expect("summary run");
let summary = std::fs::read_to_string(&summary_out).expect("read summary");
assert!(
summary.contains("UNMEASURED") && summary.contains("formal-proof"),
"an empty formal-proof report must say no collector produces that \
method, got:\n{summary}"
);
let json_out = temp_dir.path().join("out.json");
handle_analyze_proof_annotations(
temp_dir.path().to_path_buf(),
ProofAnnotationOutputFormat::Json,
false,
false,
None,
Some(VerificationMethodFilter::FormalProof),
Some(json_out.clone()),
false,
true,
10,
)
.await
.expect("json run");
let doc: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&json_out).expect("read json"))
.expect("valid json");
let filter = &doc["summary"]["verification_method_filter"];
assert_eq!(filter["requested"].as_str(), Some("formal-proof"), "{doc}");
assert!(
filter["annotations_ignoring_method_filter"]
.as_u64()
.unwrap_or(0)
> 0,
"the disclosure must carry the count the filter removed: {doc}"
);
assert!(
filter["methods_present"]
.as_array()
.is_some_and(|m| !m.is_empty()),
"the methods actually collected must be named: {doc}"
);
}
#[tokio::test]
async fn a_matching_method_and_an_empty_project_carry_no_disclosure() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
std::fs::write(
temp_dir.path().join("lib.rs"),
"pub fn safe_add(a: i32, b: i32) -> i32 { a + b }\n",
)
.expect("write");
let matched = temp_dir.path().join("matched.txt");
handle_analyze_proof_annotations(
temp_dir.path().to_path_buf(),
ProofAnnotationOutputFormat::Summary,
false,
false,
None,
Some(VerificationMethodFilter::BorrowChecker),
Some(matched.clone()),
false,
true,
10,
)
.await
.expect("matched run");
assert!(
!std::fs::read_to_string(&matched)
.expect("read")
.contains("UNMEASURED"),
"borrow-checker matches, so there is nothing to disclose"
);
let empty_dir = TempDir::new().expect("Failed to create temp dir");
let empty_out = empty_dir.path().join("empty.txt");
handle_analyze_proof_annotations(
empty_dir.path().to_path_buf(),
ProofAnnotationOutputFormat::Summary,
false,
false,
None,
Some(VerificationMethodFilter::FormalProof),
Some(empty_out.clone()),
false,
true,
10,
)
.await
.expect("empty run");
assert!(
!std::fs::read_to_string(&empty_out)
.expect("read")
.contains("UNMEASURED"),
"a project with no annotations at all yields an honest empty report"
);
}
#[tokio::test]
async fn test_handle_analyze_proof_annotations_json_format() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
std::fs::write(temp_dir.path().join("lib.rs"), "fn test() {}").expect("write");
let result = handle_analyze_proof_annotations(
temp_dir.path().to_path_buf(),
ProofAnnotationOutputFormat::Json,
false,
true,
None,
None,
None,
false,
false,
10,
)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_analyze_proof_annotations_with_filters() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
std::fs::write(temp_dir.path().join("lib.rs"), "fn test() {}").expect("write");
let result = handle_analyze_proof_annotations(
temp_dir.path().to_path_buf(),
ProofAnnotationOutputFormat::Summary,
true, false,
Some(PropertyTypeFilter::MemorySafety),
Some(VerificationMethodFilter::BorrowChecker),
None,
false,
true, 10,
)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_analyze_proof_annotations_with_output_file() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let output_path = temp_dir.path().join("output.json");
std::fs::write(temp_dir.path().join("lib.rs"), "fn test() {}").expect("write");
let result = handle_analyze_proof_annotations(
temp_dir.path().to_path_buf(),
ProofAnnotationOutputFormat::Json,
false,
false,
None,
None,
Some(output_path.clone()),
false,
false,
10,
)
.await;
assert!(result.is_ok());
assert!(output_path.exists());
}
#[tokio::test]
async fn test_format_proof_annotations_summary() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
std::fs::write(temp_dir.path().join("lib.rs"), "pub fn exported() {}").expect("write");
let result = handle_analyze_proof_annotations(
temp_dir.path().to_path_buf(),
ProofAnnotationOutputFormat::Summary,
false,
false,
None,
None,
None,
false,
false,
10,
)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_format_proof_annotations_full() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
std::fs::write(temp_dir.path().join("main.rs"), "fn main() {}").expect("write");
let result = handle_analyze_proof_annotations(
temp_dir.path().to_path_buf(),
ProofAnnotationOutputFormat::Full,
false,
true,
None,
None,
None,
false,
false,
10,
)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_format_proof_annotations_markdown() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
std::fs::write(temp_dir.path().join("lib.rs"), "fn test() {}").expect("write");
let result = handle_analyze_proof_annotations(
temp_dir.path().to_path_buf(),
ProofAnnotationOutputFormat::Markdown,
false,
false,
None,
None,
None,
false,
false,
10,
)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_format_proof_annotations_sarif() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
std::fs::write(temp_dir.path().join("lib.rs"), "unsafe fn danger() {}").expect("write");
let result = handle_analyze_proof_annotations(
temp_dir.path().to_path_buf(),
ProofAnnotationOutputFormat::Sarif,
false,
true,
None,
None,
None,
false,
false,
10,
)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn json_output_is_byte_identical_across_five_runs() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
for name in ["alpha.rs", "beta.rs", "gamma.rs", "delta.rs"] {
std::fs::write(
temp_dir.path().join(name),
"pub fn one(a: u32) -> u32 { a }\n pub const fn two(b: u32) -> u32 { b }\n pub fn three(c: String) -> String { c }\n",
)
.expect("write");
}
let render = || async {
let out = temp_dir.path().join("out.json");
handle_analyze_proof_annotations(
temp_dir.path().to_path_buf(),
ProofAnnotationOutputFormat::Json,
false,
true,
None,
None,
Some(out.clone()),
false,
true,
10,
)
.await
.expect("json render succeeds");
let content = std::fs::read_to_string(&out).expect("output written");
std::fs::remove_file(&out).ok();
content
};
let first = render().await;
assert!(
first.contains("annotationId"),
"fixture must actually produce annotations: {first}"
);
assert!(
!first.contains("dateVerified"),
"a per-run wall clock makes the document undiffable: {first}"
);
for i in 1..5 {
assert_eq!(
render().await,
first,
"run {i}: identical input must produce byte-identical JSON"
);
}
}
}
#[cfg(all(test, feature = "broken-tests"))]
mod coverage_tests {
include!("proof_annotations_coverage_tests.rs");
include!("proof_annotations_coverage_tests_part2.rs");
include!("proof_annotations_coverage_tests_part3.rs");
}