use std::collections::HashMap;
use crate::graph::node::Language;
use crate::graph::unified::concurrent::CodeGraph;
use crate::graph::unified::edge::EdgeKind;
use crate::graph::unified::edge::kind::{FfiConvention, HttpMethod};
use crate::graph::unified::file::FileId;
use crate::graph::unified::node::{NodeId, NodeKind};
use crate::graph::unified::storage::NodeEntry;
#[derive(Debug, Clone, Default)]
pub struct Pass5Stats {
pub ffi_declarations_scanned: usize,
pub ffi_edges_created: usize,
pub http_requests_scanned: usize,
pub http_endpoints_matched: usize,
pub total_edges_created: usize,
}
#[derive(Debug, Clone)]
struct PendingCrossLanguageEdge {
source: NodeId,
target: NodeId,
kind: EdgeKind,
file: FileId,
}
pub fn link_cross_language_edges(graph: &mut CodeGraph) -> Pass5Stats {
let mut stats = Pass5Stats::default();
let mut pending_edges: Vec<PendingCrossLanguageEdge> = Vec::new();
link_ffi_edges(graph, &mut stats, &mut pending_edges);
link_http_edges(graph, &mut stats, &mut pending_edges);
for edge in &pending_edges {
graph.edges_mut().add_edge_with_spans(
edge.source,
edge.target,
edge.kind.clone(),
edge.file,
vec![],
);
}
stats.total_edges_created = stats.ffi_edges_created + stats.http_endpoints_matched;
if stats.total_edges_created > 0 {
log::info!(
"Pass 5: created {} cross-language edges ({} FFI, {} HTTP)",
stats.total_edges_created,
stats.ffi_edges_created,
stats.http_endpoints_matched,
);
}
stats
}
#[derive(Debug)]
struct FfiDeclaration {
node_id: NodeId,
bare_name: String,
convention: FfiConvention,
file_id: FileId,
}
fn parse_ffi_qualified_name(qualified_name: &str) -> Option<(String, FfiConvention)> {
if let Some(rest) = qualified_name.strip_prefix("extern::") {
if let Some(pos) = rest.find("::") {
let convention_str = &rest[..pos];
let bare_name = &rest[pos + 2..];
if bare_name.is_empty() {
return None;
}
let convention = match convention_str {
"cdecl" => FfiConvention::Cdecl,
"stdcall" => FfiConvention::Stdcall,
"fastcall" => FfiConvention::Fastcall,
"system" => FfiConvention::System,
_ => FfiConvention::C,
};
return Some((bare_name.to_string(), convention));
}
} else if let Some(rest) = qualified_name
.strip_prefix("C.")
.or_else(|| qualified_name.strip_prefix("C::"))
{
if !rest.is_empty() {
return Some((rest.to_string(), FfiConvention::C));
}
} else if let Some(rest) = qualified_name.strip_prefix("native::jni::") {
if !rest.is_empty() {
return Some((rest.to_string(), FfiConvention::C));
}
} else if let Some(rest) = qualified_name
.strip_prefix("native::ctypes::")
.or_else(|| qualified_name.strip_prefix("native::cffi::"))
.or_else(|| qualified_name.strip_prefix("native::ffi::"))
.or_else(|| qualified_name.strip_prefix("native::panama::"))
{
if !rest.is_empty() {
return Some((rest.to_string(), FfiConvention::C));
}
}
None
}
fn java_to_jni_c_name(java_name: &str) -> String {
let joined_segments = java_name
.split("::")
.flat_map(|segment| segment.split('.'))
.filter(|segment| !segment.is_empty())
.collect::<Vec<_>>()
.join("_");
format!("Java_{joined_segments}")
}
fn is_java_qualified_name(java_name: &str) -> bool {
java_name.contains('.') || java_name.contains("::")
}
fn java_method_name(java_name: &str) -> Option<&str> {
java_name
.rsplit("::")
.next()
.and_then(|segment| segment.rsplit('.').next())
.filter(|segment| !segment.is_empty())
}
fn link_ffi_edges(
graph: &CodeGraph,
stats: &mut Pass5Stats,
pending: &mut Vec<PendingCrossLanguageEdge>,
) {
let ffi_declarations = collect_ffi_declarations(graph, stats);
if ffi_declarations.is_empty() {
return;
}
let c_functions = build_c_function_map(graph);
if c_functions.is_empty() {
return;
}
for decl in &ffi_declarations {
let mut matched = try_ffi_match(decl, &c_functions, stats, pending);
if !matched && is_java_qualified_name(&decl.bare_name) {
let jni_name = java_to_jni_c_name(&decl.bare_name);
let jni_decl = FfiDeclaration {
node_id: decl.node_id,
bare_name: jni_name,
convention: decl.convention,
file_id: decl.file_id,
};
matched = try_ffi_match(&jni_decl, &c_functions, stats, pending);
}
if !matched && let Some(method_name) = java_method_name(&decl.bare_name) {
let bare_decl = FfiDeclaration {
node_id: decl.node_id,
bare_name: method_name.to_string(),
convention: decl.convention,
file_id: decl.file_id,
};
try_ffi_match(&bare_decl, &c_functions, stats, pending);
}
}
}
fn try_ffi_match(
decl: &FfiDeclaration,
c_functions: &HashMap<String, Vec<(NodeId, FileId)>>,
stats: &mut Pass5Stats,
pending: &mut Vec<PendingCrossLanguageEdge>,
) -> bool {
let Some(targets) = c_functions.get(&decl.bare_name) else {
return false;
};
let mut found = false;
for &(target_node, target_file) in targets {
if target_file == decl.file_id {
continue;
}
found = true;
stats.ffi_edges_created += 1;
pending.push(PendingCrossLanguageEdge {
source: decl.node_id,
target: target_node,
kind: EdgeKind::FfiCall {
convention: decl.convention,
},
file: decl.file_id,
});
}
found
}
fn entry_structural_name(graph: &CodeGraph, entry: &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))
}
fn collect_ffi_declarations(graph: &CodeGraph, stats: &mut Pass5Stats) -> Vec<FfiDeclaration> {
let mut declarations = Vec::new();
let function_nodes = graph.indices().by_kind(NodeKind::Function);
for &node_id in function_nodes {
let Some(entry) = graph.nodes().get(node_id) else {
continue;
};
let Some(name_str) = entry_structural_name(graph, entry) else {
continue;
};
if let Some((bare_name, convention)) = parse_ffi_qualified_name(&name_str) {
stats.ffi_declarations_scanned += 1;
declarations.push(FfiDeclaration {
node_id,
bare_name,
convention,
file_id: entry.file,
});
}
}
declarations
}
fn build_c_function_map(graph: &CodeGraph) -> HashMap<String, Vec<(NodeId, FileId)>> {
let mut map: HashMap<String, Vec<(NodeId, FileId)>> = HashMap::new();
for lang in &[Language::C, Language::Cpp] {
let files = graph.files().files_by_language(*lang);
for (file_id, _path) in files {
let file_nodes = graph.indices().by_file(file_id);
for &node_id in file_nodes {
let Some(entry) = graph.nodes().get(node_id) else {
continue;
};
if entry.kind != NodeKind::Function {
continue;
}
let Some(name_str) = entry_structural_name(graph, entry) else {
continue;
};
let bare_name = name_str
.rsplit("::")
.next()
.unwrap_or(&name_str)
.to_string();
map.entry(bare_name).or_default().push((node_id, file_id));
}
}
}
map
}
#[derive(Debug)]
struct EndpointInfo {
node_id: NodeId,
method: HttpMethod,
normalized_path: String,
file_id: FileId,
}
#[derive(Debug)]
struct HttpRequestInfo {
source_node: NodeId,
method: HttpMethod,
url_path: String,
file_id: FileId,
}
fn parse_endpoint_qualified_name(qualified_name: &str) -> Option<(HttpMethod, String)> {
let rest = qualified_name.strip_prefix("route::")?;
let sep = rest.find("::")?;
let method_str = &rest[..sep];
let path = &rest[sep + 2..];
let method = match method_str {
"GET" => HttpMethod::Get,
"POST" => HttpMethod::Post,
"PUT" => HttpMethod::Put,
"DELETE" => HttpMethod::Delete,
"PATCH" => HttpMethod::Patch,
"HEAD" => HttpMethod::Head,
"OPTIONS" => HttpMethod::Options,
"ALL" => HttpMethod::All,
_ => return None,
};
Some((method, path.to_string()))
}
#[must_use]
pub fn normalize_url_path(path: &str) -> String {
let path = path.split('?').next().unwrap_or(path);
let path = path.trim_matches('/');
let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
let normalized: Vec<String> = segments
.into_iter()
.map(|seg| {
if seg.starts_with('{') && seg.ends_with('}') {
return format!(":{}", &seg[1..seg.len() - 1]);
}
if seg.starts_with('<') && seg.ends_with('>') {
let inner = &seg[1..seg.len() - 1];
let param_name = if let Some(pos) = inner.find(':') {
&inner[pos + 1..]
} else {
inner
};
return format!(":{param_name}");
}
if seg.starts_with('[') && seg.ends_with(']') {
return format!(":{}", &seg[1..seg.len() - 1]);
}
seg.to_string()
})
.collect();
normalized.join("/")
}
fn link_http_edges(
graph: &CodeGraph,
stats: &mut Pass5Stats,
pending: &mut Vec<PendingCrossLanguageEdge>,
) {
let endpoints = collect_endpoints(graph);
if endpoints.is_empty() {
return;
}
let mut endpoint_map: HashMap<(HttpMethod, String), Vec<(NodeId, FileId)>> = HashMap::new();
for ep in &endpoints {
endpoint_map
.entry((ep.method, ep.normalized_path.clone()))
.or_default()
.push((ep.node_id, ep.file_id));
}
let requests = collect_http_requests(graph, stats);
if requests.is_empty() {
return;
}
for req in &requests {
let normalized = normalize_url_path(&req.url_path);
try_http_match(req, &normalized, req.method, &endpoint_map, stats, pending);
if req.method == HttpMethod::All {
for specific_method in &[
HttpMethod::Get,
HttpMethod::Post,
HttpMethod::Put,
HttpMethod::Delete,
HttpMethod::Patch,
HttpMethod::Head,
HttpMethod::Options,
] {
try_http_match(
req,
&normalized,
*specific_method,
&endpoint_map,
stats,
pending,
);
}
} else {
try_http_match(
req,
&normalized,
HttpMethod::All,
&endpoint_map,
stats,
pending,
);
}
}
}
fn try_http_match(
req: &HttpRequestInfo,
normalized_path: &str,
lookup_method: HttpMethod,
endpoint_map: &HashMap<(HttpMethod, String), Vec<(NodeId, FileId)>>,
stats: &mut Pass5Stats,
pending: &mut Vec<PendingCrossLanguageEdge>,
) {
let key = (lookup_method, normalized_path.to_string());
if let Some(targets) = endpoint_map.get(&key) {
for &(target_node, target_file) in targets {
if target_file == req.file_id {
continue;
}
stats.http_endpoints_matched += 1;
pending.push(PendingCrossLanguageEdge {
source: req.source_node,
target: target_node,
kind: EdgeKind::HttpRequest {
method: req.method,
url: None, },
file: req.file_id,
});
}
}
}
fn collect_endpoints(graph: &CodeGraph) -> Vec<EndpointInfo> {
let mut endpoints = Vec::new();
let endpoint_nodes = graph.indices().by_kind(NodeKind::Endpoint);
for &node_id in endpoint_nodes {
let Some(entry) = graph.nodes().get(node_id) else {
continue;
};
let Some(name_str) = entry_structural_name(graph, entry) else {
continue;
};
if let Some((method, path)) = parse_endpoint_qualified_name(&name_str) {
let normalized = normalize_url_path(&path);
endpoints.push(EndpointInfo {
node_id,
method,
normalized_path: normalized,
file_id: entry.file,
});
}
}
endpoints
}
fn collect_http_requests(graph: &CodeGraph, stats: &mut Pass5Stats) -> Vec<HttpRequestInfo> {
let mut requests = Vec::new();
let forward = graph.edges().forward();
let delta = forward.delta();
for edge in delta.iter() {
if !edge.is_add() {
continue;
}
if let EdgeKind::HttpRequest { method, url } = &edge.kind {
stats.http_requests_scanned += 1;
if let Some(url_id) = url
&& let Some(url_str) = graph.strings().resolve(*url_id)
{
let Some(source_entry) = graph.nodes().get(edge.source) else {
continue;
};
requests.push(HttpRequestInfo {
source_node: edge.source,
method: *method,
url_path: url_str.to_string(),
file_id: source_entry.file,
});
}
}
}
requests
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normalize_url_path_basic() {
assert_eq!(normalize_url_path("/api/users"), "api/users");
assert_eq!(normalize_url_path("/api/users/"), "api/users");
assert_eq!(normalize_url_path("api/users"), "api/users");
}
#[test]
fn test_normalize_url_path_params() {
assert_eq!(normalize_url_path("/api/users/{id}"), "api/users/:id");
assert_eq!(normalize_url_path("/api/users/<id>"), "api/users/:id");
assert_eq!(normalize_url_path("/api/users/[id]"), "api/users/:id");
assert_eq!(normalize_url_path("/api/users/:id"), "api/users/:id");
}
#[test]
fn test_normalize_url_path_double_slashes() {
assert_eq!(normalize_url_path("/api//users"), "api/users");
assert_eq!(normalize_url_path("///api///users///"), "api/users");
}
#[test]
fn test_normalize_url_path_query_string() {
assert_eq!(
normalize_url_path("/api/users?page=1&limit=10"),
"api/users"
);
}
#[test]
fn test_normalize_url_path_empty() {
assert_eq!(normalize_url_path("/"), "");
assert_eq!(normalize_url_path(""), "");
}
#[test]
fn test_parse_ffi_rust_extern_c() {
let result = parse_ffi_qualified_name("extern::C::puts");
assert_eq!(result, Some(("puts".to_string(), FfiConvention::C)));
}
#[test]
fn test_parse_ffi_rust_extern_stdcall() {
let result = parse_ffi_qualified_name("extern::stdcall::MessageBoxA");
assert_eq!(
result,
Some(("MessageBoxA".to_string(), FfiConvention::Stdcall))
);
}
#[test]
fn test_parse_ffi_go_cgo() {
let result = parse_ffi_qualified_name("C.puts");
assert_eq!(result, Some(("puts".to_string(), FfiConvention::C)));
}
#[test]
fn test_parse_ffi_java_jni() {
let result = parse_ffi_qualified_name("native::jni::Java_MyClass_doStuff");
assert_eq!(
result,
Some(("Java_MyClass_doStuff".to_string(), FfiConvention::C))
);
}
#[test]
fn test_parse_ffi_python_ctypes() {
let result = parse_ffi_qualified_name("native::ctypes::calculate");
assert_eq!(result, Some(("calculate".to_string(), FfiConvention::C)));
}
#[test]
fn test_parse_ffi_python_cffi() {
let result = parse_ffi_qualified_name("native::cffi::calculate");
assert_eq!(result, Some(("calculate".to_string(), FfiConvention::C)));
}
#[test]
fn test_parse_ffi_php_ffi() {
let result = parse_ffi_qualified_name("native::ffi::crypto_encrypt");
assert_eq!(
result,
Some(("crypto_encrypt".to_string(), FfiConvention::C))
);
}
#[test]
fn test_parse_ffi_java_panama() {
let result = parse_ffi_qualified_name("native::panama::nativeLinker");
assert_eq!(result, Some(("nativeLinker".to_string(), FfiConvention::C)));
}
#[test]
fn test_parse_ffi_not_ffi() {
assert!(parse_ffi_qualified_name("main").is_none());
assert!(parse_ffi_qualified_name("module::func").is_none());
assert!(parse_ffi_qualified_name("").is_none());
}
#[test]
fn test_parse_ffi_edge_cases() {
assert!(parse_ffi_qualified_name("extern::C::").is_none());
assert!(parse_ffi_qualified_name("C.").is_none());
}
#[test]
fn test_parse_endpoint_get() {
let result = parse_endpoint_qualified_name("route::GET::/api/users");
assert_eq!(result, Some((HttpMethod::Get, "/api/users".to_string())));
}
#[test]
fn test_parse_endpoint_post() {
let result = parse_endpoint_qualified_name("route::POST::/api/items");
assert_eq!(result, Some((HttpMethod::Post, "/api/items".to_string())));
}
#[test]
fn test_parse_endpoint_all_methods() {
assert!(matches!(
parse_endpoint_qualified_name("route::PUT::/x"),
Some((HttpMethod::Put, _))
));
assert!(matches!(
parse_endpoint_qualified_name("route::DELETE::/x"),
Some((HttpMethod::Delete, _))
));
assert!(matches!(
parse_endpoint_qualified_name("route::PATCH::/x"),
Some((HttpMethod::Patch, _))
));
assert!(matches!(
parse_endpoint_qualified_name("route::HEAD::/x"),
Some((HttpMethod::Head, _))
));
assert!(matches!(
parse_endpoint_qualified_name("route::OPTIONS::/x"),
Some((HttpMethod::Options, _))
));
}
#[test]
fn test_parse_endpoint_invalid() {
assert!(parse_endpoint_qualified_name("not_a_route").is_none());
assert!(parse_endpoint_qualified_name("route::INVALID::/x").is_none());
}
#[test]
fn test_link_cross_language_edges_empty_graph() {
let mut graph = CodeGraph::new();
let stats = link_cross_language_edges(&mut graph);
assert_eq!(stats.total_edges_created, 0);
assert_eq!(stats.ffi_declarations_scanned, 0);
assert_eq!(stats.http_requests_scanned, 0);
}
#[test]
fn test_ffi_matching_with_real_graph() {
use crate::graph::unified::build::helper::GraphBuildHelper;
use crate::graph::unified::build::staging::StagingGraph;
use std::path::PathBuf;
let mut graph = CodeGraph::new();
let rust_path = PathBuf::from("/test/bindings.rs");
{
let mut staging = StagingGraph::new();
let mut helper = GraphBuildHelper::new(&mut staging, &rust_path, Language::Rust);
let _extern_fn = helper.add_function("extern::C::calculate_sum", None, false, false);
commit_staging_to_graph(&mut graph, &rust_path, Language::Rust, staging);
}
let c_path = PathBuf::from("/test/math.c");
{
let mut staging = StagingGraph::new();
let mut helper = GraphBuildHelper::new(&mut staging, &c_path, Language::C);
let _c_fn = helper.add_function("calculate_sum", None, false, false);
commit_staging_to_graph(&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.total_edges_created, 1);
}
#[test]
fn test_http_matching_with_real_graph() {
use crate::graph::unified::build::helper::GraphBuildHelper;
use crate::graph::unified::build::staging::StagingGraph;
use crate::graph::unified::edge::kind::HttpMethod;
use std::path::PathBuf;
let mut graph = CodeGraph::new();
let server_path = PathBuf::from("/test/server.py");
{
let mut staging = StagingGraph::new();
let mut helper = GraphBuildHelper::new(&mut staging, &server_path, Language::Python);
let _endpoint = helper.add_endpoint("route::GET::/api/users", None);
commit_staging_to_graph(&mut graph, &server_path, Language::Python, staging);
}
let client_path = PathBuf::from("/test/client.js");
{
let mut staging = StagingGraph::new();
let mut helper =
GraphBuildHelper::new(&mut staging, &client_path, Language::JavaScript);
let fetch_fn = helper.add_function("fetchUsers", None, true, false);
let target = helper.add_module("http::/api/users", None);
helper.add_http_request_edge(fetch_fn, target, HttpMethod::Get, Some("/api/users"));
commit_staging_to_graph(&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);
assert_eq!(stats.total_edges_created, 1);
}
#[test]
fn test_ffi_no_match_same_file() {
use crate::graph::unified::build::helper::GraphBuildHelper;
use crate::graph::unified::build::staging::StagingGraph;
use std::path::PathBuf;
let mut graph = CodeGraph::new();
let path = PathBuf::from("/test/combined.c");
{
let mut staging = StagingGraph::new();
let mut helper = GraphBuildHelper::new(&mut staging, &path, Language::C);
let _extern_fn = helper.add_function("extern::C::my_func", None, false, false);
let _impl_fn = helper.add_function("my_func", None, false, false);
commit_staging_to_graph(&mut graph, &path, Language::C, staging);
}
let stats = link_cross_language_edges(&mut graph);
assert_eq!(stats.ffi_edges_created, 0);
}
fn commit_staging_to_graph(
graph: &mut CodeGraph,
path: &std::path::Path,
language: Language,
mut staging: crate::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(),
);
}
}
#[test]
fn test_parse_ffi_go_cgo_double_colon() {
let result = parse_ffi_qualified_name("C::puts");
assert_eq!(result, Some(("puts".to_string(), FfiConvention::C)));
}
#[test]
fn test_parse_ffi_go_cgo_double_colon_empty() {
assert!(parse_ffi_qualified_name("C::").is_none());
}
#[test]
fn test_java_to_jni_c_name() {
assert_eq!(
java_to_jni_c_name("com.example.Class.method"),
"Java_com_example_Class_method"
);
assert_eq!(
java_to_jni_c_name("MyClass.doStuff"),
"Java_MyClass_doStuff"
);
assert_eq!(
java_to_jni_c_name("com::example::Class::method"),
"Java_com_example_Class_method"
);
}
#[test]
fn test_ffi_jni_demangling_with_real_graph() {
use crate::graph::unified::build::helper::GraphBuildHelper;
use crate::graph::unified::build::staging::StagingGraph;
use std::path::PathBuf;
let mut graph = CodeGraph::new();
let java_path = PathBuf::from("/test/MyClass.java");
{
let mut staging = StagingGraph::new();
let mut helper = GraphBuildHelper::new(&mut staging, &java_path, Language::Java);
let _jni_fn = helper.add_function(
"native::jni::com.example.MyClass.doStuff",
None,
false,
false,
);
commit_staging_to_graph(&mut graph, &java_path, Language::Java, staging);
}
let c_path = PathBuf::from("/test/jni_impl.c");
{
let mut staging = StagingGraph::new();
let mut helper = GraphBuildHelper::new(&mut staging, &c_path, Language::C);
let _c_fn = helper.add_function("Java_com_example_MyClass_doStuff", None, false, false);
commit_staging_to_graph(&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);
}
#[test]
fn test_parse_endpoint_all_method() {
let result = parse_endpoint_qualified_name("route::ALL::/health");
assert_eq!(result, Some((HttpMethod::All, "/health".to_string())));
}
#[test]
fn test_http_matching_all_method_endpoint() {
use crate::graph::unified::build::helper::GraphBuildHelper;
use crate::graph::unified::build::staging::StagingGraph;
use crate::graph::unified::edge::kind::HttpMethod;
use std::path::PathBuf;
let mut graph = CodeGraph::new();
let server_path = PathBuf::from("/test/server.ts");
{
let mut staging = StagingGraph::new();
let mut helper =
GraphBuildHelper::new(&mut staging, &server_path, Language::TypeScript);
let _endpoint = helper.add_endpoint("route::ALL::/health", None);
commit_staging_to_graph(&mut graph, &server_path, Language::TypeScript, staging);
}
let client_path = PathBuf::from("/test/client.js");
{
let mut staging = StagingGraph::new();
let mut helper =
GraphBuildHelper::new(&mut staging, &client_path, Language::JavaScript);
let fetch_fn = helper.add_function("checkHealth", None, true, false);
let target = helper.add_module("http::/health", None);
helper.add_http_request_edge(fetch_fn, target, HttpMethod::Get, Some("/health"));
commit_staging_to_graph(&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);
}
#[test]
fn test_http_matching_all_method_request() {
use crate::graph::unified::build::helper::GraphBuildHelper;
use crate::graph::unified::build::staging::StagingGraph;
use crate::graph::unified::edge::kind::HttpMethod;
use std::path::PathBuf;
let mut graph = CodeGraph::new();
let server_path = PathBuf::from("/test/server.py");
{
let mut staging = StagingGraph::new();
let mut helper = GraphBuildHelper::new(&mut staging, &server_path, Language::Python);
let _endpoint = helper.add_endpoint("route::GET::/api/data", None);
commit_staging_to_graph(&mut graph, &server_path, Language::Python, staging);
}
let client_path = PathBuf::from("/test/client.js");
{
let mut staging = StagingGraph::new();
let mut helper =
GraphBuildHelper::new(&mut staging, &client_path, Language::JavaScript);
let fetch_fn = helper.add_function("fetchData", None, true, false);
let target = helper.add_module("http::/api/data", None);
helper.add_http_request_edge(fetch_fn, target, HttpMethod::All, Some("/api/data"));
commit_staging_to_graph(&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);
}
#[test]
fn test_normalize_url_path_typed_params() {
assert_eq!(normalize_url_path("/api/users/<int:id>"), "api/users/:id");
assert_eq!(
normalize_url_path("/files/<path:subpath>"),
"files/:subpath"
);
assert_eq!(normalize_url_path("/api/users/<id>"), "api/users/:id");
}
}