use ahash::AHashSet;
use crate::extract::type_resolver::ResultAliasScope;
pub(crate) fn resolve_result_alias_scope(items: &[syn::Item], module_path: &str) -> Option<ResultAliasScope> {
let local_modules = collect_local_module_names(items);
let mut prefix: Vec<String> = Vec::new();
items.iter().find_map(|item| match item {
syn::Item::Use(item_use) => scope_from_tree(&item_use.tree, module_path, &local_modules, &mut prefix),
_ => None,
})
}
fn collect_local_module_names(items: &[syn::Item]) -> AHashSet<String> {
items
.iter()
.filter_map(|item| match item {
syn::Item::Mod(item_mod) => Some(item_mod.ident.to_string()),
_ => None,
})
.collect()
}
fn scope_from_tree(
tree: &syn::UseTree,
module_path: &str,
local_modules: &AHashSet<String>,
prefix: &mut Vec<String>,
) -> Option<ResultAliasScope> {
match tree {
syn::UseTree::Path(use_path) => {
prefix.push(use_path.ident.to_string());
let found = scope_from_tree(&use_path.tree, module_path, local_modules, prefix);
prefix.pop();
found
}
syn::UseTree::Group(group) => group
.items
.iter()
.find_map(|item| scope_from_tree(item, module_path, local_modules, prefix)),
syn::UseTree::Name(use_name) if use_name.ident == "Result" => {
Some(scope_for_prefix(prefix, module_path, local_modules))
}
syn::UseTree::Rename(use_rename) if use_rename.rename == "Result" => {
Some(scope_for_prefix(prefix, module_path, local_modules))
}
_ => None,
}
}
fn scope_for_prefix(prefix: &[String], module_path: &str, local_modules: &AHashSet<String>) -> ResultAliasScope {
let segments: Vec<&str> = prefix.iter().map(String::as_str).collect();
let mut index = 0;
let mut base: Vec<&str> = Vec::new();
match segments.first().copied() {
Some("crate") => index = 1,
Some("self") => {
index = 1;
base = split_module_path(module_path);
}
Some("super") => {
base = split_module_path(module_path);
while segments.get(index).copied() == Some("super") {
index += 1;
base.pop();
}
}
Some(first) if local_modules.contains(first) => base = split_module_path(module_path),
_ => return ResultAliasScope::Foreign,
}
base.extend(segments[index..].iter().copied());
ResultAliasScope::Crate(base.join("::"))
}
fn split_module_path(module_path: &str) -> Vec<&str> {
if module_path.is_empty() {
Vec::new()
} else {
module_path.split("::").collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn items(source: &str) -> Vec<syn::Item> {
syn::parse_str::<syn::File>(source)
.expect("test source must parse")
.items
}
#[test]
fn crate_root_import_resolves_to_the_root_module() {
let parsed = items("use crate::Result;");
assert_eq!(
resolve_result_alias_scope(&parsed, "plugins::embedding"),
Some(ResultAliasScope::Crate(String::new()))
);
}
#[test]
fn qualified_crate_import_resolves_to_the_declaring_module() {
let parsed = items("use crate::error::Result;");
assert_eq!(
resolve_result_alias_scope(&parsed, "plugins"),
Some(ResultAliasScope::Crate("error".to_string()))
);
}
#[test]
fn super_import_resolves_against_the_parent_module() {
let parsed = items("use super::error::Result;");
assert_eq!(
resolve_result_alias_scope(&parsed, "extraction::binary::model"),
Some(ResultAliasScope::Crate("extraction::binary::error".to_string()))
);
}
#[test]
fn grouped_super_import_resolves_to_the_parent_module() {
let parsed = items("use super::{ConversionResult, Result};");
assert_eq!(
resolve_result_alias_scope(&parsed, "convert_api"),
Some(ResultAliasScope::Crate(String::new()))
);
}
#[test]
fn uniform_path_into_a_locally_declared_module_is_crate_local() {
let parsed = items("pub mod error;\npub use error::{Result, SampleCrateError};");
assert_eq!(
resolve_result_alias_scope(&parsed, ""),
Some(ResultAliasScope::Crate("error".to_string()))
);
}
#[test]
fn foreign_crate_import_is_not_a_crate_local_alias() {
let parsed = items("use anyhow::Result;");
assert_eq!(
resolve_result_alias_scope(&parsed, "plugins"),
Some(ResultAliasScope::Foreign)
);
}
#[test]
fn renamed_foreign_import_does_not_bring_result_into_scope() {
let parsed = items("use anyhow::Result as AnyResult;");
assert_eq!(resolve_result_alias_scope(&parsed, "plugins"), None);
}
#[test]
fn module_without_a_result_import_has_no_scope() {
let parsed = items("use std::sync::Arc;");
assert_eq!(resolve_result_alias_scope(&parsed, "plugins"), None);
}
}