#![cfg_attr(coverage_nightly, coverage(off))]
use crate::cli::handlers::work_contract::{EvidenceType, FalsificationResult};
use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
use std::process::Command;
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub(crate) fn test_variant_coverage(
project_path: &Path,
baseline_commit: &str,
) -> Result<FalsificationResult> {
print!("Scanning match arm coverage... ");
let changed_files = get_changed_files(project_path, baseline_commit)?;
let rs_files: Vec<&String> = changed_files
.iter()
.filter(|f| f.ends_with(".rs"))
.collect();
if rs_files.is_empty() {
return Ok(FalsificationResult::passed(
"No Rust files changed".to_string(),
));
}
let mut untested_arms: Vec<(String, String)> = Vec::new();
for rel_path in &rs_files {
let full_path = project_path.join(rel_path);
let Ok(content) = std::fs::read_to_string(&full_path) else {
continue;
};
let variants = extract_large_match_variants(&content);
if variants.is_empty() {
continue;
}
let test_section = extract_test_section(&content);
for variant in &variants {
if !test_section.contains(variant) {
untested_arms.push((rel_path.to_string(), variant.clone()));
}
}
}
if untested_arms.is_empty() {
Ok(FalsificationResult::passed(format!(
"{} changed file(s) -- all match variants tested",
rs_files.len()
)))
} else {
let details: Vec<String> = untested_arms
.iter()
.take(10)
.map(|(f, v)| format!("{}::{}", f, v))
.collect();
let paths: Vec<PathBuf> = untested_arms
.iter()
.map(|(f, _)| PathBuf::from(f))
.collect();
Ok(FalsificationResult::failed(
format!(
"{} untested match variant(s): {}",
untested_arms.len(),
details.join(", ")
),
EvidenceType::FileList(paths),
))
}
}
struct MatchParser {
variants: Vec<String>,
current_arms: Vec<String>,
brace_depth: usize,
in_match: bool,
}
impl MatchParser {
fn new() -> Self {
Self {
variants: Vec::new(),
current_arms: Vec::new(),
brace_depth: 0,
in_match: false,
}
}
fn process_line(&mut self, trimmed: &str) {
if !self.in_match {
if trimmed.contains("match ") && trimmed.ends_with('{') {
self.in_match = true;
self.current_arms.clear();
self.brace_depth = 1;
}
return;
}
self.update_brace_depth(trimmed);
if self.in_match && self.brace_depth == 1 {
self.try_extract_arm(trimmed);
}
}
fn update_brace_depth(&mut self, trimmed: &str) {
for ch in trimmed.chars() {
match ch {
'{' => self.brace_depth += 1,
'}' => {
self.brace_depth -= 1;
if self.brace_depth == 0 {
self.flush_match_block();
}
}
_ => {}
}
}
}
fn flush_match_block(&mut self) {
if self.current_arms.len() >= 5 {
self.variants.append(&mut self.current_arms);
} else {
self.current_arms.clear();
}
self.in_match = false;
}
fn try_extract_arm(&mut self, trimmed: &str) {
let Some(pattern) = trimmed.split("=>").next() else {
return;
};
let pattern = pattern.trim();
if pattern == "_" || pattern.starts_with("//") || !trimmed.contains("=>") {
return;
}
let variant = pattern
.split("::")
.last()
.map(|s| {
s.trim_matches(|c: char| !c.is_alphanumeric() && c != '_')
.to_string()
})
.unwrap_or_default();
if !variant.is_empty() {
self.current_arms.push(variant);
}
}
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub(crate) fn extract_large_match_variants(content: &str) -> Vec<String> {
let mut parser = MatchParser::new();
for line in content.lines() {
parser.process_line(line.trim());
}
parser.variants
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub(crate) fn extract_test_section(content: &str) -> String {
let mut in_test = false;
let mut test_content = String::new();
for line in content.lines() {
if line.contains("#[cfg(test)]") || line.contains("#[test]") || line.contains("mod tests") {
in_test = true;
}
if in_test {
test_content.push_str(line);
test_content.push('\n');
}
}
test_content
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub(crate) fn test_fix_chain_limit(
project_path: &Path,
max_chain: usize,
) -> Result<FalsificationResult> {
print!("Analyzing fix chains... ");
let output = Command::new("git")
.args(["log", "--oneline", "--name-only", "-50"])
.current_dir(project_path)
.output()
.context("Failed to run git log")?;
if !output.status.success() {
return Ok(FalsificationResult::passed(
"Cannot read git history".to_string(),
));
}
let log = String::from_utf8_lossy(&output.stdout);
let chains = detect_fix_chains(&log, max_chain);
if chains.is_empty() {
Ok(FalsificationResult::passed(format!(
"No fix chains > {} consecutive commits",
max_chain
)))
} else {
let details: Vec<String> = chains
.iter()
.take(5)
.map(|(file, count)| format!("{} ({} consecutive)", file, count))
.collect();
let paths: Vec<PathBuf> = chains.iter().map(|(f, _)| PathBuf::from(f)).collect();
Ok(FalsificationResult::failed(
format!(
"{} file(s) with fix chains > {}: {}",
chains.len(),
max_chain,
details.join(", ")
),
EvidenceType::FileList(paths),
))
}
}
fn is_commit_line(trimmed: &str) -> bool {
trimmed.len() > 8
&& trimmed
.as_bytes()
.first()
.map(|b| b.is_ascii_hexdigit())
.unwrap_or(false)
}
fn is_fix_commit(msg: &str) -> bool {
let lower = msg.to_lowercase();
lower.contains("fix") || lower.contains("bug") || lower.contains("hotfix")
}
fn collect_violations(
streaks: &mut std::collections::HashMap<String, usize>,
max_chain: usize,
violations: &mut Vec<(String, usize)>,
) {
violations.extend(streaks.drain().filter(|(_, streak)| *streak > max_chain));
}
fn increment_streaks(files: &[String], streaks: &mut std::collections::HashMap<String, usize>) {
for file in files {
*streaks.entry(file.clone()).or_insert(0) += 1;
}
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub(crate) fn detect_fix_chains(log: &str, max_chain: usize) -> Vec<(String, usize)> {
let mut streaks: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
let mut violations: Vec<(String, usize)> = Vec::new();
let mut current_is_fix = false;
let mut current_files: Vec<String> = Vec::new();
for line in log.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if is_commit_line(trimmed) {
if current_is_fix {
increment_streaks(¤t_files, &mut streaks);
} else {
collect_violations(&mut streaks, max_chain, &mut violations);
}
current_files.clear();
current_is_fix = is_fix_commit(trimmed);
} else if trimmed.contains('.') && !trimmed.starts_with('#') {
current_files.push(trimmed.to_string());
}
}
if current_is_fix {
increment_streaks(¤t_files, &mut streaks);
}
collect_violations(&mut streaks, max_chain, &mut violations);
violations.sort_by_key(|b| std::cmp::Reverse(b.1));
violations.dedup_by(|a, b| a.0 == b.0);
violations
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub(crate) fn get_changed_files(project_path: &Path, baseline_commit: &str) -> Result<Vec<String>> {
let output = Command::new("git")
.args(["diff", "--name-only", baseline_commit, "HEAD"])
.current_dir(project_path)
.output()?;
if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout)
.lines()
.map(|s| s.to_string())
.collect())
} else {
Ok(Vec::new())
}
}