Skip to main content

sniff/
walker.rs

1use crate::config::ResolvedConfig;
2use ignore::WalkBuilder;
3use std::path::Path;
4
5#[path = "walker_filters.rs"]
6mod filters;
7
8fn walk_directory(
9    root: &Path,
10    config: &ResolvedConfig,
11    file_paths: &mut Vec<String>,
12) -> Result<(), String> {
13    let ignored = config.ignore.clone();
14    let mut builder = WalkBuilder::new(root);
15    builder.standard_filters(true);
16    builder.follow_links(false);
17    builder.filter_entry(move |entry| filters::should_descend(entry, &ignored));
18
19    for result in builder.build() {
20        match result {
21            Ok(entry) => {
22                let path = entry.path();
23                if path.is_file() && filters::should_keep_file(path, config) {
24                    file_paths.push(path.to_string_lossy().replace("\\\\?\\", ""));
25                }
26            }
27            Err(err) => {
28                return Err(format!("failed to read directory entry: {err}"));
29            }
30        }
31    }
32
33    Ok(())
34}
35
36pub fn walk(root_path: &str, config: &ResolvedConfig) -> Result<Vec<String>, String> {
37    let mut file_paths = Vec::new();
38    let root_buf = std::fs::canonicalize(root_path)
39        .map_err(|err| format!("failed to resolve scan target {root_path}: {err}"))?;
40    let root = root_buf.as_path();
41
42    if !root.exists() {
43        return Err(format!("scan target does not exist: {root_path}"));
44    }
45
46    if root.is_file() {
47        if filters::should_keep_file(root, config) {
48            file_paths.push(root.to_string_lossy().replace("\\\\?\\", ""));
49        }
50        return Ok(file_paths);
51    }
52
53    walk_directory(root, config, &mut file_paths)?;
54
55    file_paths.sort();
56    Ok(file_paths)
57}
58
59#[cfg(test)]
60mod tests {
61    use super::walk;
62    use crate::config::ResolvedConfig;
63    use std::fs;
64    use std::time::{SystemTime, UNIX_EPOCH};
65
66    #[test]
67    fn ignored_directory_names_are_pruned_without_substring_overmatch() {
68        let unique = SystemTime::now()
69            .duration_since(UNIX_EPOCH)
70            .unwrap()
71            .as_nanos();
72        let root = std::env::temp_dir().join(format!("sniff-walker-test-{unique}"));
73        let output_dir = root.join("output");
74        let next_dir = root.join(".next");
75        let src_dir = root.join("src");
76        fs::create_dir_all(&output_dir).unwrap();
77        fs::create_dir_all(&next_dir).unwrap();
78        fs::create_dir_all(&src_dir).unwrap();
79
80        fs::write(output_dir.join("main.ts"), "export function ignored() {}\n").unwrap();
81        fs::write(next_dir.join("chunk.ts"), "export function ignored() {}\n").unwrap();
82        fs::write(
83            src_dir.join("output_parser.ts"),
84            "export function kept() {}\n",
85        )
86        .unwrap();
87
88        let files = walk(root.to_str().unwrap(), &ResolvedConfig::default()).unwrap();
89        assert!(
90            files.iter().any(|path| path.contains("output_parser.ts")),
91            "expected non-ignored file to remain visible: {files:?}"
92        );
93        assert!(
94            !files.iter().any(|path| path.contains("output\\main.ts")),
95            "expected ignored output directory to be pruned: {files:?}"
96        );
97        assert!(
98            !files.iter().any(|path| path.contains(".next\\chunk.ts")),
99            "expected ignored .next directory to be pruned: {files:?}"
100        );
101
102        let _ = fs::remove_dir_all(&root);
103    }
104
105    #[test]
106    fn gold_fixture_corpus_paths_are_pruned_from_walk_results() {
107        let unique = SystemTime::now()
108            .duration_since(UNIX_EPOCH)
109            .unwrap()
110            .as_nanos();
111        let root = std::env::temp_dir().join(format!("sniff-fixture-walk-{unique}"));
112        let fixtures_dir = root.join("gold_fixtures").join("repo").join("go");
113        fs::create_dir_all(&fixtures_dir).unwrap();
114        fs::write(
115            fixtures_dir.join("math.go"),
116            "package main\n\nfunc ProcessData(values []string) []string { return values }\n",
117        )
118        .unwrap();
119
120        let files = walk(root.to_str().unwrap(), &ResolvedConfig::default()).unwrap();
121        assert!(
122            files.is_empty(),
123            "expected fixture corpus files to be skipped from walks: {files:?}"
124        );
125
126        let _ = fs::remove_dir_all(&root);
127    }
128
129    #[test]
130    fn missing_scan_targets_return_an_error() {
131        let unique = SystemTime::now()
132            .duration_since(UNIX_EPOCH)
133            .unwrap()
134            .as_nanos();
135        let root = std::env::temp_dir().join(format!("sniff-missing-walk-{unique}"));
136
137        let err = walk(root.to_str().unwrap(), &ResolvedConfig::default())
138            .expect_err("missing scan target should fail explicitly");
139        assert!(err.contains("failed to resolve scan target"), "{err}");
140    }
141}