use super::index::{name_probe, DefIndex};
use super::members::inheritance_walk_matches;
use super::receiver::{
extractor_vouches_instance, is_this_shaped_receiver, nested_candidate_visible_from_site,
};
use super::scope::FileContext;
use crate::graph::{Candidate, FragRef};
use std::collections::{HashMap, HashSet};
const AMBIGUOUS_CAP: usize = 5;
pub(super) struct Admission<'m> {
pub(super) model: Option<&'m crate::project::ProjectModel>,
pub(super) unit_of_def: Vec<Option<usize>>,
}
impl Admission<'_> {
pub(super) fn admits(&self, site_unit: Option<usize>, cand: usize) -> bool {
let Some(model) = self.model else {
return true;
};
let (Some(site), Some(cand)) = (site_unit, self.unit_of_def.get(cand).copied().flatten())
else {
return true;
};
model.reachable(site, cand) && !(model.units[cand].test && !model.units[site].test)
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(super) enum Via {
Alias,
Nested,
Qualified,
Usings,
Namespace,
Suffix,
Global,
}
pub(super) enum Resolution {
Resolved(usize, Via),
Ambiguous(Vec<usize>, Via),
External,
}
pub(super) fn narrow_by_reachability(
res: Resolution,
site_unit: Option<usize>,
admission: &Admission,
) -> Resolution {
let Resolution::Ambiguous(candidates, via) = res else {
return res;
};
let reachable: Vec<usize> = candidates
.into_iter()
.filter(|&c| admission.admits(site_unit, c))
.collect();
match reachable.as_slice() {
[] => Resolution::External,
[idx] => Resolution::Resolved(*idx, via),
_ => Resolution::Ambiguous(reachable, via),
}
}
pub(super) struct Narrowed {
pub(super) res: Resolution,
pub(super) narrowed_away: bool,
}
pub(super) fn narrow_tracked(
res: Resolution,
site_unit: Option<usize>,
admission: &Admission,
) -> Narrowed {
let was_ambiguous = matches!(res, Resolution::Ambiguous(..));
let res = narrow_by_reachability(res, site_unit, admission);
Narrowed {
narrowed_away: was_ambiguous && matches!(res, Resolution::External),
res,
}
}
pub(super) fn type_qualifier_arm_admits(via: Via, id: &str, r: &FragRef) -> bool {
(via != Via::Global || id.contains('+'))
&& (r.receiver_type.is_none() || is_this_shaped_receiver(r))
}
fn written_type_path(qualified: &str) -> std::borrow::Cow<'_, str> {
let body = match qualified.find("::") {
Some(at) => &qualified[at + 2..],
None => qualified,
};
if !body.contains('<') {
return std::borrow::Cow::Borrowed(body);
}
let mut out = String::with_capacity(body.len());
let mut depth = 0usize;
for c in body.chars() {
match c {
'<' => depth += 1,
'>' => depth = depth.saturating_sub(1),
_ if depth == 0 => out.push(c),
_ => {}
}
}
std::borrow::Cow::Owned(out)
}
fn def_path_ends_with(id: &str, written: &str) -> bool {
let (id, written) = (id.as_bytes(), written.as_bytes());
if id.len() < written.len() {
return false;
}
let (head, tail) = id.split_at(id.len() - written.len());
let boundary = head.last().is_none_or(|&b| b == b'.' || b == b'+');
boundary
&& tail
.iter()
.zip(written)
.all(|(&a, &b)| a == b || (a == b'+' && b == b'.'))
}
pub(super) fn type_candidate(index: &DefIndex, name: &str, arity: Option<usize>) -> Option<usize> {
match arity {
Some(n) => index
.qualified_name_and_arity_to_def
.get(&(name.to_string(), n))
.copied(),
None => index.qualified_name_to_def.get(name).copied(),
}
}
#[allow(
clippy::too_many_lines,
clippy::cognitive_complexity,
reason = "one ordered ladder of resolution steps tried in a fixed order; splitting it would separate steps whose fallthrough order is the whole point"
)]
pub(super) fn resolve_ref(
ref_: &FragRef,
usings: &HashSet<String>,
ns: &str,
index: &DefIndex,
aliases: &HashMap<String, String>,
file_contexts: &HashMap<String, FileContext>,
) -> Resolution {
let segments: Vec<&str> = if ns.is_empty() {
Vec::new()
} else {
ns.split('.').collect()
};
let prefixes: Vec<String> = (0..=segments.len())
.rev()
.map(|i| segments[..i].join("."))
.collect();
if ref_.qualified.is_none() {
if let Some(alias_target) = aliases.get(&ref_.name) {
return match type_candidate(index, alias_target, ref_.type_arg_count) {
Some(idx) => Resolution::Resolved(idx, Via::Alias),
None => Resolution::External,
};
}
for i in (1..=ref_.outer_types.len()).rev() {
let stack = ref_.outer_types[..i].join("+");
let candidate = if ns.is_empty() {
format!("{stack}+{}", ref_.name)
} else {
format!("{ns}.{stack}+{}", ref_.name)
};
if let Some(idx) = type_candidate(index, &candidate, ref_.type_arg_count) {
return Resolution::Resolved(idx, Via::Nested);
}
}
}
if let Some(qualified) = &ref_.qualified {
for prefix in &prefixes {
let candidate = if prefix.is_empty() {
qualified.clone()
} else {
format!("{prefix}.{qualified}")
};
if let Some(idx) = type_candidate(index, &candidate, ref_.type_arg_count) {
return Resolution::Resolved(idx, Via::Qualified);
}
}
let written = written_type_path(qualified);
let expanded: Option<String> = written
.split_once('.')
.and_then(|(head, rest)| aliases.get(head).map(|target| format!("{target}.{rest}")));
if let Some(expanded) = &expanded {
if let Some(idx) = type_candidate(index, expanded, ref_.type_arg_count) {
return Resolution::Resolved(idx, Via::Qualified);
}
}
let written = expanded.as_deref().unwrap_or(&written);
let pool: Vec<usize> = index
.simple_name_to_defs
.get(&ref_.name)
.into_iter()
.flatten()
.copied()
.filter(|idx| {
ref_.type_arg_count
.is_none_or(|n| index.member_lists[*idx].type_params.len() == n)
})
.collect();
let mut matches: Vec<usize> = pool
.iter()
.copied()
.filter(|idx| def_path_ends_with(&index.defs[*idx].id, written))
.collect();
if matches.is_empty() && pool.iter().any(|idx| index.defs[*idx].id.contains('+')) {
if let Some((qualifier, _)) = written.rsplit_once('.') {
let (head, tail) = match qualifier.rsplit_once('.') {
Some((_, tail)) => (Some(qualifier.to_string()), tail),
None => (None, qualifier),
};
let probe = FragRef {
name: tail.to_string(),
qualified: head,
..name_probe(String::new(), ns, ref_.outer_types.clone())
};
if let Resolution::Resolved(qidx, _) =
resolve_ref(&probe, usings, ns, index, aliases, file_contexts)
{
matches = pool
.iter()
.copied()
.filter(|idx| {
index.defs[*idx]
.id
.rsplit_once('+')
.and_then(|(enclosing, _)| {
index.qualified_name_to_def.get(enclosing)
})
.is_some_and(|&enclosing| {
inheritance_walk_matches(index, file_contexts, qidx, |i| {
i == enclosing
})
})
})
.collect();
}
}
}
match matches.as_slice() {
[idx] => return Resolution::Resolved(*idx, Via::Suffix),
[_, _, ..] => return Resolution::Ambiguous(matches, Via::Suffix),
_ => {}
}
}
if let Some(qualified) = ref_
.qualified
.as_ref()
.filter(|_| !extractor_vouches_instance(ref_))
{
let segs: Vec<&str> = qualified.split('.').collect();
let leaf_known = segs
.last()
.is_some_and(|leaf| index.simple_name_to_defs.contains_key(*leaf));
for k in 1..segs.len() {
if !leaf_known {
break;
}
let head_idx = if k == 1 {
let mut head = ref_.clone();
head.name = segs[0].to_string();
head.qualified = None;
head.type_arg_count = None;
match resolve_ref(&head, usings, ns, index, aliases, file_contexts) {
Resolution::Resolved(idx, _) => Some(idx),
_ => None,
}
} else {
let head = segs[..k].join(".");
prefixes.iter().find_map(|prefix| {
let candidate = if prefix.is_empty() {
head.clone()
} else {
format!("{prefix}.{head}")
};
type_candidate(index, &candidate, None)
})
};
let Some(start) = head_idx else {
continue;
};
let mut cur = start;
let mut complete = true;
let last = segs.len() - 1;
for (i, seg) in segs.iter().enumerate().skip(k) {
let arity = if i == last { ref_.type_arg_count } else { None };
match type_candidate(index, &format!("{}+{}", index.defs[cur].id, seg), arity) {
Some(next) => cur = next,
None => {
complete = false;
break;
}
}
}
if complete {
return Resolution::Resolved(cur, Via::Qualified);
}
}
}
if ref_.qualified.is_some() {
return Resolution::External;
}
let mut using_matches: Vec<usize> = Vec::new();
let mut seen_ids: HashSet<&str> = HashSet::new();
for u in usings {
for prefix in &prefixes {
let candidate = if prefix.is_empty() {
format!("{u}.{}", ref_.name)
} else {
format!("{prefix}.{u}.{}", ref_.name)
};
if let Some(idx) = type_candidate(index, &candidate, ref_.type_arg_count) {
let id = index.defs[idx].id.as_str();
if seen_ids.insert(id) {
using_matches.push(idx);
}
break;
}
}
}
if using_matches.len() == 1 {
return Resolution::Resolved(using_matches[0], Via::Usings);
}
if using_matches.len() >= 2 {
return Resolution::Ambiguous(using_matches, Via::Usings);
}
for prefix in &prefixes {
let candidate = if prefix.is_empty() {
ref_.name.clone()
} else {
format!("{prefix}.{}", ref_.name)
};
if let Some(idx) = type_candidate(index, &candidate, ref_.type_arg_count) {
return Resolution::Resolved(idx, Via::Namespace);
}
}
let matches: Vec<usize> = index
.simple_name_to_defs
.get(&ref_.name)
.into_iter()
.flatten()
.copied()
.filter(|idx| {
ref_.type_arg_count
.map_or(true, |n| index.member_lists[*idx].type_params.len() == n)
})
.filter(|idx| nested_candidate_visible_from_site(ref_, ns, *idx, index, file_contexts))
.collect();
match matches.as_slice() {
[idx] => Resolution::Resolved(*idx, Via::Global),
[_, _, ..] => Resolution::Ambiguous(matches, Via::Global),
_ => Resolution::External,
}
}
pub(super) fn capped_candidates(
index: &DefIndex,
mut candidate_indices: Vec<usize>,
) -> Vec<Candidate> {
candidate_indices.sort_by(|&a, &b| index.defs[a].id.cmp(&index.defs[b].id));
candidate_indices
.into_iter()
.take(AMBIGUOUS_CAP)
.map(|i| Candidate {
id: index.defs[i].id.clone(),
file: index.defs[i].file.clone(),
})
.collect()
}