use std::collections::{HashMap, HashSet};
use super::bus_vocab::{self, BusVocabulary, MessageIdentity};
use super::index::DefIndex;
use super::receiver::def_outer_types;
use super::scope::FileContext;
use crate::graph::{Edge, EdgesByKind, FragDef, Fragment};
const EVIDENCE_BASE_ARG: &str = "base-arg";
const EVIDENCE_NESTED_BASE_ARG: &str = "nested-base-arg";
const EVIDENCE_MEDIATOR_REQUEST: &str = "mediator-request";
const EVIDENCE_PROPERTY_ARG: &str = "property-arg";
const TEST_DOUBLE_DIRECT_VERBS: &[&str] = &[
"Setup",
"SetupGet",
"SetupSet",
"SetupSequence",
"SetupAllProperties",
"Verify",
"VerifyGet",
"VerifySet",
"VerifyAll",
"VerifyNoOtherCalls",
];
#[allow(clippy::too_many_arguments)]
fn structural_non_dispatch(
verb: &str,
arg_count: usize,
message: MessageIdentity,
ns: &str,
outer_types: &[String],
file_ctx: &FileContext,
index: &DefIndex,
file_contexts: &HashMap<String, FileContext>,
) -> bool {
let Some(candidate_defs) = index.member_name_to_defs.get(verb) else {
return false;
};
let mut matched_any = false;
for &def_idx in candidate_defs {
let Some(overloads) = index.member_lists[def_idx].method_params.get(verb) else {
continue;
};
for overload in overloads {
if overload.params.len() != arg_count {
continue;
}
matched_any = true;
let takes_message = overload.params.iter().any(|descriptor| {
bus_vocab::resolve_message_identity(
descriptor,
ns,
outer_types,
file_ctx,
index,
file_contexts,
) == Some(message)
});
if !takes_message {
return false;
}
}
}
matched_any
}
struct VerbOverload {
required: usize,
total: usize,
params: Vec<String>,
}
fn verb_overloads(
fragments_by_file: &[(String, Fragment)],
index: &DefIndex,
) -> HashMap<String, Vec<VerbOverload>> {
let mut records: Vec<Vec<&FragDef>> = vec![Vec::new(); index.defs.len()];
for (_, frag) in fragments_by_file {
for d in &frag.defs {
if let Some(&idx) = index
.qualified_name_and_arity_to_def
.get(&(d.id.clone(), d.type_params.len()))
{
records[idx].push(d);
}
}
}
let mut out: HashMap<String, Vec<VerbOverload>> = HashMap::new();
for (verb, candidate_defs) in &index.member_name_to_defs {
for &def_idx in candidate_defs {
for d in &records[def_idx] {
let Some(overload_params) = d.method_params.get(verb) else {
continue;
};
let ranges = d.method_arities.get(verb);
let entry = out.entry(verb.clone()).or_default();
for (k, params) in overload_params.iter().enumerate() {
let (required, total) = ranges
.and_then(|r| r.get(k))
.map(|&(r, t)| (r, if t < 0 { usize::MAX } else { t as usize }))
.unwrap_or((params.len(), params.len()));
entry.push(VerbOverload {
required,
total,
params: params.clone(),
});
}
}
}
}
out
}
fn is_test_double_lambda(
outer_verb: &str,
arg_position: usize,
outer_arg_count: usize,
verb_overloads: &HashMap<String, Vec<VerbOverload>>,
) -> bool {
let mut aligned_any = false;
if let Some(overloads) = verb_overloads.get(outer_verb) {
for overload in overloads {
let is_this_extension = overload
.params
.first()
.is_some_and(|p| p.starts_with("this "));
let in_range = |n: usize| overload.required <= n && n <= overload.total;
if in_range(outer_arg_count) && !(is_this_extension && arg_position == 0) {
aligned_any = true;
let is_expression_tree = overload
.params
.get(arg_position)
.is_some_and(|d| d.starts_with("Expression<"));
if !is_expression_tree {
return false;
}
}
if is_this_extension && in_range(outer_arg_count + 1) {
aligned_any = true;
let is_expression_tree = overload
.params
.get(arg_position + 1)
.is_some_and(|d| d.starts_with("Expression<"));
if !is_expression_tree {
return false;
}
}
}
}
if aligned_any {
return true;
}
TEST_DOUBLE_DIRECT_VERBS.contains(&outer_verb)
}
struct RawConsumer {
def_idx: usize,
message: String,
evidence: &'static str,
}
struct ResolvedConsumer {
def_idx: usize,
message: MessageIdentity,
evidence: &'static str,
}
fn base_message(
nested: Option<&[String]>,
flat: Option<&[String]>,
first_arg_is_array: bool,
) -> Option<(String, &'static str)> {
let flat = flat?;
let (message, from_nested) =
bus_vocab::concrete_base_argument(nested, flat, first_arg_is_array)?;
let evidence = if from_nested {
EVIDENCE_NESTED_BASE_ARG
} else if flat.len() >= 2 {
EVIDENCE_MEDIATOR_REQUEST
} else {
EVIDENCE_BASE_ARG
};
Some((message, evidence))
}
fn raw_consumers(
fragments_by_file: &[(String, Fragment)],
index: &DefIndex,
vocabulary: &BusVocabulary,
) -> Vec<RawConsumer> {
let mut out = Vec::new();
let mut seen: HashSet<(usize, String)> = HashSet::new();
let mut recognized: HashSet<usize> = HashSet::new();
for (_, frag) in fragments_by_file {
for d in &frag.defs {
let key = (d.id.clone(), d.type_params.len());
let Some(&def_idx) = index.qualified_name_and_arity_to_def.get(&key) else {
continue;
};
for base_name in &d.bases {
let is_consumer = vocabulary.consumer_bases.contains(base_name);
let is_binding_only =
!is_consumer && vocabulary.binding_only_bases.contains(base_name);
if !is_consumer && !is_binding_only {
continue;
}
if is_binding_only {
recognized.insert(def_idx);
continue;
}
if !seen.insert((def_idx, base_name.clone())) {
continue;
}
let flat = d.base_generic_args.get(base_name).map(Vec::as_slice);
let nested = d.base_type_args.get(base_name).map(Vec::as_slice);
let first_arg_is_array = d.array_message_bases.contains(base_name);
if let Some((message, evidence)) = base_message(nested, flat, first_arg_is_array) {
recognized.insert(def_idx);
out.push(RawConsumer {
def_idx,
message,
evidence,
});
}
}
}
}
append_property_messages(fragments_by_file, index, &recognized, &mut seen, &mut out);
out
}
fn append_property_messages(
fragments_by_file: &[(String, Fragment)],
index: &DefIndex,
recognized: &HashSet<usize>,
seen: &mut HashSet<(usize, String)>,
out: &mut Vec<RawConsumer>,
) {
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;
};
if !recognized.contains(&def_idx) {
continue;
}
for message in &d.property_message_args {
if seen.insert((def_idx, message.clone())) {
out.push(RawConsumer {
def_idx,
message: message.clone(),
evidence: EVIDENCE_PROPERTY_ARG,
});
}
}
}
}
}
fn resolved_consumers(
fragments_by_file: &[(String, Fragment)],
index: &DefIndex,
file_contexts: &HashMap<String, FileContext>,
vocabulary: &BusVocabulary,
) -> Vec<ResolvedConsumer> {
let mut seen_resolved: HashSet<(usize, MessageIdentity)> = HashSet::new();
raw_consumers(fragments_by_file, index, vocabulary)
.into_iter()
.filter_map(|c| {
let def = &index.defs[c.def_idx];
let file_ctx = file_contexts.get(&def.file)?;
let message = bus_vocab::resolve_message_identity(
&c.message,
&def.namespace,
&def_outer_types(def),
file_ctx,
index,
file_contexts,
)?;
if !seen_resolved.insert((c.def_idx, message)) {
return None;
}
Some(ResolvedConsumer {
def_idx: c.def_idx,
message,
evidence: c.evidence,
})
})
.collect()
}
struct KeptPublish {
file: String,
line: usize,
message: MessageIdentity,
}
fn kept_publish_sites(
fragments_by_file: &[(String, Fragment)],
index: &DefIndex,
file_contexts: &HashMap<String, FileContext>,
publish_verbs: &HashSet<String>,
verb_overloads: &HashMap<String, Vec<VerbOverload>>,
) -> Vec<KeptPublish> {
let mut out = Vec::new();
for (file, frag) in fragments_by_file {
if frag.publishes.is_empty() {
continue;
}
let Some(file_ctx) = file_contexts.get(file) else {
continue;
};
for publish in &frag.publishes {
if publish.enclosing_method.is_some() {
continue;
}
if !publish_verbs.contains(&publish.verb) {
continue;
}
let Some(message) = bus_vocab::resolve_message_identity(
&publish.message,
&publish.namespace,
&publish.outer_types,
file_ctx,
index,
file_contexts,
) else {
continue;
};
if structural_non_dispatch(
&publish.verb,
publish.arg_count,
message,
&publish.namespace,
&publish.outer_types,
file_ctx,
index,
file_contexts,
) {
continue;
}
if publish.enclosing_call.as_ref().is_some_and(|c| {
is_test_double_lambda(&c.verb, c.arg_position, c.arg_count, verb_overloads)
}) {
continue;
}
out.push(KeptPublish {
file: file.clone(),
line: publish.line,
message,
});
}
}
out
}
pub(super) fn append_bus_edges(
fragments_by_file: &[(String, Fragment)],
index: &DefIndex,
file_contexts: &HashMap<String, FileContext>,
edges: &mut Vec<Edge>,
edges_by_kind: &mut EdgesByKind,
) -> Option<bool> {
if fragments_by_file
.iter()
.all(|(_, f)| f.publishes.is_empty())
{
return None;
}
let publish_verbs = bus_vocab::derive_publish_verbs(fragments_by_file);
let verb_overloads = verb_overloads(fragments_by_file, index);
let kept = kept_publish_sites(
fragments_by_file,
index,
file_contexts,
&publish_verbs,
&verb_overloads,
);
let sent: HashSet<MessageIdentity> = kept.iter().map(|k| k.message).collect();
let (consumer_bases, binding_only_bases, derived_from_registrations) =
bus_vocab::derive_consumer_bases(fragments_by_file, index, file_contexts, &sent);
let vocabulary = BusVocabulary {
consumer_bases,
binding_only_bases,
};
let consumers = resolved_consumers(fragments_by_file, index, file_contexts, &vocabulary);
if consumers.is_empty() {
return None;
}
let mut hop_count = 0usize;
for publish in &kept {
for consumer in &consumers {
if consumer.message != publish.message {
continue;
}
let target = &index.defs[consumer.def_idx];
edges.push(Edge::BusHop {
from_file: publish.file.clone(),
from_line: publish.line,
message: publish.message.rendered_id(index),
to: target.id.clone(),
to_file: target.file.clone(),
evidence: consumer.evidence.to_string(),
});
hop_count += 1;
}
}
if hop_count == 0 {
return None;
}
edges_by_kind.bus_hop = Some(hop_count);
Some(derived_from_registrations)
}