use super::*;
pub(super) fn resolve_facade_edges_rich(
nodes: &[Node],
pending_edges: Vec<RichFacadePendingEdge>,
existing_edges: &[Edge],
) -> Vec<Edge> {
let mut by_name: HashMap<&str, Vec<&Node>> = HashMap::new();
let mut by_id: HashMap<&str, &Node> = HashMap::new();
for node in nodes {
by_name.entry(&node.name).or_default().push(node);
by_id.insert(&node.id, node);
}
let supertypes = facade_supertype_edges(nodes, &pending_edges, existing_edges, &by_name);
let mut seen = HashSet::new();
let mut edges = Vec::new();
for pending in pending_edges {
if facade_pending_is_fn_ref(&pending) {
let Some(edge) = resolve_facade_fn_ref(&pending, &by_name, &by_id, &supertypes) else {
continue;
};
let key = format!("{}:{}:{:?}", edge.source, edge.target, edge.kind);
if seen.insert(key) {
edges.push(edge);
}
continue;
}
let Some(source) = by_id.get(pending.source.as_str()).copied() else {
continue;
};
let Some(target) = resolve_facade_pending_target(&pending, source, nodes, &by_name) else {
continue;
};
let key = format!("{}:{}:{:?}", pending.source, target.id, pending.kind);
if !seen.insert(key) {
continue;
}
edges.push(Edge {
source: pending.source,
target: target.id.clone(),
kind: pending.kind,
metadata: pending.metadata,
line: pending.line,
column: pending.column,
provenance: None,
});
}
edges
}
pub(super) fn resolve_facade_pending_target<'a>(
pending: &RichFacadePendingEdge,
source: &Node,
nodes: &'a [Node],
by_name: &HashMap<&str, Vec<&'a Node>>,
) -> Option<&'a Node> {
if let Some(target) = facade_qualified_pending_target(pending, source, nodes) {
return Some(target);
}
if let Some(target) = by_name
.get(pending.target_name.as_str())
.and_then(|candidates| facade_pick_pending_target(pending, source, candidates.to_vec()))
{
return Some(target);
}
let normalized = pending
.target_name
.trim()
.trim_matches(['"', '\''])
.trim_start_matches('\\')
.replace('\\', "::");
if normalized.is_empty() {
return None;
}
if pending.kind == EdgeKind::Calls {
if let Some(target) = resolve_facade_member_call_target(&normalized, source, nodes)
&& facade_pending_target_allowed(pending, source, target)
{
return Some(target);
}
if let Some(member_name) = facade_react_native_member_name(&normalized, source.language)
&& let Some(target) = nodes
.iter()
.filter(|node| {
node.name == member_name
&& matches!(node.kind, NodeKind::Function | NodeKind::Method)
&& matches!(
node.language,
Language::ObjC | Language::Java | Language::Kotlin | Language::Cpp
)
})
.find(|node| node.language == Language::ObjC)
.or_else(|| {
nodes.iter().find(|node| {
node.name == member_name
&& matches!(node.kind, NodeKind::Function | NodeKind::Method)
&& matches!(
node.language,
Language::Java | Language::Kotlin | Language::Cpp
)
})
})
{
return Some(target);
}
}
if pending.kind == EdgeKind::References {
if let Some(prefix) = normalized.strip_suffix(":prefix") {
let relaxed_prefix = facade_relaxed_config_key(prefix);
if let Some(target) = nodes
.iter()
.filter(|node| matches!(node.language, Language::Yaml | Language::Properties))
.filter(|node| {
facade_relaxed_config_key(&node.qualified_name).starts_with(&relaxed_prefix)
})
.filter(|node| facade_pending_target_allowed(pending, source, node))
.min_by_key(|node| node.qualified_name.len())
{
return Some(target);
}
}
let relaxed = facade_relaxed_config_key(&normalized);
if let Some(target) = nodes.iter().find(|node| {
(node.qualified_name == normalized
|| facade_relaxed_config_key(&node.qualified_name) == relaxed)
&& facade_pending_target_allowed(pending, source, node)
}) {
return Some(target);
}
}
let (class_name, member_name) = facade_split_qualified_member(&normalized);
let member_name = member_name?;
facade_pick_pending_target(
pending,
source,
nodes
.iter()
.filter(|node| node.name == member_name)
.filter(|node| {
facade_edge_target_matches_kind(node, pending)
&& class_name.as_ref().is_none_or(|class_name| {
node.qualified_name.contains(class_name)
|| node.file_path.contains(class_name)
})
})
.collect(),
)
}
pub(super) fn facade_qualified_pending_target<'a>(
pending: &RichFacadePendingEdge,
source: &Node,
nodes: &'a [Node],
) -> Option<&'a Node> {
for variant in facade_qualified_target_variants(&pending.target_name, source.language) {
let candidates = nodes
.iter()
.filter(|node| node.qualified_name == variant)
.collect::<Vec<_>>();
if let Some(target) = facade_pick_pending_target(pending, source, candidates) {
return Some(target);
}
}
None
}
pub(super) fn facade_qualified_target_variants(
target_name: &str,
language: Language,
) -> Vec<String> {
let trimmed = target_name
.trim()
.trim_matches(['"', '\''])
.trim_start_matches('\\');
let mut variants = Vec::new();
if !trimmed.is_empty() {
variants.push(trimmed.to_owned());
}
if trimmed.contains('\\') {
variants.push(trimmed.replace('\\', "::"));
if language == Language::Php
&& let Some((namespace, name)) = trimmed.rsplit_once('\\')
&& !namespace.is_empty()
&& !name.is_empty()
{
variants.push(format!("{namespace}::{name}"));
}
}
dedupe_names(variants)
}
pub(super) fn facade_pick_pending_target<'a>(
pending: &RichFacadePendingEdge,
source: &Node,
candidates: Vec<&'a Node>,
) -> Option<&'a Node> {
candidates
.into_iter()
.filter(|node| facade_pending_target_allowed(pending, source, node))
.max_by_key(|node| facade_pending_target_score(pending, source, node))
}
pub(super) fn facade_pending_target_allowed(
pending: &RichFacadePendingEdge,
source: &Node,
target: &Node,
) -> bool {
if target.id == source.id || !facade_edge_target_matches_kind(target, pending) {
return false;
}
match pending.kind {
EdgeKind::References => {
matches!(target.language, Language::Yaml | Language::Properties)
|| (source.language == Language::Yaml
&& (pending.target_name.contains('\\') || pending.target_name.contains("::")))
|| same_language_family(source.language, target.language)
}
EdgeKind::Extends | EdgeKind::Implements | EdgeKind::TypeOf | EdgeKind::Returns => {
same_language_family(source.language, target.language)
}
EdgeKind::Imports | EdgeKind::Calls | EdgeKind::Instantiates => {
!crosses_known_family(source.language, target.language)
}
_ => true,
}
}
pub(super) fn facade_pending_target_score(
pending: &RichFacadePendingEdge,
source: &Node,
target: &Node,
) -> (u8, u8, u8, usize, u8, std::cmp::Reverse<usize>) {
let import_target = pending.kind == EdgeKind::Imports;
let relative_target = import_target
.then(|| facade_source_relative_target(source, &pending.target_name))
.flatten();
let exact_relative = relative_target
.as_ref()
.is_some_and(|path| path == &target.file_path);
let clean_target = facade_clean_import_target(&pending.target_name);
let suffix_match = !clean_target.is_empty()
&& (target.file_path == clean_target
|| target.file_path.ends_with(&format!("/{clean_target}")));
let same_dir = facade_dir(&source.file_path) == facade_dir(&target.file_path);
(
exact_relative as u8,
same_dir as u8,
suffix_match as u8,
facade_shared_prefix_dirs(&source.file_path, &target.file_path),
same_language_family(source.language, target.language) as u8,
std::cmp::Reverse(target.file_path.len()),
)
}
pub(super) fn facade_source_relative_target(source: &Node, target_name: &str) -> Option<String> {
let target = facade_clean_import_target(target_name);
if target.is_empty() || target.starts_with('/') || target.contains("://") {
return None;
}
let dir = facade_dir(&source.file_path);
Some(
if dir.is_empty() {
target
} else {
format!("{dir}/{target}")
}
.replace('\\', "/"),
)
}
pub(super) fn facade_clean_import_target(target_name: &str) -> String {
target_name
.trim()
.trim_matches(['"', '\''])
.trim_start_matches("./")
.to_owned()
}
pub(super) fn facade_dir(path: &str) -> &str {
path.rsplit_once('/').map(|(dir, _)| dir).unwrap_or("")
}
pub(super) fn facade_shared_prefix_dirs(a: &str, b: &str) -> usize {
a.split('/')
.zip(b.split('/'))
.take_while(|(left, right)| left == right)
.count()
}
pub(super) fn facade_edge_target_matches_kind(
node: &Node,
pending: &RichFacadePendingEdge,
) -> bool {
match pending.kind {
EdgeKind::Calls => matches!(node.kind, NodeKind::Function | NodeKind::Method),
EdgeKind::Instantiates => matches!(node.kind, NodeKind::Class | NodeKind::Struct),
EdgeKind::References
if pending
.metadata
.as_ref()
.and_then(|metadata| metadata.get("fnRef"))
.and_then(Value::as_bool)
== Some(true) =>
{
matches!(node.kind, NodeKind::Function | NodeKind::Method)
}
EdgeKind::TypeOf | EdgeKind::Returns => facade_supertype_bearing(node.kind),
_ => true,
}
}