use crate::cli::verify::{discover_functions, FunctionToVerify};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct CoverageStats {
pub total_spec_locked: usize,
pub by_section: HashMap<String, Vec<FunctionToVerify>>,
pub without_section: Vec<FunctionToVerify>,
pub with_contracts: usize,
pub without_contracts: usize,
}
pub fn generate_coverage(
workspace_root: &PathBuf,
spec_paths: Option<&[PathBuf]>,
) -> Result<CoverageStats, String> {
let mut functions = discover_functions(workspace_root)?;
if let Some(paths) = spec_paths {
super::spec_enrich::enrich_functions_with_spec(&mut functions, paths)?;
}
let mut by_section: HashMap<String, Vec<FunctionToVerify>> = HashMap::new();
let mut without_section = Vec::new();
let mut with_contracts = 0;
let mut without_contracts = 0;
for func in functions {
if func.contracts.is_empty() {
without_contracts += 1;
} else {
with_contracts += 1;
}
if let Some(section) = &func.section {
by_section.entry(section.clone()).or_default().push(func);
} else {
without_section.push(func);
}
}
Ok(CoverageStats {
total_spec_locked: with_contracts + without_contracts,
by_section,
without_section,
with_contracts,
without_contracts,
})
}
pub fn format_coverage_human(stats: &CoverageStats) -> String {
let mut output = String::new();
output.push_str("=== Spec Lock Coverage Report ===\n\n");
output.push_str(&format!(
"Total spec-locked functions: {}\n",
stats.total_spec_locked
));
output.push_str(&format!(" - With contracts: {}\n", stats.with_contracts));
output.push_str(&format!(
" - Without contracts: {}\n",
stats.without_contracts
));
if stats.total_spec_locked > 0 {
let contract_coverage =
(stats.with_contracts as f64 / stats.total_spec_locked as f64) * 100.0;
output.push_str(&format!("Contract coverage: {contract_coverage:.1}%\n"));
}
output.push('\n');
if !stats.by_section.is_empty() {
output.push_str("Coverage by Orange Paper Section:\n");
output.push_str("--------------------------------\n");
let mut sections: Vec<(&String, &Vec<FunctionToVerify>)> =
stats.by_section.iter().collect();
sections.sort_by_key(|(section, _)| {
let parts: Vec<&str> = section.split('.').collect();
let major: u32 = parts.first().and_then(|s| s.parse().ok()).unwrap_or(0);
let minor: u32 = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
let sub: u32 = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
(major, minor, sub)
});
for (section, funcs) in sections {
output.push_str(&format!(
" Section {}: {} functions\n",
section,
funcs.len()
));
for func in funcs {
let contract_status = if func.contracts.is_empty() {
"⚠️ no contracts"
} else {
"✅"
};
output.push_str(&format!(
" - {} {} ({})\n",
contract_status,
func.function_name,
func.file_path.display()
));
}
}
output.push('\n');
}
if !stats.without_section.is_empty() {
output.push_str("Functions with auto-inferred sections:\n");
output.push_str("--------------------------------------\n");
for func in &stats.without_section {
let contract_status = if func.contracts.is_empty() {
"⚠️ no contracts"
} else {
"✅"
};
output.push_str(&format!(
" {} {} ({})\n",
contract_status,
func.function_name,
func.file_path.display()
));
}
output.push('\n');
}
output.push_str("Summary:\n");
output.push_str("--------\n");
output.push_str(&format!(
" Total sections covered: {}\n",
stats.by_section.len()
));
output.push_str(&format!(
" Functions with contracts: {}\n",
stats.with_contracts
));
output.push_str(&format!(
" Functions without contracts: {}\n",
stats.without_contracts
));
if stats.total_spec_locked > 0 {
let contract_coverage =
(stats.with_contracts as f64 / stats.total_spec_locked as f64) * 100.0;
output.push_str(&format!(" Contract coverage: {contract_coverage:.1}%\n"));
}
output
}
pub fn format_coverage_json(stats: &CoverageStats) -> String {
let mut json = serde_json::json!({
"total_spec_locked": stats.total_spec_locked,
"with_contracts": stats.with_contracts,
"without_contracts": stats.without_contracts,
"contract_coverage_percent": if stats.total_spec_locked > 0 {
(stats.with_contracts as f64 / stats.total_spec_locked as f64) * 100.0
} else {
0.0
},
"by_section": {},
"without_section": []
});
let by_section_obj = json["by_section"].as_object_mut().unwrap();
for (section, funcs) in &stats.by_section {
by_section_obj.insert(
section.clone(),
serde_json::json!({
"count": funcs.len(),
"functions": funcs.iter().map(|f| serde_json::json!({
"name": f.function_name,
"file": f.file_path.display().to_string(),
"has_contracts": !f.contracts.is_empty(),
"contract_count": f.contracts.len()
})).collect::<Vec<_>>()
}),
);
}
let without_section_arr = json["without_section"].as_array_mut().unwrap();
for func in &stats.without_section {
without_section_arr.push(serde_json::json!({
"name": func.function_name,
"file": func.file_path.display().to_string(),
"has_contracts": !func.contracts.is_empty(),
"contract_count": func.contracts.len()
}));
}
serde_json::to_string_pretty(&json).unwrap_or_else(|_| "{}".to_string())
}
pub fn format_coverage_markdown(stats: &CoverageStats) -> String {
let mut output = String::new();
output.push_str("# Spec Lock Coverage Report\n\n");
output.push_str("## Overall Statistics\n\n");
output.push_str(&format!(
"- **Total spec-locked functions**: {}\n",
stats.total_spec_locked
));
output.push_str(&format!("- **With contracts**: {}\n", stats.with_contracts));
output.push_str(&format!(
"- **Without contracts**: {}\n",
stats.without_contracts
));
if stats.total_spec_locked > 0 {
let contract_coverage =
(stats.with_contracts as f64 / stats.total_spec_locked as f64) * 100.0;
output.push_str(&format!(
"- **Contract coverage**: {contract_coverage:.1}%\n"
));
}
output.push('\n');
if !stats.by_section.is_empty() {
output.push_str("## Coverage by Orange Paper Section\n\n");
output.push_str("| Section | Functions | Status |\n");
output.push_str("|---------|-----------|--------|\n");
let mut sections: Vec<(&String, &Vec<FunctionToVerify>)> =
stats.by_section.iter().collect();
sections.sort_by_key(|(section, _)| {
let parts: Vec<&str> = section.split('.').collect();
let major: u32 = parts.first().and_then(|s| s.parse().ok()).unwrap_or(0);
let minor: u32 = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
let sub: u32 = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
(major, minor, sub)
});
for (section, funcs) in sections {
let with_contracts = funcs.iter().filter(|f| !f.contracts.is_empty()).count();
let status = if with_contracts == funcs.len() {
"✅ Complete"
} else if with_contracts > 0 {
"⚠️ Partial"
} else {
"❌ No contracts"
};
output.push_str(&format!("| {} | {} | {} |\n", section, funcs.len(), status));
}
output.push('\n');
}
output
}
#[derive(Debug, Clone)]
pub struct SpecCoverageReport {
pub total_spec_functions: usize,
pub total_contracts: usize,
pub parseable_contracts: usize,
pub unparseable_contracts: usize,
pub impl_functions_with_contracts: usize,
pub impl_functions_without_contracts: usize,
pub by_section: Vec<SpecSectionCoverage>,
}
#[derive(Debug, Clone)]
pub struct SpecSectionCoverage {
pub section_id: String,
pub spec_functions: usize,
pub contracts_total: usize,
pub contracts_parseable: usize,
pub impl_functions: usize,
pub unparseable_examples: Vec<String>,
}
pub fn generate_spec_coverage_report(
_crate_path: &Path,
spec_paths: &[PathBuf],
stats: &CoverageStats,
) -> Result<SpecCoverageReport, String> {
use crate::parser::condition;
use crate::parser::orange_paper::SpecParser;
let parser = SpecParser::from_paths(spec_paths)?;
let mut total_contracts = 0usize;
let mut parseable_contracts = 0usize;
let mut by_section = Vec::new();
for (section_id, section) in parser.iter_sections() {
let mut contracts_total = 0usize;
let mut contracts_parseable = 0usize;
let mut unparseable_examples = Vec::new();
for func in §ion.functions {
for contract in &func.contracts {
contracts_total += 1;
total_contracts += 1;
let cond = &contract.condition;
if condition::extract_parseable_condition(cond).is_some() {
contracts_parseable += 1;
parseable_contracts += 1;
} else if unparseable_examples.len() < 3 {
let short = if cond.len() > 60 {
format!("{}...", &cond[..57])
} else {
cond.clone()
};
unparseable_examples.push(short);
}
}
}
let impl_functions = stats
.by_section
.get(section_id)
.map(|v| v.len())
.unwrap_or(0);
by_section.push(SpecSectionCoverage {
section_id: section_id.clone(),
spec_functions: section.functions.len(),
contracts_total,
contracts_parseable,
impl_functions,
unparseable_examples,
});
}
by_section.sort_by_key(|s| {
let parts: Vec<&str> = s.section_id.split('.').collect();
let major: u32 = parts.first().and_then(|p| p.parse().ok()).unwrap_or(0);
let minor: u32 = parts.get(1).and_then(|p| p.parse().ok()).unwrap_or(0);
let sub: u32 = parts.get(2).and_then(|p| p.parse().ok()).unwrap_or(0);
(major, minor, sub)
});
let total_spec_functions = by_section.iter().map(|s| s.spec_functions).sum();
let unparseable_contracts = total_contracts - parseable_contracts;
Ok(SpecCoverageReport {
total_spec_functions,
total_contracts,
parseable_contracts,
unparseable_contracts,
impl_functions_with_contracts: stats.with_contracts,
impl_functions_without_contracts: stats.without_contracts,
by_section,
})
}
pub fn format_spec_coverage_human(report: &SpecCoverageReport) -> String {
let mut output = String::new();
output.push_str("=== Spec Coverage Report (Theorems → Contracts → Parseable) ===\n\n");
output.push_str(&format!(
"Spec functions: {}\n",
report.total_spec_functions
));
output.push_str(&format!("Total contracts: {}\n", report.total_contracts));
let pct = if report.total_contracts > 0 {
100.0 * report.parseable_contracts as f64 / report.total_contracts as f64
} else {
0.0
};
output.push_str(&format!(
" Parseable: {} ({:.1}%)\n",
report.parseable_contracts, pct
));
output.push_str(&format!(
" Unparseable: {}\n",
report.unparseable_contracts
));
output.push_str(&format!(
"\nImpl functions with contracts: {}\n",
report.impl_functions_with_contracts
));
output.push_str(&format!(
"Impl functions without contracts: {}\n\n",
report.impl_functions_without_contracts
));
output.push_str("By section:\n");
output.push_str("Section | Spec Funcs | Contracts | Parseable | Impl\n");
output.push_str("--------|------------|-----------|-----------|-----\n");
for s in &report.by_section {
let spct = if s.contracts_total > 0 {
100.0 * s.contracts_parseable as f64 / s.contracts_total as f64
} else {
0.0
};
output.push_str(&format!(
"{} | {} | {} | {} ({:.0}%) | {}\n",
s.section_id,
s.spec_functions,
s.contracts_total,
s.contracts_parseable,
spct,
s.impl_functions
));
if !s.unparseable_examples.is_empty() {
for ex in &s.unparseable_examples {
output.push_str(&format!(" Unparseable: {ex}\n"));
}
}
}
output
}
pub fn format_spec_coverage_json(report: &SpecCoverageReport) -> String {
serde_json::json!({
"total_spec_functions": report.total_spec_functions,
"total_contracts": report.total_contracts,
"parseable_contracts": report.parseable_contracts,
"unparseable_contracts": report.unparseable_contracts,
"parseable_percent": if report.total_contracts > 0 { 100.0 * report.parseable_contracts as f64 / report.total_contracts as f64 } else { 0.0 },
"impl_with_contracts": report.impl_functions_with_contracts,
"impl_without_contracts": report.impl_functions_without_contracts,
"by_section": report.by_section.iter().map(|s| serde_json::json!({
"section": s.section_id,
"spec_functions": s.spec_functions,
"contracts_total": s.contracts_total,
"contracts_parseable": s.contracts_parseable,
"impl_functions": s.impl_functions,
"unparseable_examples": s.unparseable_examples,
})).collect::<Vec<_>>(),
})
.to_string()
}
pub fn format_spec_coverage_markdown(report: &SpecCoverageReport) -> String {
let mut output = String::new();
output.push_str("# Spec Coverage Report\n\n");
output.push_str("| Metric | Count |\n|--------|-------|\n");
output.push_str(&format!(
"| Spec functions | {} |\n",
report.total_spec_functions
));
output.push_str(&format!(
"| Total contracts | {} |\n",
report.total_contracts
));
output.push_str(&format!("| Parseable | {} |\n", report.parseable_contracts));
output.push_str(&format!(
"| Unparseable | {} |\n",
report.unparseable_contracts
));
let pct = if report.total_contracts > 0 {
100.0 * report.parseable_contracts as f64 / report.total_contracts as f64
} else {
0.0
};
output.push_str(&format!("| Parseable % | {pct:.1} |\n\n"));
output.push_str("## By Section\n\n| Section | Spec | Contracts | Parseable | Impl |\n|---------|------|-----------|-----------|------|\n");
for s in &report.by_section {
let spct = if s.contracts_total > 0 {
100.0 * s.contracts_parseable as f64 / s.contracts_total as f64
} else {
0.0
};
output.push_str(&format!(
"| {} | {} | {} | {} ({:.0}%) | {} |\n",
s.section_id,
s.spec_functions,
s.contracts_total,
s.contracts_parseable,
spct,
s.impl_functions
));
}
output
}