Skip to main content

crap_core/core/
walker.rs

1//! Filesystem walker — discovers Rust source files for analysis,
2//! respecting `.gitignore` and user-provided exclude patterns.
3//!
4//! Lives in `crap-core` even though it carries a hardcoded `.rs`
5//! extension filter today: the only AST-purity gate that matters here
6//! is "no `syn` / `quote` / `tree_sitter` / `swc` / `oxc` imports."
7//! `ignore::WalkBuilder` is purely filesystem-walking machinery and
8//! satisfies that gate. The `.rs` literal is a function-body wart that
9//! `analyze<P: ParseDiagnostic>` will parameterize in S4 once the
10//! parse-diagnostic type can carry the language-specific extension(s)
11//! it consumes.
12//!
13//! Extracted from `crates/crap4rs/src/core/mod.rs::discover_rust_files`
14//! during S3 (crap4rs#135) so that S4's relocation of `core::analyze` to
15//! `crap-core` doesn't need to upward-import from the Rust adapter.
16
17use std::path::{Path, PathBuf};
18
19use anyhow::{Context, Result};
20use ignore::WalkBuilder;
21
22/// Walk the source directory and collect all `.rs` files, respecting
23/// .gitignore and user-provided exclude patterns.
24pub fn discover_rust_files(
25    src: &Path,
26    exclude: &[String],
27    respect_gitignore: bool,
28) -> Result<Vec<PathBuf>> {
29    let mut builder = WalkBuilder::new(src);
30    builder.git_ignore(respect_gitignore);
31
32    // Add exclude patterns as overrides
33    if !exclude.is_empty() {
34        let mut overrides = ignore::overrides::OverrideBuilder::new(src);
35        for pattern in exclude {
36            overrides
37                .add(&format!("!{pattern}"))
38                .with_context(|| format!("invalid exclude pattern: {pattern}"))?;
39        }
40        builder.overrides(overrides.build()?);
41    }
42
43    let mut files = Vec::new();
44    for entry in builder.build() {
45        let entry = entry?;
46        if entry.file_type().is_some_and(|ft| ft.is_file())
47            && entry.path().extension().is_some_and(|ext| ext == "rs")
48        {
49            files.push(entry.into_path());
50        }
51    }
52
53    // Sort for deterministic output
54    files.sort();
55    Ok(files)
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use std::fs;
62
63    #[test]
64    fn discover_rust_files_finds_nested() {
65        let dir = tempfile::tempdir().unwrap();
66        let src = dir.path().join("src");
67        fs::create_dir_all(src.join("sub")).unwrap();
68        fs::write(src.join("lib.rs"), "").unwrap();
69        fs::write(src.join("sub").join("mod.rs"), "").unwrap();
70        fs::write(src.join("readme.txt"), "").unwrap();
71
72        let files = discover_rust_files(&src, &[], false).unwrap();
73        assert_eq!(files.len(), 2);
74        assert!(files.iter().all(|f| f.extension().unwrap() == "rs"));
75    }
76
77    #[test]
78    fn discover_rust_files_sorted_deterministically() {
79        let dir = tempfile::tempdir().unwrap();
80        let src = dir.path().join("src");
81        fs::create_dir_all(&src).unwrap();
82        fs::write(src.join("z.rs"), "").unwrap();
83        fs::write(src.join("a.rs"), "").unwrap();
84        fs::write(src.join("m.rs"), "").unwrap();
85
86        let files = discover_rust_files(&src, &[], false).unwrap();
87        let names: Vec<_> = files.iter().map(|f| f.file_name().unwrap()).collect();
88        assert_eq!(names, vec!["a.rs", "m.rs", "z.rs"]);
89    }
90}