normalize_native_rules/walk.rs
1/// Shared file-walking utilities for native rule checks.
2///
3/// All walkers respect `.gitignore` (and nested `.gitignore` files in subdirectories)
4/// via the `ignore` crate. Directory exclusions and ignore-file selection are
5/// configurable via [`WalkConfig`] (from `[walk]` in `.normalize/config.toml`).
6use normalize_rules_config::{PathFilter, WalkConfig};
7use std::path::Path;
8
9/// Configure a [`ignore::WalkBuilder`] according to the given [`WalkConfig`].
10///
11/// - Enables/disables `.gitignore` based on `ignore_files`.
12/// - Adds any additional ignore files via `WalkBuilder::add_ignore`.
13/// - Applies `exclude` patterns in a `filter_entry` closure.
14fn configure_walk_builder(
15 builder: &mut ignore::WalkBuilder,
16 walk_config: &WalkConfig,
17 root: &Path,
18) {
19 let ignore_files = walk_config.ignore_files();
20 let has_gitignore = ignore_files.contains(&".gitignore");
21
22 builder.hidden(false);
23 builder.git_ignore(has_gitignore);
24 builder.git_global(has_gitignore);
25 builder.git_exclude(has_gitignore);
26
27 // Add any non-.gitignore ignore files
28 for file in &ignore_files {
29 if *file != ".gitignore" {
30 let ignore_path = root.join(file);
31 if ignore_path.exists() {
32 builder.add_ignore(ignore_path);
33 }
34 }
35 }
36}
37
38/// Build a directory walker rooted at `root`, configured by [`WalkConfig`].
39///
40/// - Respects ignore files as configured (default: `.gitignore`, `.git/info/exclude`,
41/// and global gitignore).
42/// - Skips directories matching `exclude` patterns (default: `.git`).
43/// - Visits hidden files/directories (filtering delegated to ignore files and caller).
44///
45/// Returns a flat iterator of successfully-read `DirEntry` values.
46pub fn gitignore_walk(
47 root: &Path,
48 walk_config: &WalkConfig,
49) -> impl Iterator<Item = ignore::DirEntry> {
50 let mut builder = ignore::WalkBuilder::new(root);
51 configure_walk_builder(&mut builder, walk_config, root);
52 // Compile gitignore-style exclude patterns once, anchored at root.
53 let excludes = walk_config.compiled_excludes(root);
54 let root_owned = root.to_path_buf();
55 builder.filter_entry(move |e| {
56 let path = e.path();
57 let rel = path.strip_prefix(&root_owned).unwrap_or(path);
58 // Empty rel (root itself) — never exclude.
59 if rel.as_os_str().is_empty() {
60 return true;
61 }
62 let is_dir = e.file_type().is_some_and(|ft| ft.is_dir());
63 !excludes
64 .matched_path_or_any_parents(rel, is_dir)
65 .is_ignore()
66 });
67 builder.build().filter_map(|e| e.ok())
68}
69
70/// Like [`gitignore_walk`], but additionally filters file entries through a [`PathFilter`].
71///
72/// Directory entries are always passed through (so the walker can descend into them).
73/// Only file entries are tested against `--only` / `--exclude` patterns using
74/// their path relative to `root`.
75#[allow(dead_code)]
76pub fn filtered_gitignore_walk<'a>(
77 root: &'a Path,
78 filter: &'a PathFilter,
79 walk_config: &'a WalkConfig,
80) -> Box<dyn Iterator<Item = ignore::DirEntry> + 'a> {
81 if filter.is_empty() {
82 return Box::new(gitignore_walk(root, walk_config));
83 }
84 Box::new(gitignore_walk(root, walk_config).filter(move |entry| {
85 // Always pass directories through — callers may need to descend.
86 if entry.file_type().is_some_and(|ft| ft.is_dir()) {
87 return true;
88 }
89 let rel = entry.path().strip_prefix(root).unwrap_or(entry.path());
90 filter.matches_path(rel)
91 }))
92}