use std::path::Path;
use alint_core::{Engine, RuleEntry, WalkOptions, walk};
use alint_rules::builtin_registry;
fn engine_from_yaml(yaml: &str) -> Engine {
let spec: alint_core::RuleSpec = serde_yaml_ng::from_str(yaml).unwrap();
let registry = builtin_registry();
let rule = registry.build(&spec).unwrap();
Engine::from_entries(vec![RuleEntry::new(rule)], registry)
}
fn run_and_collect_paths(engine: &Engine, root: &Path) -> Vec<String> {
let opts = WalkOptions::default();
let index = walk(root, &opts).unwrap();
let report = engine.run(root, &index).unwrap();
let mut paths: Vec<String> = report
.results
.iter()
.flat_map(|r| r.violations.iter())
.filter_map(|v| {
v.path
.as_deref()
.map(|p| p.display().to_string().replace('\\', "/"))
})
.collect();
paths.sort();
paths
}
fn polyglot_tree() -> tempfile::TempDir {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
std::fs::create_dir_all(root.join("crates/api/src")).unwrap();
std::fs::create_dir_all(root.join("services/web/scripts")).unwrap();
std::fs::write(
root.join("crates/api/Cargo.toml"),
"[package]\nname='api'\n",
)
.unwrap();
std::fs::write(root.join("crates/api/src/main.rs"), "fn main() { } \n").unwrap();
std::fs::write(
root.join("services/web/scripts/migrate.rs"),
"// not really rust \n",
)
.unwrap();
tmp
}
#[test]
fn baseline_without_scope_filter_fires_on_every_match() {
let tmp = polyglot_tree();
let engine = engine_from_yaml(
"id: nws\n\
kind: no_trailing_whitespace\n\
level: warning\n\
paths: '**/*.rs'\n",
);
let paths = run_and_collect_paths(&engine, tmp.path());
assert_eq!(
paths,
vec![
"crates/api/src/main.rs".to_string(),
"services/web/scripts/migrate.rs".to_string(),
],
"without scope_filter both .rs files should fire",
);
}
#[test]
fn scope_filter_skips_files_without_ancestor_manifest() {
let tmp = polyglot_tree();
let engine = engine_from_yaml(
"id: nws\n\
kind: no_trailing_whitespace\n\
level: warning\n\
paths: '**/*.rs'\n\
scope_filter:\n has_ancestor: Cargo.toml\n",
);
let paths = run_and_collect_paths(&engine, tmp.path());
assert_eq!(
paths,
vec!["crates/api/src/main.rs".to_string()],
"scope_filter must skip .rs files outside any Cargo.toml subtree",
);
}
#[test]
fn scope_filter_with_two_name_list() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
std::fs::create_dir_all(root.join("app")).unwrap();
std::fs::create_dir_all(root.join("scripts")).unwrap();
std::fs::write(root.join("app/setup.py"), "# stub\n").unwrap();
std::fs::write(root.join("app/main.py"), "x = 1 \n").unwrap();
std::fs::write(root.join("scripts/oneoff.py"), "y = 2 \n").unwrap();
let engine = engine_from_yaml(
"id: nws\n\
kind: no_trailing_whitespace\n\
level: warning\n\
paths: '**/*.py'\n\
scope_filter:\n has_ancestor:\n - pyproject.toml\n - setup.py\n",
);
let paths = run_and_collect_paths(&engine, root);
assert_eq!(
paths,
vec!["app/main.py".to_string()],
"scope_filter list must accept either name; out-of-scope file must skip",
);
}