#[path = "support/mod.rs"]
mod support;
use sqry_core::graph::GraphBuilder;
use sqry_core::graph::unified::build::staging::StagingGraph;
use sqry_core::graph::unified::build::staging::StagingOp;
use sqry_core::graph::unified::build::test_helpers::{
assert_has_call_edge, assert_has_export_edge, assert_has_import_edge, assert_has_inherits_edge,
count_operations,
};
use sqry_core::graph::unified::edge::{EdgeKind, HttpMethod};
use sqry_lang_javascript::JavaScriptGraphBuilder;
use std::collections::HashMap;
use std::path::Path;
use tree_sitter::Parser;
fn parse_javascript(source: &str) -> tree_sitter::Tree {
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_javascript::LANGUAGE.into())
.expect("Error loading JavaScript grammar");
parser.parse(source, None).expect("Error parsing")
}
fn build_string_lookup(staging: &StagingGraph) -> HashMap<u32, String> {
let mut lookup = HashMap::new();
for op in staging.operations() {
if let StagingOp::InternString { local_id, value } = op {
lookup.insert(local_id.index(), value.clone());
}
}
lookup
}
fn has_http_request_edge(staging: &StagingGraph, method: HttpMethod, url: Option<&str>) -> bool {
let strings = build_string_lookup(staging);
for op in staging.operations() {
if let StagingOp::AddEdge {
kind:
EdgeKind::HttpRequest {
method: edge_method,
url: edge_url,
},
..
} = op
{
if *edge_method != method {
continue;
}
let url_value = edge_url.and_then(|id| strings.get(&id.index()).cloned());
if url_value.as_deref() == url {
return true;
}
}
}
false
}
#[test]
fn graph_builder_extracts_calls_correctly() {
let source = r"
function helper() { return 1; }
function entry() { return helper(); }
function nested() {
const lam = () => helper();
return lam();
}
";
let tree = parse_javascript(source);
let file = Path::new("test_graph_builder_extracts_calls_correctly.js");
let mut staging = StagingGraph::new();
let builder = JavaScriptGraphBuilder::default();
builder
.build_graph(&tree, source.as_bytes(), file, &mut staging)
.expect("build graph");
assert_has_call_edge(&staging, "entry", "helper");
}
#[test]
fn graph_builder_handles_method_calls() {
let source = r"
class Widget {
helper() { return 1; }
process() { return this.helper(); }
}
";
let tree = parse_javascript(source);
let file = Path::new("test_graph_builder_handles_method_calls.js");
let mut staging = StagingGraph::new();
let builder = JavaScriptGraphBuilder::default();
builder
.build_graph(&tree, source.as_bytes(), file, &mut staging)
.expect("build graph");
assert_has_call_edge(&staging, "Widget::process", "Widget::helper");
}
#[test]
fn graph_builder_handles_arrow_functions() {
let source = r"
function helper() { return 1; }
const process = () => { return helper(); };
";
let tree = parse_javascript(source);
let file = Path::new("test_graph_builder_handles_arrow_functions.js");
let mut staging = StagingGraph::new();
let builder = JavaScriptGraphBuilder::default();
builder
.build_graph(&tree, source.as_bytes(), file, &mut staging)
.expect("build graph");
assert_has_call_edge(&staging, "process", "helper");
}
#[test]
fn graph_builder_handles_async_calls() {
let source = r"
async function fetchData() {
return await fetch('/api/data');
}
";
let tree = parse_javascript(source);
let file = Path::new("test_graph_builder_handles_async_calls.js");
let mut staging = StagingGraph::new();
let builder = JavaScriptGraphBuilder::default();
builder
.build_graph(&tree, source.as_bytes(), file, &mut staging)
.expect("build graph");
assert_has_call_edge(&staging, "fetchData", "fetch");
}
#[test]
fn graph_builder_detects_fetch_calls() {
let source = r"
async function getData() {
const response = await fetch('/api/users');
return response.json();
}
";
let tree = parse_javascript(source);
let file = Path::new("test_graph_builder_detects_fetch_calls.js");
let mut staging = StagingGraph::new();
let builder = JavaScriptGraphBuilder::default();
builder
.build_graph(&tree, source.as_bytes(), file, &mut staging)
.expect("build graph");
assert_has_call_edge(&staging, "getData", "fetch");
assert_has_call_edge(&staging, "getData", "response::json");
}
#[test]
fn graph_builder_detects_axios_calls() {
let source = r"
async function getUsers() {
return await axios.get('/api/users');
}
";
let tree = parse_javascript(source);
let file = Path::new("test_graph_builder_detects_axios_calls.js");
let mut staging = StagingGraph::new();
let builder = JavaScriptGraphBuilder::default();
builder
.build_graph(&tree, source.as_bytes(), file, &mut staging)
.expect("build graph");
assert_has_call_edge(&staging, "getUsers", "axios::get");
}
#[test]
fn graph_builder_handles_constructor_calls() {
let source = r"
function createWidget() {
return new Widget();
}
";
let tree = parse_javascript(source);
let file = Path::new("test_graph_builder_handles_constructor_calls.js");
let mut staging = StagingGraph::new();
let builder = JavaScriptGraphBuilder::default();
builder
.build_graph(&tree, source.as_bytes(), file, &mut staging)
.expect("build graph");
assert_has_call_edge(&staging, "createWidget", "Widget");
}
#[test]
fn graph_builder_handles_empty_file() {
let source = "";
let tree = parse_javascript(source);
let file = Path::new("empty.js");
let mut staging = StagingGraph::new();
let builder = JavaScriptGraphBuilder::default();
let result = builder.build_graph(&tree, source.as_bytes(), file, &mut staging);
assert!(result.is_ok(), "Empty file should build successfully");
let counts = count_operations(&staging);
assert_eq!(counts.edges, 0);
assert_eq!(counts.nodes, 0);
}
#[test]
fn graph_builder_handles_comments_and_literals() {
let source = r#"
// This is a comment
function test() {
const str = "fetch('/api/data')";
const num = 123;
/* Multi-line comment
with code-like text: helper()
*/
return str;
}
"#;
let tree = parse_javascript(source);
let file = Path::new("test_graph_builder_handles_comments_and_literals.js");
let mut staging = StagingGraph::new();
let builder = JavaScriptGraphBuilder::default();
builder
.build_graph(&tree, source.as_bytes(), file, &mut staging)
.expect("build graph");
let call_edge_count = staging
.edges()
.filter(|e| matches!(e.kind, EdgeKind::Calls { .. }))
.count();
assert_eq!(
call_edge_count, 0,
"Should not detect calls from comments/strings"
);
let counts = count_operations(&staging);
assert!(counts.nodes > 0, "Should detect function definition");
}
#[test]
fn graph_builder_handles_nested_calls() {
let source = r"
function a() { return b(); }
function b() { return c(); }
function c() { return 1; }
";
let tree = parse_javascript(source);
let file = Path::new("test_graph_builder_handles_nested_calls.js");
let mut staging = StagingGraph::new();
let builder = JavaScriptGraphBuilder::default();
builder
.build_graph(&tree, source.as_bytes(), file, &mut staging)
.expect("build graph");
assert_has_call_edge(&staging, "a", "b");
assert_has_call_edge(&staging, "b", "c");
}
#[test]
fn graph_builder_handles_top_level_calls() {
let source = r"
import { bootstrap } from './app';
// Top-level call - should be captured with <module> context
bootstrap();
// IIFE - should be captured
(function() {
initialize();
})();
// Top-level constructor
const app = new Application();
app.start();
// Named function for reference
function initialize() {
console.log('init');
}
";
let tree = parse_javascript(source);
let file = Path::new("test_graph_builder_handles_top_level_calls.js");
let mut staging = StagingGraph::new();
let builder = JavaScriptGraphBuilder::default();
builder
.build_graph(&tree, source.as_bytes(), file, &mut staging)
.expect("build graph");
let module_name = "<module>";
assert_has_call_edge(&staging, module_name, "bootstrap");
assert_has_call_edge(&staging, module_name, "Application");
assert_has_call_edge(&staging, module_name, "app::start");
assert_has_call_edge(&staging, "initialize", "console::log");
}
#[test]
fn graph_builder_validates_http_edges() {
let source = r"
async function fetchUsers() {
const response = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify({name: 'test'})
});
return response.json();
}
async function axiosCall() {
const result = await axios.get('/api/data');
return result.data;
}
";
let tree = parse_javascript(source);
let file = Path::new("test_graph_builder_validates_http_edges.js");
let mut staging = StagingGraph::new();
let builder = JavaScriptGraphBuilder::default();
builder
.build_graph(&tree, source.as_bytes(), file, &mut staging)
.expect("build graph");
assert_has_call_edge(&staging, "fetchUsers", "fetch");
assert_has_call_edge(&staging, "axiosCall", "axios::get");
assert!(
has_http_request_edge(&staging, HttpMethod::Post, Some("/api/users")),
"missing HttpRequest edge for POST /api/users"
);
assert!(
has_http_request_edge(&staging, HttpMethod::Get, Some("/api/data")),
"missing HttpRequest edge for GET /api/data"
);
}
#[test]
fn graph_builder_detects_es6_imports() {
let source = r"
import React from 'react';
import { useState, useEffect } from 'react';
import * as Utils from './utils';
import sidebar from './components/sidebar';
function MyComponent() {
return null;
}
";
let tree = parse_javascript(source);
let file = Path::new("src/app/main.js");
let mut staging = StagingGraph::new();
let builder = JavaScriptGraphBuilder::default();
builder
.build_graph(&tree, source.as_bytes(), file, &mut staging)
.expect("build graph");
let module_name = "<module>";
assert_has_import_edge(&staging, module_name, "react");
assert_has_import_edge(&staging, module_name, "src/app/utils");
assert_has_import_edge(&staging, module_name, "src/app/components/sidebar");
}
#[test]
fn graph_builder_detects_default_export_function() {
let source = r"
export default function myFunc() {
return 42;
}
";
let tree = parse_javascript(source);
let file = Path::new("test_default_export.js");
let mut staging = StagingGraph::new();
let builder = JavaScriptGraphBuilder::default();
builder
.build_graph(&tree, source.as_bytes(), file, &mut staging)
.expect("build graph");
let module_name = "<module>";
assert_has_export_edge(&staging, module_name, "myFunc");
}
#[test]
fn graph_builder_detects_default_export_class() {
let source = r"
export default class MyComponent {
render() { return null; }
}
";
let tree = parse_javascript(source);
let file = Path::new("test_default_class_export.js");
let mut staging = StagingGraph::new();
let builder = JavaScriptGraphBuilder::default();
builder
.build_graph(&tree, source.as_bytes(), file, &mut staging)
.expect("build graph");
let module_name = "<module>";
assert_has_export_edge(&staging, module_name, "MyComponent");
}
#[test]
fn graph_builder_detects_commonjs_object_exports() {
let source = r"
function helper(x) {
return x * 2;
}
function calculate(a, b) {
return helper(a) + helper(b);
}
module.exports = {
helper,
calculate
};
";
let tree = parse_javascript(source);
let file = Path::new("test_commonjs_object_exports.js");
let mut staging = StagingGraph::new();
let builder = JavaScriptGraphBuilder::default();
builder
.build_graph(&tree, source.as_bytes(), file, &mut staging)
.expect("build graph");
let module_name = "<module>";
assert_has_export_edge(&staging, module_name, "helper");
assert_has_export_edge(&staging, module_name, "calculate");
}
#[test]
fn graph_builder_detects_commonjs_default_export() {
let source = r"
function myFunction() {
return 42;
}
module.exports = myFunction;
";
let tree = parse_javascript(source);
let file = Path::new("test_commonjs_default_export.js");
let mut staging = StagingGraph::new();
let builder = JavaScriptGraphBuilder::default();
builder
.build_graph(&tree, source.as_bytes(), file, &mut staging)
.expect("build graph");
let module_name = "<module>";
assert_has_export_edge(&staging, module_name, "myFunction");
}
#[test]
fn graph_builder_detects_commonjs_property_exports() {
let source = r"
function foo() {
return 1;
}
function bar() {
return 2;
}
exports.foo = foo;
exports.bar = bar;
";
let tree = parse_javascript(source);
let file = Path::new("test_commonjs_property_exports.js");
let mut staging = StagingGraph::new();
let builder = JavaScriptGraphBuilder::default();
builder
.build_graph(&tree, source.as_bytes(), file, &mut staging)
.expect("build graph");
let module_name = "<module>";
assert_has_export_edge(&staging, module_name, "foo");
assert_has_export_edge(&staging, module_name, "bar");
}
#[test]
fn graph_builder_detects_commonjs_module_exports_property() {
let source = r"
function handler() {
return 'handled';
}
module.exports.handler = handler;
module.exports.version = '1.0.0';
";
let tree = parse_javascript(source);
let file = Path::new("test_commonjs_module_exports_property.js");
let mut staging = StagingGraph::new();
let builder = JavaScriptGraphBuilder::default();
builder
.build_graph(&tree, source.as_bytes(), file, &mut staging)
.expect("build graph");
let module_name = "<module>";
assert_has_export_edge(&staging, module_name, "handler");
assert_has_export_edge(&staging, module_name, "version");
}
#[test]
fn graph_builder_detects_commonjs_anonymous_function_export() {
let source = r"
module.exports = function() {
return 42;
};
";
let tree = parse_javascript(source);
let file = Path::new("test_commonjs_anonymous_export.js");
let mut staging = StagingGraph::new();
let builder = JavaScriptGraphBuilder::default();
builder
.build_graph(&tree, source.as_bytes(), file, &mut staging)
.expect("build graph");
let module_name = "<module>";
assert_has_export_edge(&staging, module_name, "default");
}
#[test]
fn graph_builder_detects_commonjs_class_export() {
let source = r"
class MyClass {
constructor() {
this.value = 42;
}
getValue() {
return this.value;
}
}
module.exports = MyClass;
";
let tree = parse_javascript(source);
let file = Path::new("test_commonjs_class_export.js");
let mut staging = StagingGraph::new();
let builder = JavaScriptGraphBuilder::default();
builder
.build_graph(&tree, source.as_bytes(), file, &mut staging)
.expect("build graph");
let module_name = "<module>";
assert_has_export_edge(&staging, module_name, "MyClass");
}
#[test]
fn graph_builder_detects_commonjs_arrow_function_export() {
let source = r"
module.exports = () => {
return 'arrow';
};
";
let tree = parse_javascript(source);
let file = Path::new("test_commonjs_arrow_export.js");
let mut staging = StagingGraph::new();
let builder = JavaScriptGraphBuilder::default();
builder
.build_graph(&tree, source.as_bytes(), file, &mut staging)
.expect("build graph");
let module_name = "<module>";
assert_has_export_edge(&staging, module_name, "default");
}
#[test]
fn graph_builder_detects_commonjs_object_with_pair() {
let source = r"
function internalHelper() {
return 'helper';
}
function publicAPI() {
return 'api';
}
module.exports = {
helper: internalHelper,
api: publicAPI
};
";
let tree = parse_javascript(source);
let file = Path::new("test_commonjs_object_with_pair.js");
let mut staging = StagingGraph::new();
let builder = JavaScriptGraphBuilder::default();
builder
.build_graph(&tree, source.as_bytes(), file, &mut staging)
.expect("build graph");
let module_name = "<module>";
assert_has_export_edge(&staging, module_name, "helper");
assert_has_export_edge(&staging, module_name, "api");
}
#[test]
fn graph_builder_detects_class_inheritance() {
let source = r"
class Parent {
method() {}
}
class Child extends Parent {
childMethod() {}
}
";
let tree = parse_javascript(source);
let file = Path::new("test_class_inheritance.js");
let mut staging = StagingGraph::new();
let builder = JavaScriptGraphBuilder::default();
builder
.build_graph(&tree, source.as_bytes(), file, &mut staging)
.expect("build graph");
assert_has_inherits_edge(&staging, "Child", "Parent");
}
#[test]
fn graph_builder_detects_qualified_extends_path() {
let source = r"
class Child extends Module.Parent {
method() {}
}
";
let tree = parse_javascript(source);
let file = Path::new("test_qualified_extends.js");
let mut staging = StagingGraph::new();
let builder = JavaScriptGraphBuilder::default();
builder
.build_graph(&tree, source.as_bytes(), file, &mut staging)
.expect("build graph");
assert_has_inherits_edge(&staging, "Child", "Module::Parent");
}
#[test]
fn graph_builder_handles_react_component_qualified_extends() {
let source = r"
import React from 'react';
class MyComponent extends React.Component {
render() {
return null;
}
}
";
let tree = parse_javascript(source);
let file = Path::new("test_react_component.js");
let mut staging = StagingGraph::new();
let builder = JavaScriptGraphBuilder::default();
builder
.build_graph(&tree, source.as_bytes(), file, &mut staging)
.expect("build graph");
assert_has_inherits_edge(&staging, "MyComponent", "React::Component");
}
use sqry_core::graph::unified::node::NodeKind;
fn has_endpoint_with_name(staging: &StagingGraph, name_substring: &str) -> bool {
staging.operations().iter().any(|op| {
if let StagingOp::AddNode { entry, .. } = op
&& matches!(entry.kind, NodeKind::Endpoint)
&& let Some(resolved) = staging.resolve_local_string(entry.name)
{
return resolved.contains(name_substring);
}
false
})
}
fn collect_endpoint_names(staging: &StagingGraph) -> Vec<String> {
staging
.operations()
.iter()
.filter_map(|op| {
if let StagingOp::AddNode { entry, .. } = op
&& matches!(entry.kind, NodeKind::Endpoint)
{
return staging.resolve_local_string(entry.name).map(String::from);
}
None
})
.collect()
}
#[test]
fn test_js_express_all_creates_endpoint() {
let source = r#"
const express = require("express");
const app = express();
app.all("/health", (req, res) => {
res.json({ status: "ok" });
});
"#;
let tree = parse_javascript(source);
let file = Path::new("test_js_express_all.js");
let mut staging = StagingGraph::new();
let builder = JavaScriptGraphBuilder::default();
builder
.build_graph(&tree, source.as_bytes(), file, &mut staging)
.expect("build graph");
assert!(
has_endpoint_with_name(&staging, "route::ALL::/health"),
"Expected Endpoint 'route::ALL::/health', found: {:?}",
collect_endpoint_names(&staging)
);
assert!(
!has_endpoint_with_name(&staging, "route::GET::/health"),
"app.all() should NOT create route::GET, found: {:?}",
collect_endpoint_names(&staging)
);
}