use super::anchor_index::AnchorInfo;
use super::check_b_coverage::{
build_adapter_coverage, build_adapter_reachable_targets, AdapterCoverage,
};
use super::pub_fns::PubFnInfo;
use super::workspace_graph::{canonical_name_for_pub_fn, CallGraph};
use super::HandlerTouchpoints;
use crate::adapters::analyzers::architecture::compiled::CompiledCallParity;
use crate::adapters::analyzers::architecture::{MatchLocation, ViolationKind};
use std::collections::{HashMap, HashSet};
pub(crate) fn check_missing_adapter<'ast>(
pub_fns_by_layer: &HashMap<String, Vec<PubFnInfo<'ast>>>,
graph: &CallGraph,
touchpoints: &HandlerTouchpoints,
cp: &CompiledCallParity,
) -> Vec<MatchLocation> {
let empty_targets: Vec<PubFnInfo<'ast>> = Vec::new();
let targets = pub_fns_by_layer.get(&cp.target).unwrap_or(&empty_targets);
let coverage = build_adapter_coverage(pub_fns_by_layer, touchpoints, cp);
let reachable = build_adapter_reachable_targets(&coverage, graph, &cp.target, &cp.adapters);
let ctx = TargetCtx {
cp,
coverage: &coverage,
reachable: &reachable,
};
let mut out = Vec::new();
for info in targets {
let canonical = canonical_name_for_pub_fn(info);
if graph.is_anchor_backed_concrete(&canonical, &cp.target, &cp.adapters)
&& !any_adapter_reaches_concrete(&canonical, &coverage)
{
continue;
}
if let Some(hit) = inspect_target(info, &ctx) {
out.push(hit);
}
}
for (anchor, info) in graph.target_anchor_capabilities(&cp.target, &cp.adapters) {
if let Some(hit) = inspect_anchor(anchor, info, &ctx) {
out.push(hit);
}
}
out
}
fn inspect_anchor(anchor: &str, info: &AnchorInfo, ctx: &TargetCtx<'_>) -> Option<MatchLocation> {
if is_anchor_excluded(anchor, info, ctx.cp) {
return None;
}
let reached = adapters_reaching(anchor, ctx.coverage, &ctx.cp.adapters);
let missing = adapters_missing(&reached, &ctx.cp.adapters);
if missing.is_empty() {
return None;
}
if reached.is_empty()
&& (ctx.reachable.contains(anchor)
|| any_impl_canonical_covered_or_reachable(info, ctx.coverage, ctx.reachable))
{
return None;
}
let location = info.location.as_ref()?;
let mut reached = reached;
reached.sort();
Some(MatchLocation {
file: location.file.clone(),
line: location.line,
column: location.column,
kind: ViolationKind::CallParityMissingAdapter {
target_fn: anchor.to_string(),
target_layer: ctx.cp.target.clone(),
reached_adapters: reached,
missing_adapters: missing,
},
})
}
struct TargetCtx<'a> {
cp: &'a CompiledCallParity,
coverage: &'a AdapterCoverage,
reachable: &'a HashSet<String>,
}
fn inspect_target(info: &PubFnInfo<'_>, ctx: &TargetCtx<'_>) -> Option<MatchLocation> {
let canonical = canonical_name_for_pub_fn(info);
if is_excluded(&canonical, ctx.cp) {
return None;
}
let reached = adapters_reaching(&canonical, ctx.coverage, &ctx.cp.adapters);
let missing = adapters_missing(&reached, &ctx.cp.adapters);
if missing.is_empty() {
return None;
}
if reached.is_empty() && ctx.reachable.contains(&canonical) {
return None;
}
Some(build_finding(
info,
canonical,
reached,
missing,
&ctx.cp.target,
))
}
fn any_adapter_reaches_concrete(concrete: &str, coverage: &AdapterCoverage) -> bool {
coverage.values().any(|set| set.contains(concrete))
}
fn any_impl_canonical_covered_or_reachable(
info: &AnchorInfo,
coverage: &AdapterCoverage,
reachable: &HashSet<String>,
) -> bool {
info.impl_method_canonicals.iter().any(|impl_canon| {
reachable.contains(impl_canon) || coverage.values().any(|set| set.contains(impl_canon))
})
}
fn is_excluded(canonical: &str, cp: &CompiledCallParity) -> bool {
let stripped = canonical.strip_prefix("crate::").unwrap_or(canonical);
cp.exclude_targets.is_match(stripped)
}
fn is_anchor_excluded(anchor: &str, info: &AnchorInfo, cp: &CompiledCallParity) -> bool {
if is_excluded(anchor, cp) {
return true;
}
info.impl_method_canonicals
.iter()
.any(|impl_canon| is_excluded(impl_canon, cp))
}
fn adapters_reaching(target: &str, coverage: &AdapterCoverage, adapters: &[String]) -> Vec<String> {
adapters
.iter()
.filter(|a| {
coverage
.get(a.as_str())
.is_some_and(|set| set.contains(target))
})
.cloned()
.collect()
}
fn adapters_missing(reached: &[String], adapters: &[String]) -> Vec<String> {
adapters
.iter()
.filter(|a| !reached.iter().any(|r| r == a.as_str()))
.cloned()
.collect()
}
fn build_finding(
info: &PubFnInfo<'_>,
canonical: String,
mut reached: Vec<String>,
missing: Vec<String>,
target_layer: &str,
) -> MatchLocation {
reached.sort();
MatchLocation {
file: info.file.clone(),
line: info.line,
column: 0,
kind: ViolationKind::CallParityMissingAdapter {
target_fn: canonical,
target_layer: target_layer.to_string(),
reached_adapters: reached,
missing_adapters: missing,
},
}
}