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