Skip to main content

lintel_validate/
discover.rs

1use std::path::{Path, PathBuf};
2
3use crate::parsers;
4
5/// Walk `root` respecting `.gitignore`, returning files with known config extensions.
6///
7/// Applies `excludes` glob patterns to filter results.
8///
9/// # Errors
10///
11/// Returns an error if the directory walk encounters an I/O error.
12pub fn discover_files(root: &str, excludes: &[String]) -> Result<Vec<PathBuf>, anyhow::Error> {
13    let walker = ignore::WalkBuilder::new(root)
14        .hidden(false) // don't skip dotfiles (e.g. .eslintrc.json)
15        .git_ignore(true)
16        .git_global(true)
17        .git_exclude(true)
18        .build();
19
20    let mut files = Vec::new();
21    for entry in walker {
22        let entry = entry?;
23        let path = entry.path();
24        if !path.is_file() {
25            continue;
26        }
27        if parsers::detect_format(path).is_none() {
28            continue;
29        }
30        if is_excluded(path, excludes) {
31            continue;
32        }
33        files.push(path.to_path_buf());
34    }
35
36    files.sort();
37    Ok(files)
38}
39
40fn is_excluded(path: &Path, excludes: &[String]) -> bool {
41    let path_str = match path.to_str() {
42        Some(s) => s.strip_prefix("./").unwrap_or(s),
43        None => return false,
44    };
45    excludes
46        .iter()
47        .any(|pattern| glob_match::glob_match(pattern, path_str))
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53    use std::fs;
54
55    #[test]
56    fn discovers_known_extensions() -> anyhow::Result<()> {
57        let tmp = tempfile::tempdir()?;
58        fs::write(tmp.path().join("a.json"), "{}")?;
59        fs::write(tmp.path().join("b.yaml"), "key: val")?;
60        fs::write(tmp.path().join("c.yml"), "key: val")?;
61        fs::write(tmp.path().join("d.json5"), "{}")?;
62        fs::write(tmp.path().join("e.jsonc"), "{}")?;
63        fs::write(tmp.path().join("f.txt"), "nope")?;
64        fs::write(tmp.path().join("g.nix"), "{ }")?;
65
66        let root = tmp.path().to_str().expect("temp dir should be valid UTF-8");
67        let files = discover_files(root, &[])?;
68        assert_eq!(files.len(), 5);
69        assert!(files.iter().all(|f| parsers::detect_format(f).is_some()));
70        Ok(())
71    }
72
73    #[test]
74    fn respects_exclude_patterns() -> anyhow::Result<()> {
75        let tmp = tempfile::tempdir()?;
76        let sub = tmp.path().join("vendor");
77        fs::create_dir_all(&sub)?;
78        fs::write(tmp.path().join("a.json"), "{}")?;
79        fs::write(sub.join("b.json"), "{}")?;
80
81        let root = tmp.path().to_str().expect("temp dir should be valid UTF-8");
82        let files = discover_files(root, &["**/vendor/**".to_string()])?;
83        assert_eq!(files.len(), 1);
84        Ok(())
85    }
86
87    #[test]
88    fn discovers_dotfiles() -> anyhow::Result<()> {
89        let tmp = tempfile::tempdir()?;
90        fs::write(tmp.path().join(".eslintrc.json"), "{}")?;
91
92        let root = tmp.path().to_str().expect("temp dir should be valid UTF-8");
93        let files = discover_files(root, &[])?;
94        assert_eq!(files.len(), 1);
95        Ok(())
96    }
97}