phpdoc-parser 0.11.0

Structural PHPDoc parser. Parses doc-comment blocks into a tag/prose AST with accurate spans; tag bodies exposed as raw text.
Documentation
use rayon::prelude::*;
use std::sync::Mutex;

fn collect_phpt_files(dir: &std::path::Path) -> Vec<std::path::PathBuf> {
    let mut paths = Vec::new();
    for entry in std::fs::read_dir(dir).unwrap().filter_map(|e| e.ok()) {
        let path = entry.path();
        if path.is_dir() {
            paths.extend(collect_phpt_files(&path));
        } else if path.extension().is_some_and(|ext| ext == "phpt") {
            paths.push(path);
        }
    }
    paths
}

fn parse_fixture(content: &str) -> (String, String) {
    let rest = content
        .strip_prefix("===input===\n")
        .expect("fixture must start with ===input===");
    let output_pos = rest
        .find("===output===\n")
        .expect("fixture must have ===output=== section");
    let input = rest[..output_pos].trim_end_matches('\n').to_owned();
    let output = rest[output_pos + "===output===\n".len()..]
        .trim_end_matches('\n')
        .to_owned();
    (input, output)
}

fn run_fixture(input: &str) -> String {
    let doc = phpdoc_parser::parse(input);
    serde_json::to_string_pretty(&doc).unwrap()
}

fn write_fixture(path: &std::path::Path, input: &str, actual: &str) {
    std::fs::write(
        path,
        format!("===input===\n{input}\n===output===\n{actual}\n"),
    )
    .unwrap();
}

#[test]
fn fixtures() {
    let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
    let update = std::env::var("UPDATE_FIXTURES").is_ok();
    let mut paths = collect_phpt_files(&dir);
    paths.sort();

    if paths.is_empty() {
        return;
    }

    let failures = Mutex::new(Vec::new());

    paths.par_iter().for_each(|path| {
        let rel = path
            .strip_prefix(&dir)
            .unwrap()
            .to_string_lossy()
            .to_string();
        let content = std::fs::read_to_string(path).unwrap();
        let (input, expected) = parse_fixture(&content);
        let actual = run_fixture(&input);

        if update {
            write_fixture(path, &input, &actual);
        } else if actual != expected {
            failures.lock().unwrap().push(format!(
                "output mismatch in {rel}\ninput:\n{input}\nexpected:\n{expected}\nactual:\n{actual}"
            ));
        }
    });

    let f = failures.into_inner().unwrap();
    assert!(
        f.is_empty(),
        "phpdoc fixture failures:\n{}",
        f.join("\n\n---\n\n")
    );
}