1use std::collections::HashMap;
14
15use syn::visit::Visit;
16use syn::{File, ItemUse, Path, UseTree};
17
18#[derive(Debug, Clone)]
22pub struct PathResolver {
23 target_canonical_segments: Vec<String>,
26
27 local_aliases: HashMap<String, Vec<String>>,
32
33 has_potential_glob_import: bool,
36}
37
38impl PathResolver {
39 pub fn new(canonical_path: &str) -> Option<Self> {
53 if canonical_path.is_empty() {
54 return None;
55 }
56
57 let segments: Vec<String> = canonical_path.split("::").map(String::from).collect();
58
59 if segments.is_empty() {
60 return None;
61 }
62
63 Some(Self {
64 target_canonical_segments: segments,
65 local_aliases: HashMap::new(),
66 has_potential_glob_import: false,
67 })
68 }
69
70 #[allow(dead_code)]
75 pub fn simple(name: &str) -> Self {
76 Self {
77 target_canonical_segments: vec![name.to_string()],
78 local_aliases: HashMap::new(),
79 has_potential_glob_import: false,
80 }
81 }
82
83 pub fn scan_file(&mut self, file: &File) {
87 let mut scanner = UseStatementScanner {
88 target_canonical_segments: &self.target_canonical_segments,
89 local_aliases: &mut self.local_aliases,
90 has_potential_glob_import: &mut self.has_potential_glob_import,
91 };
92 scanner.visit_file(file);
93 }
94
95 pub fn matches_target(&self, path: &Path) -> bool {
109 if path.segments.is_empty() {
110 return false;
111 }
112
113 let path_segments: Vec<String> = path
114 .segments
115 .iter()
116 .map(|seg| seg.ident.to_string())
117 .collect();
118
119 if path_segments == self.target_canonical_segments {
122 return true;
123 }
124
125 for i in 1..=path_segments.len() {
129 let prefix = &path_segments[0..i];
130 let prefix_str = prefix.join("::");
131
132 if let Some(canonical_prefix) = self.local_aliases.get(&prefix_str) {
133 let mut full_path = canonical_prefix.clone();
135 full_path.extend_from_slice(&path_segments[i..]);
136
137 if full_path == self.target_canonical_segments {
138 return true;
139 }
140 }
141 }
142
143 if path_segments.len() == 1
147 && let Some(canonical) = self.local_aliases.get(&path_segments[0])
148 {
149 return canonical == &self.target_canonical_segments;
150 }
151
152 false
153 }
154
155 #[cfg_attr(not(test), allow(dead_code))]
167 pub fn path_ends_with(&self, path: &Path, preceding_segment: &str) -> bool {
168 let segments: Vec<_> = path.segments.iter().collect();
169 let len = segments.len();
170
171 if len >= 2 {
172 segments[len - 2].ident == preceding_segment
173 } else {
174 false
175 }
176 }
177
178 #[cfg_attr(not(test), allow(dead_code))]
180 pub fn target_name(&self) -> &str {
181 self.target_canonical_segments
182 .last()
183 .map(std::string::String::as_str)
184 .expect("canonical path should have at least one segment")
185 }
186
187 #[cfg_attr(not(test), allow(dead_code))]
193 pub fn might_match_via_glob(&self, path: &Path) -> bool {
194 if !self.has_potential_glob_import {
195 return false;
196 }
197
198 path.segments
200 .last()
201 .map(|seg| seg.ident == self.target_name())
202 .unwrap_or(false)
203 }
204}
205
206struct UseStatementScanner<'a> {
208 target_canonical_segments: &'a [String],
209 local_aliases: &'a mut HashMap<String, Vec<String>>,
210 has_potential_glob_import: &'a mut bool,
211}
212
213impl<'a> UseStatementScanner<'a> {
214 fn process_use_tree(&mut self, tree: &UseTree, prefix: Vec<String>) {
216 match tree {
217 UseTree::Path(path) => {
218 let mut new_prefix = prefix;
219 new_prefix.push(path.ident.to_string());
220 self.process_use_tree(&path.tree, new_prefix);
221 }
222 UseTree::Name(name) => {
223 let mut full_path = prefix.clone();
225 full_path.push(name.ident.to_string());
226
227 let local_name = name.ident.to_string();
229 self.local_aliases.insert(local_name, full_path.clone());
230
231 if !prefix.is_empty() {
235 let prefix_str = prefix.join("::");
236 self.local_aliases.insert(prefix_str, prefix);
237 }
238 }
239 UseTree::Rename(rename) => {
240 let mut full_path = prefix;
242 full_path.push(rename.ident.to_string());
243
244 let local_name = rename.rename.to_string();
245 self.local_aliases.insert(local_name, full_path);
246 }
247 UseTree::Glob(_glob) => {
248 if self.is_potential_glob_for_target(&prefix) {
251 *self.has_potential_glob_import = true;
252 }
253 }
254 UseTree::Group(group) => {
255 for tree in &group.items {
257 self.process_use_tree(tree, prefix.clone());
258 }
259 }
260 }
261 }
262
263 fn is_potential_glob_for_target(&self, glob_prefix: &[String]) -> bool {
265 if self.target_canonical_segments.len() <= glob_prefix.len() {
267 return false;
268 }
269
270 for (i, segment) in glob_prefix.iter().enumerate() {
272 if i >= self.target_canonical_segments.len() {
273 return false;
274 }
275 if segment != &self.target_canonical_segments[i] {
276 return false;
277 }
278 }
279
280 self.target_canonical_segments.len() == glob_prefix.len() + 1
282 }
283}
284
285impl<'ast, 'a> Visit<'ast> for UseStatementScanner<'a> {
286 fn visit_item_use(&mut self, node: &'ast ItemUse) {
287 self.process_use_tree(&node.tree, Vec::new());
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use syn::parse_quote;
294
295 use super::*;
296
297 #[test]
298 fn test_exact_canonical_path_match() {
299 let resolver = PathResolver::new("crate::compiler::types::IRValue").unwrap();
300 let path: Path = parse_quote!(crate::compiler::types::IRValue);
301 assert!(resolver.matches_target(&path));
302 }
303
304 #[test]
305 fn test_simple_import() {
306 let mut resolver = PathResolver::new("crate::compiler::types::IRValue").unwrap();
307 let file: File = parse_quote! {
308 use crate::compiler::types::IRValue;
309
310 fn foo() {}
311 };
312 resolver.scan_file(&file);
313
314 let path: Path = parse_quote!(IRValue);
315 assert!(resolver.matches_target(&path));
316 }
317
318 #[test]
319 fn test_module_import() {
320 let mut resolver = PathResolver::new("crate::compiler::types::IRValue").unwrap();
321 let file: File = parse_quote! {
322 use crate::compiler::types;
323
324 fn foo() {}
325 };
326 resolver.scan_file(&file);
327
328 let path: Path = parse_quote!(types::IRValue);
329 assert!(resolver.matches_target(&path));
330 }
331
332 #[test]
333 fn test_aliased_import() {
334 let mut resolver = PathResolver::new("crate::compiler::types::IRValue").unwrap();
335 let file: File = parse_quote! {
336 use crate::compiler::types::IRValue as IV;
337
338 fn foo() {}
339 };
340 resolver.scan_file(&file);
341
342 let path: Path = parse_quote!(IV);
343 assert!(resolver.matches_target(&path));
344 }
345
346 #[test]
347 fn test_does_not_match_different_path() {
348 let resolver = PathResolver::new("crate::compiler::types::IRValue").unwrap();
349 let path: Path = parse_quote!(crate::other::types::IRValue);
350 assert!(!resolver.matches_target(&path));
351 }
352
353 #[test]
354 fn test_does_not_match_without_import() {
355 let mut resolver = PathResolver::new("crate::compiler::types::IRValue").unwrap();
356 let file: File = parse_quote! {
357 fn foo() {}
359 };
360 resolver.scan_file(&file);
361
362 let path: Path = parse_quote!(IRValue);
363 assert!(!resolver.matches_target(&path));
364 }
365
366 #[test]
367 fn test_glob_import_detection() {
368 let mut resolver = PathResolver::new("crate::compiler::types::IRValue").unwrap();
369 let file: File = parse_quote! {
370 use crate::compiler::types::*;
371
372 fn foo() {}
373 };
374 resolver.scan_file(&file);
375
376 let path: Path = parse_quote!(IRValue);
377 assert!(resolver.might_match_via_glob(&path));
378 }
379
380 #[test]
381 fn test_path_ends_with() {
382 let resolver = PathResolver::new("crate::compiler::types::IRValue").unwrap();
383
384 let path1: Path = parse_quote!(IRValue::HashMap);
385 assert!(resolver.path_ends_with(&path1, "IRValue"));
386
387 let path2: Path = parse_quote!(crate::compiler::types::IRValue::HashMap);
388 assert!(resolver.path_ends_with(&path2, "IRValue"));
389
390 let path3: Path = parse_quote!(OtherEnum::HashMap);
391 assert!(!resolver.path_ends_with(&path3, "IRValue"));
392 }
393
394 #[test]
395 fn test_grouped_imports() {
396 let mut resolver = PathResolver::new("crate::compiler::types::IRValue").unwrap();
397 let file: File = parse_quote! {
398 use crate::compiler::types::{IRValue, Frame};
399
400 fn foo() {}
401 };
402 resolver.scan_file(&file);
403
404 let path: Path = parse_quote!(IRValue);
405 assert!(resolver.matches_target(&path));
406 }
407}