use std::collections::HashMap;
use syn::{visit::Visit, File, ItemUse, Path, UseTree};
#[derive(Debug, Clone)]
pub struct PathResolver {
target_canonical_segments: Vec<String>,
target_simple_name: String,
local_aliases: HashMap<String, Vec<String>>,
has_potential_glob_import: bool,
}
impl PathResolver {
pub fn new(canonical_path: &str) -> Option<Self> {
if canonical_path.is_empty() {
return None;
}
let segments: Vec<String> = canonical_path
.split("::")
.map(String::from)
.collect();
if segments.is_empty() {
return None;
}
let simple_name = segments.last().unwrap().clone();
Some(Self {
target_canonical_segments: segments,
target_simple_name: simple_name,
local_aliases: HashMap::new(),
has_potential_glob_import: false,
})
}
pub fn simple(name: &str) -> Self {
Self {
target_canonical_segments: vec![name.to_string()],
target_simple_name: name.to_string(),
local_aliases: HashMap::new(),
has_potential_glob_import: false,
}
}
pub fn scan_file(&mut self, file: &File) {
let mut scanner = UseStatementScanner {
target_canonical_segments: &self.target_canonical_segments,
local_aliases: &mut self.local_aliases,
has_potential_glob_import: &mut self.has_potential_glob_import,
};
scanner.visit_file(file);
}
pub fn matches_target(&self, path: &Path) -> bool {
if path.segments.is_empty() {
return false;
}
let path_segments: Vec<String> = path
.segments
.iter()
.map(|seg| seg.ident.to_string())
.collect();
if path_segments == self.target_canonical_segments {
return true;
}
for i in 1..=path_segments.len() {
let prefix = &path_segments[0..i];
let prefix_str = prefix.join("::");
if let Some(canonical_prefix) = self.local_aliases.get(&prefix_str) {
let mut full_path = canonical_prefix.clone();
full_path.extend_from_slice(&path_segments[i..]);
if full_path == self.target_canonical_segments {
return true;
}
}
}
if path_segments.len() == 1 {
if let Some(canonical) = self.local_aliases.get(&path_segments[0]) {
return canonical == &self.target_canonical_segments;
}
}
false
}
pub fn path_ends_with(&self, path: &Path, preceding_segment: &str) -> bool {
let segments: Vec<_> = path.segments.iter().collect();
let len = segments.len();
if len >= 2 {
segments[len - 2].ident == preceding_segment
} else {
false
}
}
pub fn target_name(&self) -> &str {
&self.target_simple_name
}
pub fn might_match_via_glob(&self, path: &Path) -> bool {
if !self.has_potential_glob_import {
return false;
}
path.segments
.last()
.map(|seg| seg.ident == self.target_simple_name)
.unwrap_or(false)
}
}
struct UseStatementScanner<'a> {
target_canonical_segments: &'a [String],
local_aliases: &'a mut HashMap<String, Vec<String>>,
has_potential_glob_import: &'a mut bool,
}
impl<'a> UseStatementScanner<'a> {
fn process_use_tree(&mut self, tree: &UseTree, prefix: Vec<String>) {
match tree {
UseTree::Path(path) => {
let mut new_prefix = prefix.clone();
new_prefix.push(path.ident.to_string());
self.process_use_tree(&path.tree, new_prefix);
}
UseTree::Name(name) => {
let mut full_path = prefix.clone();
full_path.push(name.ident.to_string());
let local_name = name.ident.to_string();
self.local_aliases.insert(local_name, full_path.clone());
if !prefix.is_empty() {
let prefix_str = prefix.join("::");
self.local_aliases.insert(prefix_str, prefix);
}
}
UseTree::Rename(rename) => {
let mut full_path = prefix.clone();
full_path.push(rename.ident.to_string());
let local_name = rename.rename.to_string();
self.local_aliases.insert(local_name, full_path);
}
UseTree::Glob(_glob) => {
if self.is_potential_glob_for_target(&prefix) {
*self.has_potential_glob_import = true;
}
}
UseTree::Group(group) => {
for tree in &group.items {
self.process_use_tree(tree, prefix.clone());
}
}
}
}
fn is_potential_glob_for_target(&self, glob_prefix: &[String]) -> bool {
if self.target_canonical_segments.len() <= glob_prefix.len() {
return false;
}
for (i, segment) in glob_prefix.iter().enumerate() {
if i >= self.target_canonical_segments.len() {
return false;
}
if segment != &self.target_canonical_segments[i] {
return false;
}
}
self.target_canonical_segments.len() == glob_prefix.len() + 1
}
}
impl<'ast, 'a> Visit<'ast> for UseStatementScanner<'a> {
fn visit_item_use(&mut self, node: &'ast ItemUse) {
self.process_use_tree(&node.tree, Vec::new());
}
}
#[cfg(test)]
mod tests {
use super::*;
use syn::parse_quote;
#[test]
fn test_exact_canonical_path_match() {
let resolver = PathResolver::new("crate::compiler::types::IRValue").unwrap();
let path: Path = parse_quote!(crate::compiler::types::IRValue);
assert!(resolver.matches_target(&path));
}
#[test]
fn test_simple_import() {
let mut resolver = PathResolver::new("crate::compiler::types::IRValue").unwrap();
let file: File = parse_quote! {
use crate::compiler::types::IRValue;
fn foo() {}
};
resolver.scan_file(&file);
let path: Path = parse_quote!(IRValue);
assert!(resolver.matches_target(&path));
}
#[test]
fn test_module_import() {
let mut resolver = PathResolver::new("crate::compiler::types::IRValue").unwrap();
let file: File = parse_quote! {
use crate::compiler::types;
fn foo() {}
};
resolver.scan_file(&file);
let path: Path = parse_quote!(types::IRValue);
assert!(resolver.matches_target(&path));
}
#[test]
fn test_aliased_import() {
let mut resolver = PathResolver::new("crate::compiler::types::IRValue").unwrap();
let file: File = parse_quote! {
use crate::compiler::types::IRValue as IV;
fn foo() {}
};
resolver.scan_file(&file);
let path: Path = parse_quote!(IV);
assert!(resolver.matches_target(&path));
}
#[test]
fn test_does_not_match_different_path() {
let resolver = PathResolver::new("crate::compiler::types::IRValue").unwrap();
let path: Path = parse_quote!(crate::other::types::IRValue);
assert!(!resolver.matches_target(&path));
}
#[test]
fn test_does_not_match_without_import() {
let mut resolver = PathResolver::new("crate::compiler::types::IRValue").unwrap();
let file: File = parse_quote! {
fn foo() {}
};
resolver.scan_file(&file);
let path: Path = parse_quote!(IRValue);
assert!(!resolver.matches_target(&path));
}
#[test]
fn test_glob_import_detection() {
let mut resolver = PathResolver::new("crate::compiler::types::IRValue").unwrap();
let file: File = parse_quote! {
use crate::compiler::types::*;
fn foo() {}
};
resolver.scan_file(&file);
let path: Path = parse_quote!(IRValue);
assert!(resolver.might_match_via_glob(&path));
}
#[test]
fn test_path_ends_with() {
let resolver = PathResolver::new("crate::compiler::types::IRValue").unwrap();
let path1: Path = parse_quote!(IRValue::HashMap);
assert!(resolver.path_ends_with(&path1, "IRValue"));
let path2: Path = parse_quote!(crate::compiler::types::IRValue::HashMap);
assert!(resolver.path_ends_with(&path2, "IRValue"));
let path3: Path = parse_quote!(OtherEnum::HashMap);
assert!(!resolver.path_ends_with(&path3, "IRValue"));
}
#[test]
fn test_grouped_imports() {
let mut resolver = PathResolver::new("crate::compiler::types::IRValue").unwrap();
let file: File = parse_quote! {
use crate::compiler::types::{IRValue, Frame};
fn foo() {}
};
resolver.scan_file(&file);
let path: Path = parse_quote!(IRValue);
assert!(resolver.matches_target(&path));
}
}