rust_diff_analyzer/classifier/path_classifier.rs
1// SPDX-FileCopyrightText: 2025 RAprogramm <andrey.rozanov.vl@gmail.com>
2// SPDX-License-Identifier: MIT
3
4use std::path::Path;
5
6/// Checks whether a path matches a `/`-separated pattern by whole components
7///
8/// The pattern's components must appear consecutively in the path. A trailing
9/// slash requires the match to be a directory, i.e. at least one more path
10/// component must follow. Substrings never match partial components, so
11/// `tests/` does not match `src/attests/mod.rs` and `src/gen` does not match
12/// `src/generic.rs`.
13///
14/// # Arguments
15///
16/// * `path` - Path to check
17/// * `pattern` - `/`-separated component pattern
18///
19/// # Returns
20///
21/// `true` if the pattern's components appear consecutively in the path
22///
23/// # Examples
24///
25/// ```
26/// use std::path::Path;
27///
28/// use rust_diff_analyzer::classifier::path_classifier::path_matches_pattern;
29///
30/// assert!(path_matches_pattern(
31/// Path::new("tests/integration.rs"),
32/// "tests/"
33/// ));
34/// assert!(path_matches_pattern(
35/// Path::new("crate/tests/it.rs"),
36/// "tests/"
37/// ));
38/// assert!(!path_matches_pattern(
39/// Path::new("src/attests/mod.rs"),
40/// "tests/"
41/// ));
42/// assert!(!path_matches_pattern(
43/// Path::new("src/generic.rs"),
44/// "src/gen"
45/// ));
46/// ```
47pub fn path_matches_pattern(path: &Path, pattern: &str) -> bool {
48 let requires_directory = pattern.ends_with('/');
49 let pattern_components: Vec<&str> = pattern.split('/').filter(|c| !c.is_empty()).collect();
50 if pattern_components.is_empty() {
51 return false;
52 }
53
54 let components: Vec<String> = path
55 .components()
56 .map(|c| c.as_os_str().to_string_lossy().into_owned())
57 .collect();
58 let needed = pattern_components.len();
59 if components.len() < needed {
60 return false;
61 }
62
63 for start in 0..=(components.len() - needed) {
64 let matches = components[start..start + needed]
65 .iter()
66 .zip(&pattern_components)
67 .all(|(component, pattern_component)| component == pattern_component);
68 if matches && (!requires_directory || start + needed < components.len()) {
69 return true;
70 }
71 }
72
73 false
74}
75
76/// Checks if path is in examples directory
77///
78/// # Arguments
79///
80/// * `path` - Path to check
81///
82/// # Returns
83///
84/// `true` if path is in examples/
85///
86/// # Examples
87///
88/// ```
89/// use std::path::Path;
90///
91/// use rust_diff_analyzer::classifier::path_classifier::is_example_path;
92///
93/// assert!(is_example_path(Path::new("examples/demo.rs")));
94/// assert!(!is_example_path(Path::new("src/lib.rs")));
95/// ```
96pub fn is_example_path(path: &Path) -> bool {
97 path_matches_pattern(path, "examples/")
98}
99
100/// Checks if path is in benches directory
101///
102/// # Arguments
103///
104/// * `path` - Path to check
105///
106/// # Returns
107///
108/// `true` if path is in benches/
109///
110/// # Examples
111///
112/// ```
113/// use std::path::Path;
114///
115/// use rust_diff_analyzer::classifier::path_classifier::is_bench_path;
116///
117/// assert!(is_bench_path(Path::new("benches/bench.rs")));
118/// assert!(!is_bench_path(Path::new("src/lib.rs")));
119/// ```
120pub fn is_bench_path(path: &Path) -> bool {
121 path_matches_pattern(path, "benches/")
122}
123
124/// Checks if path is in tests directory
125///
126/// # Arguments
127///
128/// * `path` - Path to check
129///
130/// # Returns
131///
132/// `true` if path is in tests/
133///
134/// # Examples
135///
136/// ```
137/// use std::path::Path;
138///
139/// use rust_diff_analyzer::classifier::path_classifier::is_test_path;
140///
141/// assert!(is_test_path(Path::new("tests/integration.rs")));
142/// assert!(!is_test_path(Path::new("src/lib.rs")));
143/// ```
144pub fn is_test_path(path: &Path) -> bool {
145 path_matches_pattern(path, "tests/")
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn test_example_paths() {
154 assert!(is_example_path(Path::new("examples/main.rs")));
155 assert!(is_example_path(Path::new("crate/examples/demo.rs")));
156 assert!(!is_example_path(Path::new("src/examples.rs")));
157 }
158
159 #[test]
160 fn test_bench_paths() {
161 assert!(is_bench_path(Path::new("benches/perf.rs")));
162 assert!(!is_bench_path(Path::new("src/benches.rs")));
163 }
164
165 #[test]
166 fn test_test_paths() {
167 assert!(is_test_path(Path::new("tests/integration.rs")));
168 assert!(!is_test_path(Path::new("src/tests.rs")));
169 }
170
171 #[test]
172 fn test_component_boundaries() {
173 assert!(!is_test_path(Path::new("src/attests/mod.rs")));
174 assert!(!is_test_path(Path::new("src/contests/x.rs")));
175 assert!(!is_bench_path(Path::new("src/benches_util/x.rs")));
176 assert!(is_test_path(Path::new("crate/tests/deep/it.rs")));
177 }
178
179 #[test]
180 fn test_pattern_matching_rules() {
181 assert!(path_matches_pattern(Path::new("src/gen/out.rs"), "src/gen"));
182 assert!(!path_matches_pattern(
183 Path::new("src/generic.rs"),
184 "src/gen"
185 ));
186 assert!(path_matches_pattern(
187 Path::new("src/gen/out.rs"),
188 "src/gen/"
189 ));
190 assert!(!path_matches_pattern(Path::new("a/tests"), "tests/"));
191 assert!(!path_matches_pattern(Path::new("src/lib.rs"), ""));
192 }
193}