use std::collections::{HashMap, HashSet};
use super::edges::type_probe;
use super::index::DefIndex;
use super::ladder::{resolve_ref, Resolution};
use super::receiver::def_outer_types;
use super::scope::FileContext;
use crate::graph::{FragDef, Fragment};
const ROOT_CONSUMER_BASES: &[&str] = &[
"IConsumer",
"BaseConsumer",
"IAmInitiatedBy",
"IWorkHandler",
"IRequestHandler",
];
const ROOT_PUBLISH_VERBS: &[&str] = &["Publish", "PublishAsync", "SubmitJob", "Reply", "Send"];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(super) struct MessageIdentity {
pub(super) def_idx: usize,
pub(super) rank: u8,
}
impl MessageIdentity {
pub(super) fn plain(def_idx: usize) -> Self {
MessageIdentity { def_idx, rank: 0 }
}
pub(super) fn rendered_id(&self, index: &DefIndex) -> String {
let mut id = index.defs[self.def_idx].id.clone();
for _ in 0..self.rank {
id.push_str("[]");
}
id
}
}
fn split_array_suffix(text: &str) -> (&str, u8) {
let mut rest = text;
let mut rank: u8 = 0;
while let Some(stripped) = rest.strip_suffix("[]") {
rest = stripped;
rank = rank.saturating_add(1);
}
(rest, rank)
}
pub(super) fn resolve_message_identity(
raw: &str,
ns: &str,
outer_types: &[String],
file_ctx: &FileContext,
index: &DefIndex,
file_contexts: &HashMap<String, FileContext>,
) -> Option<MessageIdentity> {
let (element, rank) = split_array_suffix(raw);
let def_idx = resolve_type_text(element, ns, outer_types, file_ctx, index, file_contexts)?;
Some(MessageIdentity { def_idx, rank })
}
pub(super) struct BusVocabulary {
pub(super) consumer_bases: HashSet<String>,
pub(super) binding_only_bases: HashSet<String>,
}
pub(super) fn resolve_type_text(
raw: &str,
ns: &str,
outer_types: &[String],
file_ctx: &FileContext,
index: &DefIndex,
file_contexts: &HashMap<String, FileContext>,
) -> Option<usize> {
let probe = type_probe(raw, ns, outer_types);
match resolve_ref(
&probe,
&file_ctx.usings,
ns,
index,
&file_ctx.aliases,
file_contexts,
) {
Resolution::Resolved(idx, _) => Some(idx),
Resolution::Ambiguous(..) | Resolution::External => None,
}
}
fn simple_name(id: &str) -> &str {
let tail = id.rsplit_once('.').map_or(id, |(_, t)| t);
tail.rsplit_once('+').map_or(tail, |(_, t)| t)
}
fn split_top_level_args(inner: &str) -> Vec<String> {
let mut parts = Vec::new();
let mut depth: i32 = 0;
let mut start = 0usize;
for (i, ch) in inner.char_indices() {
match ch {
'<' => depth += 1,
'>' => depth -= 1,
',' if depth == 0 => {
parts.push(inner[start..i].to_string());
start = i + 1;
}
_ => {}
}
}
parts.push(inner[start..].to_string());
parts
}
fn single_nested_argument(descriptor: &str) -> Option<String> {
let open = descriptor.find('<')?;
if !descriptor.ends_with('>') {
return None;
}
let inner = &descriptor[open + 1..descriptor.len() - 1];
match split_top_level_args(inner).as_slice() {
[single] if !single.is_empty() => Some(single.clone()),
_ => None,
}
}
pub(super) fn concrete_base_argument(
nested: Option<&[String]>,
flat: &[String],
first_arg_is_array: bool,
) -> Option<(String, bool)> {
let first_flat = flat.first()?;
if first_flat == "*" {
return None;
}
if let Some(inner) = nested
.and_then(<[String]>::first)
.and_then(|d| single_nested_argument(d))
{
return Some((inner, true));
}
let message = if first_arg_is_array {
format!("{first_flat}[]")
} else {
first_flat.clone()
};
Some((message, false))
}
pub(super) fn derive_publish_verbs(fragments_by_file: &[(String, Fragment)]) -> HashSet<String> {
let mut publish_verbs: HashSet<String> = ROOT_PUBLISH_VERBS
.iter()
.map(|s| (*s).to_string())
.collect();
harvest_wrappers(fragments_by_file, &mut publish_verbs);
publish_verbs
}
pub(super) fn derive_consumer_bases(
fragments_by_file: &[(String, Fragment)],
index: &DefIndex,
file_contexts: &HashMap<String, FileContext>,
sent: &HashSet<MessageIdentity>,
) -> (HashSet<String>, HashSet<String>, bool) {
let mut consumer_bases: HashSet<String> = ROOT_CONSUMER_BASES
.iter()
.map(|s| (*s).to_string())
.collect();
let mut binding_only_bases: HashSet<String> = HashSet::new();
let contributed = harvest_registrations(
fragments_by_file,
index,
file_contexts,
sent,
&mut consumer_bases,
&mut binding_only_bases,
);
close_over_intermediates(
fragments_by_file,
index,
&mut consumer_bases,
&mut binding_only_bases,
);
binding_only_bases.retain(|base| !consumer_bases.contains(base));
(consumer_bases, binding_only_bases, contributed)
}
fn property_messages_by_def(
fragments_by_file: &[(String, Fragment)],
index: &DefIndex,
) -> HashMap<usize, Vec<String>> {
let mut out: HashMap<usize, Vec<String>> = HashMap::new();
for (_, frag) in fragments_by_file {
for d in &frag.defs {
if d.property_message_args.is_empty() {
continue;
}
let key = (d.id.clone(), d.type_params.len());
let Some(&def_idx) = index.qualified_name_and_arity_to_def.get(&key) else {
continue;
};
out.entry(def_idx)
.or_default()
.extend(d.property_message_args.iter().cloned());
}
}
out
}
fn def_binds_sent_message(
def_idx: usize,
property_messages: &HashMap<usize, Vec<String>>,
sent: &HashSet<MessageIdentity>,
index: &DefIndex,
file_contexts: &HashMap<String, FileContext>,
) -> bool {
let Some(messages) = property_messages.get(&def_idx) else {
return false;
};
let def = &index.defs[def_idx];
let Some(file_ctx) = file_contexts.get(&def.file) else {
return false;
};
let outer_types = def_outer_types(def);
messages.iter().any(|message| {
resolve_message_identity(
message,
&def.namespace,
&outer_types,
file_ctx,
index,
file_contexts,
)
.is_some_and(|identity| sent.contains(&identity))
})
}
fn descriptor_type_texts(descriptor: &str) -> Vec<String> {
let mut out = Vec::new();
let mut stack = vec![descriptor
.strip_prefix("this ")
.unwrap_or(descriptor)
.to_string()];
while let Some(candidate) = stack.pop() {
if let Some(open) = candidate.find('<') {
if candidate.ends_with('>') {
out.push(candidate[..open].to_string());
let inner = &candidate[open + 1..candidate.len() - 1];
stack.extend(split_top_level_args(inner));
continue;
}
}
out.push(candidate);
}
out
}
fn def_receives_message(
def_idx: usize,
message: MessageIdentity,
index: &DefIndex,
file_contexts: &HashMap<String, FileContext>,
) -> bool {
let def = &index.defs[def_idx];
let ns = &def.namespace;
let outer_types = def_outer_types(def);
for overloads in index.member_lists[def_idx].method_params.values() {
for overload in overloads {
let Some(file_ctx) = file_contexts.get(&overload.file) else {
continue;
};
let receives = overload.params.iter().any(|descriptor| {
descriptor_type_texts(descriptor).iter().any(|text| {
resolve_message_identity(text, ns, &outer_types, file_ctx, index, file_contexts)
== Some(message)
})
});
if receives {
return true;
}
}
}
false
}
fn harvest_registrations(
fragments_by_file: &[(String, Fragment)],
index: &DefIndex,
file_contexts: &HashMap<String, FileContext>,
sent: &HashSet<MessageIdentity>,
consumer_bases: &mut HashSet<String>,
binding_only_bases: &mut HashSet<String>,
) -> bool {
let mut by_id: HashMap<&str, Vec<&FragDef>> = HashMap::new();
for (_, frag) in fragments_by_file {
for d in &frag.defs {
by_id.entry(d.id.as_str()).or_default().push(d);
}
}
let property_messages = property_messages_by_def(fragments_by_file, index);
let mut contributed = false;
for (file, frag) in fragments_by_file {
if frag.handler_registrations.is_empty() {
continue;
}
let Some(file_ctx) = file_contexts.get(file) else {
continue;
};
for registration in &frag.handler_registrations {
let Some(def_idx) = resolve_type_text(
®istration.handler,
®istration.namespace,
&[],
file_ctx,
index,
file_contexts,
) else {
continue;
};
if sent.contains(&MessageIdentity::plain(def_idx)) {
continue;
}
let r = &index.defs[def_idx];
let Some(r_file_ctx) = file_contexts.get(&r.file) else {
continue;
};
let r_ns = r.namespace.clone();
let r_outer = def_outer_types(r);
let receives_bound_message =
def_binds_sent_message(def_idx, &property_messages, sent, index, file_contexts);
for d in by_id.get(r.id.as_str()).map_or(&[][..], Vec::as_slice) {
for base in &d.bases {
if consumer_bases.contains(base) {
continue; }
let Some(flat) = d.base_generic_args.get(base) else {
continue;
};
let first_arg_is_array = d.array_message_bases.contains(base);
let Some((message_text, _)) = concrete_base_argument(
d.base_type_args.get(base).map(Vec::as_slice),
flat,
first_arg_is_array,
) else {
continue;
};
let argument_admits = resolve_message_identity(
&message_text,
&r_ns,
&r_outer,
r_file_ctx,
index,
file_contexts,
)
.is_some_and(|message| {
sent.contains(&message)
&& def_receives_message(def_idx, message, index, file_contexts)
});
if argument_admits {
if consumer_bases.insert(base.clone()) {
contributed = true;
}
} else if receives_bound_message && binding_only_bases.insert(base.clone()) {
contributed = true;
}
}
}
}
}
contributed
}
fn close_over_intermediates(
fragments_by_file: &[(String, Fragment)],
index: &DefIndex,
consumer_bases: &mut HashSet<String>,
binding_only_bases: &mut HashSet<String>,
) {
loop {
let mut added = false;
for (_, frag) in fragments_by_file {
for d in &frag.defs {
let key = (d.id.clone(), d.type_params.len());
if !index.qualified_name_and_arity_to_def.contains_key(&key) {
continue;
}
let name = simple_name(&d.id);
if consumer_bases.contains(name) {
continue;
}
let mut becomes_consumer = false;
let mut becomes_binding_only = false;
for base in &d.bases {
let passes_through = d
.base_generic_args
.get(base)
.and_then(|args| args.first())
.is_some_and(|first| first == "*");
if !passes_through {
continue;
}
if consumer_bases.contains(base) {
becomes_consumer = true;
} else if binding_only_bases.contains(base) {
becomes_binding_only = true;
}
}
if becomes_consumer {
consumer_bases.insert(name.to_string());
added = true;
} else if becomes_binding_only && binding_only_bases.insert(name.to_string()) {
added = true;
}
}
}
if !added {
return;
}
}
}
fn harvest_wrappers(fragments_by_file: &[(String, Fragment)], publish_verbs: &mut HashSet<String>) {
let seed = publish_verbs.clone();
for (_, frag) in fragments_by_file {
for publish in &frag.publishes {
let Some(method) = &publish.enclosing_method else {
continue;
};
if seed.contains(&publish.verb) {
publish_verbs.insert(method.clone());
}
}
}
}