1use std::collections::HashMap;
15use syn::{visit::Visit, File, ItemUse, Path, UseTree};
16
17#[derive(Debug, Clone)]
21pub struct PathResolver {
22 target_canonical_segments: Vec<String>,
25
26 target_simple_name: String,
29
30 local_aliases: HashMap<String, Vec<String>>,
35
36 has_potential_glob_import: bool,
39}
40
41impl PathResolver {
42 pub fn new(canonical_path: &str) -> Option<Self> {
56 if canonical_path.is_empty() {
57 return None;
58 }
59
60 let segments: Vec<String> = canonical_path
61 .split("::")
62 .map(String::from)
63 .collect();
64
65 if segments.is_empty() {
66 return None;
67 }
68
69 let simple_name = segments.last().unwrap().clone();
70
71 Some(Self {
72 target_canonical_segments: segments,
73 target_simple_name: simple_name,
74 local_aliases: HashMap::new(),
75 has_potential_glob_import: false,
76 })
77 }
78
79 pub fn simple(name: &str) -> Self {
84 Self {
85 target_canonical_segments: vec![name.to_string()],
86 target_simple_name: name.to_string(),
87 local_aliases: HashMap::new(),
88 has_potential_glob_import: false,
89 }
90 }
91
92 pub fn scan_file(&mut self, file: &File) {
96 let mut scanner = UseStatementScanner {
97 target_canonical_segments: &self.target_canonical_segments,
98 local_aliases: &mut self.local_aliases,
99 has_potential_glob_import: &mut self.has_potential_glob_import,
100 };
101 scanner.visit_file(file);
102 }
103
104 pub fn matches_target(&self, path: &Path) -> bool {
118 if path.segments.is_empty() {
119 return false;
120 }
121
122 let path_segments: Vec<String> = path
123 .segments
124 .iter()
125 .map(|seg| seg.ident.to_string())
126 .collect();
127
128 if path_segments == self.target_canonical_segments {
131 return true;
132 }
133
134 for i in 1..=path_segments.len() {
138 let prefix = &path_segments[0..i];
139 let prefix_str = prefix.join("::");
140
141 if let Some(canonical_prefix) = self.local_aliases.get(&prefix_str) {
142 let mut full_path = canonical_prefix.clone();
144 full_path.extend_from_slice(&path_segments[i..]);
145
146 if full_path == self.target_canonical_segments {
147 return true;
148 }
149 }
150 }
151
152 if path_segments.len() == 1 {
156 if let Some(canonical) = self.local_aliases.get(&path_segments[0]) {
157 return canonical == &self.target_canonical_segments;
158 }
159 }
160
161 false
162 }
163
164 pub fn path_ends_with(&self, path: &Path, preceding_segment: &str) -> bool {
176 let segments: Vec<_> = path.segments.iter().collect();
177 let len = segments.len();
178
179 if len >= 2 {
180 segments[len - 2].ident == preceding_segment
181 } else {
182 false
183 }
184 }
185
186 pub fn target_name(&self) -> &str {
188 &self.target_simple_name
189 }
190
191 pub fn might_match_via_glob(&self, path: &Path) -> bool {
197 if !self.has_potential_glob_import {
198 return false;
199 }
200
201 path.segments
203 .last()
204 .map(|seg| seg.ident == self.target_simple_name)
205 .unwrap_or(false)
206 }
207}
208
209struct UseStatementScanner<'a> {
211 target_canonical_segments: &'a [String],
212 local_aliases: &'a mut HashMap<String, Vec<String>>,
213 has_potential_glob_import: &'a mut bool,
214}
215
216impl<'a> UseStatementScanner<'a> {
217 fn process_use_tree(&mut self, tree: &UseTree, prefix: Vec<String>) {
219 match tree {
220 UseTree::Path(path) => {
221 let mut new_prefix = prefix.clone();
222 new_prefix.push(path.ident.to_string());
223 self.process_use_tree(&path.tree, new_prefix);
224 }
225 UseTree::Name(name) => {
226 let mut full_path = prefix.clone();
228 full_path.push(name.ident.to_string());
229
230 let local_name = name.ident.to_string();
232 self.local_aliases.insert(local_name, full_path.clone());
233
234 if !prefix.is_empty() {
237 let prefix_str = prefix.join("::");
238 self.local_aliases.insert(prefix_str, prefix);
239 }
240 }
241 UseTree::Rename(rename) => {
242 let mut full_path = prefix.clone();
244 full_path.push(rename.ident.to_string());
245
246 let local_name = rename.rename.to_string();
247 self.local_aliases.insert(local_name, full_path);
248 }
249 UseTree::Glob(_glob) => {
250 if self.is_potential_glob_for_target(&prefix) {
253 *self.has_potential_glob_import = true;
254 }
255 }
256 UseTree::Group(group) => {
257 for tree in &group.items {
259 self.process_use_tree(tree, prefix.clone());
260 }
261 }
262 }
263 }
264
265 fn is_potential_glob_for_target(&self, glob_prefix: &[String]) -> bool {
267 if self.target_canonical_segments.len() <= glob_prefix.len() {
269 return false;
270 }
271
272 for (i, segment) in glob_prefix.iter().enumerate() {
274 if i >= self.target_canonical_segments.len() {
275 return false;
276 }
277 if segment != &self.target_canonical_segments[i] {
278 return false;
279 }
280 }
281
282 self.target_canonical_segments.len() == glob_prefix.len() + 1
284 }
285}
286
287impl<'ast, 'a> Visit<'ast> for UseStatementScanner<'a> {
288 fn visit_item_use(&mut self, node: &'ast ItemUse) {
289 self.process_use_tree(&node.tree, Vec::new());
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296 use syn::parse_quote;
297
298 #[test]
299 fn test_exact_canonical_path_match() {
300 let resolver = PathResolver::new("crate::compiler::types::IRValue").unwrap();
301 let path: Path = parse_quote!(crate::compiler::types::IRValue);
302 assert!(resolver.matches_target(&path));
303 }
304
305 #[test]
306 fn test_simple_import() {
307 let mut resolver = PathResolver::new("crate::compiler::types::IRValue").unwrap();
308 let file: File = parse_quote! {
309 use crate::compiler::types::IRValue;
310
311 fn foo() {}
312 };
313 resolver.scan_file(&file);
314
315 let path: Path = parse_quote!(IRValue);
316 assert!(resolver.matches_target(&path));
317 }
318
319 #[test]
320 fn test_module_import() {
321 let mut resolver = PathResolver::new("crate::compiler::types::IRValue").unwrap();
322 let file: File = parse_quote! {
323 use crate::compiler::types;
324
325 fn foo() {}
326 };
327 resolver.scan_file(&file);
328
329 let path: Path = parse_quote!(types::IRValue);
330 assert!(resolver.matches_target(&path));
331 }
332
333 #[test]
334 fn test_aliased_import() {
335 let mut resolver = PathResolver::new("crate::compiler::types::IRValue").unwrap();
336 let file: File = parse_quote! {
337 use crate::compiler::types::IRValue as IV;
338
339 fn foo() {}
340 };
341 resolver.scan_file(&file);
342
343 let path: Path = parse_quote!(IV);
344 assert!(resolver.matches_target(&path));
345 }
346
347 #[test]
348 fn test_does_not_match_different_path() {
349 let resolver = PathResolver::new("crate::compiler::types::IRValue").unwrap();
350 let path: Path = parse_quote!(crate::other::types::IRValue);
351 assert!(!resolver.matches_target(&path));
352 }
353
354 #[test]
355 fn test_does_not_match_without_import() {
356 let mut resolver = PathResolver::new("crate::compiler::types::IRValue").unwrap();
357 let file: File = parse_quote! {
358 fn foo() {}
360 };
361 resolver.scan_file(&file);
362
363 let path: Path = parse_quote!(IRValue);
364 assert!(!resolver.matches_target(&path));
365 }
366
367 #[test]
368 fn test_glob_import_detection() {
369 let mut resolver = PathResolver::new("crate::compiler::types::IRValue").unwrap();
370 let file: File = parse_quote! {
371 use crate::compiler::types::*;
372
373 fn foo() {}
374 };
375 resolver.scan_file(&file);
376
377 let path: Path = parse_quote!(IRValue);
378 assert!(resolver.might_match_via_glob(&path));
379 }
380
381 #[test]
382 fn test_path_ends_with() {
383 let resolver = PathResolver::new("crate::compiler::types::IRValue").unwrap();
384
385 let path1: Path = parse_quote!(IRValue::HashMap);
386 assert!(resolver.path_ends_with(&path1, "IRValue"));
387
388 let path2: Path = parse_quote!(crate::compiler::types::IRValue::HashMap);
389 assert!(resolver.path_ends_with(&path2, "IRValue"));
390
391 let path3: Path = parse_quote!(OtherEnum::HashMap);
392 assert!(!resolver.path_ends_with(&path3, "IRValue"));
393 }
394
395 #[test]
396 fn test_grouped_imports() {
397 let mut resolver = PathResolver::new("crate::compiler::types::IRValue").unwrap();
398 let file: File = parse_quote! {
399 use crate::compiler::types::{IRValue, Frame};
400
401 fn foo() {}
402 };
403 resolver.scan_file(&file);
404
405 let path: Path = parse_quote!(IRValue);
406 assert!(resolver.matches_target(&path));
407 }
408}