use ahash::{AHashMap, AHashSet};
use bonsai_common::{qualified_name_segments, short_qualified_tail, FileId, SymbolId};
use bonsai_index::GlobalIndex;
use bonsai_lang_api::{
module_local_binding, AliasTarget, DeclKind, ImportSpec, ModulePath, ModulePathSyntax, Visibility,
WILDCARD_IMPORT_ALIAS_PREFIX,
};
use std::{borrow::Cow, sync::Arc};
#[derive(Clone, Debug)]
pub struct ResolveContext<'a> {
pub caller_file: FileId,
pub caller_module: &'a ModulePath,
pub receiver_type: Option<SymbolId>,
pub alias_map: Option<&'a AHashMap<String, AliasTarget>>,
pub file_path_lookup: Option<FilePathLookup<'a>>,
pub file_path_match_lookup: Option<FilePathMatchLookup<'a>>,
pub same_directory_unqualified_calls: bool,
pub module_path_syntax: ModulePathSyntax,
}
impl<'a> ResolveContext<'a> {
#[must_use]
pub fn new(caller_file: FileId, caller_module: &'a ModulePath) -> Self {
Self {
caller_file,
caller_module,
receiver_type: None,
alias_map: None,
file_path_lookup: None,
file_path_match_lookup: None,
same_directory_unqualified_calls: false,
module_path_syntax: ModulePathSyntax::none(),
}
}
#[must_use]
pub fn with_receiver_type(mut self, receiver_type: SymbolId) -> Self {
self.receiver_type = Some(receiver_type);
self
}
#[must_use]
pub fn with_alias_map(mut self, alias_map: &'a AHashMap<String, AliasTarget>) -> Self {
self.alias_map = Some(alias_map);
self
}
#[must_use]
pub fn with_file_path_lookup(mut self, lookup: &'a dyn Fn(FileId) -> Option<String>) -> Self {
self.file_path_lookup = Some(FilePathLookup { lookup });
self
}
#[must_use]
pub fn with_file_path_match_lookup(mut self, lookup: &'a dyn Fn(&str, FileId) -> bool) -> Self {
self.file_path_match_lookup = Some(FilePathMatchLookup { lookup });
self
}
#[must_use]
pub fn with_same_directory_unqualified_calls(mut self, enabled: bool) -> Self {
self.same_directory_unqualified_calls = enabled;
self
}
#[must_use]
pub fn with_module_path_syntax(mut self, syntax: ModulePathSyntax) -> Self {
self.module_path_syntax = syntax;
self
}
}
#[derive(Clone, Copy)]
pub struct FilePathLookup<'a> {
lookup: &'a dyn Fn(FileId) -> Option<String>,
}
impl<'a> FilePathLookup<'a> {
fn path_for(self, file: FileId) -> Option<String> {
(self.lookup)(file)
}
}
impl std::fmt::Debug for FilePathLookup<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("FilePathLookup(..)")
}
}
#[derive(Clone, Copy)]
pub struct FilePathMatchLookup<'a> {
lookup: &'a dyn Fn(&str, FileId) -> bool,
}
impl<'a> FilePathMatchLookup<'a> {
fn matches(self, target_module: &str, file: FileId) -> bool {
(self.lookup)(target_module, file)
}
}
impl std::fmt::Debug for FilePathMatchLookup<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("FilePathMatchLookup(..)")
}
}
#[must_use]
pub fn visibility_allows(
decl: &bonsai_lang_api::Decl,
decl_file: FileId,
decl_module: &ModulePath,
ctx: &ResolveContext<'_>,
) -> bool {
match decl.visibility {
Visibility::Public | Visibility::Protected | Visibility::Internal => true,
Visibility::Private => {
decl_file == ctx.caller_file
}
Visibility::Module => {
if decl_module.is_empty() || ctx.caller_module.is_empty() {
decl_file == ctx.caller_file
} else {
decl_module.matches(ctx.caller_module)
}
}
Visibility::Crate => {
if decl_module.is_empty() || ctx.caller_module.is_empty() {
decl_file == ctx.caller_file
} else {
decl_module.shares_top_segment(ctx.caller_module)
}
}
}
}
#[must_use]
pub fn resolve_callable_with_context(
global: &GlobalIndex,
name: &str,
ctx: &ResolveContext<'_>,
) -> Vec<bonsai_common::FuncId> {
use bonsai_lang_api::DeclKind;
let collect = |lookup: &str| {
global
.find_by_name(lookup)
.iter()
.filter_map(|symbol| {
let decl = global.decl_of(*symbol)?;
let decl_file = global.declaring_file(*symbol)?;
Some((decl, decl_file))
})
.filter(|(decl, _)| {
matches!(
decl.kind,
DeclKind::Function | DeclKind::Method | DeclKind::Constructor
)
})
.filter(|(decl, decl_file)| visibility_allows(decl, *decl_file, &decl.module_path, ctx))
.filter(|(decl, _)| match ctx.receiver_type {
Some(recv) => method_parent_matches_receiver_type(global, decl.parent, recv, ctx),
None => true,
})
.map(|(decl, _)| bonsai_common::FuncId::new(decl.symbol.raw()))
.collect::<Vec<_>>()
};
let collect_caller_lexical_scope = |lookup: &str| {
let mut out = collect(lookup);
retain_caller_lexical_func_candidates(global, &mut out, ctx);
out
};
let resolve_alias = |lookup: &str| {
let mut out = Vec::new();
if let Some(rewrite) = rewrite_through_alias_map_with_target(lookup, ctx) {
out = collect(&rewrite.rewritten);
if out.is_empty() {
let tail = bonsai_common::short_qualified_tail(&rewrite.rewritten);
if let (Some(target_module), true) =
(rewrite.target_module.as_deref(), tail != rewrite.rewritten)
{
out = collect(tail);
out.retain(|func| candidate_in_alias_target(global, *func, target_module, ctx));
}
}
}
out
};
let mut out = collect_caller_lexical_scope(name);
if out.is_empty() {
out = resolve_alias(name);
}
if out.is_empty() && unqualified_lookup_name(name) {
for target_module in wildcard_import_modules(ctx) {
let mut candidates = collect(name);
candidates.retain(|func| candidate_in_alias_target(global, *func, target_module, ctx));
out.extend(candidates);
}
dedup_func_ids(&mut out);
}
if out.is_empty() {
if let Some((receiver, method)) = split_member_head_tail(name) {
out = resolve_callable_member_with_context(global, receiver, method, ctx);
}
}
if out.is_empty() {
out = resolve_workspace_rooted_call(global, name, ctx);
}
out
}
fn unqualified_lookup_name(name: &str) -> bool {
let trimmed = name.trim();
!trimmed.is_empty() && bonsai_common::qualified_name_owner(trimmed).is_none()
}
fn wildcard_import_modules<'a>(ctx: &'a ResolveContext<'_>) -> Vec<&'a str> {
let Some(map) = ctx.alias_map else {
return Vec::new();
};
let mut modules = Vec::new();
for (key, target) in map {
if !key.starts_with(WILDCARD_IMPORT_ALIAS_PREFIX) {
continue;
}
if let AliasTarget::Namespace { module } = target {
if !module.is_empty() && !modules.iter().any(|seen| seen == module) {
modules.push(module.as_str());
}
}
}
modules
}
fn retain_caller_lexical_func_candidates(
global: &GlobalIndex,
candidates: &mut Vec<bonsai_common::FuncId>,
ctx: &ResolveContext<'_>,
) {
candidates.retain(|func| {
let sym = SymbolId::new(func.raw());
let Some(decl) = global.decl_of(sym) else {
return false;
};
let Some(decl_file) = global.declaring_file(sym) else {
return false;
};
candidate_in_caller_lexical_scope(decl, decl_file, ctx)
});
}
fn retain_caller_lexical_symbol_candidates(
global: &GlobalIndex,
candidates: &mut Vec<SymbolId>,
ctx: &ResolveContext<'_>,
) {
candidates.retain(|symbol| {
let Some(decl) = global.decl_of(*symbol) else {
return false;
};
let Some(decl_file) = global.declaring_file(*symbol) else {
return false;
};
candidate_in_caller_lexical_scope(decl, decl_file, ctx)
});
}
fn candidate_in_caller_lexical_scope(
decl: &bonsai_lang_api::Decl,
decl_file: FileId,
ctx: &ResolveContext<'_>,
) -> bool {
if matches!(
decl.kind,
bonsai_lang_api::DeclKind::Method | bonsai_lang_api::DeclKind::Constructor
) {
return decl_file == ctx.caller_file;
}
decl_file == ctx.caller_file
|| (!decl.module_path.is_empty() && decl.module_path.matches(ctx.caller_module))
|| same_directory_unqualified_module_candidate(decl, decl_file, ctx)
}
fn same_directory_unqualified_module_candidate(
decl: &bonsai_lang_api::Decl,
decl_file: FileId,
ctx: &ResolveContext<'_>,
) -> bool {
if decl_file == ctx.caller_file {
return false;
}
let Some(lookup) = ctx.file_path_lookup else {
return false;
};
let Some(decl_path) = lookup.path_for(decl_file) else {
return false;
};
let Some(caller_path) = lookup.path_for(ctx.caller_file) else {
return false;
};
if (!decl.module_path.is_empty() || !ctx.caller_module.is_empty())
&& !ctx.same_directory_unqualified_calls
{
return false;
}
file_parent_dir(&decl_path).is_some_and(|decl_dir| file_parent_dir(&caller_path) == Some(decl_dir))
}
fn file_parent_dir(path: &str) -> Option<&str> {
let trimmed = path.trim();
let idx = trimmed.rfind(['/', '\\'])?;
Some(&trimmed[..idx])
}
fn resolve_workspace_rooted_call(
global: &GlobalIndex,
name: &str,
ctx: &ResolveContext<'_>,
) -> Vec<bonsai_common::FuncId> {
use bonsai_lang_api::DeclKind;
let stripped = strip_module_path_prefix(name, ctx.module_path_syntax);
if stripped.is_empty() {
return Vec::new();
}
let Some((mod_path, fn_name)) = split_module_call_tail(stripped) else {
return Vec::new();
};
if mod_path.is_empty() || fn_name.is_empty() {
return Vec::new();
}
let mut out = Vec::new();
for sym in global.find_by_name(fn_name) {
let Some(decl) = global.decl_of(*sym) else {
continue;
};
if !matches!(
decl.kind,
DeclKind::Function | DeclKind::Method | DeclKind::Constructor
) {
continue;
}
let Some(decl_file) = global.declaring_file(*sym) else {
continue;
};
if !visibility_allows(decl, decl_file, &decl.module_path, ctx) {
continue;
}
if !module_target_matches_decl_module_path(mod_path, &decl.module_path)
&& !alias_target_matches_file(ctx, mod_path, decl_file)
{
continue;
}
out.push(bonsai_common::FuncId::new(decl.symbol.raw()));
}
dedup_func_ids(&mut out);
out
}
#[must_use]
pub fn strip_module_path_prefix(name: &str, syntax: ModulePathSyntax) -> &str {
let trimmed = name.trim();
let mut rest = trimmed;
let mut stripped_repeatable = false;
loop {
let Some(next) = syntax
.repeatable_rooted_prefixes
.iter()
.find_map(|prefix| rest.strip_prefix(prefix))
else {
break;
};
rest = next;
stripped_repeatable = true;
}
if stripped_repeatable {
return rest;
}
syntax
.rooted_prefixes
.iter()
.find_map(|prefix| trimmed.strip_prefix(prefix))
.unwrap_or(trimmed)
}
fn split_module_call_tail(name: &str) -> Option<(&str, &str)> {
bonsai_common::split_qualified_name_owner_tail(name)
}
fn resolve_callable_member_with_context(
global: &GlobalIndex,
receiver: &str,
method: &str,
ctx: &ResolveContext<'_>,
) -> Vec<bonsai_common::FuncId> {
use bonsai_lang_api::DeclKind;
if receiver.trim().is_empty() || method.trim().is_empty() {
return Vec::new();
}
let mut out = Vec::new();
for class_sym in resolve_class(global, receiver, ctx) {
let Some(class_file) = global.declaring_file(class_sym) else {
continue;
};
for decl in global.decls_in(class_file) {
if decl.parent != Some(class_sym) {
continue;
}
if !matches!(
decl.kind,
DeclKind::Function | DeclKind::Method | DeclKind::Constructor
) {
continue;
}
if decl.name != method && short_qualified_tail(&decl.name) != method {
continue;
}
let Some(decl_file) = global.declaring_file(decl.symbol) else {
continue;
};
if !visibility_allows(decl, decl_file, &decl.module_path, ctx) {
continue;
}
out.push(bonsai_common::FuncId::new(decl.symbol.raw()));
}
}
dedup_func_ids(&mut out);
out
}
fn split_member_head_tail(name: &str) -> Option<(&str, &str)> {
bonsai_common::split_qualified_name_owner_tail(name)
}
fn method_parent_matches_receiver_type(
global: &GlobalIndex,
method_parent: Option<SymbolId>,
receiver_type: SymbolId,
ctx: &ResolveContext<'_>,
) -> bool {
let Some(method_parent) = method_parent else {
return false;
};
if method_parent == receiver_type {
return true;
}
let mut seen = AHashSet::new();
let mut stack = vec![receiver_type];
while let Some(class_sym) = stack.pop() {
if !seen.insert(class_sym) {
continue;
}
let Some(class_decl) = global.decl_of(class_sym) else {
continue;
};
for base in &class_decl.bases {
for base_sym in resolve_class(global, base, ctx) {
if base_sym == method_parent {
return true;
}
stack.push(base_sym);
}
}
}
false
}
fn rewrite_through_alias_map_with_target(name: &str, ctx: &ResolveContext<'_>) -> Option<AliasRewrite> {
rewrite_through_alias_map_with_mode(name, ctx, AliasRewriteMode::Callable)
}
fn rewrite_through_alias_map_with_type_target(name: &str, ctx: &ResolveContext<'_>) -> Option<AliasRewrite> {
rewrite_through_alias_map_with_mode(name, ctx, AliasRewriteMode::Type)
}
#[derive(Clone, Copy)]
enum AliasRewriteMode {
Callable,
Type,
}
fn rewrite_through_alias_map_with_mode(
name: &str,
ctx: &ResolveContext<'_>,
mode: AliasRewriteMode,
) -> Option<AliasRewrite> {
let map = ctx.alias_map?;
if let Some(target) = map.get(name) {
return Some(alias_rewrite_from_target(target, None, mode, map));
}
let (head, tail) = split_alias_head_tail(name)?;
let target = map.get(head)?;
Some(alias_rewrite_from_target(target, Some(tail), mode, map))
}
fn alias_rewrite_from_target(
target: &AliasTarget,
tail: Option<&str>,
mode: AliasRewriteMode,
map: &AHashMap<String, AliasTarget>,
) -> AliasRewrite {
let mut current = target;
let mut seen_type_names = AHashSet::new();
while let AliasTarget::Type { type_name } = current {
if !seen_type_names.insert(type_name.as_str()) {
break;
}
let Some(resolved) = map.get(type_name) else {
break;
};
current = resolved;
}
let rewritten = match tail {
Some(tail) => current.rewrite_with_tail(tail),
None => match mode {
AliasRewriteMode::Callable => current.callable_target_text(),
AliasRewriteMode::Type => current.target_text(),
},
};
AliasRewrite::from_target(
current,
rewritten,
matches!(mode, AliasRewriteMode::Type) && tail.is_some(),
)
}
fn split_alias_head_tail(name: &str) -> Option<(&str, &str)> {
if let Some((head, tail)) = name.split_once("::") {
return Some((head, tail));
}
if let Some((head, tail)) = name.split_once('.') {
return Some((head, tail));
}
if let Some((head, tail)) = name.split_once(':') {
return Some((head, tail));
}
None
}
#[derive(Clone, Debug)]
struct AliasRewrite {
rewritten: String,
target_module: Option<String>,
}
impl AliasRewrite {
fn from_target(target: &AliasTarget, rewritten: String, nested_type_member: bool) -> Self {
let target_module = match target {
AliasTarget::Namespace { module } if !module.trim().is_empty() => Some(module.clone()),
AliasTarget::Member { module, member }
if nested_type_member && !module.trim().is_empty() && !member.trim().is_empty() =>
{
Some(format!("{module}.{member}"))
}
AliasTarget::Member { module, .. } if !module.trim().is_empty() => Some(module.clone()),
_ => None,
};
Self {
rewritten,
target_module,
}
}
}
trait AliasTargetExt {
fn target_text(&self) -> String;
fn callable_target_text(&self) -> String;
fn rewrite_with_tail(&self, tail: &str) -> String;
}
impl AliasTargetExt for AliasTarget {
fn target_text(&self) -> String {
match self {
AliasTarget::Member { module, member } => format!("{module}.{member}"),
AliasTarget::Namespace { module } => module.clone(),
AliasTarget::Type { type_name } => type_name.clone(),
}
}
fn callable_target_text(&self) -> String {
match self {
AliasTarget::Member { module, member } => format!("{module}.{member}"),
AliasTarget::Namespace { module } => module.clone(),
AliasTarget::Type { type_name } => type_name.clone(),
}
}
fn rewrite_with_tail(&self, tail: &str) -> String {
let prefix = match self {
AliasTarget::Namespace { module } => module.clone(),
AliasTarget::Member { module, member } => format!("{module}.{member}"),
AliasTarget::Type { type_name } => type_name.clone(),
};
format!("{prefix}.{tail}")
}
}
#[must_use]
pub fn module_target_matches_decl_module_path(
target_module: &str,
decl_module: &bonsai_lang_api::ModulePath,
) -> bool {
module_target_matches_decl_module_path_with_syntax(target_module, decl_module, ModulePathSyntax::none())
}
#[must_use]
pub fn module_target_matches_decl_module_path_with_syntax(
target_module: &str,
decl_module: &bonsai_lang_api::ModulePath,
syntax: ModulePathSyntax,
) -> bool {
module_target_matches_decl_module_path_impl(target_module, decl_module, syntax, true)
}
#[must_use]
pub fn module_target_exactly_matches_decl_module_path_with_syntax(
target_module: &str,
decl_module: &bonsai_lang_api::ModulePath,
syntax: ModulePathSyntax,
) -> bool {
module_target_matches_decl_module_path_impl(target_module, decl_module, syntax, false)
}
fn module_target_matches_decl_module_path_impl(
target_module: &str,
decl_module: &bonsai_lang_api::ModulePath,
syntax: ModulePathSyntax,
allow_terminal_trailer: bool,
) -> bool {
if target_module.is_empty() || decl_module.is_empty() {
return false;
}
let target_module = strip_module_path_prefix(target_module, syntax);
let target_module = target_module.trim_matches(bonsai_common::is_name_punctuation);
let target_segments = bonsai_common::qualified_name_segments(target_module);
if target_segments.is_empty() {
return false;
}
let decl_segments = &decl_module.segments;
if try_suffix_match(&target_segments, decl_segments) {
return true;
}
if allow_terminal_trailer && target_segments.len() > 1 {
let trimmed = &target_segments[..target_segments.len() - 1];
if try_suffix_match(trimmed, decl_segments) {
return true;
}
}
false
}
fn try_suffix_match(target: &[&str], decl: &[String]) -> bool {
if target.is_empty() || target.len() > decl.len() {
return false;
}
let suffix_start = decl.len() - target.len();
decl[suffix_start..]
.iter()
.zip(target.iter())
.all(|(decl_seg, target_seg)| decl_seg == target_seg)
}
fn candidate_in_alias_target(
global: &GlobalIndex,
func: bonsai_common::FuncId,
target_module: &str,
ctx: &ResolveContext<'_>,
) -> bool {
symbol_in_alias_target(global, SymbolId::new(func.raw()), target_module, ctx)
}
fn symbol_in_alias_target(
global: &GlobalIndex,
symbol: SymbolId,
target_module: &str,
ctx: &ResolveContext<'_>,
) -> bool {
let Some(decl) = global.decl_of(symbol) else {
return false;
};
if module_target_matches_decl_module_path_from_context(target_module, &decl.module_path, ctx) {
return true;
}
let Some(decl_file) = global.declaring_file(symbol) else {
return false;
};
alias_target_matches_file(ctx, target_module, decl_file)
}
fn alias_target_matches_file(ctx: &ResolveContext<'_>, target_module: &str, file: FileId) -> bool {
let target_module = strip_module_path_prefix(target_module, ctx.module_path_syntax);
if let Some(lookup) = ctx.file_path_match_lookup {
return lookup.matches(target_module, file);
}
ctx.file_path_lookup
.and_then(|lookup| lookup.path_for(file))
.is_some_and(|path| module_target_matches_path(target_module, &path))
}
fn module_target_matches_decl_module_path_from_context(
target_module: &str,
decl_module: &bonsai_lang_api::ModulePath,
ctx: &ResolveContext<'_>,
) -> bool {
if let Some(target_segments) = relative_module_target_segments(target_module, ctx.caller_module) {
return decl_module.segments == target_segments;
}
module_target_matches_decl_module_path_with_syntax(target_module, decl_module, ctx.module_path_syntax)
}
fn relative_module_target_segments(
target_module: &str,
caller_module: &bonsai_lang_api::ModulePath,
) -> Option<Vec<String>> {
let target = target_module.trim();
if !(target == "."
|| target == ".."
|| target.starts_with("./")
|| target.starts_with("../")
|| target.starts_with(".\\")
|| target.starts_with("..\\"))
{
return None;
}
if caller_module.segments.is_empty() {
return None;
}
let normalized = target.replace('\\', "/");
let mut segments = caller_module.segments.clone();
segments.pop();
for raw in normalized.split('/') {
let part = raw.trim();
if part.is_empty() || part == "." {
continue;
}
if part == ".." {
segments.pop()?;
continue;
}
segments.push(strip_extension(part).to_string());
}
(!segments.is_empty()).then_some(segments)
}
#[must_use]
pub fn callee_without_call_args(callee: &str) -> &str {
callee.split('(').next().unwrap_or(callee).trim()
}
pub fn push_unique_func(out: &mut Vec<bonsai_common::FuncId>, func: bonsai_common::FuncId) {
if !out.contains(&func) {
out.push(func);
}
}
pub fn push_unique_string(out: &mut Vec<String>, value: String) {
if !value.is_empty() && !out.iter().any(|existing| existing == &value) {
out.push(value);
}
}
#[must_use]
pub fn canonical_dispatch_type_name(name: &str) -> String {
short_tail(name)
.trim_start_matches(bonsai_common::is_name_punctuation)
.trim_end_matches("()")
.trim()
.to_string()
}
#[must_use]
pub fn split_qualified_head_tail(name: &str) -> Option<(&str, &str)> {
bonsai_common::split_qualified_name_head_tail(name)
}
#[must_use]
pub fn namespace_alias_target_tail<'a>(
name: &'a str,
alias_targets: &'a AHashMap<String, AliasTarget>,
) -> Option<(&'a str, &'a str)> {
let (head, tail) = split_qualified_head_tail(name)?;
match alias_targets.get(head)? {
AliasTarget::Namespace { module } if !module.is_empty() && !tail.is_empty() => {
Some((module.as_str(), tail))
}
_ => None,
}
}
#[must_use]
pub fn qualified_module_alias_call(name: &str, aliases: &AHashMap<String, String>) -> bool {
let Some((head, _)) = split_qualified_head_tail(name) else {
return false;
};
aliases.contains_key(head)
}
#[must_use]
pub fn export_name_variants(alias_tail: &str, caller_export_aliases: &[&'static str]) -> Vec<String> {
let mut variants = Vec::new();
for receiver in caller_export_aliases {
push_unique(&mut variants, format!("{receiver}.{alias_tail}"));
}
push_unique(&mut variants, alias_tail.to_string());
variants
}
#[must_use]
pub fn is_super_receiver(receiver: &str) -> bool {
is_super_receiver_with_tokens(receiver, &[])
}
#[must_use]
pub fn is_super_receiver_with_tokens(receiver: &str, tokens: &[&str]) -> bool {
let receiver = receiver
.trim()
.trim_start_matches(bonsai_common::is_name_punctuation);
tokens.contains(&receiver)
}
#[must_use]
pub fn enclosing_class_for_decl<'a>(
global: &'a GlobalIndex,
decl: &bonsai_lang_api::Decl,
) -> Option<&'a bonsai_lang_api::Decl> {
use bonsai_lang_api::DeclKind;
if let Some(parent) = decl.parent {
if let Some(parent_decl) = global.decl_of(parent) {
if matches!(
parent_decl.kind,
DeclKind::Class | DeclKind::Struct | DeclKind::Trait | DeclKind::Interface | DeclKind::Enum
) {
return Some(parent_decl);
}
}
}
None
}
pub fn collect_transitive_base_type_names(
global: &GlobalIndex,
class_sym: bonsai_common::SymbolId,
ctx: &ResolveContext<'_>,
out: &mut AHashSet<String>,
) {
let Some(class_decl) = global.decl_of(class_sym) else {
return;
};
let Some(class_file) = global.declaring_file(class_sym) else {
return;
};
let base_ctx = class_decl_context(ctx, class_file, &class_decl.module_path);
for base in &class_decl.bases {
let canonical = canonical_dispatch_type_name(base);
if !out.insert(canonical) {
continue;
}
for base_sym in resolve_class(global, base, &base_ctx) {
collect_transitive_base_type_names(global, base_sym, &base_ctx, out);
}
}
}
#[must_use]
pub fn prune_receiver_type_names_for_dispatch(
type_names: Vec<String>,
global: &GlobalIndex,
ctx: &ResolveContext<'_>,
) -> Vec<String> {
if type_names.len() < 2 {
return type_names;
}
let canonical_types: Vec<String> = type_names
.iter()
.map(|name| canonical_dispatch_type_name(name))
.collect();
let mut inherited = AHashSet::new();
for type_name in &type_names {
for class_sym in resolve_class(global, type_name, ctx) {
collect_transitive_base_type_names(global, class_sym, ctx, &mut inherited);
}
}
let mut out = Vec::new();
for (idx, type_name) in type_names.into_iter().enumerate() {
if inherited.contains(&canonical_types[idx])
&& canonical_types
.iter()
.enumerate()
.any(|(other_idx, other)| other_idx != idx && other != &canonical_types[idx])
{
continue;
}
push_unique_string(&mut out, type_name);
}
out
}
pub fn collect_method_candidates_for_class(
global: &GlobalIndex,
class_sym: bonsai_common::SymbolId,
method_name: &str,
ctx: &ResolveContext<'_>,
seen: &mut AHashSet<bonsai_common::SymbolId>,
out: &mut Vec<bonsai_common::FuncId>,
) {
let mut seen_classes = AHashSet::new();
collect_method_candidates_for_class_inner(
global,
class_sym,
method_name,
ctx,
seen,
&mut seen_classes,
out,
);
}
#[derive(Debug, Default)]
pub struct MethodCandidateCache {
entries: AHashMap<MethodCandidateCacheKey, Vec<bonsai_common::FuncId>>,
peer_class_index: Option<Arc<PeerClassIndex>>,
}
pub type PeerClassIndex = AHashMap<(String, ModulePath), Vec<SymbolId>>;
impl MethodCandidateCache {
#[must_use]
pub fn with_peer_class_index(peer_class_index: Arc<PeerClassIndex>) -> Self {
Self {
entries: AHashMap::new(),
peer_class_index: Some(peer_class_index),
}
}
}
#[must_use]
pub fn build_shared_peer_class_index(global: &GlobalIndex) -> Arc<PeerClassIndex> {
Arc::new(build_peer_class_index(global))
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct MethodCandidateCacheKey {
class_sym: SymbolId,
method_name: String,
caller_file: FileId,
caller_module: ModulePath,
}
impl MethodCandidateCacheKey {
fn new(class_sym: SymbolId, method_name: &str, ctx: &ResolveContext<'_>) -> Self {
Self {
class_sym,
method_name: method_name.to_string(),
caller_file: ctx.caller_file,
caller_module: ctx.caller_module.clone(),
}
}
}
pub fn collect_method_candidates_for_class_cached(
global: &GlobalIndex,
class_sym: bonsai_common::SymbolId,
method_name: &str,
ctx: &ResolveContext<'_>,
seen: &mut AHashSet<bonsai_common::SymbolId>,
out: &mut Vec<bonsai_common::FuncId>,
cache: &mut MethodCandidateCache,
) {
let mut seen_classes = AHashSet::new();
for func in collect_method_candidates_for_class_cached_inner(
global,
class_sym,
method_name,
ctx,
&mut seen_classes,
cache,
) {
let sym = SymbolId::new(func.raw());
if seen.insert(sym) {
out.push(func);
}
}
}
fn collect_method_candidates_for_class_cached_inner(
global: &GlobalIndex,
class_sym: bonsai_common::SymbolId,
method_name: &str,
ctx: &ResolveContext<'_>,
seen_classes: &mut AHashSet<bonsai_common::SymbolId>,
cache: &mut MethodCandidateCache,
) -> Vec<bonsai_common::FuncId> {
if !seen_classes.insert(class_sym) {
return Vec::new();
}
let key = MethodCandidateCacheKey::new(class_sym, method_name, ctx);
if let Some(cached) = cache.entries.get(&key) {
return cached.clone();
}
let Some(class_decl) = global.decl_of(class_sym) else {
return Vec::new();
};
if !matches!(
class_decl.kind,
DeclKind::Class
| DeclKind::Struct
| DeclKind::Trait
| DeclKind::Interface
| DeclKind::Enum
| DeclKind::Import
) {
return Vec::new();
}
let Some(class_file) = global.declaring_file(class_sym) else {
return Vec::new();
};
let mut out = Vec::new();
let mut local_fallback = Vec::new();
for decl in global.decls_in(class_file) {
if decl.name != method_name {
continue;
}
if !matches!(
decl.kind,
DeclKind::Function | DeclKind::Method | DeclKind::Constructor
) {
continue;
}
let Some(decl_file) = global.declaring_file(decl.symbol) else {
continue;
};
if !visibility_allows(decl, decl_file, &decl.module_path, ctx) {
continue;
}
if decl_belongs_to_class(decl, class_sym, class_decl) {
let func = bonsai_common::FuncId::new(decl.symbol.raw());
if callable_decl_has_body(decl) {
push_unique_func(&mut out, func);
} else {
push_unique_func(&mut local_fallback, func);
}
}
}
if !out.is_empty() {
cache.entries.insert(key, out.clone());
return out;
}
if !class_decl_has_owned_callable_body(global, class_sym, class_decl) {
collect_peer_partial_class_method_candidates_cached(
global,
class_sym,
class_decl,
method_name,
ctx,
seen_classes,
&mut out,
cache,
);
if !out.is_empty() {
cache.entries.insert(key, out.clone());
return out;
}
}
let base_ctx = class_decl_context(ctx, class_file, &class_decl.module_path);
for base in &class_decl.bases {
for base_sym in resolve_class(global, base, &base_ctx) {
for func in collect_method_candidates_for_class_cached_inner(
global,
base_sym,
method_name,
ctx,
seen_classes,
cache,
) {
push_unique_func(&mut out, func);
}
}
}
if out.is_empty() {
out = local_fallback;
}
cache.entries.insert(key, out.clone());
out
}
fn collect_method_candidates_for_class_inner(
global: &GlobalIndex,
class_sym: bonsai_common::SymbolId,
method_name: &str,
ctx: &ResolveContext<'_>,
seen_methods: &mut AHashSet<bonsai_common::SymbolId>,
seen_classes: &mut AHashSet<bonsai_common::SymbolId>,
out: &mut Vec<bonsai_common::FuncId>,
) {
use bonsai_lang_api::DeclKind;
if !seen_classes.insert(class_sym) {
return;
}
let Some(class_decl) = global.decl_of(class_sym) else {
return;
};
if !matches!(
class_decl.kind,
DeclKind::Class
| DeclKind::Struct
| DeclKind::Trait
| DeclKind::Interface
| DeclKind::Enum
| DeclKind::Import
) {
return;
}
let Some(class_file) = global.declaring_file(class_sym) else {
return;
};
let before = out.len();
let mut matched_local_method = false;
let mut local_fallback = Vec::new();
for decl in global.decls_in(class_file) {
if decl.name != method_name {
continue;
}
if !matches!(
decl.kind,
DeclKind::Function | DeclKind::Method | DeclKind::Constructor
) {
continue;
}
let Some(decl_file) = global.declaring_file(decl.symbol) else {
continue;
};
if !visibility_allows(decl, decl_file, &decl.module_path, ctx) {
continue;
}
if decl_belongs_to_class(decl, class_sym, class_decl) {
if callable_decl_has_body(decl) {
if seen_methods.insert(decl.symbol) {
matched_local_method = true;
out.push(bonsai_common::FuncId::new(decl.symbol.raw()));
}
} else {
local_fallback.push(decl.symbol);
}
}
}
if matched_local_method {
return;
}
if !class_decl_has_owned_callable_body(global, class_sym, class_decl)
&& collect_peer_partial_class_method_candidates(
global,
class_sym,
class_decl,
method_name,
ctx,
seen_methods,
seen_classes,
out,
)
{
return;
}
let base_ctx = class_decl_context(ctx, class_file, &class_decl.module_path);
for base in &class_decl.bases {
for base_sym in resolve_class(global, base, &base_ctx) {
collect_method_candidates_for_class_inner(
global,
base_sym,
method_name,
ctx,
seen_methods,
seen_classes,
out,
);
}
}
if out.len() == before {
for symbol in local_fallback {
if seen_methods.insert(symbol) {
out.push(bonsai_common::FuncId::new(symbol.raw()));
}
}
}
}
#[allow(clippy::too_many_arguments)] fn collect_peer_partial_class_method_candidates(
global: &GlobalIndex,
class_sym: SymbolId,
class_decl: &bonsai_lang_api::Decl,
method_name: &str,
ctx: &ResolveContext<'_>,
seen_methods: &mut AHashSet<SymbolId>,
seen_classes: &mut AHashSet<SymbolId>,
out: &mut Vec<bonsai_common::FuncId>,
) -> bool {
let before = out.len();
for peer_sym in peer_partial_class_symbols(global, class_sym, class_decl, None) {
if peer_sym == class_sym || seen_classes.contains(&peer_sym) {
continue;
}
let Some(peer_decl) = global.decl_of(peer_sym) else {
continue;
};
let Some(peer_file) = global.declaring_file(peer_sym) else {
continue;
};
if !matches!(
peer_decl.kind,
DeclKind::Class | DeclKind::Struct | DeclKind::Trait | DeclKind::Interface | DeclKind::Enum
) || peer_decl.name != class_decl.name
|| !peer_partial_class_matches(class_decl, class_sym, peer_decl, peer_sym, global)
|| !visibility_allows(peer_decl, peer_file, &peer_decl.module_path, ctx)
{
continue;
}
collect_method_candidates_for_class_inner(
global,
peer_sym,
method_name,
ctx,
seen_methods,
seen_classes,
out,
);
}
out.len() > before
}
#[allow(clippy::too_many_arguments)] fn collect_peer_partial_class_method_candidates_cached(
global: &GlobalIndex,
class_sym: SymbolId,
class_decl: &bonsai_lang_api::Decl,
method_name: &str,
ctx: &ResolveContext<'_>,
seen_classes: &mut AHashSet<SymbolId>,
out: &mut Vec<bonsai_common::FuncId>,
cache: &mut MethodCandidateCache,
) {
for peer_sym in peer_partial_class_symbols(global, class_sym, class_decl, Some(cache)) {
if peer_sym == class_sym || seen_classes.contains(&peer_sym) {
continue;
}
let Some(peer_decl) = global.decl_of(peer_sym) else {
continue;
};
let Some(peer_file) = global.declaring_file(peer_sym) else {
continue;
};
if !matches!(
peer_decl.kind,
DeclKind::Class | DeclKind::Struct | DeclKind::Trait | DeclKind::Interface | DeclKind::Enum
) || peer_decl.name != class_decl.name
|| !peer_partial_class_matches(class_decl, class_sym, peer_decl, peer_sym, global)
|| !visibility_allows(peer_decl, peer_file, &peer_decl.module_path, ctx)
{
continue;
}
for func in collect_method_candidates_for_class_cached_inner(
global,
peer_sym,
method_name,
ctx,
seen_classes,
cache,
) {
push_unique_func(out, func);
}
}
}
fn peer_partial_class_symbols(
global: &GlobalIndex,
class_sym: SymbolId,
class_decl: &bonsai_lang_api::Decl,
cache: Option<&mut MethodCandidateCache>,
) -> Vec<SymbolId> {
if class_decl.module_path.is_empty() {
let Some(class_file) = global.declaring_file(class_sym) else {
return Vec::new();
};
return global
.decls_in(class_file)
.iter()
.filter(|decl| {
decl.symbol != class_sym
&& decl.name == class_decl.name
&& matches!(
decl.kind,
DeclKind::Class
| DeclKind::Struct
| DeclKind::Trait
| DeclKind::Interface
| DeclKind::Enum
)
})
.map(|decl| decl.symbol)
.collect();
}
let key = (class_decl.name.clone(), class_decl.module_path.clone());
if let Some(cache) = cache {
let index = cache
.peer_class_index
.get_or_insert_with(|| build_shared_peer_class_index(global));
return index.get(&key).cloned().unwrap_or_default();
}
build_peer_class_index(global).remove(&key).unwrap_or_default()
}
fn build_peer_class_index(global: &GlobalIndex) -> PeerClassIndex {
let mut index: PeerClassIndex = AHashMap::new();
for file in global.all_files() {
for decl in global.decls_in(file) {
if decl.module_path.is_empty()
|| !matches!(
decl.kind,
DeclKind::Class
| DeclKind::Struct
| DeclKind::Trait
| DeclKind::Interface
| DeclKind::Enum
)
{
continue;
}
index
.entry((decl.name.clone(), decl.module_path.clone()))
.or_default()
.push(decl.symbol);
}
}
index
}
fn peer_partial_class_matches(
class_decl: &bonsai_lang_api::Decl,
class_sym: SymbolId,
peer_decl: &bonsai_lang_api::Decl,
peer_sym: SymbolId,
global: &GlobalIndex,
) -> bool {
let Some(class_file) = global.declaring_file(class_sym) else {
return false;
};
let Some(peer_file) = global.declaring_file(peer_sym) else {
return false;
};
if class_file == peer_file {
return true;
}
class_decl.module_path.matches(&peer_decl.module_path)
}
#[must_use]
pub fn class_symbols_share_semantic_identity(global: &GlobalIndex, left: SymbolId, right: SymbolId) -> bool {
if left == right {
return true;
}
let Some(left_decl) = global.decl_of(left) else {
return false;
};
let Some(right_decl) = global.decl_of(right) else {
return false;
};
let class_like = |decl: &bonsai_lang_api::Decl| {
matches!(
decl.kind,
DeclKind::Class | DeclKind::Struct | DeclKind::Trait | DeclKind::Interface | DeclKind::Enum
)
};
class_like(left_decl)
&& class_like(right_decl)
&& left_decl.name == right_decl.name
&& peer_partial_class_matches(left_decl, left, right_decl, right, global)
}
fn class_decl_has_owned_callable_body(
global: &GlobalIndex,
class_sym: SymbolId,
class_decl: &bonsai_lang_api::Decl,
) -> bool {
let Some(class_file) = global.declaring_file(class_sym) else {
return false;
};
global.decls_in(class_file).iter().any(|decl| {
matches!(
decl.kind,
DeclKind::Function | DeclKind::Method | DeclKind::Constructor
) && callable_decl_has_body(decl)
&& decl_belongs_to_class(decl, class_sym, class_decl)
})
}
fn callable_decl_has_body(decl: &bonsai_lang_api::Decl) -> bool {
decl.body_span.is_some() || !decl.flow_events.is_empty()
}
fn decl_belongs_to_class(
decl: &bonsai_lang_api::Decl,
class_sym: SymbolId,
class_decl: &bonsai_lang_api::Decl,
) -> bool {
if decl.parent == Some(class_sym) {
return true;
}
if matches!(decl.kind, DeclKind::Method | DeclKind::Constructor) && decl.parent.is_some() {
return false;
}
let class_span = class_decl.body_span.unwrap_or(class_decl.span);
decl.name_span.file == class_span.file
&& decl.name_span.start >= class_span.start
&& decl.name_span.end <= class_span.end
}
fn class_decl_context<'a>(
inherited: &ResolveContext<'a>,
class_file: FileId,
class_module: &'a ModulePath,
) -> ResolveContext<'a> {
let mut ctx = ResolveContext::new(class_file, class_module);
if let Some(alias_map) = inherited.alias_map {
ctx = ctx.with_alias_map(alias_map);
}
if let Some(file_path_lookup) = inherited.file_path_lookup {
ctx = ctx.with_file_path_lookup(file_path_lookup.lookup);
}
if let Some(file_path_match_lookup) = inherited.file_path_match_lookup {
ctx = ctx.with_file_path_match_lookup(file_path_match_lookup.lookup);
}
ctx = ctx.with_same_directory_unqualified_calls(inherited.same_directory_unqualified_calls);
ctx = ctx.with_module_path_syntax(inherited.module_path_syntax);
ctx
}
pub fn extend_alias_targets_with_declared_types(
alias_targets: &mut AHashMap<String, AliasTarget>,
type_aliases: &[bonsai_lang_api::TypeAliasBinding],
) {
for alias in type_aliases {
if alias.name.is_empty() || alias.type_name.is_empty() {
continue;
}
alias_targets
.entry(alias.name.clone())
.or_insert_with(|| AliasTarget::Type {
type_name: alias.type_name.clone(),
});
}
}
#[must_use]
pub fn module_target_matches_path(alias_target: &str, file_path: &str) -> bool {
let target_parts = module_target_parts(alias_target);
let path_parts = module_path_parts(file_path);
module_target_parts_match_path_parts(&target_parts, &path_parts)
}
#[must_use]
pub fn module_target_parts(alias_target: &str) -> Vec<String> {
let target: Cow<'_, str> = if alias_target.contains('\\') {
Cow::Owned(alias_target.replace('\\', "/"))
} else {
Cow::Borrowed(alias_target)
};
module_import_parts(&target)
}
#[must_use]
pub fn module_target_parts_match_path_parts(target_parts: &[String], path_parts: &[String]) -> bool {
let Some(target_leaf) = target_parts.last() else {
return false;
};
if target_parts.len() > 1 {
if path_parts
.windows(target_parts.len())
.any(|window| window == target_parts)
{
return true;
}
for suffix_start in 1..target_parts.len() {
let suffix = &target_parts[suffix_start..];
if !suffix.is_empty()
&& suffix.len() <= path_parts.len()
&& path_parts_contains_workspace_suffix(path_parts, suffix)
{
return true;
}
}
return false;
}
if path_parts
.last()
.is_some_and(|file| strip_extension(file) == target_leaf.as_str())
{
return true;
}
if path_parts
.iter()
.rev()
.nth(1)
.is_some_and(|parent| parent == target_leaf)
{
return true;
}
false
}
fn path_parts_contains_workspace_suffix(path_parts: &[String], suffix: &[String]) -> bool {
if suffix.is_empty() || suffix.len() > path_parts.len() {
return false;
}
if suffix.len() > 1 {
return path_parts.windows(suffix.len()).any(|window| window == suffix);
}
let Some(leaf) = suffix.first() else {
return false;
};
path_parts
.iter()
.take(path_parts.len().saturating_sub(1))
.any(|part| part == leaf)
}
#[must_use]
pub fn module_import_parts(text: &str) -> Vec<String> {
let normalized = bonsai_common::normalize_qualified_name(text);
let parts: Vec<&str> = if normalized.contains('/') {
normalized.split('/').collect()
} else {
normalized.split('.').collect()
};
parts
.into_iter()
.filter_map(|part| {
let part = part.trim();
(!part.is_empty() && part != "." && part != ".." && part != "*")
.then(|| strip_extension(part).to_string())
})
.collect()
}
#[must_use]
pub fn module_path_parts(text: &str) -> Vec<String> {
text.split(['/', '\\'])
.filter_map(|part| {
let part = part.trim();
(!part.is_empty() && part != "." && part != "..").then(|| strip_extension(part).to_string())
})
.collect()
}
#[must_use]
pub fn strip_extension(part: &str) -> &str {
part.rsplit_once('.').map_or(part, |(stem, _)| stem)
}
#[must_use]
pub fn resolve_class(
global: &GlobalIndex,
name: &str,
ctx: &ResolveContext<'_>,
) -> Vec<bonsai_common::SymbolId> {
use bonsai_lang_api::DeclKind;
let name = strip_module_path_prefix(name, ctx.module_path_syntax);
let collect = |lookup: &str| {
global
.find_by_name(lookup)
.iter()
.filter_map(|symbol| {
let decl = global.decl_of(*symbol)?;
let decl_file = global.declaring_file(*symbol)?;
Some((decl, decl_file))
})
.filter(|(decl, _)| {
matches!(
decl.kind,
DeclKind::Class
| DeclKind::Struct
| DeclKind::Trait
| DeclKind::Interface
| DeclKind::Enum
| DeclKind::Import
)
})
.filter(|(decl, decl_file)| visibility_allows(decl, *decl_file, &decl.module_path, ctx))
.map(|(decl, _)| decl.symbol)
.collect::<Vec<_>>()
};
let collect_caller_lexical_scope = |lookup: &str| {
let mut candidates = collect(lookup);
retain_caller_lexical_symbol_candidates(global, &mut candidates, ctx);
candidates
};
let collect_caller_file_scope = |lookup: &str| {
global
.decls_in(ctx.caller_file)
.iter()
.filter(|decl| {
decl.name == lookup
|| decl
.qualified_name
.as_deref()
.is_some_and(|qualified| qualified == lookup)
})
.filter(|decl| {
matches!(
decl.kind,
DeclKind::Class
| DeclKind::Struct
| DeclKind::Trait
| DeclKind::Interface
| DeclKind::Enum
| DeclKind::Import
)
})
.filter(|decl| visibility_allows(decl, ctx.caller_file, &decl.module_path, ctx))
.map(|decl| decl.symbol)
.collect::<Vec<_>>()
};
let collect_relative_qualified_scope = |lookup: &str| {
let wanted = qualified_name_segments(lookup);
let Some(tail) = wanted.last().copied().filter(|_| wanted.len() > 1) else {
return Vec::new();
};
let mut candidates = collect(tail);
candidates.retain(|symbol| {
global
.decl_of(*symbol)
.and_then(|decl| decl.qualified_name.as_deref())
.is_some_and(|qualified| {
let observed = qualified_name_segments(qualified);
relative_qualified_type_matches(&observed, &wanted, ctx.caller_module)
})
});
candidates
};
let mut out = Vec::new();
for lookup in type_lookup_variants(name) {
out.extend(collect_caller_file_scope(&lookup));
if !out.is_empty() {
dedup_symbols(&mut out);
return out;
}
}
for lookup in type_lookup_variants(name) {
out.extend(collect_relative_qualified_scope(&lookup));
if !out.is_empty() {
dedup_symbols(&mut out);
return out;
}
}
if let Some(rewrite) = rewrite_through_alias_map_with_type_target(name, ctx) {
for lookup in type_lookup_variants(&rewrite.rewritten) {
out.extend(collect(&lookup));
if !out.is_empty() {
dedup_symbols(&mut out);
return out;
}
if let Some(target_module) = rewrite.target_module.as_deref() {
for exact_lookup in
alias_target_qualified_class_lookup_names(name, &lookup, target_module, ctx)
{
out.extend(collect(&exact_lookup));
}
if !out.is_empty() {
dedup_symbols(&mut out);
return out;
}
for alias_lookup in alias_bound_class_lookup_names(name, &lookup) {
let mut candidates = collect(&alias_lookup);
candidates.retain(|sym| symbol_in_alias_target(global, *sym, target_module, ctx));
out.extend(candidates);
}
if !out.is_empty() {
dedup_symbols(&mut out);
return out;
}
}
let tail = bonsai_common::short_qualified_tail(&lookup);
if tail != lookup {
let mut candidates: Vec<SymbolId> = collect(tail);
if let Some(target_module) = rewrite.target_module.as_deref() {
candidates.retain(|sym| {
global.decl_of(*sym).is_some_and(|decl| {
let path_match = global
.declaring_file(*sym)
.is_some_and(|file| alias_target_matches_file(ctx, target_module, file));
module_target_matches_decl_module_path_from_context(
target_module,
&decl.module_path,
ctx,
) || path_match
})
});
}
out.extend(candidates);
if !out.is_empty() {
dedup_symbols(&mut out);
return out;
}
}
}
dedup_symbols(&mut out);
return out;
}
for lookup in type_lookup_variants(name) {
out.extend(collect_caller_lexical_scope(&lookup));
if !out.is_empty() {
dedup_symbols(&mut out);
return out;
}
}
if out.is_empty() && unqualified_lookup_name(name) {
for lookup in type_lookup_variants(name) {
for target_module in wildcard_import_modules(ctx) {
let mut candidates = collect(&lookup);
candidates.retain(|symbol| symbol_in_alias_target(global, *symbol, target_module, ctx));
out.extend(candidates);
}
if !out.is_empty() {
dedup_symbols(&mut out);
return out;
}
}
}
out
}
fn relative_qualified_type_matches(
observed: &[&str],
wanted: &[&str],
caller_module: &bonsai_lang_api::ModulePath,
) -> bool {
if observed == wanted {
return true;
}
(1..=caller_module.segments.len()).rev().any(|prefix_len| {
observed.len() == prefix_len + wanted.len()
&& observed[..prefix_len]
.iter()
.zip(&caller_module.segments[..prefix_len])
.all(|(observed, expected)| *observed == expected)
&& observed[prefix_len..] == *wanted
})
}
fn alias_target_qualified_class_lookup_names(
local_name: &str,
rewritten: &str,
target_module: &str,
ctx: &ResolveContext<'_>,
) -> Vec<String> {
let Some(target_segments) = relative_module_target_segments(target_module, ctx.caller_module) else {
return Vec::new();
};
if target_segments.is_empty() {
return Vec::new();
}
let module_prefix = target_segments.join(".");
let mut out = Vec::new();
for alias_lookup in alias_bound_class_lookup_names(local_name, rewritten) {
let tail = bonsai_common::short_qualified_tail(&alias_lookup).trim();
if !tail.is_empty() {
push_unique(&mut out, format!("{module_prefix}.{tail}"));
}
}
if let Some(module_leaf) = target_segments.last() {
push_unique(&mut out, format!("{module_prefix}.{module_leaf}"));
}
out
}
fn alias_bound_class_lookup_names(local_name: &str, rewritten: &str) -> Vec<String> {
let mut out = Vec::new();
for value in [local_name.trim(), rewritten.trim()] {
push_unique(&mut out, value.to_string());
let tail = bonsai_common::short_qualified_tail(value);
if tail != value {
push_unique(&mut out, tail.trim().to_string());
}
}
out
}
fn type_lookup_variants(raw: &str) -> Vec<String> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return Vec::new();
}
let mut out = Vec::new();
push_unique(&mut out, trimmed.to_string());
let without_array = strip_trailing_array_suffixes(trimmed);
push_unique(&mut out, without_array.to_string());
let without_nullable = without_array.trim_end_matches('?').trim();
push_unique(&mut out, without_nullable.to_string());
let erased = erase_angle_generics(without_nullable);
push_unique(&mut out, erased.trim().to_string());
out
}
fn strip_trailing_array_suffixes(mut text: &str) -> &str {
loop {
let trimmed = text.trim_end();
if let Some(rest) = trimmed.strip_suffix("[]") {
text = rest.trim_end();
continue;
}
return trimmed;
}
}
fn erase_angle_generics(text: &str) -> String {
let mut out = String::with_capacity(text.len());
let mut depth = 0usize;
for ch in text.chars() {
match ch {
'<' => depth = depth.saturating_add(1),
'>' => depth = depth.saturating_sub(1),
_ if depth == 0 => out.push(ch),
_ => {}
}
}
out
}
fn push_unique(out: &mut Vec<String>, value: String) {
if !value.is_empty() && !out.iter().any(|existing| existing == &value) {
out.push(value);
}
}
fn dedup_symbols(out: &mut Vec<SymbolId>) {
let mut seen = AHashSet::new();
out.retain(|symbol| seen.insert(*symbol));
}
fn dedup_func_ids(out: &mut Vec<bonsai_common::FuncId>) {
let mut seen = AHashSet::new();
out.retain(|func| seen.insert(func.raw()));
}
#[doc(hidden)]
#[must_use]
pub fn resolve_callable(global: &GlobalIndex, name: &str) -> Vec<bonsai_common::FuncId> {
use bonsai_lang_api::DeclKind;
let collect = |lookup: &str| {
global
.find_by_name(lookup)
.iter()
.filter_map(|symbol| global.decl_of(*symbol))
.filter(|decl| {
matches!(
decl.kind,
DeclKind::Function | DeclKind::Method | DeclKind::Constructor
)
})
.map(|decl| bonsai_common::FuncId::new(decl.symbol.raw()))
.collect::<Vec<_>>()
};
collect(name)
}
#[must_use]
pub fn short_tail(name: &str) -> &str {
short_qualified_tail(name)
}
#[must_use]
pub fn alias_map_for_file(imports: &[ImportSpec]) -> AHashMap<String, String> {
let mut map: AHashMap<String, String> = AHashMap::new();
for import in imports {
if let (Some(local), Some(original)) = (import.alias.as_deref(), import.original_name.as_deref()) {
if local != original && !local.is_empty() && !original.is_empty() {
map.insert(local.to_string(), original.to_string());
}
}
if let Some(local) = import.alias.as_deref() {
if import.original_name.is_none() && !local.is_empty() && !import.module.is_empty() {
map.entry(local.to_string())
.or_insert_with(|| import.module.clone());
}
}
if !import.is_wildcard && import.alias.is_none() && import.original_name.is_none() {
if let Some(local) = module_local_binding(&import.module) {
map.entry(local).or_insert_with(|| import.module.clone());
}
}
}
map
}
#[must_use]
pub fn semantic_import_binding_map_for_file(imports: &[ImportSpec]) -> AHashMap<String, String> {
let mut map = AHashMap::new();
for import in imports {
if import.is_wildcard {
if let Some(alias) = import.alias.as_deref().filter(|alias| !alias.is_empty()) {
map.insert(alias.to_string(), import.module.clone());
}
continue;
}
if let Some(member) = import
.original_name
.as_deref()
.filter(|member| !member.is_empty())
{
let local = import.alias.as_deref().unwrap_or(member);
if local.is_empty() {
continue;
}
let target = if import.module.trim().is_empty() {
member.to_string()
} else {
format!("{}.{}", import.module.trim(), member)
};
map.insert(local.to_string(), target);
continue;
}
if let Some(local) = import.alias.as_deref().filter(|alias| !alias.is_empty()) {
map.insert(local.to_string(), import.module.clone());
continue;
}
if let Some(local) = module_local_binding(&import.module) {
map.insert(local, import.module.clone());
}
}
map
}
#[cfg(test)]
mod tests;