Skip to main content

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}