rs_hack/
path_resolver.rs

1/// Path resolution module for safely matching qualified paths in Rust code.
2///
3/// This module helps determine whether a path in code refers to a specific target
4/// by tracking use statements and validating path qualifications.
5///
6/// Example: When looking for `crate::compiler::types::IRValue::Variant`, this resolver
7/// will match:
8/// - `IRValue::Variant` (if `use crate::compiler::types::IRValue;` exists)
9/// - `types::IRValue::Variant` (if `use crate::compiler;` exists)
10/// - `crate::compiler::types::IRValue::Variant` (fully qualified)
11///
12/// But will NOT match:
13/// - `OtherEnum::Variant` (different enum entirely)
14/// - `IRValue::Variant` (if no appropriate use statement exists)
15
16use std::collections::HashMap;
17use syn::{visit::Visit, File, ItemUse, Path, UseTree};
18
19/// Tracks use statements and validates whether paths refer to a specific target.
20///
21/// This is generic enough to work for enums, structs, functions, traits, etc.
22#[derive(Debug, Clone)]
23pub struct PathResolver {
24    /// The canonical fully-qualified path we're looking for
25    /// e.g., ["crate", "compiler", "types", "IRValue"]
26    target_canonical_segments: Vec<String>,
27
28    /// The simple name of the target (last segment of canonical path)
29    /// e.g., "IRValue"
30    target_simple_name: String,
31
32    /// Maps local names/aliases to their canonical path segments
33    /// e.g., "IRValue" -> ["crate", "compiler", "types", "IRValue"]
34    /// e.g., "types" -> ["crate", "compiler", "types"]
35    /// e.g., "IV" -> ["crate", "compiler", "types", "IRValue"] (aliased)
36    local_aliases: HashMap<String, Vec<String>>,
37
38    /// Tracks if we found a glob import that might include our target
39    /// e.g., `use crate::compiler::types::*;`
40    has_potential_glob_import: bool,
41}
42
43impl PathResolver {
44    /// Create a new path resolver for a specific canonical path.
45    ///
46    /// # Arguments
47    /// * `canonical_path` - The fully qualified path (e.g., "crate::compiler::types::IRValue")
48    ///
49    /// # Returns
50    /// A new PathResolver, or None if the path is invalid
51    ///
52    /// # Example
53    /// ```
54    /// let resolver = PathResolver::new("crate::compiler::types::IRValue");
55    /// ```
56    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    /// Create a resolver that only matches exact simple paths (backward compatible mode).
81    ///
82    /// This matches the old behavior where only `EnumName::Variant` is matched,
83    /// without any use statement tracking.
84    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    /// Scan a file to build the local alias map from use statements.
94    ///
95    /// This should be called once per file before using `matches_target()`.
96    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    /// Check if a path definitely refers to our target.
106    ///
107    /// This uses conservative matching - only returns true if we're certain
108    /// the path refers to our target based on:
109    /// 1. Exact canonical path match
110    /// 2. Import alias resolution
111    /// 3. Module path resolution
112    ///
113    /// # Arguments
114    /// * `path` - The syn::Path to check
115    ///
116    /// # Returns
117    /// true if the path definitely refers to our target
118    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        // Case 1: Exact canonical path match
130        // e.g., `crate::compiler::types::IRValue` matches exactly
131        if path_segments == self.target_canonical_segments {
132            return true;
133        }
134
135        // Case 2: Check if any prefix is an alias we know about
136        // e.g., if `use crate::compiler::types;` exists,
137        // then `types::IRValue` should match
138        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                // Rebuild the full path using the canonical prefix
144                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        // Case 3: Simple import case
154        // e.g., if `use crate::compiler::types::IRValue;` exists,
155        // then just `IRValue` should match
156        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    /// Check if a path ends with the preceding segment (e.g., enum name).
166    ///
167    /// This is useful for matching patterns like `EnumName::VariantName`
168    /// regardless of what the variant name is, when combined with path validation.
169    ///
170    /// # Arguments
171    /// * `path` - The path to check
172    /// * `preceding_segment` - The segment before the variant (e.g., "IRValue" for enum variants)
173    ///
174    /// # Returns
175    /// true if the path has at least 2 segments and the second-to-last matches preceding_segment
176    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    /// Get the simple name of the target.
188    pub fn target_name(&self) -> &str {
189        &self.target_simple_name
190    }
191
192    /// Check if a path could potentially match via glob import.
193    ///
194    /// Returns true if:
195    /// - We found a glob import that could include our target
196    /// - The path's simple name matches our target
197    pub fn might_match_via_glob(&self, path: &Path) -> bool {
198        if !self.has_potential_glob_import {
199            return false;
200        }
201
202        // Check if the last segment matches our target name
203        path.segments
204            .last()
205            .map(|seg| seg.ident == self.target_simple_name)
206            .unwrap_or(false)
207    }
208}
209
210/// Visitor that scans use statements to build the alias map.
211struct 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    /// Process a use tree and extract aliases.
219    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                // Simple import: `use crate::foo::Bar;`
228                let mut full_path = prefix.clone();
229                full_path.push(name.ident.to_string());
230
231                // Map the simple name to the full path
232                let local_name = name.ident.to_string();
233                self.local_aliases.insert(local_name, full_path.clone());
234
235                // Also map intermediate paths
236                // e.g., `use crate::compiler::types;` maps "types" to ["crate", "compiler", "types"]
237                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                // Aliased import: `use crate::foo::Bar as Baz;`
244                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                // Glob import: `use crate::foo::*;`
252                // Check if this glob could import our target
253                if self.is_potential_glob_for_target(&prefix) {
254                    *self.has_potential_glob_import = true;
255                }
256            }
257            UseTree::Group(group) => {
258                // Grouped imports: `use crate::foo::{Bar, Baz};`
259                for tree in &group.items {
260                    self.process_use_tree(tree, prefix.clone());
261                }
262            }
263        }
264    }
265
266    /// Check if a glob import could potentially import our target.
267    fn is_potential_glob_for_target(&self, glob_prefix: &[String]) -> bool {
268        // Check if our target starts with this prefix
269        if self.target_canonical_segments.len() <= glob_prefix.len() {
270            return false;
271        }
272
273        // Check if the glob prefix matches the start of our target
274        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        // The glob is one level above our target
284        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            // No imports
360            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}