use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EddReport {
pub is_simulation_project: bool,
pub total_simulation_fns: usize,
pub documented_fns: usize,
pub undocumented_fns: Vec<EddViolation>,
pub compliance_pct: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EddViolation {
pub file: String,
pub function_name: String,
pub line: usize,
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub fn is_simulation_project(project_path: &Path) -> bool {
let cargo_toml = project_path.join("Cargo.toml");
if !cargo_toml.exists() {
return false;
}
match std::fs::read_to_string(&cargo_toml) {
Ok(content) => {
content.contains("simular")
|| content.contains("trueno-sim")
|| content.contains("trueno_sim")
}
Err(_) => false,
}
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub fn check_edd_compliance(project_path: &Path) -> EddReport {
if !is_simulation_project(project_path) {
return EddReport {
is_simulation_project: false,
total_simulation_fns: 0,
documented_fns: 0,
undocumented_fns: vec![],
compliance_pct: 100.0,
};
}
let src_dir = project_path.join("src");
if !src_dir.exists() {
return EddReport {
is_simulation_project: true,
total_simulation_fns: 0,
documented_fns: 0,
undocumented_fns: vec![],
compliance_pct: 100.0,
};
}
let mut total_fns = 0;
let mut documented = 0;
let mut violations = Vec::new();
scan_dir_for_edd(
&src_dir,
project_path,
&mut total_fns,
&mut documented,
&mut violations,
);
let compliance_pct = if total_fns == 0 {
100.0
} else {
(documented as f64 / total_fns as f64) * 100.0
};
EddReport {
is_simulation_project: true,
total_simulation_fns: total_fns,
documented_fns: documented,
undocumented_fns: violations,
compliance_pct,
}
}
fn scan_dir_for_edd(
dir: &Path,
project_root: &Path,
total_fns: &mut usize,
documented: &mut usize,
violations: &mut Vec<EddViolation>,
) {
let entries = match std::fs::read_dir(dir) {
Ok(entries) => entries,
Err(_) => return,
};
for entry in entries.filter_map(|e| e.ok()) {
let path = entry.path();
if path.is_dir() {
scan_dir_for_edd(&path, project_root, total_fns, documented, violations);
} else if path.extension().is_some_and(|e| e == "rs") {
check_file_edd(&path, project_root, total_fns, documented, violations);
}
}
}
fn check_file_edd(
file_path: &Path,
project_root: &Path,
total_fns: &mut usize,
documented: &mut usize,
violations: &mut Vec<EddViolation>,
) {
let content = match std::fs::read_to_string(file_path) {
Ok(c) => c,
Err(_) => return,
};
let lines: Vec<&str> = content.lines().collect();
let relative_path = file_path
.strip_prefix(project_root)
.unwrap_or(file_path)
.to_string_lossy()
.to_string();
for (i, line) in lines.iter().enumerate() {
let trimmed = line.trim();
if !trimmed.starts_with("pub fn ") && !trimmed.starts_with("pub async fn ") {
continue;
}
let fn_name = extract_fn_name(trimmed);
*total_fns += 1;
let doc_block = collect_doc_comments(&lines, i);
if has_math_notation(&doc_block) {
*documented += 1;
} else {
violations.push(EddViolation {
file: relative_path.clone(),
function_name: fn_name,
line: i + 1,
});
}
}
}
fn extract_fn_name(line: &str) -> String {
let after_fn = if line.contains("async fn ") {
line.split("async fn ").nth(1)
} else {
line.split("fn ").nth(1)
};
match after_fn {
Some(rest) => rest
.split(|c: char| c == '(' || c == '<' || c.is_whitespace())
.next()
.unwrap_or("unknown")
.to_string(),
None => "unknown".to_string(),
}
}
fn collect_doc_comments(lines: &[&str], fn_line: usize) -> String {
let mut doc_lines = Vec::new();
let mut i = fn_line.saturating_sub(1);
loop {
let trimmed = lines[i].trim();
if trimmed.starts_with("///") || trimmed.starts_with("//!") {
doc_lines.push(trimmed);
} else if trimmed.starts_with("#[") || trimmed.is_empty() {
} else {
break;
}
if i == 0 {
break;
}
i -= 1;
}
doc_lines.reverse();
doc_lines.join("\n")
}
fn has_math_notation(doc: &str) -> bool {
if doc.contains("$$") {
return true;
}
let bytes = doc.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'$' && (i + 2 < bytes.len()) {
if let Some(end) = doc.get(i + 1..).unwrap_or_default().find('$') {
let inner = doc.get(i + 1..i + 1 + end).unwrap_or_default();
if !inner.trim().is_empty() {
return true;
}
}
}
i += 1;
}
if doc.contains("```math") {
return true;
}
let latex_commands = [
"\\frac",
"\\sum",
"\\int",
"\\partial",
"\\nabla",
"\\sqrt",
"\\alpha",
"\\beta",
"\\gamma",
"\\delta",
"\\epsilon",
"\\theta",
"\\lambda",
"\\sigma",
"\\omega",
"\\mathbf",
"\\mathrm",
"\\vec",
"\\hat",
"\\dot",
"\\cdot",
"\\times",
"\\approx",
"\\equiv",
"\\leq",
"\\geq",
];
for cmd in &latex_commands {
if doc.contains(cmd) {
return true;
}
}
false
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_not_simulation_project() {
let report = check_edd_compliance(&PathBuf::from("."));
assert!(!report.is_simulation_project);
assert_eq!(report.compliance_pct, 100.0);
}
#[test]
fn test_nonexistent_path() {
let report = check_edd_compliance(&PathBuf::from("/nonexistent/path"));
assert!(!report.is_simulation_project);
}
#[test]
fn test_has_math_notation_display() {
assert!(has_math_notation("/// Formula: $$ E = mc^2 $$"));
}
#[test]
fn test_has_math_notation_inline() {
assert!(has_math_notation("/// Uses $\\alpha$ decay"));
}
#[test]
fn test_has_math_notation_code_block() {
assert!(has_math_notation("/// ```math\n/// x = y + z\n/// ```"));
}
#[test]
fn test_has_math_notation_latex_command() {
assert!(has_math_notation("/// Computes \\frac{a}{b}"));
}
#[test]
fn test_no_math_notation() {
assert!(!has_math_notation("/// This function does stuff"));
}
#[test]
fn test_extract_fn_name_simple() {
assert_eq!(
extract_fn_name("pub fn calculate(x: f64) -> f64"),
"calculate"
);
}
#[test]
fn test_extract_fn_name_generic() {
assert_eq!(extract_fn_name("pub fn process<T>(val: T)"), "process");
}
#[test]
fn test_extract_fn_name_async() {
assert_eq!(extract_fn_name("pub async fn fetch_data()"), "fetch_data");
}
#[test]
fn test_collect_doc_comments() {
let lines = vec![
"/// First line",
"/// Second line",
"#[inline]",
"pub fn foo() {}",
];
let doc = collect_doc_comments(&lines, 3);
assert!(doc.contains("First line"));
assert!(doc.contains("Second line"));
}
#[test]
fn test_is_simulation_project_false() {
assert!(!is_simulation_project(&PathBuf::from(".")));
}
}