use super::local_symbols::{scope_for_local, FileScope};
use crate::adapters::analyzers::architecture::forbidden_rule::{
file_to_module_segments, resolve_to_crate_absolute, resolve_to_crate_absolute_in,
};
use crate::adapters::shared::use_tree::{AliasMap, AliasTarget, ScopedAliasMap};
use std::collections::{HashMap, HashSet};
pub(super) fn canonical_from_type(
ty: &syn::Type,
alias_map: &AliasMap,
local_symbols: &HashSet<String>,
crate_root_modules: &HashSet<String>,
importing_file: &str,
) -> Option<Vec<String>> {
let inner = strip_wrappers(ty);
match inner {
syn::Type::Path(tp) => {
let segments: Vec<String> = tp
.path
.segments
.iter()
.map(|s| s.ident.to_string())
.collect();
if tp.path.leading_colon.is_some() {
return None;
}
canonicalise_type_segments(
&segments,
alias_map,
local_symbols,
crate_root_modules,
importing_file,
)
}
_ => None,
}
}
fn strip_wrappers(ty: &syn::Type) -> &syn::Type {
match ty {
syn::Type::Reference(r) => strip_wrappers(&r.elem),
syn::Type::Paren(p) => strip_wrappers(&p.elem),
syn::Type::Path(tp) => {
let Some(last) = tp.path.segments.last() else {
return ty;
};
let name = last.ident.to_string();
if !matches!(name.as_str(), "Box" | "Arc" | "Rc" | "Cow") {
return ty;
}
let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
return ty;
};
for arg in &args.args {
if let syn::GenericArgument::Type(inner) = arg {
return strip_wrappers(inner);
}
}
ty
}
_ => ty,
}
}
pub(crate) struct CanonScope<'a> {
pub file: &'a FileScope<'a>,
pub mod_stack: &'a [String],
pub reexports: Option<&'a super::reexports::ReexportMap>,
}
pub(super) fn canonicalise_type_segments(
segments: &[String],
alias_map: &AliasMap,
local_symbols: &HashSet<String>,
crate_root_modules: &HashSet<String>,
importing_file: &str,
) -> Option<Vec<String>> {
let empty_scoped = ScopedAliasMap::new();
let empty_decls = HashMap::new();
let file = FileScope {
path: importing_file,
alias_map,
aliases_per_scope: &empty_scoped,
local_symbols,
local_decl_scopes: &empty_decls,
crate_root_modules,
workspace_module_paths: None,
};
canonicalise_type_segments_in_scope(
segments,
&CanonScope {
file: &file,
mod_stack: &[],
reexports: None,
},
)
}
pub(crate) fn canonicalise_workspace_path(
segments: &[String],
leading_colon_set: bool,
scope: &CanonScope<'_>,
) -> Option<Vec<String>> {
if leading_colon_set {
return None;
}
let resolved = canonicalise_type_segments_in_scope(segments, scope)?;
Some(super::reexports::apply_reexport_substitution(
resolved,
scope.reexports,
))
}
pub(crate) fn canonicalise_type_segments_in_scope(
segments: &[String],
scope: &CanonScope<'_>,
) -> Option<Vec<String>> {
if segments.is_empty() {
return None;
}
let file = scope.file;
if matches!(segments[0].as_str(), "crate" | "self" | "super") {
let resolved = resolve_to_crate_absolute_in(file.path, scope.mod_stack, segments)?;
let mut full = vec!["crate".to_string()];
full.extend(resolved);
return Some(full);
}
if let Some(alias) = lookup_alias(scope, &segments[0]) {
let mut full = alias.segments.to_vec();
full.extend_from_slice(&segments[1..]);
return normalize_after_alias(full, alias.absolute_root, scope);
}
if file.local_symbols.contains(&segments[0]) {
if let Some(mod_path) =
scope_for_local(file.local_decl_scopes, &segments[0], scope.mod_stack)
{
let mut full = vec!["crate".to_string()];
full.extend(file_to_module_segments(file.path));
full.extend(mod_path.iter().cloned());
full.extend_from_slice(segments);
return Some(full);
}
}
if file.crate_root_modules.contains(&segments[0]) {
let mut full = vec!["crate".to_string()];
full.extend_from_slice(segments);
return Some(full);
}
None
}
fn lookup_alias<'a>(scope: &'a CanonScope<'a>, name: &str) -> Option<&'a AliasTarget> {
if let Some(map) = scope.file.aliases_per_scope.get(scope.mod_stack) {
return map.get(name);
}
scope.file.alias_map.get(name)
}
fn normalize_after_alias(
expanded: Vec<String>,
absolute_root: bool,
scope: &CanonScope<'_>,
) -> Option<Vec<String>> {
if absolute_root {
return Some(expanded);
}
let file = scope.file;
let mod_stack = scope.mod_stack;
match expanded.first().map(|s| s.as_str()) {
Some("self") | Some("super") => {
let resolved = resolve_to_crate_absolute_in(file.path, mod_stack, &expanded)?;
let mut full = vec!["crate".to_string()];
full.extend(resolved);
Some(full)
}
Some("crate") => Some(expanded),
Some(first)
if is_workspace_submodule(file.workspace_module_paths, file.path, mod_stack, first) =>
{
let mut with_self = vec!["self".to_string()];
with_self.extend(expanded);
let resolved = resolve_to_crate_absolute_in(file.path, mod_stack, &with_self)?;
let mut full = vec!["crate".to_string()];
full.extend(resolved);
Some(full)
}
Some(first) if file.crate_root_modules.contains(first) => {
let mut full = vec!["crate".to_string()];
full.extend(expanded);
Some(full)
}
_ => Some(expanded),
}
}
fn is_workspace_submodule(
workspace_module_paths: Option<&HashSet<Vec<String>>>,
importing_file: &str,
mod_stack: &[String],
first: &str,
) -> bool {
let Some(paths) = workspace_module_paths else {
return false;
};
let mut candidate =
crate::adapters::analyzers::architecture::forbidden_rule::file_to_module_segments(
importing_file,
);
candidate.extend_from_slice(mod_stack);
candidate.push(first.to_string());
paths.contains(&candidate)
}
pub(super) fn normalize_alias_expansion(
expanded: Vec<String>,
absolute_root: bool,
scope: &CanonScope<'_>,
) -> Option<Vec<String>> {
normalize_after_alias(expanded, absolute_root, scope)
}
pub(super) fn extract_let_binding(
local: &syn::Local,
alias_map: &AliasMap,
local_symbols: &HashSet<String>,
crate_root_modules: &HashSet<String>,
importing_file: &str,
) -> Option<(String, Vec<String>)> {
let (name, annotated_ty) = extract_pat_name_and_type(&local.pat)?;
if let Some(ty) = annotated_ty {
if let Some(canonical) = canonical_from_type(
ty,
alias_map,
local_symbols,
crate_root_modules,
importing_file,
) {
return Some((name, canonical));
}
}
let init = local.init.as_ref()?;
let canonical = binding_type_from_init(
&init.expr,
alias_map,
local_symbols,
crate_root_modules,
importing_file,
)?;
Some((name, canonical))
}
fn extract_pat_name_and_type(pat: &syn::Pat) -> Option<(String, Option<&syn::Type>)> {
match pat {
syn::Pat::Ident(pi) => Some((pi.ident.to_string(), None)),
syn::Pat::Type(pt) => {
if let syn::Pat::Ident(pi) = pt.pat.as_ref() {
Some((pi.ident.to_string(), Some(pt.ty.as_ref())))
} else {
None
}
}
_ => None,
}
}
fn binding_type_from_init(
expr: &syn::Expr,
alias_map: &AliasMap,
local_symbols: &HashSet<String>,
crate_root_modules: &HashSet<String>,
importing_file: &str,
) -> Option<Vec<String>> {
let mut cur = expr;
loop {
match cur {
syn::Expr::Try(t) => cur = &t.expr,
syn::Expr::Await(a) => cur = &a.base,
syn::Expr::Paren(p) => cur = &p.expr,
_ => break,
}
}
let call = match cur {
syn::Expr::Call(c) => c,
_ => return None,
};
let path = match call.func.as_ref() {
syn::Expr::Path(p) => &p.path,
_ => return None,
};
let segments: Vec<String> = path.segments.iter().map(|s| s.ident.to_string()).collect();
if segments.len() < 2 {
return None;
}
let type_segments = &segments[..segments.len() - 1];
if path.leading_colon.is_some() {
return None;
}
canonicalise_type_segments(
type_segments,
alias_map,
local_symbols,
crate_root_modules,
importing_file,
)
}