use std::collections::{HashMap, HashSet};
use serde_json::json;
use crate::db::queries::QueryBuilder;
use crate::resolution::types::ResolutionContext;
use crate::types::{Edge, EdgeKind, Language, Node, NodeKind};
use super::common::{
MAX_CALLBACKS_PER_CHANNEL, children_of_kind, edge, semantic_duplicate_methods, slice_lines,
};
pub(super) fn flutter_build_edges(
queries: &mut QueryBuilder,
ctx: &mut dyn ResolutionContext,
) -> Vec<Edge> {
let mut edges = Vec::new();
let mut seen = HashSet::new();
for cls in queries
.get_nodes_by_kind(NodeKind::Class)
.unwrap_or_default()
{
let children = children_of_kind(queries, &cls.id, NodeKind::Method);
let Some(build) = children
.iter()
.find(|node| node.name == "build" && node.file_path.ends_with(".dart"))
.cloned()
else {
continue;
};
for method in children.into_iter().take(MAX_CALLBACKS_PER_CHANNEL) {
if method.id == build.id {
continue;
}
let src = ctx
.read_file(&method.file_path)
.and_then(|c| slice_lines(&c, method.start_line, method.end_line));
if !src
.as_deref()
.map(|s| s.contains("setState("))
.unwrap_or(false)
{
continue;
}
let key = format!("{}>{}", method.id, build.id);
if !seen.insert(key) {
continue;
}
edges.push(edge(
&method.id,
&build.id,
EdgeKind::Calls,
Some(method.start_line),
"flutter-build",
[
("via", json!("setState")),
(
"registeredAt",
json!(format!("{}:{}", build.file_path, build.start_line)),
),
],
));
}
}
edges
}
pub(super) fn cpp_override_edges(queries: &mut QueryBuilder) -> Vec<Edge> {
let mut edges = Vec::new();
let mut seen = HashSet::new();
for cls in queries
.get_nodes_by_kind(NodeKind::Class)
.unwrap_or_default()
{
let sub_methods = children_of_kind(queries, &cls.id, NodeKind::Method)
.into_iter()
.filter(|node| node.language == Language::Cpp)
.collect::<Vec<_>>();
if sub_methods.is_empty() {
continue;
}
for ext in queries
.get_outgoing_edges(&cls.id, Some(vec![EdgeKind::Extends]), None)
.unwrap_or_default()
{
let Some(base) = queries.get_node_by_id(&ext.target).unwrap_or(None) else {
continue;
};
if base.language != Language::Cpp || base.id == cls.id {
continue;
}
let base_methods = children_of_kind(queries, &base.id, NodeKind::Method)
.into_iter()
.map(|node| (node.name.clone(), node))
.collect::<HashMap<_, _>>();
for method in sub_methods.iter().take(MAX_CALLBACKS_PER_CHANNEL) {
let Some(base_method) = base_methods.get(&method.name) else {
continue;
};
if base_method.id == method.id {
continue;
}
let key = format!("{}>{}", base_method.id, method.id);
if !seen.insert(key) {
continue;
}
edges.push(edge(
&base_method.id,
&method.id,
EdgeKind::Calls,
Some(base_method.start_line),
"cpp-override",
[
("via", json!(method.name)),
(
"registeredAt",
json!(format!("{}:{}", method.file_path, method.start_line)),
),
],
));
}
}
}
edges
}
pub(super) fn interface_override_edges(queries: &mut QueryBuilder) -> Vec<Edge> {
let iface_langs = HashSet::from([
Language::Java,
Language::Kotlin,
Language::CSharp,
Language::TypeScript,
Language::JavaScript,
Language::Swift,
Language::Scala,
Language::Go,
Language::Rust,
]);
let mut edges = Vec::new();
let mut seen = HashSet::new();
for kind in [NodeKind::Class, NodeKind::Struct] {
for cls in queries.get_nodes_by_kind(kind).unwrap_or_default() {
let impl_methods = children_of_kind(queries, &cls.id, NodeKind::Method)
.into_iter()
.filter(|node| iface_langs.contains(&node.language))
.collect::<Vec<_>>();
if impl_methods.is_empty() {
continue;
}
for sup in queries
.get_outgoing_edges(
&cls.id,
Some(vec![EdgeKind::Implements, EdgeKind::Extends]),
None,
)
.unwrap_or_default()
{
let Some(base) = queries.get_node_by_id(&sup.target).unwrap_or(None) else {
continue;
};
if !iface_langs.contains(&base.language) || base.id == cls.id {
continue;
}
let mut impl_by_name: HashMap<String, Vec<Node>> = HashMap::new();
for method in &impl_methods {
impl_by_name
.entry(method.name.clone())
.or_default()
.push(method.clone());
}
let mut added = 0;
for base_method in children_of_kind(queries, &base.id, NodeKind::Method) {
if added >= MAX_CALLBACKS_PER_CHANNEL {
break;
}
let base_methods = semantic_duplicate_methods(queries, &base_method);
for method in impl_by_name.get(&base_method.name).into_iter().flatten() {
if added >= MAX_CALLBACKS_PER_CHANNEL || base_method.id == method.id {
continue;
}
for source_method in &base_methods {
if added >= MAX_CALLBACKS_PER_CHANNEL || source_method.id == method.id {
break;
}
let key = format!("{}>{}", source_method.id, method.id);
if !seen.insert(key) {
continue;
}
edges.push(edge(
&source_method.id,
&method.id,
EdgeKind::Calls,
Some(source_method.start_line),
"interface-impl",
[
("via", json!(method.name)),
(
"registeredAt",
json!(format!(
"{}:{}",
method.file_path, method.start_line
)),
),
],
));
added += 1;
}
}
}
}
}
}
edges
}
pub(super) fn kotlin_expect_actual_edges(queries: &mut QueryBuilder) -> Vec<Edge> {
let mut edges = Vec::new();
let mut seen = HashSet::new();
let actuals = queries
.get_all_nodes()
.unwrap_or_default()
.into_iter()
.filter(|node| {
node.language == Language::Kotlin
&& node
.decorators
.as_ref()
.map(|d| d.iter().any(|x| x == "actual"))
.unwrap_or(false)
})
.collect::<Vec<_>>();
for actual in actuals {
for cand in queries
.get_nodes_by_qualified_name_exact(&actual.qualified_name)
.unwrap_or_default()
.into_iter()
.take(MAX_CALLBACKS_PER_CHANNEL)
{
if cand.language != Language::Kotlin
|| cand.id == actual.id
|| cand.file_path == actual.file_path
{
continue;
}
if cand
.decorators
.as_ref()
.map(|d| d.iter().any(|x| x == "actual"))
.unwrap_or(false)
{
continue;
}
if !kmp_kinds_compatible(cand.kind, actual.kind) {
continue;
}
let key = format!("{}>{}", cand.id, actual.id);
if seen.insert(key) {
edges.push(edge(
&cand.id,
&actual.id,
EdgeKind::Calls,
Some(cand.start_line),
"kotlin-expect-actual",
[
("via", json!(actual.name)),
(
"registeredAt",
json!(format!("{}:{}", actual.file_path, actual.start_line)),
),
],
));
}
}
}
edges
}
fn kmp_kinds_compatible(a: NodeKind, b: NodeKind) -> bool {
let type_kinds = HashSet::from([
NodeKind::Class,
NodeKind::Interface,
NodeKind::Struct,
NodeKind::Enum,
NodeKind::TypeAlias,
]);
a == b || (type_kinds.contains(&a) && type_kinds.contains(&b))
}