use normalize_languages::GrammarLoader;
use normalize_rules_config::{PathFilter, WalkConfig};
use normalize_syntax_rules::{DebugFlags, apply_fixes, load_all_rules, run_rules};
use std::path::{Path, PathBuf};
fn fixtures_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
}
fn find_fixture_dirs(dir: &Path) -> Vec<PathBuf> {
let mut result = Vec::new();
let Ok(entries) = std::fs::read_dir(dir) else {
return result;
};
for entry in entries.flatten() {
let path = entry.path();
if !path.is_dir() {
continue;
}
let has_fixtures = std::fs::read_dir(&path)
.map(|es| {
es.flatten().any(|e| {
let name = e.file_name();
let s = name.to_string_lossy();
(s.starts_with("match.") || s.starts_with("no_match.") || s.starts_with("fix."))
&& e.path().is_file()
})
})
.unwrap_or(false);
if has_fixtures {
result.push(path);
} else {
result.extend(find_fixture_dirs(&path));
}
}
result
}
fn derive_rule_id(fixture_dir: &Path, fixtures_root: &Path) -> String {
fixture_dir
.strip_prefix(fixtures_root)
.expect("fixture dir must be under fixtures root")
.components()
.map(|c| c.as_os_str().to_string_lossy().into_owned())
.collect::<Vec<_>>()
.join("/")
}
fn find_fixture_file(dir: &Path, prefix: &str) -> Option<(PathBuf, String)> {
let expected_prefix = format!("{prefix}.expected.");
std::fs::read_dir(dir).ok()?.flatten().find_map(|e| {
let name = e.file_name();
let s = name.to_string_lossy().into_owned();
if s.starts_with(&format!("{prefix}."))
&& !s.starts_with(&expected_prefix)
&& e.path().is_file()
{
let ext = s[prefix.len() + 1..].to_string();
Some((e.path(), ext))
} else {
None
}
})
}
fn find_expected_file(dir: &Path, ext: &str) -> Option<PathBuf> {
let name = format!("fix.expected.{ext}");
let path = dir.join(&name);
path.is_file().then_some(path)
}
#[test]
fn test_rule_fixtures() {
let fixtures_root = fixtures_dir();
let loader = GrammarLoader::new();
let debug = DebugFlags { timing: false };
let mut failures: Vec<String> = Vec::new();
let mut tested = 0;
let fixture_dirs = {
let mut dirs = find_fixture_dirs(&fixtures_root);
dirs.sort(); dirs
};
for fixture_dir in &fixture_dirs {
let rule_id = derive_rule_id(fixture_dir, &fixtures_root);
let mut rules = load_all_rules(fixture_dir, &Default::default());
let mut found = false;
for r in rules.iter_mut() {
if r.id == rule_id {
r.enabled = true;
found = true;
} else {
r.enabled = false;
}
}
if !found {
failures.push(format!(
"MISSING RULE: `{rule_id}` — no builtin rule found for this fixture directory"
));
continue;
}
let findings = run_rules(
&rules,
fixture_dir,
fixture_dir,
&loader,
None,
None,
None,
&debug,
None,
&PathFilter::default(),
&WalkConfig::default(),
);
let match_findings: Vec<_> = findings
.iter()
.filter(|f| f.file.file_stem().map(|s| s == "match").unwrap_or(false))
.collect();
let no_match_findings: Vec<_> = findings
.iter()
.filter(|f| f.file.file_stem().map(|s| s == "no_match").unwrap_or(false))
.collect();
let match_file_exists = std::fs::read_dir(fixture_dir)
.map(|es| {
es.flatten()
.any(|e| e.file_name().to_string_lossy().starts_with("match."))
})
.unwrap_or(false);
if match_file_exists && match_findings.is_empty() {
failures.push(format!(
"`{rule_id}`: match.* produced no findings (expected ≥1)"
));
}
if !no_match_findings.is_empty() {
let details: Vec<_> = no_match_findings
.iter()
.map(|f| {
format!(
" {}:{}: {}",
f.file.display(),
f.start_line,
f.matched_text
)
})
.collect();
failures.push(format!(
"`{rule_id}`: no_match.* produced {} unexpected finding(s):\n{}",
no_match_findings.len(),
details.join("\n")
));
}
tested += 1;
if let Some((fix_src, ext)) = find_fixture_file(fixture_dir, "fix") {
match run_fix_fixture(
fixture_dir,
&fix_src,
&ext,
&rule_id,
&rules,
&loader,
&debug,
) {
Ok(()) => {}
Err(msg) => failures.push(msg),
}
}
}
if !failures.is_empty() {
panic!(
"{} rule fixture failure(s) (out of {} tested):\n\n{}",
failures.len(),
tested,
failures.join("\n\n")
);
}
assert!(
tested >= 10,
"expected at least 10 fixture tests, only found {tested} — are the fixture files missing?"
);
println!("Tested {tested} rule fixtures.");
}
fn run_fix_fixture(
fixture_dir: &Path,
fix_src: &Path,
ext: &str,
rule_id: &str,
rules: &[normalize_syntax_rules::Rule],
loader: &GrammarLoader,
debug: &DebugFlags,
) -> Result<(), String> {
let expected_path = match find_expected_file(fixture_dir, ext) {
Some(p) => p,
None => {
return Err(format!(
"`{rule_id}`: fix.{ext} exists but fix.expected.{ext} is missing"
));
}
};
let expected = std::fs::read_to_string(&expected_path)
.map_err(|e| format!("`{rule_id}`: failed to read fix.expected.{ext}: {e}"))?;
let input = std::fs::read_to_string(fix_src)
.map_err(|e| format!("`{rule_id}`: failed to read fix.{ext}: {e}"))?;
let tmp = tempfile::tempdir_in(fixture_dir)
.map_err(|e| format!("`{rule_id}`: failed to create tempdir: {e}"))?;
let tmp_file = tmp.path().join(format!("fix.{ext}"));
std::fs::write(&tmp_file, &input)
.map_err(|e| format!("`{rule_id}`: failed to write temp fix file: {e}"))?;
const MAX_PASSES: usize = 10;
for pass in 0..MAX_PASSES {
let findings = run_rules(
rules,
tmp.path(),
tmp.path(),
loader,
None,
None,
None,
debug,
None,
&PathFilter::default(),
&WalkConfig::default(),
);
let fixable: Vec<_> = findings.into_iter().filter(|f| f.fix.is_some()).collect();
if fixable.is_empty() {
break;
}
apply_fixes(&fixable)
.map_err(|e| format!("`{rule_id}`: apply_fixes pass {pass} failed: {e}"))?;
}
let actual = std::fs::read_to_string(&tmp_file)
.map_err(|e| format!("`{rule_id}`: failed to read fixed output: {e}"))?;
if actual == expected {
Ok(())
} else {
Err(format!(
"`{rule_id}`: fix output mismatch\n--- expected (fix.expected.{ext}) ---\n{expected}\n--- actual ---\n{actual}"
))
}
}
#[test]
fn test_rust_missing_module_doc() {
let fixture_dir =
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/rust/missing-module-doc");
let loader = GrammarLoader::new();
let debug = DebugFlags { timing: false };
let mut rules = load_all_rules(&fixture_dir, &Default::default());
for r in rules.iter_mut() {
r.enabled = r.id == "rust/missing-module-doc";
}
let lib_rs = fixture_dir.join("lib.rs");
let findings = run_rules(
&rules,
&lib_rs,
&fixture_dir,
&loader,
None,
None,
None,
&debug,
None,
&PathFilter::default(),
&WalkConfig::default(),
);
assert!(
!findings.is_empty(),
"rust/missing-module-doc: lib.rs (no //! comment) should produce findings"
);
let tmp = tempfile::tempdir_in(&fixture_dir).expect("failed to create tempdir");
let tmp_lib = tmp.path().join("lib.rs");
std::fs::write(
&tmp_lib,
"//! This module is documented.\n\npub struct Foo;\n",
)
.expect("failed to write tmp lib.rs");
let no_match_findings = run_rules(
&rules,
&tmp_lib,
&fixture_dir,
&loader,
None,
None,
None,
&debug,
None,
&PathFilter::default(),
&WalkConfig::default(),
);
assert!(
no_match_findings.is_empty(),
"rust/missing-module-doc: lib.rs with //! docs should produce no findings"
);
}