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