use crate::Result;
use crate::validation::{Severity, Violation, ViolationType};
use std::path::Path;
pub fn validate_doc_presence(
rust_file: &Path,
lines: &[&str],
violations: &mut Vec<Violation>,
) -> Result<()> {
let filename = rust_file.file_name().and_then(|n| n.to_str()).unwrap_or("");
if filename != "lib.rs" && filename != "mod.rs" {
return Ok(());
}
let is_generated = lines
.iter()
.any(|line| line.trim() == "// @generated" || line.trim().starts_with("// @generated "));
if is_generated {
return Ok(());
}
let has_module_doc = lines
.iter()
.any(|line| line.trim_start().starts_with("//!"));
if !has_module_doc {
violations.push(Violation {
violation_type: ViolationType::MissingModuleDoc,
file: rust_file.to_path_buf(),
line: 1,
message: format!(
"FERROUS FORGE [DOC STANDARD] — Missing Module Documentation\n \
File: {}\n \
Module root missing //! documentation. Add at minimum:\n \
//! Brief description of what this module does.",
rust_file.display()
),
severity: Severity::Warning,
});
}
Ok(())
}
pub fn validate_cargo_doc_config(
cargo_file: &Path,
content: &str,
violations: &mut Vec<Violation>,
) -> Result<()> {
if !content.contains("[lints.rustdoc]") && !content.contains("[lints]") {
violations.push(Violation {
violation_type: ViolationType::MissingDocConfig,
file: cargo_file.to_path_buf(),
line: 0,
message: "FERROUS FORGE [DOC STANDARD] — Missing rustdoc lint configuration\n \
Cargo.toml is missing [lints.rustdoc] section.\n \
Run 'ferrous-forge init --project' to inject the full rustdoc lint block."
.to_string(),
severity: Severity::Warning,
});
}
Ok(())
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use std::path::PathBuf;
fn run_doc_check(filename: &str, source: &str) -> Vec<Violation> {
let path = PathBuf::from(format!("crate/src/{filename}"));
let lines: Vec<&str> = source.lines().collect();
let mut violations = Vec::new();
validate_doc_presence(&path, &lines, &mut violations).unwrap();
violations
}
#[test]
fn mod_rs_with_doc_passes() {
let v = run_doc_check("mod.rs", "//! Module docs\npub mod foo;");
assert!(v.is_empty());
}
#[test]
fn mod_rs_without_doc_fails() {
let v = run_doc_check("mod.rs", "pub mod foo;");
assert_eq!(v.len(), 1);
assert_eq!(v[0].violation_type, ViolationType::MissingModuleDoc);
}
#[test]
fn lib_rs_without_doc_fails() {
let v = run_doc_check("lib.rs", "pub fn hello() {}");
assert_eq!(v.len(), 1);
assert_eq!(v[0].violation_type, ViolationType::MissingModuleDoc);
}
#[test]
fn regular_file_skipped() {
let v = run_doc_check("main.rs", "fn main() {}");
assert!(v.is_empty());
}
#[test]
fn generated_mod_rs_skipped() {
let source = "// @generated\n\npub mod events;";
let v = run_doc_check("mod.rs", source);
assert!(v.is_empty(), "should skip @generated files");
}
#[test]
fn generated_with_reason_skipped() {
let source = "// @generated by build.rs\n\npub mod events;";
let v = run_doc_check("mod.rs", source);
assert!(v.is_empty(), "should skip @generated with reason");
}
#[test]
fn generated_lib_rs_skipped() {
let source = "// @generated\n\npub fn init() {}";
let v = run_doc_check("lib.rs", source);
assert!(v.is_empty(), "should skip @generated lib.rs");
}
#[test]
fn generated_with_doc_still_passes() {
let source = "//! Module docs\n// @generated\npub mod events;";
let v = run_doc_check("mod.rs", source);
assert!(v.is_empty());
}
#[test]
fn at_generated_in_normal_comment_not_matched() {
let source = "// this has @generated in it\npub mod foo;";
let v = run_doc_check("mod.rs", source);
assert_eq!(v.len(), 1, "partial match should not skip validation");
}
}