Skip to main content

perl_workspace_ignore/
lib.rs

1//! Canonical workspace noise filtering rules.
2//!
3//! This crate centralizes the shared ignore directory policy used by workspace
4//! discovery and runtime workspace operations.
5
6#![deny(unsafe_code)]
7#![warn(rust_2018_idioms)]
8#![warn(missing_docs)]
9#![warn(clippy::all)]
10
11use std::path::{Component, Path};
12
13const SKIPPED_DIRS: [&str; 6] = [".git", ".hg", ".svn", "target", "node_modules", ".cache"];
14
15/// Returns true when `name` matches one of the canonical workspace noise directories.
16#[must_use]
17pub fn is_skipped_dir_name(name: &str) -> bool {
18    SKIPPED_DIRS.contains(&name)
19}
20
21/// Returns true when any path component belongs to the canonical skipped directory set.
22#[must_use]
23pub fn path_contains_skipped_component(path: &Path) -> bool {
24    for component in path.components() {
25        if let Component::Normal(name) = component
26            && let Some(value) = name.to_str()
27            && is_skipped_dir_name(value)
28        {
29            return true;
30        }
31    }
32
33    false
34}
35
36#[cfg(test)]
37mod tests {
38    use super::{SKIPPED_DIRS, is_skipped_dir_name, path_contains_skipped_component};
39    use std::path::Path;
40
41    // ── is_skipped_dir_name: default patterns ──────────────────────────
42
43    #[test]
44    fn test_skipped_dir_name_git_matches() {
45        assert!(is_skipped_dir_name(".git"));
46    }
47
48    #[test]
49    fn test_skipped_dir_name_hg_matches() {
50        assert!(is_skipped_dir_name(".hg"));
51    }
52
53    #[test]
54    fn test_skipped_dir_name_svn_matches() {
55        assert!(is_skipped_dir_name(".svn"));
56    }
57
58    #[test]
59    fn test_skipped_dir_name_target_matches() {
60        assert!(is_skipped_dir_name("target"));
61    }
62
63    #[test]
64    fn test_skipped_dir_name_node_modules_matches() {
65        assert!(is_skipped_dir_name("node_modules"));
66    }
67
68    #[test]
69    fn test_skipped_dir_name_cache_matches() {
70        assert!(is_skipped_dir_name(".cache"));
71    }
72
73    #[test]
74    fn test_skipped_dir_name_all_canonical_patterns() {
75        // Verify every entry in the constant is recognized
76        for name in &SKIPPED_DIRS {
77            assert!(is_skipped_dir_name(name), "expected '{name}' to be skipped");
78        }
79    }
80
81    #[test]
82    fn test_skipped_dir_name_constant_has_expected_count() {
83        assert_eq!(SKIPPED_DIRS.len(), 6);
84    }
85
86    // ── is_skipped_dir_name: non-matching names ────────────────────────
87
88    #[test]
89    fn test_skipped_dir_name_rejects_common_directories() {
90        for name in ["src", "lib", "blib", "tmp", "vendor", "dist", "build"] {
91            assert!(!is_skipped_dir_name(name), "'{name}' should not be skipped");
92        }
93    }
94
95    #[test]
96    fn test_skipped_dir_name_case_sensitive_git() {
97        // Matching is case-sensitive; ".Git" and ".GIT" are not skipped
98        assert!(!is_skipped_dir_name(".Git"));
99        assert!(!is_skipped_dir_name(".GIT"));
100        assert!(!is_skipped_dir_name(".GIT"));
101    }
102
103    #[test]
104    fn test_skipped_dir_name_case_sensitive_target() {
105        assert!(!is_skipped_dir_name("Target"));
106        assert!(!is_skipped_dir_name("TARGET"));
107    }
108
109    #[test]
110    fn test_skipped_dir_name_case_sensitive_node_modules() {
111        assert!(!is_skipped_dir_name("Node_Modules"));
112        assert!(!is_skipped_dir_name("NODE_MODULES"));
113    }
114
115    #[test]
116    fn test_skipped_dir_name_prefix_suffix_not_matched() {
117        // Substrings and extensions must not match
118        assert!(!is_skipped_dir_name(".git2"));
119        assert!(!is_skipped_dir_name("git"));
120        assert!(!is_skipped_dir_name(".gitignore"));
121        assert!(!is_skipped_dir_name("my_target"));
122        assert!(!is_skipped_dir_name("targets"));
123        assert!(!is_skipped_dir_name("node_modules_backup"));
124    }
125
126    #[test]
127    fn test_skipped_dir_name_empty_string() {
128        assert!(!is_skipped_dir_name(""));
129    }
130
131    #[test]
132    fn test_skipped_dir_name_whitespace() {
133        assert!(!is_skipped_dir_name(" "));
134        assert!(!is_skipped_dir_name(" .git"));
135        assert!(!is_skipped_dir_name(".git "));
136    }
137
138    #[test]
139    fn test_skipped_dir_name_dot_only() {
140        assert!(!is_skipped_dir_name("."));
141        assert!(!is_skipped_dir_name(".."));
142    }
143
144    #[test]
145    fn test_skipped_dir_name_slash_embedded() {
146        // A name containing a slash is never a single directory name
147        assert!(!is_skipped_dir_name(".git/objects"));
148        assert!(!is_skipped_dir_name("node_modules/pkg"));
149    }
150
151    // ── path_contains_skipped_component: basic matching ────────────────
152
153    #[test]
154    fn test_path_skipped_component_git_at_root() {
155        assert!(path_contains_skipped_component(Path::new(".git")));
156    }
157
158    #[test]
159    fn test_path_skipped_component_git_nested() {
160        assert!(path_contains_skipped_component(Path::new("repo/.git/objects/pack")));
161    }
162
163    #[test]
164    fn test_path_skipped_component_node_modules_middle() {
165        assert!(path_contains_skipped_component(Path::new(
166            "workspace/node_modules/Some/Module.pm"
167        )));
168    }
169
170    #[test]
171    fn test_path_skipped_component_target_leaf() {
172        assert!(path_contains_skipped_component(Path::new("project/target")));
173    }
174
175    #[test]
176    fn test_path_skipped_component_each_pattern_in_path() {
177        for name in &SKIPPED_DIRS {
178            let p = format!("some/dir/{name}/file.txt");
179            assert!(
180                path_contains_skipped_component(Path::new(&p)),
181                "path containing '{name}' should be flagged"
182            );
183        }
184    }
185
186    // ── path_contains_skipped_component: clean paths ───────────────────
187
188    #[test]
189    fn test_path_clean_lib_path_not_skipped() {
190        assert!(!path_contains_skipped_component(Path::new("repo/lib/My/Module.pm")));
191    }
192
193    #[test]
194    fn test_path_clean_src_path_not_skipped() {
195        assert!(!path_contains_skipped_component(Path::new("crates/perl-parser/src/lib.rs")));
196    }
197
198    #[test]
199    fn test_path_clean_deep_nesting_not_skipped() {
200        assert!(!path_contains_skipped_component(Path::new("a/b/c/d/e/f/g/h/file.pm")));
201    }
202
203    // ── path_contains_skipped_component: nested / multiple skipped ─────
204
205    #[test]
206    fn test_path_multiple_skipped_components() {
207        // Both .git and node_modules present
208        assert!(path_contains_skipped_component(Path::new(".git/node_modules/something")));
209    }
210
211    #[test]
212    fn test_path_skipped_inside_skipped() {
213        assert!(path_contains_skipped_component(Path::new("target/.cache/build")));
214    }
215
216    #[test]
217    fn test_path_deeply_nested_skipped() {
218        assert!(path_contains_skipped_component(Path::new("a/b/c/d/e/.svn/f/g")));
219    }
220
221    // ── path_contains_skipped_component: edge cases ────────────────────
222
223    #[test]
224    fn test_path_empty_path_not_skipped() {
225        assert!(!path_contains_skipped_component(Path::new("")));
226    }
227
228    #[test]
229    fn test_path_dot_not_skipped() {
230        assert!(!path_contains_skipped_component(Path::new(".")));
231    }
232
233    #[test]
234    fn test_path_dotdot_not_skipped() {
235        assert!(!path_contains_skipped_component(Path::new("..")));
236    }
237
238    #[test]
239    fn test_path_root_only_not_skipped() {
240        assert!(!path_contains_skipped_component(Path::new("/")));
241    }
242
243    #[test]
244    fn test_path_absolute_with_skipped() {
245        assert!(path_contains_skipped_component(Path::new("/home/user/project/.git/config")));
246    }
247
248    #[test]
249    fn test_path_absolute_without_skipped() {
250        assert!(!path_contains_skipped_component(Path::new("/home/user/project/lib/Module.pm")));
251    }
252
253    #[test]
254    fn test_path_trailing_slash_with_skipped() {
255        // Path::new normalizes trailing separators; the component is still detected
256        assert!(path_contains_skipped_component(Path::new("repo/.git/")));
257    }
258
259    #[test]
260    fn test_path_dotdot_before_skipped() {
261        assert!(path_contains_skipped_component(Path::new("../other/node_modules/pkg")));
262    }
263
264    #[test]
265    fn test_path_dot_before_skipped() {
266        assert!(path_contains_skipped_component(Path::new("./target/debug/binary")));
267    }
268
269    #[test]
270    fn test_path_skipped_name_as_file_extension_not_matched() {
271        // "file.target" is a single component; not equal to "target"
272        assert!(!path_contains_skipped_component(Path::new("dir/file.target")));
273    }
274
275    #[test]
276    fn test_path_skipped_name_substring_in_component() {
277        // "my_target_dir" contains "target" as a substring but is not equal
278        assert!(!path_contains_skipped_component(Path::new("project/my_target_dir/file.rs")));
279    }
280
281    #[test]
282    fn test_path_only_skipped_component() {
283        // Path consists solely of one skipped name
284        assert!(path_contains_skipped_component(Path::new("node_modules")));
285        assert!(path_contains_skipped_component(Path::new(".hg")));
286    }
287
288    #[test]
289    fn test_path_single_non_skipped_component() {
290        assert!(!path_contains_skipped_component(Path::new("src")));
291        assert!(!path_contains_skipped_component(Path::new("lib")));
292    }
293
294    #[test]
295    fn test_path_unicode_component_not_skipped() {
296        assert!(!path_contains_skipped_component(Path::new("repo/\u{00e9}t\u{00e9}/Module.pm")));
297    }
298
299    #[test]
300    fn test_path_case_sensitive_git_in_path() {
301        assert!(!path_contains_skipped_component(Path::new("repo/.Git/config")));
302        assert!(!path_contains_skipped_component(Path::new("repo/.GIT/config")));
303    }
304}