use std::path::Path;
use sqry_core::graph::unified::build::{BuildConfig, build_unified_graph};
use sqry_core::graph::unified::edge::EdgeKind;
use sqry_core::graph::unified::node::NodeKind;
use sqry_core::plugin::PluginManager;
use sqry_lang_c::CPlugin;
use sqry_lang_go::GoPlugin;
use sqry_lang_java::JavaPlugin;
use sqry_lang_javascript::JavaScriptPlugin;
use sqry_lang_python::PythonPlugin;
use sqry_lang_rust::RustPlugin;
use sqry_lang_typescript::TypeScriptPlugin;
fn create_cross_language_plugin_manager() -> PluginManager {
let mut manager = PluginManager::new();
manager.register_builtin(Box::new(RustPlugin::default()));
manager.register_builtin(Box::new(CPlugin::default()));
manager.register_builtin(Box::new(JavaScriptPlugin::default()));
manager.register_builtin(Box::new(PythonPlugin::default()));
manager.register_builtin(Box::new(GoPlugin::default()));
manager.register_builtin(Box::new(JavaPlugin::default()));
manager.register_builtin(Box::new(TypeScriptPlugin::default()));
manager
}
fn structural_name(
graph: &sqry_core::graph::unified::concurrent::CodeGraph,
entry: &sqry_core::graph::unified::storage::NodeEntry,
) -> Option<std::sync::Arc<str>> {
entry
.qualified_name
.and_then(|qualified_name_id| graph.strings().resolve(qualified_name_id))
.or_else(|| graph.strings().resolve(entry.name))
}
#[test]
fn test_ffi_rust_to_c_linking() {
let fixtures_path = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("test-fixtures/cross-language/ffi");
let plugins = create_cross_language_plugin_manager();
let config = BuildConfig::default();
let graph = build_unified_graph(&fixtures_path, &plugins, &config)
.expect("should build graph from FFI fixtures");
assert!(graph.node_count() > 0, "graph should have nodes");
let function_nodes = graph.indices().by_kind(NodeKind::Function);
let mut ffi_declarations = Vec::new();
let mut c_functions = Vec::new();
for &node_id in function_nodes {
let Some(entry) = graph.nodes().get(node_id) else {
continue;
};
let Some(name) = structural_name(&graph, entry) else {
continue;
};
let name_str: &str = &name;
if name_str.starts_with("extern::") {
ffi_declarations.push((node_id, name.to_string()));
} else if name_str == "calculate_sum" || name_str == "calculate_product" {
c_functions.push((node_id, name.to_string()));
}
}
assert!(
!ffi_declarations.is_empty(),
"should have FFI declaration nodes (extern::C::calculate_sum, etc.)"
);
assert!(
!c_functions.is_empty(),
"should have C function nodes (calculate_sum, etc.)"
);
let mut ffi_edges = Vec::new();
for (node_id, _name) in &ffi_declarations {
let edges = graph.edges().edges_from(*node_id);
for edge in edges {
if matches!(edge.kind, EdgeKind::FfiCall { .. }) {
ffi_edges.push(edge);
}
}
}
assert!(
!ffi_edges.is_empty(),
"Pass 5 should create FfiCall edges linking Rust extern to C functions. \
FFI declarations: {ffi_declarations:?}, C functions: {c_functions:?}"
);
for edge in &ffi_edges {
let source_entry = graph.nodes().get(edge.source).expect("source node exists");
let target_entry = graph.nodes().get(edge.target).expect("target node exists");
assert_ne!(
source_entry.file, target_entry.file,
"FfiCall edge should cross file boundaries"
);
}
}
#[test]
fn test_http_js_to_python_linking() {
let fixtures_path = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("test-fixtures/cross-language/http");
let plugins = create_cross_language_plugin_manager();
let config = BuildConfig::default();
let graph = build_unified_graph(&fixtures_path, &plugins, &config)
.expect("should build graph from HTTP fixtures");
assert!(graph.node_count() > 0, "graph should have nodes");
let endpoint_nodes = graph.indices().by_kind(NodeKind::Endpoint);
let mut endpoints = Vec::new();
for &node_id in endpoint_nodes {
let Some(entry) = graph.nodes().get(node_id) else {
continue;
};
let Some(name) = structural_name(&graph, entry) else {
continue;
};
endpoints.push((node_id, (*name).to_string()));
}
assert!(
!endpoints.is_empty(),
"Python plugin should create Endpoint nodes for Flask routes"
);
let js_functions: Vec<_> = graph
.indices()
.by_kind(NodeKind::Function)
.iter()
.filter_map(|&node_id| {
let entry = graph.nodes().get(node_id)?;
let name = structural_name(&graph, entry)?;
let name_str: &str = &name;
if name_str == "fetchUsers" || name_str == "createItem" {
Some((node_id, name.to_string()))
} else {
None
}
})
.collect();
assert!(
!js_functions.is_empty(),
"should have JavaScript function nodes (fetchUsers, createItem)"
);
let mut http_request_edges = Vec::new();
for (node_id, _name) in &js_functions {
let edges = graph.edges().edges_from(*node_id);
for edge in edges {
if matches!(edge.kind, EdgeKind::HttpRequest { .. }) {
http_request_edges.push(edge);
}
}
}
assert!(
!http_request_edges.is_empty(),
"JavaScript plugin should create HttpRequest edges for fetch() calls. \
JS functions: {js_functions:?}"
);
let cross_file_http_edges: Vec<_> = http_request_edges
.iter()
.filter(|edge| {
let Some(target_entry) = graph.nodes().get(edge.target) else {
return false;
};
target_entry.kind == NodeKind::Endpoint
})
.collect();
assert!(
!cross_file_http_edges.is_empty(),
"Pass 5 should create HttpRequest edges from JS functions to Python Endpoint nodes. \
Endpoints found: {endpoints:?}, HTTP edges: {http_request_edges:?}"
);
}
#[test]
fn test_combined_ffi_and_http_linking() {
let fixtures_path = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("test-fixtures/cross-language");
let plugins = create_cross_language_plugin_manager();
let config = BuildConfig::default();
let graph = build_unified_graph(&fixtures_path, &plugins, &config)
.expect("should build graph from combined fixtures");
let mut ffi_edge_count = 0;
let mut http_cross_file_count = 0;
let all_function_nodes = graph.indices().by_kind(NodeKind::Function);
for &node_id in all_function_nodes {
let edges = graph.edges().edges_from(node_id);
for edge in edges {
match &edge.kind {
EdgeKind::FfiCall { .. } => {
let source = graph.nodes().get(edge.source);
let target = graph.nodes().get(edge.target);
if let (Some(src), Some(tgt)) = (source, target)
&& src.file != tgt.file
{
ffi_edge_count += 1;
}
}
EdgeKind::HttpRequest { .. } => {
if let Some(target) = graph.nodes().get(edge.target)
&& target.kind == NodeKind::Endpoint
{
http_cross_file_count += 1;
}
}
_ => {}
}
}
}
assert!(
ffi_edge_count > 0,
"combined graph should have cross-file FfiCall edges"
);
assert!(
http_cross_file_count > 0,
"combined graph should have cross-file HttpRequest edges to Endpoints"
);
}
#[test]
fn test_pass5_stats_reported_correctly() {
use sqry_core::graph::node::Language;
use sqry_core::graph::unified::build::helper::GraphBuildHelper;
use sqry_core::graph::unified::build::pass5_cross_language::link_cross_language_edges;
use sqry_core::graph::unified::build::staging::StagingGraph;
use sqry_core::graph::unified::concurrent::CodeGraph;
use sqry_core::graph::unified::edge::kind::HttpMethod;
let mut graph = CodeGraph::new();
let server_path = std::path::PathBuf::from("/test/server.py");
{
let mut staging = StagingGraph::new();
let mut helper = GraphBuildHelper::new(&mut staging, &server_path, Language::Python);
let _ep = helper.add_endpoint("route::GET::/api/data", None);
commit_staging(&mut graph, &server_path, Language::Python, staging);
}
let client_path = std::path::PathBuf::from("/test/client.js");
{
let mut staging = StagingGraph::new();
let mut helper = GraphBuildHelper::new(&mut staging, &client_path, Language::JavaScript);
let func = helper.add_function("getData", None, true, false);
let target = helper.add_module("http::/api/data", None);
helper.add_http_request_edge(func, target, HttpMethod::Get, Some("/api/data"));
commit_staging(&mut graph, &client_path, Language::JavaScript, staging);
}
let rust_path = std::path::PathBuf::from("/test/bindings.rs");
{
let mut staging = StagingGraph::new();
let mut helper = GraphBuildHelper::new(&mut staging, &rust_path, Language::Rust);
let _f = helper.add_function("extern::C::compute", None, false, false);
commit_staging(&mut graph, &rust_path, Language::Rust, staging);
}
let c_path = std::path::PathBuf::from("/test/math.c");
{
let mut staging = StagingGraph::new();
let mut helper = GraphBuildHelper::new(&mut staging, &c_path, Language::C);
let _f = helper.add_function("compute", None, false, false);
commit_staging(&mut graph, &c_path, Language::C, staging);
}
let stats = link_cross_language_edges(&mut graph);
assert_eq!(stats.ffi_declarations_scanned, 1);
assert_eq!(stats.ffi_edges_created, 1);
assert_eq!(stats.http_requests_scanned, 1);
assert_eq!(stats.http_endpoints_matched, 1);
assert_eq!(stats.total_edges_created, 2);
}
#[test]
fn test_ffi_go_cgo_to_c_linking() {
let fixtures_path = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("test-fixtures/cross-language/ffi");
let plugins = create_cross_language_plugin_manager();
let config = BuildConfig::default();
let graph = build_unified_graph(&fixtures_path, &plugins, &config)
.expect("should build graph from FFI fixtures");
let function_nodes = graph.indices().by_kind(NodeKind::Function);
let mut cgo_declarations = Vec::new();
let mut c_functions = Vec::new();
for &node_id in function_nodes {
let Some(entry) = graph.nodes().get(node_id) else {
continue;
};
let Some(name) = structural_name(&graph, entry) else {
continue;
};
let name_str: &str = &name;
if name_str.starts_with("C.") || name_str.starts_with("C::") {
cgo_declarations.push((node_id, name.to_string()));
} else if name_str == "calculate_sum" || name_str == "calculate_product" {
c_functions.push((node_id, name.to_string()));
}
}
assert!(
graph.node_count() > 0,
"graph should have nodes from fixtures"
);
assert!(
!c_functions.is_empty(),
"should have C function nodes (calculate_sum, calculate_product)"
);
}
#[test]
fn test_http_go_to_js_linking() {
let fixtures_path = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("test-fixtures/cross-language/http");
let plugins = create_cross_language_plugin_manager();
let config = BuildConfig::default();
let graph = build_unified_graph(&fixtures_path, &plugins, &config)
.expect("should build graph from HTTP fixtures");
let endpoint_nodes = graph.indices().by_kind(NodeKind::Endpoint);
let mut go_endpoints = Vec::new();
for &node_id in endpoint_nodes {
let Some(entry) = graph.nodes().get(node_id) else {
continue;
};
let Some(name) = structural_name(&graph, entry) else {
continue;
};
let name_str: &str = &name;
if name_str.contains("/api/users") || name_str.contains("/api/items") {
go_endpoints.push((node_id, name.to_string()));
}
}
assert!(
!go_endpoints.is_empty(),
"Go plugin should create Endpoint nodes for HandleFunc routes"
);
}
#[test]
fn test_http_java_spring_with_class_prefix() {
let fixtures_path = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("test-fixtures/cross-language/http");
let plugins = create_cross_language_plugin_manager();
let config = BuildConfig::default();
let graph = build_unified_graph(&fixtures_path, &plugins, &config)
.expect("should build graph from HTTP fixtures");
let endpoint_nodes = graph.indices().by_kind(NodeKind::Endpoint);
let mut java_endpoints = Vec::new();
for &node_id in endpoint_nodes {
let Some(entry) = graph.nodes().get(node_id) else {
continue;
};
let Some(name) = structural_name(&graph, entry) else {
continue;
};
let name_str: &str = &name;
if name_str.contains("route::") && name_str.contains("/api/") {
java_endpoints.push(name.to_string());
}
}
assert!(
!java_endpoints.is_empty(),
"Java Spring plugin should create Endpoint nodes with class-level prefix. \
All endpoints: {:?}",
{
let all: Vec<_> = endpoint_nodes
.iter()
.filter_map(|&nid| {
let e = graph.nodes().get(nid)?;
let n = graph.strings().resolve(e.name)?;
Some(n.to_string())
})
.collect();
all
}
);
let has_prefixed = java_endpoints
.iter()
.any(|name| name.contains("/api/users") || name.contains("/api/items"));
assert!(
has_prefixed,
"Java endpoints should have class-level prefix composed: {:?}",
java_endpoints
);
}
#[test]
fn test_http_all_method_endpoint_matching() {
use sqry_core::graph::node::Language;
use sqry_core::graph::unified::build::helper::GraphBuildHelper;
use sqry_core::graph::unified::build::pass5_cross_language::link_cross_language_edges;
use sqry_core::graph::unified::build::staging::StagingGraph;
use sqry_core::graph::unified::concurrent::CodeGraph;
use sqry_core::graph::unified::edge::kind::HttpMethod;
let mut graph = CodeGraph::new();
let server_path = std::path::PathBuf::from("/test/server.ts");
{
let mut staging = StagingGraph::new();
let mut helper = GraphBuildHelper::new(&mut staging, &server_path, Language::TypeScript);
let _ep = helper.add_endpoint("route::ALL::/health", None);
commit_staging(&mut graph, &server_path, Language::TypeScript, staging);
}
let client_path = std::path::PathBuf::from("/test/client.js");
{
let mut staging = StagingGraph::new();
let mut helper = GraphBuildHelper::new(&mut staging, &client_path, Language::JavaScript);
let func = helper.add_function("checkHealth", None, true, false);
let target = helper.add_module("http::/health", None);
helper.add_http_request_edge(func, target, HttpMethod::Get, Some("/health"));
commit_staging(&mut graph, &client_path, Language::JavaScript, staging);
}
let stats = link_cross_language_edges(&mut graph);
assert_eq!(stats.http_requests_scanned, 1);
assert_eq!(
stats.http_endpoints_matched, 1,
"GET request to /health should match ALL endpoint at /health"
);
}
fn commit_staging(
graph: &mut sqry_core::graph::unified::concurrent::CodeGraph,
path: &std::path::Path,
language: sqry_core::graph::node::Language,
mut staging: sqry_core::graph::unified::build::staging::StagingGraph,
) {
let file_id = graph
.files_mut()
.register_with_language(path, Some(language))
.expect("register file");
staging.apply_file_id(file_id);
let string_remap = staging
.commit_strings(graph.strings_mut())
.expect("commit strings");
staging
.apply_string_remap(&string_remap)
.expect("apply string remap");
let node_id_mapping = staging
.commit_nodes(graph.nodes_mut())
.expect("commit nodes");
let index_entries: Vec<_> = node_id_mapping
.values()
.filter_map(|&actual_id| {
graph.nodes().get(actual_id).map(|entry| {
(
actual_id,
entry.kind,
entry.name,
entry.qualified_name,
entry.file,
)
})
})
.collect();
for (node_id, kind, name, qualified_name, file) in index_entries {
graph
.indices_mut()
.add(node_id, kind, name, qualified_name, file);
}
let edges = staging.get_remapped_edges(&node_id_mapping);
for edge in edges {
graph.edges_mut().add_edge_with_spans(
edge.source,
edge.target,
edge.kind.clone(),
file_id,
edge.spans.clone(),
);
}
}