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}