pdftract/xtask/src/bin/rustdoc_coverage.rs
jedarden 1c6f26ecaa fix(bf-4mkhv): clean up unused imports in hash.rs
The bead description mentioned compile errors in hash.rs from API drift,
but those errors were either already fixed or misattributed. The API usage
was already correct:
- compute_fingerprint already takes 3 arguments with source
- len() already propagates Result with ?
- read_at method already used correctly
- Catalog fields accessed via trailer correctly

Only cleanup: removed unused std::fs::File and std::io imports.

Verification: notes/bf-4mkhv.md
2026-06-01 09:43:48 -04:00

253 lines
7 KiB
Rust

//! Calculate rustdoc coverage for pdftract-core.
//!
//! Counts public items and those with worked examples (```rust blocks).
use std::fs;
use std::path::Path;
#[derive(Debug, Default)]
struct CoverageStats {
total_modules: usize,
documented_modules: usize,
total_functions: usize,
documented_functions: usize,
total_structs: usize,
documented_structs: usize,
total_enums: usize,
documented_enums: usize,
total_traits: usize,
documented_traits: usize,
total_type_aliases: usize,
documented_type_aliases: usize,
total_consts: usize,
documented_consts: usize,
}
impl CoverageStats {
fn total_items(&self) -> usize {
self.total_modules
+ self.total_functions
+ self.total_structs
+ self.total_enums
+ self.total_traits
+ self.total_type_aliases
+ self.total_consts
}
fn documented_items(&self) -> usize {
self.documented_modules
+ self.documented_functions
+ self.documented_structs
+ self.documented_enums
+ self.documented_traits
+ self.documented_type_aliases
+ self.documented_consts
}
fn coverage_pct(&self) -> f64 {
if self.total_items() == 0 {
0.0
} else {
(self.documented_items() as f64 / self.total_items() as f64) * 100.0
}
}
}
fn has_worked_example(doc: &str) -> bool {
doc.contains("```rust")
}
fn analyze_file(path: &Path, stats: &mut CoverageStats) {
let content = match fs::read_to_string(path) {
Ok(c) => c,
Err(_) => return,
};
let lines: Vec<&str> = content.lines().collect();
let mut i = 0;
while i < lines.len() {
let line = lines[i];
// Skip private items and doc comments
if line.trim().starts_with("///") || line.trim().starts_with("//!") {
i += 1;
continue;
}
// Collect preceding doc comments
let mut doc = String::new();
let mut j = i;
while j > 0 && (lines[j - 1].trim().starts_with("///") || lines[j - 1].trim().starts_with("//!")) {
doc.push_str(lines[j - 1].trim());
doc.push('\n');
j -= 1;
}
let has_example = has_worked_example(&doc);
// Public modules (excluding re-exports)
if line.contains("pub mod") && !line.contains("pub use") {
stats.total_modules += 1;
if has_example {
stats.documented_modules += 1;
}
}
// Public functions
else if line.contains("pub fn") || line.contains("pub async fn") {
stats.total_functions += 1;
if has_example {
stats.documented_functions += 1;
}
}
// Public structs
else if line.contains("pub struct") {
stats.total_structs += 1;
if has_example {
stats.documented_structs += 1;
}
}
// Public enums
else if line.contains("pub enum") {
stats.total_enums += 1;
if has_example {
stats.documented_enums += 1;
}
}
// Public traits
else if line.contains("pub trait") {
stats.total_traits += 1;
if has_example {
stats.documented_traits += 1;
}
}
// Public type aliases
else if line.contains("pub type") {
stats.total_type_aliases += 1;
if has_example {
stats.documented_type_aliases += 1;
}
}
// Public constants
else if line.contains("pub const") || line.contains("pub static") {
stats.total_consts += 1;
if has_example {
stats.documented_consts += 1;
}
}
i += 1;
}
}
fn main() {
let src_dir = Path::new("crates/pdftract-core/src");
let mut stats = CoverageStats::default();
// Analyze all .rs files
for entry in walkdir::WalkDir::new(src_dir)
.into_iter()
.filter_map(|e| e.ok())
{
let path = entry.path();
if path.extension().map_or(false, |e| e == "rs") {
analyze_file(path, &mut stats);
}
}
println!("=== Rustdoc Coverage Report for pdftract-core ===\n");
println!("{:<25} {:>10} {:>10} {:>10}", "Category", "Total", "Documented", "Coverage");
println!("{}", "-" * 59);
println!(
"{:<25} {:>10} {:>10} {:>9.1}%",
"Modules",
stats.total_modules,
stats.documented_modules,
if stats.total_modules > 0 {
(stats.documented_modules as f64 / stats.total_modules as f64) * 100.0
} else {
0.0
}
);
println!(
"{:<25} {:>10} {:>10} {:>9.1}%",
"Functions",
stats.total_functions,
stats.documented_functions,
if stats.total_functions > 0 {
(stats.documented_functions as f64 / stats.total_functions as f64) * 100.0
} else {
0.0
}
);
println!(
"{:<25} {:>10} {:>10} {:>9.1}%",
"Structs",
stats.total_structs,
stats.documented_structs,
if stats.total_structs > 0 {
(stats.documented_structs as f64 / stats.total_structs as f64) * 100.0
} else {
0.0
}
);
println!(
"{:<25} {:>10} {:>10} {:>9.1}%",
"Enums",
stats.total_enums,
stats.documented_enums,
if stats.total_enums > 0 {
(stats.documented_enums as f64 / stats.total_enums as f64) * 100.0
} else {
0.0
}
);
println!(
"{:<25} {:>10} {:>10} {:>9.1}%",
"Traits",
stats.total_traits,
stats.documented_traits,
if stats.total_traits > 0 {
(stats.documented_traits as f64 / stats.total_traits as f64) * 100.0
} else {
0.0
}
);
println!(
"{:<25} {:>10} {:>10} {:>9.1}%",
"Type Aliases",
stats.total_type_aliases,
stats.documented_type_aliases,
if stats.total_type_aliases > 0 {
(stats.documented_type_aliases as f64 / stats.total_type_aliases as f64) * 100.0
} else {
0.0
}
);
println!(
"{:<25} {:>10} {:>10} {:>9.1}%",
"Constants",
stats.total_consts,
stats.documented_consts,
if stats.total_consts > 0 {
(stats.documented_consts as f64 / stats.total_consts as f64) * 100.0
} else {
0.0
}
);
println!("{}", "-" * 59);
println!(
"{:<25} {:>10} {:>10} {:>9.1}%",
"TOTAL",
stats.total_items(),
stats.documented_items(),
stats.coverage_pct()
);
println!("\nTarget: 80.0%");
if stats.coverage_pct() >= 80.0 {
println!("Status: PASS ✓");
} else {
println!("Status: FAIL - Need {:.1}% more", 80.0 - stats.coverage_pct());
}
}