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