mod candidates;
pub(super) use candidates::{collect_private_candidates, PrivateCandidate};
use super::touchpoints::{compute_touchpoints, TouchpointContext};
use super::workspace_graph::CallGraph;
use crate::adapters::analyzers::architecture::compiled::CompiledCallParity;
use crate::adapters::analyzers::architecture::{MatchLocation, ViolationKind};
use std::collections::{HashMap, HashSet};
pub(super) fn enrich_with_hints(
findings: &mut [MatchLocation],
graph: &CallGraph,
cp: &CompiledCallParity,
candidates: &[PrivateCandidate],
) {
if candidates.is_empty() {
return;
}
let by_adapter = group_by_adapter(candidates);
let mut probe = HintProbe {
graph,
cp,
by_adapter: &by_adapter,
candidate_touchpoints: HashMap::new(),
};
for f in findings {
if let ViolationKind::CallParityMissingAdapter {
target_fn,
missing_adapters,
hint,
..
} = &mut f.kind
{
*hint = probe.hint_for(target_fn, missing_adapters);
}
}
}
struct HintProbe<'a> {
graph: &'a CallGraph,
cp: &'a CompiledCallParity,
by_adapter: &'a HashMap<&'a str, Vec<&'a PrivateCandidate>>,
candidate_touchpoints: HashMap<String, HashSet<String>>,
}
impl<'a> HintProbe<'a> {
fn hint_for(&mut self, target_fn: &str, missing_adapters: &[String]) -> Option<String> {
let by_adapter = self.collect_hits_per_adapter(missing_adapters, target_fn);
if by_adapter.is_empty() {
None
} else {
Some(format_hint(&by_adapter))
}
}
fn collect_hits_per_adapter(
&mut self,
missing_adapters: &[String],
target_fn: &str,
) -> Vec<(String, Vec<&'a PrivateCandidate>)> {
let mut out: Vec<(String, Vec<&PrivateCandidate>)> = Vec::new();
for adapter in missing_adapters {
let Some(adapter_candidates) = self.by_adapter.get(adapter.as_str()) else {
continue;
};
let mut hits: Vec<&PrivateCandidate> = Vec::new();
for candidate in adapter_candidates.iter().copied() {
if self.candidate_reaches_target(candidate, adapter, target_fn) {
hits.push(candidate);
}
}
if !hits.is_empty() {
hits.sort_by(|a, b| {
a.file
.cmp(&b.file)
.then(a.line.cmp(&b.line))
.then(a.fn_name.cmp(&b.fn_name))
});
out.push((adapter.clone(), hits));
}
}
out
}
fn candidate_reaches_target(
&mut self,
candidate: &PrivateCandidate,
adapter: &str,
target_fn: &str,
) -> bool {
let graph = self.graph;
let cp = self.cp;
let canonical = candidate.canonical.clone();
let touchpoints = self
.candidate_touchpoints
.entry(canonical.clone())
.or_insert_with(|| {
let ctx = TouchpointContext {
graph,
target_layer: &cp.target,
call_depth: cp.call_depth,
origin_adapter: adapter,
adapter_layers: &cp.adapters,
};
compute_touchpoints(&canonical, &ctx)
});
touchpoints.contains(target_fn)
}
}
fn group_by_adapter(candidates: &[PrivateCandidate]) -> HashMap<&str, Vec<&PrivateCandidate>> {
let mut out: HashMap<&str, Vec<&PrivateCandidate>> = HashMap::new();
for c in candidates {
if let Some(layer) = c.layer.as_deref() {
out.entry(layer).or_default().push(c);
}
}
out
}
fn format_hint(by_adapter: &[(String, Vec<&PrivateCandidate>)]) -> String {
let mut lines: Vec<String> = Vec::new();
for (adapter, hits) in by_adapter {
let (noun, verb) = if hits.len() == 1 {
("fn", "reaches")
} else {
("fns", "reach")
};
lines.push(format!(
"{} private {noun} in {adapter} transitively {verb} this target:",
hits.len()
));
for c in hits {
let attrs = c
.attr_names
.iter()
.map(|n| format!("#[{n}]"))
.collect::<Vec<_>>()
.join(" ");
lines.push(format!(
" - {}:{} {} has {} attribute(s)",
c.file, c.line, c.fn_name, attrs
));
}
}
lines.push(
"Add the attribute name to `[architecture.call_parity] promoted_attributes` if it marks a macro-generated handler entry point."
.to_string(),
);
lines.join("\n")
}