use std::collections::{BTreeMap, BTreeSet};
use ucm_core::BlockId;
use crate::model::*;
use super::languages::rust::rust_last_path_segment;
use super::{ascend_directory, normalize_relative_join, parent_directory};
pub(super) fn resolve_relationship_target_ids(
source_file: &str,
language: CodeLanguage,
relationship: &ExtractedRelationship,
top_level_symbol_ids: &BTreeMap<(String, String), Vec<BlockId>>,
imported_symbol_targets_by_file: &BTreeMap<String, BTreeMap<String, Vec<BlockId>>>,
known_files: &BTreeSet<String>,
) -> Vec<BlockId> {
let mut target_ids = Vec::new();
if let Some(local_ids) =
top_level_symbol_ids.get(&(source_file.to_string(), relationship.target_name.clone()))
{
target_ids.extend(local_ids.iter().copied());
}
if let Some(imported_ids) = imported_symbol_targets_by_file
.get(source_file)
.and_then(|bindings| bindings.get(&relationship.target_name))
{
target_ids.extend(imported_ids.iter().copied());
}
if language == CodeLanguage::Rust && relationship.target_expr.contains("::") {
if let ImportResolution::Resolved(target_file) = resolve_import(
source_file,
&language,
&relationship.target_expr,
known_files,
) {
if let Some(name) = rust_last_path_segment(&relationship.target_expr) {
if let Some(ids) = top_level_symbol_ids.get(&(target_file, name)) {
target_ids.extend(ids.iter().copied());
}
}
}
}
let mut unique_ids = Vec::new();
for target_id in target_ids {
if !unique_ids.contains(&target_id) {
unique_ids.push(target_id);
}
}
unique_ids
}
#[allow(clippy::too_many_arguments)]
pub(super) fn resolve_alias_target_ids(
source_file: &str,
language: CodeLanguage,
alias: &ExtractedAlias,
top_level_symbol_ids: &BTreeMap<(String, String), Vec<BlockId>>,
exported_symbol_targets_by_file: &BTreeMap<String, BTreeMap<String, Vec<BlockId>>>,
imported_symbol_targets_by_file: &BTreeMap<String, BTreeMap<String, Vec<BlockId>>>,
imported_module_targets_by_file: &BTreeMap<String, BTreeMap<String, Vec<String>>>,
imported_module_paths_by_file: &BTreeMap<String, BTreeMap<String, Vec<String>>>,
alias_names_by_scope: &BTreeMap<(String, String), BTreeSet<String>>,
aliased_symbol_targets_by_scope: &BTreeMap<(String, String), BTreeMap<String, Vec<BlockId>>>,
known_files: &BTreeSet<String>,
) -> Vec<BlockId> {
let mut target_ids = Vec::new();
if alias.target_expr == alias.target_name {
let local_scope_key = alias_scope_key(alias.owner_identity.as_deref());
if let Some(alias_ids) = aliased_symbol_targets_by_scope
.get(&(source_file.to_string(), local_scope_key.clone()))
.and_then(|aliases| aliases.get(&alias.target_name))
{
return alias_ids.clone();
}
if !local_scope_key.is_empty()
&& alias_names_by_scope
.get(&(source_file.to_string(), local_scope_key))
.is_some_and(|aliases| aliases.contains(&alias.target_name))
{
return Vec::new();
}
let top_scope_key = alias_scope_key(None);
if let Some(alias_ids) = aliased_symbol_targets_by_scope
.get(&(source_file.to_string(), top_scope_key.clone()))
.and_then(|aliases| aliases.get(&alias.target_name))
{
return alias_ids.clone();
}
if alias_names_by_scope
.get(&(source_file.to_string(), top_scope_key))
.is_some_and(|aliases| aliases.contains(&alias.target_name))
{
return Vec::new();
}
}
if let Some(local_ids) =
top_level_symbol_ids.get(&(source_file.to_string(), alias.target_name.clone()))
{
target_ids.extend(local_ids.iter().copied());
}
if let Some(imported_ids) = imported_symbol_targets_by_file
.get(source_file)
.and_then(|bindings| bindings.get(&alias.target_name))
{
target_ids.extend(imported_ids.iter().copied());
}
if let Some((module_alias, member_name)) = member_usage_parts(&alias.target_expr) {
if let Some(target_files) = imported_module_targets_by_file
.get(source_file)
.and_then(|aliases| aliases.get(&module_alias))
{
for target_file in target_files {
if let Some(ids) = exported_symbol_targets_by_file
.get(target_file)
.and_then(|exports| exports.get(&member_name))
{
target_ids.extend(ids.iter().copied());
}
}
}
}
if language == CodeLanguage::Rust && alias.target_expr.contains("::") {
if let Some((module_alias, remainder)) = rust_alias_path_parts(&alias.target_expr) {
if let Some(module_paths) = imported_module_paths_by_file
.get(source_file)
.and_then(|aliases| aliases.get(&module_alias))
{
for module_path in module_paths {
let expanded_expr = format!("{module_path}::{remainder}");
if let ImportResolution::Resolved(target_file) =
resolve_import(source_file, &language, &expanded_expr, known_files)
{
if let Some(name) = rust_last_path_segment(&expanded_expr) {
if let Some(ids) = top_level_symbol_ids.get(&(target_file, name)) {
target_ids.extend(ids.iter().copied());
}
}
}
}
}
}
if let ImportResolution::Resolved(target_file) =
resolve_import(source_file, &language, &alias.target_expr, known_files)
{
if let Some(name) = rust_last_path_segment(&alias.target_expr) {
if let Some(ids) = top_level_symbol_ids.get(&(target_file, name)) {
target_ids.extend(ids.iter().copied());
}
}
}
}
let mut unique_ids = Vec::new();
for target_id in target_ids {
if !unique_ids.contains(&target_id) {
unique_ids.push(target_id);
}
}
unique_ids
}
#[allow(clippy::too_many_arguments)]
pub(super) fn resolve_usage_target_ids(
source_file: &str,
language: CodeLanguage,
usage: &ExtractedUsage,
top_level_symbol_ids: &BTreeMap<(String, String), Vec<BlockId>>,
exported_symbol_targets_by_file: &BTreeMap<String, BTreeMap<String, Vec<BlockId>>>,
imported_symbol_targets_by_file: &BTreeMap<String, BTreeMap<String, Vec<BlockId>>>,
imported_module_targets_by_file: &BTreeMap<String, BTreeMap<String, Vec<String>>>,
imported_module_paths_by_file: &BTreeMap<String, BTreeMap<String, Vec<String>>>,
alias_names_by_scope: &BTreeMap<(String, String), BTreeSet<String>>,
alias_records_by_scope: &BTreeMap<(String, String), BTreeMap<String, Vec<ExtractedAlias>>>,
aliased_symbol_targets_by_scope: &BTreeMap<(String, String), BTreeMap<String, Vec<BlockId>>>,
known_files: &BTreeSet<String>,
) -> Vec<BlockId> {
if usage.target_expr == usage.target_name {
let local_scope_key = alias_scope_key(Some(&usage.source_identity));
if let Some(alias_ids) = aliased_symbol_targets_by_scope
.get(&(source_file.to_string(), local_scope_key.clone()))
.and_then(|aliases| aliases.get(&usage.target_name))
{
return alias_ids.clone();
}
if alias_names_by_scope
.get(&(source_file.to_string(), local_scope_key.clone()))
.is_some_and(|aliases| aliases.contains(&usage.target_name))
{
return Vec::new();
}
let top_scope_key = alias_scope_key(None);
if let Some(alias_ids) = aliased_symbol_targets_by_scope
.get(&(source_file.to_string(), top_scope_key.clone()))
.and_then(|aliases| aliases.get(&usage.target_name))
{
return alias_ids.clone();
}
if alias_names_by_scope
.get(&(source_file.to_string(), top_scope_key))
.is_some_and(|aliases| aliases.contains(&usage.target_name))
{
return Vec::new();
}
}
let mut target_ids = Vec::new();
if let Some(local_ids) =
top_level_symbol_ids.get(&(source_file.to_string(), usage.target_name.clone()))
{
target_ids.extend(local_ids.iter().copied());
}
if let Some(imported_ids) = imported_symbol_targets_by_file
.get(source_file)
.and_then(|bindings| bindings.get(&usage.target_name))
{
target_ids.extend(imported_ids.iter().copied());
}
if let Some((module_alias, member_name)) = member_usage_parts(&usage.target_expr) {
let target_files = resolve_module_alias_target_files(
source_file,
Some(&usage.source_identity),
&module_alias,
imported_module_targets_by_file,
alias_names_by_scope,
alias_records_by_scope,
);
for target_file in target_files {
if let Some(ids) = exported_symbol_targets_by_file
.get(&target_file)
.and_then(|exports| exports.get(&member_name))
{
target_ids.extend(ids.iter().copied());
}
}
}
if language == CodeLanguage::Rust && usage.target_expr.contains("::") {
if let Some((module_alias, remainder)) = rust_alias_path_parts(&usage.target_expr) {
if let Some(module_paths) = imported_module_paths_by_file
.get(source_file)
.and_then(|aliases| aliases.get(&module_alias))
{
for module_path in module_paths {
let expanded_expr = format!("{module_path}::{remainder}");
if let ImportResolution::Resolved(target_file) =
resolve_import(source_file, &language, &expanded_expr, known_files)
{
if let Some(name) = rust_last_path_segment(&expanded_expr) {
if let Some(ids) = top_level_symbol_ids.get(&(target_file, name)) {
target_ids.extend(ids.iter().copied());
}
}
}
}
}
}
if let ImportResolution::Resolved(target_file) =
resolve_import(source_file, &language, &usage.target_expr, known_files)
{
if let Some(name) = rust_last_path_segment(&usage.target_expr) {
if let Some(ids) = top_level_symbol_ids.get(&(target_file, name)) {
target_ids.extend(ids.iter().copied());
}
}
}
}
let mut unique_ids = Vec::new();
for target_id in target_ids {
if !unique_ids.contains(&target_id) {
unique_ids.push(target_id);
}
}
unique_ids
}
pub(super) fn member_usage_parts(text: &str) -> Option<(String, String)> {
let (left, right) = text.split_once('.')?;
let left = left.trim();
let right = right.trim();
if left.is_empty() || right.is_empty() {
None
} else {
Some((left.to_string(), right.to_string()))
}
}
pub(super) fn resolve_module_alias_target_files(
source_file: &str,
owner_identity: Option<&str>,
alias_name: &str,
imported_module_targets_by_file: &BTreeMap<String, BTreeMap<String, Vec<String>>>,
alias_names_by_scope: &BTreeMap<(String, String), BTreeSet<String>>,
alias_records_by_scope: &BTreeMap<(String, String), BTreeMap<String, Vec<ExtractedAlias>>>,
) -> Vec<String> {
let mut visited = BTreeSet::new();
resolve_module_alias_target_files_recursive(
source_file,
owner_identity,
alias_name,
imported_module_targets_by_file,
alias_names_by_scope,
alias_records_by_scope,
&mut visited,
)
}
pub(super) fn resolve_module_alias_target_files_recursive(
source_file: &str,
owner_identity: Option<&str>,
alias_name: &str,
imported_module_targets_by_file: &BTreeMap<String, BTreeMap<String, Vec<String>>>,
alias_names_by_scope: &BTreeMap<(String, String), BTreeSet<String>>,
alias_records_by_scope: &BTreeMap<(String, String), BTreeMap<String, Vec<ExtractedAlias>>>,
visited: &mut BTreeSet<(String, String, String)>,
) -> Vec<String> {
let visit_key = (
source_file.to_string(),
alias_scope_key(owner_identity),
alias_name.to_string(),
);
if !visited.insert(visit_key) {
return Vec::new();
}
if let Some(target_files) = imported_module_targets_by_file
.get(source_file)
.and_then(|aliases| aliases.get(alias_name))
{
return target_files.clone();
}
for scope_key in [alias_scope_key(owner_identity), alias_scope_key(None)] {
let scope_identity = (source_file.to_string(), scope_key.clone());
if let Some(alias_entries) = alias_records_by_scope
.get(&scope_identity)
.and_then(|aliases| aliases.get(alias_name))
{
if alias_entries.len() != 1 {
return Vec::new();
}
let alias = &alias_entries[0];
if alias.target_expr != alias.target_name || alias.target_expr.is_empty() {
return Vec::new();
}
return resolve_module_alias_target_files_recursive(
source_file,
alias.owner_identity.as_deref(),
&alias.target_name,
imported_module_targets_by_file,
alias_names_by_scope,
alias_records_by_scope,
visited,
);
}
if alias_names_by_scope
.get(&scope_identity)
.is_some_and(|aliases| aliases.contains(alias_name))
{
return Vec::new();
}
if scope_key.is_empty() {
break;
}
}
Vec::new()
}
pub(super) fn rust_alias_path_parts(text: &str) -> Option<(String, String)> {
let (left, right) = text.split_once("::")?;
let left = left.trim();
let right = right.trim();
if left.is_empty() || right.is_empty() {
None
} else {
Some((left.to_string(), right.to_string()))
}
}
pub(super) fn alias_scope_key(owner_identity: Option<&str>) -> String {
owner_identity.unwrap_or("").to_string()
}
pub(super) fn resolve_import(
source_file: &str,
language: &CodeLanguage,
module: &str,
known_files: &BTreeSet<String>,
) -> ImportResolution {
match language {
CodeLanguage::Rust => resolve_rust_import(source_file, module, known_files),
CodeLanguage::Python => resolve_python_import(source_file, module, known_files),
CodeLanguage::TypeScript | CodeLanguage::JavaScript => {
resolve_ts_import(source_file, module, known_files)
}
}
}
pub(super) fn resolve_ts_import(
source_file: &str,
module: &str,
known_files: &BTreeSet<String>,
) -> ImportResolution {
if !module.starts_with('.') {
return ImportResolution::External;
}
let source_dir = parent_directory(source_file);
let joined = normalize_relative_join(&source_dir, module);
find_known_candidate(ts_candidates(&joined), known_files)
.map(ImportResolution::Resolved)
.unwrap_or(ImportResolution::Unresolved)
}
pub(super) fn ts_candidates(base: &str) -> Vec<String> {
let exts = ["ts", "tsx", "js", "jsx"];
let mut out = Vec::new();
if has_known_extension(base, &exts) {
out.push(base.to_string());
} else {
for ext in exts {
out.push(format!("{}.{}", base, ext));
}
for ext in exts {
out.push(format!("{}/index.{}", base, ext));
}
}
out
}
pub(super) fn resolve_python_import(
source_file: &str,
module: &str,
known_files: &BTreeSet<String>,
) -> ImportResolution {
let source_dir = parent_directory(source_file);
let mut dots = 0usize;
for ch in module.chars() {
if ch == '.' {
dots += 1;
} else {
break;
}
}
let module_tail = module.trim_start_matches('.');
let base_dir = if dots > 0 {
ascend_directory(&source_dir, dots.saturating_sub(1))
} else {
String::new()
};
let module_path = module_tail.replace('.', "/");
let joined = if base_dir.is_empty() {
module_path
} else if module_path.is_empty() {
base_dir
} else {
format!("{}/{}", base_dir, module_path)
};
match find_known_candidate(py_candidates(&joined), known_files) {
Some(candidate) => ImportResolution::Resolved(candidate),
None if dots == 0 => ImportResolution::External,
None => ImportResolution::Unresolved,
}
}
pub(super) fn py_candidates(base: &str) -> Vec<String> {
if base.is_empty() {
return Vec::new();
}
if base.ends_with(".py") {
return vec![base.to_string()];
}
vec![format!("{}.py", base), format!("{}/__init__.py", base)]
}
pub(super) fn resolve_rust_import(
source_file: &str,
module: &str,
known_files: &BTreeSet<String>,
) -> ImportResolution {
if module.starts_with("std::") || module.starts_with("core::") || module.starts_with("alloc::")
{
return ImportResolution::External;
}
if let Some(name) = module.strip_prefix("mod:") {
let source_dir = parent_directory(source_file);
let local = normalize_relative_join(&source_dir, name);
return find_known_candidate(
[format!("{}.rs", local), format!("{}/mod.rs", local)],
known_files,
)
.map(ImportResolution::Resolved)
.unwrap_or(ImportResolution::Unresolved);
}
let source_dir = parent_directory(source_file);
let crate_root = rust_module_root(source_file);
let explicitly_local = module.starts_with("crate::")
|| module.starts_with("self::")
|| module.starts_with("super::");
let (base_dir, path_segments) = if let Some(rest) = module.strip_prefix("crate::") {
(
crate_root.clone(),
rest.split("::").map(|s| s.to_string()).collect::<Vec<_>>(),
)
} else if let Some(rest) = module.strip_prefix("self::") {
(
source_dir.clone(),
rest.split("::").map(|s| s.to_string()).collect::<Vec<_>>(),
)
} else if module.starts_with("super::") {
let mut rest = module;
let mut super_count = 0usize;
while let Some(next) = rest.strip_prefix("super::") {
super_count += 1;
rest = next;
}
(
ascend_directory(&source_dir, super_count),
rest.split("::").map(|s| s.to_string()).collect::<Vec<_>>(),
)
} else {
(
crate_root.clone(),
module
.split("::")
.map(|s| s.to_string())
.collect::<Vec<_>>(),
)
};
if let Some(candidate) = find_known_candidate(
(1..=path_segments.len()).rev().flat_map(|trimmed| {
let joined = path_segments[..trimmed].join("/");
if joined.is_empty() {
return Vec::new();
}
let candidate_base = if base_dir.is_empty() {
joined
} else {
format!("{}/{}", base_dir, joined)
};
vec![
format!("{}.rs", candidate_base),
format!("{}/mod.rs", candidate_base),
]
}),
known_files,
) {
return ImportResolution::Resolved(candidate);
}
if module.starts_with("crate::") && path_segments.len() == 1 {
if let Some(entry_file) = rust_crate_entry_file(&crate_root, known_files) {
return ImportResolution::Resolved(entry_file);
}
}
if explicitly_local {
return ImportResolution::Unresolved;
}
let first_segment = path_segments
.first()
.map(|s| s.as_str())
.unwrap_or_default();
if rust_root_module_exists(&crate_root, first_segment, known_files) {
ImportResolution::Unresolved
} else {
ImportResolution::External
}
}
pub(super) fn find_known_candidate<I>(
candidates: I,
known_files: &BTreeSet<String>,
) -> Option<String>
where
I: IntoIterator<Item = String>,
{
candidates
.into_iter()
.find(|candidate| known_files.contains(candidate))
}
pub(super) fn rust_crate_entry_file(
crate_root: &str,
known_files: &BTreeSet<String>,
) -> Option<String> {
find_known_candidate(
[
format!("{}/lib.rs", crate_root),
format!("{}/main.rs", crate_root),
format!("{}/mod.rs", crate_root),
],
known_files,
)
}
pub(super) fn rust_module_root(source_file: &str) -> String {
let parts: Vec<&str> = source_file.split('/').collect();
if let Some((index, _)) = parts.iter().enumerate().rfind(|(_, part)| **part == "src") {
return parts[..=index].join("/");
}
parent_directory(source_file)
}
pub(super) fn rust_root_module_exists(
crate_root: &str,
segment: &str,
known_files: &BTreeSet<String>,
) -> bool {
if segment.is_empty() {
return false;
}
[
format!("{}/{}.rs", crate_root, segment),
format!("{}/{}/mod.rs", crate_root, segment),
]
.into_iter()
.any(|candidate| known_files.contains(&candidate))
}
#[cfg(test)]
pub(super) fn expand_rust_use_declaration(text: &str) -> Vec<String> {
expand_rust_use_declaration_items(text)
.into_iter()
.map(|(module, _, _)| module)
.collect()
}
pub(super) fn expand_rust_use_declaration_items(text: &str) -> Vec<(String, Option<String>, bool)> {
let trimmed = text.trim();
let Some(use_index) = trimmed.find("use ") else {
return Vec::new();
};
let expr = trimmed[use_index + 4..].trim().trim_end_matches(';').trim();
expand_rust_use_tree_items("", expr)
}
pub(super) fn expand_rust_use_tree_items(
prefix: &str,
expr: &str,
) -> Vec<(String, Option<String>, bool)> {
let trimmed = expr.trim();
if trimmed.is_empty() {
return Vec::new();
}
if trimmed.starts_with('{') && trimmed.ends_with('}') {
return split_top_level_commas(&trimmed[1..trimmed.len() - 1])
.into_iter()
.flat_map(|part| expand_rust_use_tree_items(prefix, &part))
.collect();
}
if let Some(open_idx) = find_top_level_char(trimmed, '{') {
let prefix_part = trimmed[..open_idx].trim().trim_end_matches("::");
let close_idx = matching_brace_index(trimmed, open_idx).unwrap_or(trimmed.len() - 1);
let inner = &trimmed[open_idx + 1..close_idx];
let combined_prefix = join_rust_use_prefix(prefix, prefix_part);
return split_top_level_commas(inner)
.into_iter()
.flat_map(|part| expand_rust_use_tree_items(&combined_prefix, &part))
.collect();
}
let wildcard = trimmed == "*" || trimmed.ends_with("::*");
let (raw_segment, local_alias) = trimmed
.rsplit_once(" as ")
.map(|(left, right)| (left.trim(), Some(right.trim().to_string())))
.unwrap_or((trimmed, None));
let segment = raw_segment
.trim_end_matches("::*")
.trim_start_matches("::")
.trim();
if segment.is_empty() {
return Vec::new();
}
if segment == "self" || segment == "*" {
return if prefix.is_empty() {
Vec::new()
} else {
vec![(prefix.to_string(), local_alias, wildcard || segment == "*")]
};
}
vec![(join_rust_use_prefix(prefix, segment), local_alias, wildcard)]
}
pub(super) fn split_top_level_commas(input: &str) -> Vec<String> {
let mut out = Vec::new();
let mut depth = 0usize;
let mut start = 0usize;
for (index, ch) in input.char_indices() {
match ch {
'{' => depth += 1,
'}' => depth = depth.saturating_sub(1),
',' if depth == 0 => {
let part = input[start..index].trim();
if !part.is_empty() {
out.push(part.to_string());
}
start = index + ch.len_utf8();
}
_ => {}
}
}
let tail = input[start..].trim();
if !tail.is_empty() {
out.push(tail.to_string());
}
out
}
pub(super) fn find_top_level_char(input: &str, needle: char) -> Option<usize> {
let mut depth = 0usize;
for (index, ch) in input.char_indices() {
match ch {
'{' if ch == needle && depth == 0 => return Some(index),
'{' => depth += 1,
'}' => depth = depth.saturating_sub(1),
_ => {}
}
}
None
}
pub(super) fn matching_brace_index(input: &str, open_idx: usize) -> Option<usize> {
let mut depth = 0usize;
for (index, ch) in input.char_indices().skip_while(|(idx, _)| *idx < open_idx) {
match ch {
'{' => depth += 1,
'}' => {
depth = depth.saturating_sub(1);
if depth == 0 {
return Some(index);
}
}
_ => {}
}
}
None
}
pub(super) fn join_rust_use_prefix(prefix: &str, segment: &str) -> String {
let clean_prefix = prefix
.trim()
.trim_end_matches("::")
.trim_start_matches("::");
let clean_segment = segment
.trim()
.trim_end_matches("::")
.trim_start_matches("::");
if clean_prefix.is_empty() {
clean_segment.to_string()
} else if clean_segment.is_empty() {
clean_prefix.to_string()
} else {
format!("{}::{}", clean_prefix, clean_segment)
}
}
pub(super) fn has_known_extension(path: &str, exts: &[&str]) -> bool {
exts.iter().any(|ext| path.ends_with(&format!(".{}", ext)))
}