#![allow(dead_code)]
use std::collections::{BTreeMap, BTreeSet};
use syn::{Item, Path, UseTree};
use crate::types::{EmitError, TypePath};
#[derive(Debug, Clone, Default)]
pub(crate) struct FileImports {
pub(crate) simple: BTreeMap<String, TypePath>,
pub(crate) globs: BTreeSet<TypePath>,
}
impl FileImports {
pub(crate) fn resolve_ident(&self, ident: &str) -> Option<TypePath> {
self.simple.get(ident).cloned()
}
}
pub(crate) fn parse_imports(file: &syn::File) -> FileImports {
let mut out = FileImports::default();
imports_from_items(&file.items, &mut out);
out
}
fn imports_from_items(items: &[Item], out: &mut FileImports) {
for item in items {
if let Item::Use(item_use) = item {
walk_use_tree(&item_use.tree, &mut Vec::new(), out);
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ModuleImports {
by_module: BTreeMap<Vec<String>, FileImports>,
}
impl ModuleImports {
pub(crate) fn get(&self, module: &[String]) -> Option<&FileImports> {
self.by_module.get(module)
}
pub fn merge(&mut self, other: ModuleImports) {
for (module, imports) in other.by_module {
self.by_module.entry(module).or_insert(imports);
}
}
}
pub(crate) fn collect_module_imports(file: &syn::File, prefix: &[String], out: &mut ModuleImports) {
collect_items_into(&file.items, prefix, out);
}
fn collect_items_into(items: &[Item], prefix: &[String], out: &mut ModuleImports) {
let entry = out.by_module.entry(prefix.to_vec()).or_default();
imports_from_items(items, entry);
for item in items {
if let Item::Mod(m) = item
&& let Some((_, inner)) = &m.content
{
let mut sub = prefix.to_vec();
sub.push(m.ident.to_string());
collect_items_into(inner, &sub, out);
}
}
}
const MAX_IMPORT_DEPTH: u8 = 16;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Resolution {
Resolved(TypePath),
NotInPool,
Ambiguous(Vec<TypePath>),
}
pub fn resolve_reference(
segments: &[String],
module: &[String],
pool: &BTreeMap<TypePath, syn::Item>,
imports: &ModuleImports,
) -> Resolution {
match segments {
[] => Resolution::NotInPool,
[only] => resolve_bare_ident(only, module, pool, imports, 0),
multi => match absolutize(multi, module, pool, imports) {
Some(abs) => match TypePath::new(abs) {
Ok(tp) if pool.contains_key(&tp) => Resolution::Resolved(tp),
_ => Resolution::NotInPool,
},
None => Resolution::NotInPool,
},
}
}
fn resolve_bare_ident(
ident: &str,
module: &[String],
pool: &BTreeMap<TypePath, syn::Item>,
imports: &ModuleImports,
depth: u8,
) -> Resolution {
if let Some(file_imports) = imports.get(module)
&& let Some(target) = file_imports.resolve_ident(ident)
{
return resolve_import_target(target.segments(), module, pool, imports, depth);
}
let mut same_module = module.to_vec();
same_module.push(ident.to_string());
if let Ok(path) = TypePath::new(same_module)
&& pool.contains_key(&path)
{
return Resolution::Resolved(path);
}
terminal_resolution(ident, pool, module.first().map(String::as_str))
}
fn resolve_import_target(
target: &[String],
in_module: &[String],
pool: &BTreeMap<TypePath, syn::Item>,
imports: &ModuleImports,
depth: u8,
) -> Resolution {
let Some(leaf) = target.last().cloned() else {
return Resolution::NotInPool;
};
let Some(abs) = absolutize(target, in_module, pool, imports) else {
return terminal_resolution(&leaf, pool, in_module.first().map(String::as_str));
};
let Ok(abs_path) = TypePath::new(abs.clone()) else {
return Resolution::NotInPool;
};
if pool.contains_key(&abs_path) {
return Resolution::Resolved(abs_path);
}
if depth < MAX_IMPORT_DEPTH && abs.len() >= 2 {
let reexport_module = &abs[..abs.len() - 1];
match resolve_bare_ident(&leaf, reexport_module, pool, imports, depth + 1) {
resolved @ (Resolution::Resolved(_) | Resolution::Ambiguous(_)) => return resolved,
Resolution::NotInPool => {}
}
}
terminal_resolution(&leaf, pool, in_module.first().map(String::as_str))
}
fn absolutize(
target: &[String],
in_module: &[String],
pool: &BTreeMap<TypePath, syn::Item>,
imports: &ModuleImports,
) -> Option<Vec<String>> {
let (first, rest) = target.split_first()?;
match first.as_str() {
"crate" => Some([&in_module[..1.min(in_module.len())], rest].concat()),
"self" => Some([in_module, rest].concat()),
"super" => {
let parent = in_module.split_last().map(|(_, p)| p)?;
Some([parent, rest].concat())
}
_ => {
let mut candidate_module = in_module.to_vec();
candidate_module.push(first.clone());
if is_known_module(&candidate_module, pool, imports) {
return Some([in_module, target].concat());
}
if is_known_module(std::slice::from_ref(first), pool, imports) {
return Some(target.to_vec());
}
None
}
}
}
fn is_known_module(prefix: &[String], pool: &BTreeMap<TypePath, syn::Item>, imports: &ModuleImports) -> bool {
if imports.get(prefix).is_some() {
return true;
}
pool.keys().any(|k| {
let segs = k.segments();
segs.len() > prefix.len() && &segs[..prefix.len()] == prefix
})
}
fn terminal_resolution(ident: &str, pool: &BTreeMap<TypePath, syn::Item>, home_root: Option<&str>) -> Resolution {
let matches: Vec<TypePath> = pool.keys().filter(|p| p.terminal() == ident).cloned().collect();
let home: Vec<TypePath> = match home_root {
Some(root) => {
matches.iter().filter(|p| p.segments().first().map(String::as_str) == Some(root)).cloned().collect()
}
None => Vec::new(),
};
let candidates = if home.is_empty() { matches } else { home };
match candidates.len() {
0 => Resolution::NotInPool,
1 => Resolution::Resolved(candidates.into_iter().next().expect("len checked")),
_ => Resolution::Ambiguous(candidates),
}
}
fn walk_use_tree(tree: &UseTree, prefix: &mut Vec<String>, out: &mut FileImports) {
match tree {
UseTree::Path(p) => {
prefix.push(p.ident.to_string());
walk_use_tree(&p.tree, prefix, out);
prefix.pop();
}
UseTree::Name(name) => {
let ident = name.ident.to_string();
let mut segments = prefix.clone();
segments.push(ident.clone());
if let Ok(path) = TypePath::new(segments) {
out.simple.insert(ident, path);
}
}
UseTree::Rename(rename) => {
let canonical_ident = rename.ident.to_string();
let local_ident = rename.rename.to_string();
let mut segments = prefix.clone();
segments.push(canonical_ident);
if let Ok(path) = TypePath::new(segments) {
out.simple.insert(local_ident, path);
}
}
UseTree::Glob(_) => {
if !prefix.is_empty()
&& let Ok(path) = TypePath::new(prefix.clone())
{
out.globs.insert(path);
}
}
UseTree::Group(group) => {
for inner in &group.items {
walk_use_tree(inner, prefix, out);
}
}
}
}
fn path_segments(path: &Path) -> Vec<String> {
path.segments.iter().map(|seg| seg.ident.to_string()).collect()
}
pub(crate) fn canonicalize(
path: &Path,
imports: &FileImports,
referenced_by: &TypePath,
) -> Result<TypePath, EmitError> {
let mut segments = path_segments(path);
if segments.is_empty() {
return Err(EmitError::UnresolvedReference {
name: "<empty path>".to_string(),
referenced_by: referenced_by.clone(),
});
}
if segments.len() > 1 {
if segments.first().map(String::as_str) == Some("crate") {
segments.remove(0);
}
return TypePath::new(segments).map_err(|_| EmitError::UnresolvedReference {
name: "<empty after crate:: stripped>".to_string(),
referenced_by: referenced_by.clone(),
});
}
let ident = &segments[0];
if let Some(path) = imports.resolve_ident(ident) {
return Ok(path);
}
if !imports.globs.is_empty() {
let globs_rendered: Vec<String> =
imports.globs.iter().map(|p| format!("use {}::*;", p.segments().join("::"))).collect();
return Err(EmitError::UnresolvedReference {
name: format!(
"`{ident}` (may come from {}; qualify the reference (e.g., chrono::{ident}) or replace the glob with \
an explicit `use`)",
globs_rendered.join(", ")
),
referenced_by: referenced_by.clone(),
});
}
TypePath::new(vec![ident.clone()])
.map_err(|_| EmitError::UnresolvedReference { name: ident.clone(), referenced_by: referenced_by.clone() })
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_file(src: &str) -> syn::File {
syn::parse_str(src).expect("parse file")
}
fn rooted(segments: &[&str]) -> Vec<String> {
std::iter::once("crate".to_string()).chain(segments.iter().map(|s| (*s).to_string())).collect()
}
fn tp(segments: &[&str]) -> TypePath {
TypePath::new(rooted(segments)).expect("non-empty")
}
fn tp_in(segments: &[&str]) -> TypePath {
TypePath::new(segments.iter().map(|s| (*s).to_string()).collect()).expect("non-empty")
}
fn parse_path(src: &str) -> Path {
syn::parse_str(src).expect("parse path")
}
fn pool_from(entries: &[(&[&str], &str)]) -> BTreeMap<TypePath, syn::Item> {
entries
.iter()
.map(|(segs, src)| {
(TypePath::new(rooted(segs)).expect("non-empty"), syn::parse_str::<syn::Item>(src).expect("parse item"))
})
.collect()
}
fn with_root(
mut pool: BTreeMap<TypePath, syn::Item>,
entries: &[(&[&str], &str)],
) -> BTreeMap<TypePath, syn::Item> {
for (segs, src) in entries {
let key = TypePath::new(segs.iter().map(|s| (*s).to_string()).collect()).expect("non-empty");
pool.insert(key, syn::parse_str::<syn::Item>(src).expect("parse item"));
}
pool
}
fn imports_from(entries: &[(&[&str], &str)]) -> ModuleImports {
let mut imports = ModuleImports::default();
for (module, src) in entries {
let file = parse_file(src);
collect_module_imports(&file, &rooted(module), &mut imports);
}
imports
}
fn imports_in(entries: &[(&[&str], &str)]) -> ModuleImports {
let mut imports = ModuleImports::default();
for (module, src) in entries {
let file = parse_file(src);
let prefix: Vec<String> = module.iter().map(|s| (*s).to_string()).collect();
collect_module_imports(&file, &prefix, &mut imports);
}
imports
}
fn seg(parts: &[&str]) -> Vec<String> {
parts.iter().map(|s| (*s).to_string()).collect()
}
fn md(parts: &[&str]) -> Vec<String> {
rooted(parts)
}
#[test]
fn reference_resolves_relative_reexport_chain() {
let pool = pool_from(&[
(&["schema", "vault", "VaultConfig"], "pub struct VaultConfig { pub template: String }"),
(&["vault", "VaultConfig"], "pub struct VaultConfig { pub enabled: bool }"),
]);
let imports = imports_from(&[
(&["api", "v1", "vault"], "use crate::schema::VaultConfig;"),
(&["schema"], "pub use vault::VaultConfig;"),
]);
let r = resolve_reference(&seg(&["VaultConfig"]), &md(&["api", "v1", "vault"]), &pool, &imports);
assert_eq!(r, Resolution::Resolved(tp(&["schema", "vault", "VaultConfig"])), "got {r:?}");
}
#[test]
fn reference_without_disambiguating_use_is_ambiguous() {
let pool = pool_from(&[
(&["schema", "vault", "VaultConfig"], "pub struct VaultConfig { pub template: String }"),
(&["vault", "VaultConfig"], "pub struct VaultConfig { pub enabled: bool }"),
]);
let imports = ModuleImports::default();
let r = resolve_reference(&seg(&["VaultConfig"]), &md(&["api", "v1", "vault"]), &pool, &imports);
match r {
Resolution::Ambiguous(cands) => assert_eq!(cands.len(), 2, "got {cands:?}"),
other => panic!("expected Ambiguous, got {other:?}"),
}
}
#[test]
fn reference_through_crate_absolute_reexport_chain() {
let pool = pool_from(&[(&["core", "Foo"], "pub struct Foo { pub x: u32 }")]);
let imports = imports_from(&[(&["c"], "use crate::facade::Foo;"), (&["facade"], "pub use crate::core::Foo;")]);
let r = resolve_reference(&seg(&["Foo"]), &md(&["c"]), &pool, &imports);
assert_eq!(r, Resolution::Resolved(tp(&["core", "Foo"])), "got {r:?}");
}
#[test]
fn cross_crate_use_hits_the_sibling_key_exactly() {
let pool = with_root(
BTreeMap::new(),
&[(&["pumice_config", "ui", "ThemePreference"], "pub enum ThemePreference { Light, Dark }")],
);
let imports = imports_from(&[(&["schema", "settings"], "use pumice_config::ThemePreference;")]);
let r = resolve_reference(&seg(&["ThemePreference"]), &md(&["schema", "settings"]), &pool, &imports);
assert_eq!(r, Resolution::Resolved(tp_in(&["pumice_config", "ui", "ThemePreference"])), "got {r:?}");
}
#[test]
fn cross_crate_qualified_path_resolves_without_a_use() {
let pool =
with_root(BTreeMap::new(), &[(&["vaultpolish_core", "lint", "Severity"], "pub enum Severity { Error }")]);
let r = resolve_reference(
&seg(&["vaultpolish_core", "lint", "Severity"]),
&md(&["api", "v1", "scan"]),
&pool,
&ModuleImports::default(),
);
assert_eq!(r, Resolution::Resolved(tp_in(&["vaultpolish_core", "lint", "Severity"])), "got {r:?}");
}
#[test]
fn bare_ident_colliding_across_roots_takes_the_local_one() {
let pool = with_root(
pool_from(&[(&["schema", "scan", "Severity"], "pub enum Severity { Error, Warning, Info }")]),
&[(&["vaultpolish_core", "lint", "Severity"], "pub enum Severity { Error }")],
);
let r = resolve_reference(&seg(&["Severity"]), &md(&["api", "v1", "scan"]), &pool, &ModuleImports::default());
assert_eq!(r, Resolution::Resolved(tp(&["schema", "scan", "Severity"])), "got {r:?}");
}
#[test]
fn a_sibling_referencing_itself_stays_in_its_own_crate() {
let pool = with_root(
pool_from(&[(&["lint", "Severity"], "pub enum Severity { Local }")]),
&[(&["vaultpolish_core", "lint", "Severity"], "pub enum Severity { Sibling }")],
);
let r = resolve_reference(
&seg(&["crate", "lint", "Severity"]),
&["vaultpolish_core".to_string(), "scan".to_string()],
&pool,
&ModuleImports::default(),
);
assert_eq!(r, Resolution::Resolved(tp_in(&["vaultpolish_core", "lint", "Severity"])), "got {r:?}");
let r =
resolve_reference(&seg(&["crate", "lint", "Severity"]), &md(&["scan"]), &pool, &ModuleImports::default());
assert_eq!(r, Resolution::Resolved(tp(&["lint", "Severity"])), "got {r:?}");
}
#[test]
fn same_root_collision_is_still_ambiguous() {
let pool = with_root(
pool_from(&[
(&["a", "Severity"], "pub enum Severity { X }"),
(&["b", "Severity"], "pub enum Severity { Y }"),
]),
&[(&["vaultpolish_core", "lint", "Severity"], "pub enum Severity { Z }")],
);
let r = resolve_reference(&seg(&["Severity"]), &md(&["api"]), &pool, &ModuleImports::default());
match r {
Resolution::Ambiguous(cands) => {
assert_eq!(cands.len(), 2, "only the two local candidates compete: {cands:?}");
assert!(cands.iter().all(|p| p.segments()[0] == "crate"), "got {cands:?}");
}
other => panic!("expected Ambiguous, got {other:?}"),
}
}
#[test]
fn external_use_with_no_pool_match_is_not_in_pool() {
let pool = pool_from(&[(&["models", "Workout"], "pub struct Workout { pub id: u64 }")]);
let imports = imports_from(&[(&["c"], "use chrono::DateTime;")]);
let r = resolve_reference(&seg(&["DateTime"]), &md(&["c"]), &pool, &imports);
assert_eq!(r, Resolution::NotInPool, "got {r:?}");
}
#[test]
fn reference_unique_terminal_without_imports_resolves() {
let pool = pool_from(&[(&["schema", "backup", "BackupManifest"], "pub struct BackupManifest { pub v: u32 }")]);
let r = resolve_reference(&seg(&["BackupManifest"]), &md(&["api"]), &pool, &ModuleImports::default());
assert_eq!(r, Resolution::Resolved(tp(&["schema", "backup", "BackupManifest"])), "got {r:?}");
}
#[test]
fn reference_qualified_crate_path_matches_exact_key() {
let pool = pool_from(&[(&["models", "Workout"], "pub struct Workout { pub id: u64 }")]);
let r =
resolve_reference(&seg(&["crate", "models", "Workout"]), &md(&["api"]), &pool, &ModuleImports::default());
assert_eq!(r, Resolution::Resolved(tp(&["models", "Workout"])), "got {r:?}");
}
#[test]
fn parse_simple_use() {
let f = parse_file("use chrono::DateTime;");
let imports = parse_imports(&f);
assert_eq!(imports.simple.get("DateTime"), Some(&tp_in(&["chrono", "DateTime"])));
}
#[test]
fn parse_use_with_rename() {
let f = parse_file("use chrono::DateTime as Moment;");
let imports = parse_imports(&f);
assert_eq!(imports.simple.get("Moment"), Some(&tp_in(&["chrono", "DateTime"])));
assert!(!imports.simple.contains_key("DateTime"));
}
#[test]
fn parse_use_with_group() {
let f = parse_file("use chrono::{DateTime, NaiveDate, NaiveTime};");
let imports = parse_imports(&f);
assert_eq!(imports.simple.get("DateTime"), Some(&tp_in(&["chrono", "DateTime"])));
assert_eq!(imports.simple.get("NaiveDate"), Some(&tp_in(&["chrono", "NaiveDate"])));
assert_eq!(imports.simple.get("NaiveTime"), Some(&tp_in(&["chrono", "NaiveTime"])));
}
#[test]
fn parse_nested_group() {
let f = parse_file("use foo::{bar::Baz, qux::{Quux, Quuux as Q}};");
let imports = parse_imports(&f);
assert_eq!(imports.simple.get("Baz"), Some(&tp_in(&["foo", "bar", "Baz"])));
assert_eq!(imports.simple.get("Quux"), Some(&tp_in(&["foo", "qux", "Quux"])));
assert_eq!(imports.simple.get("Q"), Some(&tp_in(&["foo", "qux", "Quuux"])));
}
#[test]
fn parse_glob_import() {
let f = parse_file("use chrono::*;");
let imports = parse_imports(&f);
assert!(imports.globs.contains(&tp_in(&["chrono"])));
assert!(imports.simple.is_empty());
}
#[test]
fn parse_multiple_glob_imports() {
let f = parse_file("use chrono::*; use uuid::*;");
let imports = parse_imports(&f);
assert!(imports.globs.contains(&tp_in(&["chrono"])));
assert!(imports.globs.contains(&tp_in(&["uuid"])));
}
#[test]
fn canonicalize_single_segment_via_imports() {
let f = parse_file("use chrono::DateTime;");
let imports = parse_imports(&f);
let path = parse_path("DateTime");
let resolved = canonicalize(&path, &imports, &tp_in(&["Foo"])).unwrap();
assert_eq!(resolved, tp_in(&["chrono", "DateTime"]));
}
#[test]
fn canonicalize_single_segment_via_rename() {
let f = parse_file("use chrono::DateTime as Moment;");
let imports = parse_imports(&f);
let path = parse_path("Moment");
let resolved = canonicalize(&path, &imports, &tp_in(&["Foo"])).unwrap();
assert_eq!(resolved, tp_in(&["chrono", "DateTime"]));
}
#[test]
fn canonicalize_unresolved_single_segment_falls_through() {
let f = parse_file("");
let imports = parse_imports(&f);
let path = parse_path("MyWorkout");
let resolved = canonicalize(&path, &imports, &tp_in(&["Foo"])).unwrap();
assert_eq!(resolved, tp_in(&["MyWorkout"]));
}
#[test]
fn canonicalize_unresolved_with_glob_emits_hint() {
let f = parse_file("use chrono::*;");
let imports = parse_imports(&f);
let path = parse_path("DateTime");
let err = canonicalize(&path, &imports, &tp_in(&["Foo"])).unwrap_err();
match err {
EmitError::UnresolvedReference { name, .. } => {
assert!(name.contains("DateTime"), "name was: {name}");
assert!(name.contains("chrono"), "name was: {name}");
assert!(name.contains("glob") || name.contains("qualify"), "hint missing: {name}");
}
other => panic!("expected UnresolvedReference, got {other:?}"),
}
}
#[test]
fn canonicalize_multi_segment_taken_as_qualified() {
let f = parse_file("");
let imports = parse_imports(&f);
let path = parse_path("chrono::DateTime");
let resolved = canonicalize(&path, &imports, &tp_in(&["Foo"])).unwrap();
assert_eq!(resolved, tp_in(&["chrono", "DateTime"]));
}
#[test]
fn canonicalize_strips_crate_prefix() {
let f = parse_file("");
let imports = parse_imports(&f);
let path = parse_path("crate::models::Workout");
let resolved = canonicalize(&path, &imports, &tp_in(&["Foo"])).unwrap();
assert_eq!(resolved, tp_in(&["models", "Workout"]));
}
#[test]
fn canonicalize_strips_generic_args() {
let f = parse_file("");
let imports = parse_imports(&f);
let path = parse_path("chrono::DateTime<Utc>");
let resolved = canonicalize(&path, &imports, &tp_in(&["Foo"])).unwrap();
assert_eq!(resolved, tp_in(&["chrono", "DateTime"]));
}
}