use super::arity::{arity_accepts, base_arity, generic_args_unify, resolve_ref_by_arity};
use super::index::{name_probe, DefIndex, MethodOverloadParams};
use super::ladder::{resolve_ref, Resolution};
use super::members::{
base_member_declared, declares_member, extension_closure_key, inheritance_walk_find,
inheritance_walk_matches, MemberShape,
};
use super::scope::FileContext;
use crate::graph::{Def, FragFact, FragLambdaSlot, FragRef};
use std::collections::{HashMap, HashSet};
pub(super) fn extractor_vouches_instance(r: &FragRef) -> bool {
r.receiver_type.is_some()
|| r.receiver_local
|| r.receiver_property_owner.is_some()
|| r.receiver_call_owner.is_some()
}
pub(super) fn is_this_shaped_receiver(r: &FragRef) -> bool {
match (&r.receiver_type, r.outer_types.last()) {
(Some(rt), Some(outer)) => rt == outer,
_ => false,
}
}
fn declared_field_or_property_type<'a>(
index: &'a DefIndex,
idx: usize,
name: &str,
) -> Option<(&'a FragFact, &'a str)> {
let lists = &index.member_lists[idx];
let (fact, files) = match lists.field_types.get(name) {
Some(fact) => (fact, &lists.field_type_files),
None => (lists.property_types.get(name)?, &lists.property_type_files),
};
let file = files
.get(name)
.map_or(index.defs[idx].file.as_str(), String::as_str);
Some((fact, file))
}
pub(super) struct ReceiverFieldType {
pub(super) type_name: String,
pub(super) declaring_def: usize,
pub(super) declaring_file: String,
}
pub(super) fn def_outer_types(def: &Def) -> Vec<String> {
let chain = if def.namespace.is_empty() {
def.id.as_str()
} else {
def.id
.strip_prefix(&format!("{}.", def.namespace))
.unwrap_or(def.name.as_str())
};
chain.split('+').map(str::to_string).collect()
}
pub(super) fn bare_receiver_field_or_property_type(
index: &DefIndex,
ns: &str,
r: &FragRef,
usings: &HashSet<String>,
aliases: &HashMap<String, String>,
file_contexts: &HashMap<String, FileContext>,
) -> Option<ReceiverFieldType> {
let innermost = r.outer_types.last()?;
let probe = name_probe(innermost.clone(), ns, r.outer_types.clone());
let Resolution::Resolved(start, _) =
resolve_ref(&probe, usings, ns, index, aliases, file_contexts)
else {
return None;
};
if let Some((fact, declaring_file)) = declared_field_or_property_type(index, start, &r.name) {
return Some(ReceiverFieldType {
type_name: fact.type_name.clone(),
declaring_def: start,
declaring_file: declaring_file.to_string(),
});
}
let ctx = file_contexts.get(&index.defs[start].file)?;
let base_ns = index.defs[start].namespace.clone();
for base in &index.member_lists[start].bases {
let probe = name_probe(base.clone(), &base_ns, Vec::new());
if let Resolution::Resolved(bidx, _) = resolve_ref_by_arity(
probe,
Some(base_arity(index, start, base)),
&ctx.usings,
&base_ns,
index,
&ctx.aliases,
file_contexts,
) {
let mut found: Option<ReceiverFieldType> = None;
inheritance_walk_find(index, file_contexts, bidx, |idx| {
match declared_field_or_property_type(index, idx, &r.name) {
Some((fact, declaring_file)) => {
found = Some(ReceiverFieldType {
type_name: fact.type_name.clone(),
declaring_def: idx,
declaring_file: declaring_file.to_string(),
});
true
}
None => false,
}
});
if found.is_some() {
return found;
}
}
}
None
}
fn descriptor_head(text: &str) -> &str {
let end = text.find(['<', '[', '?']).unwrap_or(text.len());
&text[..end]
}
fn descriptor_args(text: &str) -> Vec<String> {
let Some(open) = text.find('<') else {
return Vec::new();
};
if !text.ends_with('>') {
return Vec::new();
}
let inner = &text[open + 1..text.len() - 1];
let mut args: Vec<String> = Vec::new();
let mut depth = 0usize;
let mut start = 0usize;
for (i, ch) in inner.char_indices() {
match ch {
'<' => depth += 1,
'>' => depth = depth.saturating_sub(1),
',' if depth == 0 => {
args.push(inner[start..i].to_string());
start = i + ch.len_utf8();
}
_ => {}
}
}
args.push(inner[start..].to_string());
args
}
fn delegate_parameters(
text: &str,
index: &DefIndex,
file_contexts: &HashMap<String, FileContext>,
declaring_def: usize,
declaring_file: &str,
) -> Option<Vec<String>> {
if text.ends_with(']') {
return None;
}
let args = descriptor_args(text);
if descriptor_head(text) == "Expression" && args.len() == 1 {
return delegate_invoke_parameters(
&args[0],
index,
file_contexts,
declaring_def,
declaring_file,
);
}
delegate_invoke_parameters(text, index, file_contexts, declaring_def, declaring_file)
}
fn delegate_invoke_parameters(
text: &str,
index: &DefIndex,
file_contexts: &HashMap<String, FileContext>,
declaring_def: usize,
declaring_file: &str,
) -> Option<Vec<String>> {
if text.ends_with(']') {
return None;
}
let head = descriptor_head(text);
let args = descriptor_args(text);
match head {
"Action" if !args.is_empty() => Some(args),
"Func" if args.len() >= 2 => {
let mut params = args;
params.pop();
Some(params)
}
"Predicate" if args.len() == 1 => Some(args),
"Action" | "Func" | "Predicate" | "" => None,
_ => {
let ctx = file_contexts.get(declaring_file)?;
let def = &index.defs[declaring_def];
let probe = name_probe(
head.to_string(),
def.namespace.as_str(),
def_outer_types(def),
);
let Resolution::Resolved(didx, _) = resolve_ref(
&probe,
&ctx.usings,
def.namespace.as_str(),
index,
&ctx.aliases,
file_contexts,
) else {
return None;
};
if index.defs[didx].kind != "delegate" {
return None;
}
index.member_lists[didx]
.method_params
.get("Invoke")
.and_then(|overloads| overloads.first())
.map(|o| o.params.clone())
}
}
}
pub(super) fn lambda_slot_receiver_type(
index: &DefIndex,
file_contexts: &HashMap<String, FileContext>,
ns: &str,
usings: &HashSet<String>,
aliases: &HashMap<String, String>,
r: &FragRef,
slot: &FragLambdaSlot,
) -> Option<ReceiverFieldType> {
let probe = name_probe(slot.owner.clone(), ns, r.outer_types.clone());
let owner = match resolve_ref(&probe, usings, ns, index, aliases, file_contexts) {
Resolution::Resolved(oidx, _) => Some(oidx),
_ => None,
};
let mut candidates: Vec<(&MethodOverloadParams, usize, usize)> = Vec::new();
if let Some(oidx) = owner {
let declaring = if index.member_lists[oidx]
.method_params
.contains_key(&slot.member)
{
Some(oidx)
} else {
base_member_declared(index, file_contexts, oidx, Some(&slot.member), None)
};
if let Some(didx) = declaring {
if let Some(overloads) = index.member_lists[didx].method_params.get(&slot.member) {
for overload in overloads {
candidates.push((overload, didx, slot.arg_index));
}
}
}
}
if candidates.is_empty() {
let key = match owner {
Some(oidx) => {
extension_closure_key(index, file_contexts, oidx, &slot.member).map(|(k, _)| k)
}
None => Some(format!("{} {}", slot.member, slot.owner)),
};
if let Some(candidate_list) = key.and_then(|k| index.extension_index.get(&k)) {
for cand in candidate_list {
let Some(overloads) = index.member_lists[cand.def_idx]
.method_params
.get(&slot.member)
else {
continue;
};
for overload in overloads {
let matches_this = overload
.params
.first()
.and_then(|p| p.strip_prefix("this "))
.is_some_and(|t| descriptor_head(t) == cand.entry.this_type);
if matches_this {
candidates.push((overload, cand.def_idx, slot.arg_index + 1));
}
}
}
}
}
let mut agreed: Option<(String, String, usize, String)> = None;
for (overload, declaring_def, position) in candidates {
let shift = position - slot.arg_index;
if slot.arg_count + shift > overload.params.len() || position >= overload.params.len() {
continue;
}
let text = overload.params[position]
.strip_prefix("this ")
.unwrap_or(&overload.params[position]);
let Some(params) =
delegate_parameters(text, index, file_contexts, declaring_def, &overload.file)
else {
continue;
};
if params.len() != slot.arity || slot.index >= params.len() {
continue;
}
let head = descriptor_head(¶ms[slot.index]);
if head.is_empty() || head == "*" {
return None;
}
let meaning = descriptor_meaning(index, file_contexts, declaring_def, &overload.file, head);
match &agreed {
Some((known, _, _, _)) if *known != meaning => return None,
Some(_) => {}
None => {
agreed = Some((
meaning,
head.to_string(),
declaring_def,
overload.file.clone(),
))
}
}
}
agreed.map(
|(_, type_name, declaring_def, declaring_file)| ReceiverFieldType {
type_name,
declaring_def,
declaring_file,
},
)
}
fn descriptor_meaning(
index: &DefIndex,
file_contexts: &HashMap<String, FileContext>,
declaring_def: usize,
declaring_file: &str,
head: &str,
) -> String {
let def = &index.defs[declaring_def];
let resolved = file_contexts.get(declaring_file).and_then(|ctx| {
let probe = name_probe(
head.to_string(),
def.namespace.as_str(),
def_outer_types(def),
);
match resolve_ref(
&probe,
&ctx.usings,
def.namespace.as_str(),
index,
&ctx.aliases,
file_contexts,
) {
Resolution::Resolved(didx, _) => Some(index.defs[didx].id.clone()),
_ => None,
}
});
resolved.unwrap_or_else(|| format!("?{head}"))
}
fn nominally_assignable(
index: &DefIndex,
file_contexts: &HashMap<String, FileContext>,
start: usize,
type_name: &str,
receiver_args: Option<&Vec<String>>,
args_known: bool,
) -> bool {
inheritance_walk_matches(index, file_contexts, start, |idx| {
(index.defs[idx].name == type_name
&& (!args_known
|| index.member_lists[idx].type_params.len() == receiver_args.map_or(0, Vec::len)))
|| index.member_lists[idx].bases.iter().any(|b| {
b == type_name
&& (!args_known
|| generic_args_unify(
index.member_lists[idx]
.base_generic_args
.iter()
.find(|(k, _)| k == b)
.map(|(_, v)| v),
receiver_args,
))
})
})
}
pub(super) type AssignabilityCache = HashMap<(usize, String, Option<Vec<String>>, bool), bool>;
fn nominally_assignable_cached(
cache: &mut AssignabilityCache,
index: &DefIndex,
file_contexts: &HashMap<String, FileContext>,
start: usize,
type_name: &str,
receiver_args: Option<&Vec<String>>,
args_known: bool,
) -> bool {
let key = (
start,
type_name.to_string(),
receiver_args.cloned(),
args_known,
);
if let Some(&answer) = cache.get(&key) {
return answer;
}
let answer = nominally_assignable(
index,
file_contexts,
start,
type_name,
receiver_args,
args_known,
);
cache.insert(key, answer);
answer
}
#[allow(clippy::too_many_arguments)]
pub(super) fn receiver_admits_candidate(
index: &DefIndex,
file_contexts: &HashMap<String, FileContext>,
cache: &mut AssignabilityCache,
candidate: usize,
member: &str,
shape: MemberShape,
arg_count: Option<usize>,
receiver_type: &str,
receiver_args: Option<&Vec<String>>,
args_known: bool,
) -> bool {
let instance_vouches = match shape {
MemberShape::Call => index.defs[candidate].methods.iter().any(|m| m == member),
MemberShape::Read => declares_member(index, candidate, Some(member), None),
};
if instance_vouches
&& nominally_assignable_cached(
cache,
index,
file_contexts,
candidate,
receiver_type,
receiver_args,
args_known,
)
{
return true;
}
index
.extension_index
.get(&format!("{member} {receiver_type}"))
.map(Vec::as_slice)
.unwrap_or(&[])
.iter()
.any(|c| {
c.def_idx == candidate
&& arg_count.is_none_or(|n| arity_accepts(&c.entry, n))
&& generic_args_unify(c.entry.this_args.as_ref(), receiver_args)
})
}
pub(super) fn nested_candidate_visible_from_site(
ref_: &FragRef,
ns: &str,
candidate: usize,
index: &DefIndex,
file_contexts: &HashMap<String, FileContext>,
) -> bool {
let Some((enclosing_id, _)) = index.defs[candidate].id.rsplit_once('+') else {
return true;
};
let Some(&enclosing_candidate) = index.qualified_name_to_def.get(enclosing_id) else {
return false;
};
for depth in (1..=ref_.outer_types.len()).rev() {
let enclosing_site = ref_.outer_types[..depth].join("+");
let enclosing_site = if ns.is_empty() {
enclosing_site
} else {
format!("{ns}.{enclosing_site}")
};
if let Some(&site_idx) = index.qualified_name_to_def.get(&enclosing_site) {
return inheritance_walk_matches(index, file_contexts, site_idx, |idx| {
idx == enclosing_candidate
});
}
}
false
}