use std::{collections::HashMap, path::Path, sync::Arc};
use sqry_core::graph::unified::build::helper::CalleeKindHint;
use sqry_core::graph::unified::edge::kind::TypeOfContext;
use sqry_core::graph::unified::edge::{ExportKind, FfiConvention, HttpMethod};
use sqry_core::graph::unified::{GraphBuildHelper, NodeId, StagingGraph};
use sqry_core::graph::{GraphBuilder, GraphBuilderError, GraphResult, Language, Position, Span};
use sqry_core::relations::SyntheticNameBuilder;
use tree_sitter::{Node, Tree};
use super::jsdoc_parser::{extract_jsdoc_comment, parse_jsdoc_tags};
use super::local_scopes;
use super::type_extractor::{canonical_type_string, extract_type_names};
const DEFAULT_SCOPE_DEPTH: usize = 4;
type CallEdgeData = (NodeId, NodeId, u8, bool, Option<Span>);
type ConstructorEdgeData = (NodeId, NodeId, u8, Option<Span>);
#[derive(Debug, Clone, Copy)]
pub struct JavaScriptGraphBuilder {
max_scope_depth: usize,
}
impl Default for JavaScriptGraphBuilder {
fn default() -> Self {
Self {
max_scope_depth: DEFAULT_SCOPE_DEPTH,
}
}
}
impl JavaScriptGraphBuilder {
#[must_use]
pub fn new(max_scope_depth: usize) -> Self {
Self { max_scope_depth }
}
}
fn infer_visibility(qualified_name: &str) -> &'static str {
let name_part = qualified_name.rsplit('.').next().unwrap_or(qualified_name);
if name_part.starts_with('_') {
"private"
} else {
"public"
}
}
impl GraphBuilder for JavaScriptGraphBuilder {
fn build_graph(
&self,
tree: &Tree,
content: &[u8],
file: &Path,
staging: &mut StagingGraph,
) -> GraphResult<()> {
let mut helper = GraphBuildHelper::new(staging, file, Language::JavaScript);
let file_arc = Arc::from(file.to_string_lossy().to_string());
let ast_graph = ASTGraph::from_tree(tree, content, self.max_scope_depth).map_err(|e| {
GraphBuilderError::ParseError {
span: Span::default(),
reason: e,
}
})?;
for context in ast_graph.contexts() {
let span = Some(Span::from_bytes(context.span.0, context.span.1));
let visibility = infer_visibility(&context.qualified_name);
if context.qualified_name.contains('.') {
helper.add_method_with_visibility(
&context.qualified_name,
span,
context.is_async,
false, Some(visibility),
);
} else {
helper.add_function_with_visibility(
&context.qualified_name,
span,
context.is_async,
false, Some(visibility),
);
}
}
let mut scope_tree = local_scopes::build(tree.root_node(), content)?;
let mut cursor = tree.root_node().walk();
extract_edges_recursive(
tree.root_node(),
&mut cursor,
content,
&file_arc,
&ast_graph,
&mut helper,
&mut scope_tree,
)?;
process_jsdoc_annotations(tree.root_node(), content, &mut helper)?;
Ok(())
}
fn language(&self) -> Language {
Language::JavaScript
}
}
fn extract_edges_recursive<'a>(
node: Node<'a>,
cursor: &mut tree_sitter::TreeCursor<'a>,
content: &[u8],
file: &Arc<str>,
ast_graph: &ASTGraph,
helper: &mut GraphBuildHelper,
scope_tree: &mut local_scopes::JavaScriptScopeTree,
) -> GraphResult<()> {
match node.kind() {
"call_expression" => {
let _ = build_http_request_edge(ast_graph, node, content, helper);
let _ = detect_route_endpoint(node, content, helper);
let is_ffi = build_ffi_call_edge(ast_graph, node, content, helper)?;
if !is_ffi {
if let Some((caller_id, callee_id, argument_count, is_async, span)) =
build_call_edge_with_helper(ast_graph, node, content, helper)?
{
helper.add_call_edge_full_with_span(
caller_id,
callee_id,
argument_count,
is_async,
span.into_iter().collect(),
);
}
}
}
"new_expression" => {
let is_ffi = build_ffi_new_edge(ast_graph, node, content, helper)?;
if !is_ffi {
if let Some((caller_id, callee_id, argument_count, span)) =
build_constructor_edge_with_helper(ast_graph, node, content, helper)?
{
helper.add_call_edge_full_with_span(
caller_id,
callee_id,
argument_count,
false,
span.into_iter().collect(),
);
}
}
}
"import_statement" => {
if let Some((from_id, to_id)) =
build_import_edge_with_helper(node, content, file, helper)?
{
helper.add_import_edge(from_id, to_id);
}
}
"export_statement" => {
build_export_edges_with_helper(node, content, file, helper);
}
"expression_statement" => {
build_commonjs_export_edges(node, content, helper);
}
"class_declaration" | "class" => {
build_inherits_edge_with_helper(node, content, helper);
}
"identifier" => {
local_scopes::handle_identifier_for_reference(node, content, scope_tree, helper);
}
_ => {}
}
let children: Vec<_> = node.children(cursor).collect();
for child in children {
let mut child_cursor = child.walk();
extract_edges_recursive(
child,
&mut child_cursor,
content,
file,
ast_graph,
helper,
scope_tree,
)?;
}
Ok(())
}
fn build_call_edge_with_helper(
ast_graph: &ASTGraph,
call_node: Node<'_>,
content: &[u8],
helper: &mut GraphBuildHelper,
) -> GraphResult<Option<CallEdgeData>> {
let module_context;
let call_context = if let Some(ctx) = ast_graph.get_callable_context(call_node.id()) {
ctx
} else {
module_context = CallContext {
name: Arc::from("<module>"),
qualified_name: "<module>".to_string(),
span: (0, content.len()),
is_async: false,
};
&module_context
};
let Some(callee_expr) = call_node.child_by_field_name("function") else {
return Ok(None);
};
let raw_callee_text = callee_expr
.utf8_text(content)
.map_err(|_| GraphBuilderError::ParseError {
span: span_from_node(call_node),
reason: "failed to read call expression".to_string(),
})?
.trim()
.to_string();
let callee_text = if raw_callee_text.contains("?.") {
normalize_optional_chain(&raw_callee_text)
} else {
raw_callee_text
};
if callee_text.is_empty() {
return Ok(None);
}
let callee_simple = simple_name(&callee_text);
if callee_simple.is_empty() {
return Ok(None);
}
let caller_qname = call_context.qualified_name();
let target_qname = if let Some(method_name) = callee_text.strip_prefix("this.") {
if let Some(scope_idx) = caller_qname.rfind('.') {
let class_name = &caller_qname[..scope_idx];
format!("{}.{}", class_name, simple_name(method_name))
} else {
callee_text.clone()
}
} else if callee_text.starts_with("super.") || callee_text.contains('.') {
callee_text.clone()
} else {
callee_simple.to_string()
};
let source_id = ensure_caller_node(helper, call_context);
let call_site_span = span_from_node(call_node);
let target_id = helper.ensure_callee(&target_qname, call_site_span, CalleeKindHint::Function);
let span = Some(call_site_span);
let argument_count = u8::try_from(count_arguments(call_node)).unwrap_or(u8::MAX);
let is_async = check_uses_await(call_node);
Ok(Some((source_id, target_id, argument_count, is_async, span)))
}
#[derive(Debug, Clone)]
struct HttpRequestInfo {
method: HttpMethod,
url: Option<String>,
}
fn build_http_request_edge(
ast_graph: &ASTGraph,
call_node: Node<'_>,
content: &[u8],
helper: &mut GraphBuildHelper,
) -> bool {
let Some(info) = extract_http_request_info(call_node, content) else {
return false;
};
let caller_id = get_caller_node_id(ast_graph, call_node, content, helper);
let target_name = info.url.as_ref().map_or_else(
|| format!("http::{}", info.method.as_str()),
|url| format!("http::{url}"),
);
let target_id = helper.add_module(&target_name, Some(span_from_node(call_node)));
helper.add_http_request_edge(caller_id, target_id, info.method, info.url.as_deref());
true
}
fn detect_route_endpoint(
call_node: Node<'_>,
content: &[u8],
helper: &mut GraphBuildHelper,
) -> bool {
let Some(callee) = call_node.child_by_field_name("function") else {
return false;
};
if callee.kind() != "member_expression" {
return false;
}
let Some(property) = callee.child_by_field_name("property") else {
return false;
};
let Ok(method_name) = property.utf8_text(content) else {
return false;
};
let method_name = method_name.trim();
let method_str = match method_name {
"get" => "GET",
"post" => "POST",
"put" => "PUT",
"delete" => "DELETE",
"patch" => "PATCH",
"all" => "ALL",
_ => return false,
};
let Some(args) = call_node.child_by_field_name("arguments") else {
return false;
};
let mut cursor = args.walk();
let first_arg = args
.children(&mut cursor)
.find(|child| !matches!(child.kind(), "(" | ")" | ","));
let Some(first_arg) = first_arg else {
return false;
};
let Some(path) = extract_string_literal(&first_arg, content) else {
return false;
};
let qualified_name = format!("route::{method_str}::{path}");
let endpoint_id = helper.add_endpoint(&qualified_name, Some(span_from_node(call_node)));
let mut handler_cursor = args.walk();
let handler_arg = args
.children(&mut handler_cursor)
.filter(|child| !matches!(child.kind(), "(" | ")" | ","))
.nth(1);
if let Some(handler_node) = handler_arg
&& let Ok(handler_text) = handler_node.utf8_text(content)
{
let handler_name = handler_text.trim();
if !handler_name.is_empty()
&& matches!(handler_node.kind(), "identifier" | "member_expression")
{
let handler_id = helper.ensure_callee(
handler_name,
span_from_node(handler_node),
CalleeKindHint::Function,
);
helper.add_contains_edge(endpoint_id, handler_id);
}
}
true
}
fn extract_http_request_info(call_node: Node<'_>, content: &[u8]) -> Option<HttpRequestInfo> {
let callee = call_node.child_by_field_name("function")?;
let callee_text = callee.utf8_text(content).ok()?.trim().to_string();
if callee_text == "fetch" {
return Some(extract_fetch_http_info(call_node, content));
}
if callee_text == "axios" {
return extract_axios_http_info(call_node, content);
}
if let Some(method_name) = callee_text.strip_prefix("axios.") {
let method = http_method_from_name(method_name)?;
let url = extract_first_arg_url(call_node, content);
return Some(HttpRequestInfo { method, url });
}
None
}
fn extract_fetch_http_info(call_node: Node<'_>, content: &[u8]) -> HttpRequestInfo {
let url = extract_first_arg_url(call_node, content);
let method = extract_method_from_options(call_node, content).unwrap_or(HttpMethod::Get);
HttpRequestInfo { method, url }
}
fn extract_axios_http_info(call_node: Node<'_>, content: &[u8]) -> Option<HttpRequestInfo> {
let args = call_node.child_by_field_name("arguments")?;
let mut cursor = args.walk();
let mut non_trivia = args
.children(&mut cursor)
.filter(|child| !matches!(child.kind(), "(" | ")" | ","));
let first_arg = non_trivia.next()?;
let second_arg = non_trivia.next();
if first_arg.kind() == "object" {
let (method, url) = extract_method_and_url_from_object(first_arg, content);
return Some(HttpRequestInfo {
method: method.unwrap_or(HttpMethod::Get),
url,
});
}
let url = extract_string_literal(&first_arg, content);
let method = if let Some(config) = second_arg {
if config.kind() == "object" {
extract_method_from_object(config, content)
} else {
None
}
} else {
None
};
Some(HttpRequestInfo {
method: method.unwrap_or(HttpMethod::Get),
url,
})
}
fn extract_first_arg_url(call_node: Node<'_>, content: &[u8]) -> Option<String> {
let args = call_node.child_by_field_name("arguments")?;
let mut cursor = args.walk();
let first_arg = args
.children(&mut cursor)
.find(|child| !matches!(child.kind(), "(" | ")" | ","))?;
extract_string_literal(&first_arg, content)
}
fn extract_method_from_options(call_node: Node<'_>, content: &[u8]) -> Option<HttpMethod> {
let args = call_node.child_by_field_name("arguments")?;
let mut cursor = args.walk();
let mut non_trivia = args
.children(&mut cursor)
.filter(|child| !matches!(child.kind(), "(" | ")" | ","));
let _first_arg = non_trivia.next()?;
let second_arg = non_trivia.next()?;
if second_arg.kind() != "object" {
return None;
}
extract_method_from_object(second_arg, content)
}
fn extract_method_from_object(obj_node: Node<'_>, content: &[u8]) -> Option<HttpMethod> {
let (method, _url) = extract_method_and_url_from_object(obj_node, content);
method
}
fn extract_method_and_url_from_object(
obj_node: Node<'_>,
content: &[u8],
) -> (Option<HttpMethod>, Option<String>) {
let mut method = None;
let mut url = None;
let mut cursor = obj_node.walk();
for child in obj_node.children(&mut cursor) {
if child.kind() != "pair" {
continue;
}
let Some(key_node) = child.child_by_field_name("key") else {
continue;
};
let key_text = extract_object_key_text(&key_node, content);
let Some(value_node) = child.child_by_field_name("value") else {
continue;
};
if key_text.as_deref() == Some("method") {
if let Some(value) = extract_string_literal(&value_node, content) {
method = http_method_from_name(&value);
}
} else if key_text.as_deref() == Some("url") {
url = extract_string_literal(&value_node, content);
}
}
(method, url)
}
fn extract_object_key_text(node: &Node<'_>, content: &[u8]) -> Option<String> {
let raw = node.utf8_text(content).ok()?.trim().to_string();
if let Some(value) = extract_string_literal(node, content) {
return Some(value);
}
if raw.is_empty() {
return None;
}
Some(raw)
}
fn http_method_from_name(name: &str) -> Option<HttpMethod> {
match name.trim().to_ascii_lowercase().as_str() {
"get" => Some(HttpMethod::Get),
"post" => Some(HttpMethod::Post),
"put" => Some(HttpMethod::Put),
"delete" => Some(HttpMethod::Delete),
"patch" => Some(HttpMethod::Patch),
"head" => Some(HttpMethod::Head),
"options" => Some(HttpMethod::Options),
_ => None,
}
}
fn build_constructor_edge_with_helper(
ast_graph: &ASTGraph,
new_node: Node<'_>,
content: &[u8],
helper: &mut GraphBuildHelper,
) -> GraphResult<Option<ConstructorEdgeData>> {
let module_context;
let call_context = if let Some(ctx) = ast_graph.get_callable_context(new_node.id()) {
ctx
} else {
module_context = CallContext {
name: Arc::from("<module>"),
qualified_name: "<module>".to_string(),
span: (0, content.len()),
is_async: false,
};
&module_context
};
let Some(constructor_expr) = new_node.child_by_field_name("constructor") else {
return Ok(None);
};
let constructor_text = constructor_expr
.utf8_text(content)
.map_err(|_| GraphBuilderError::ParseError {
span: span_from_node(new_node),
reason: "failed to read constructor expression".to_string(),
})?
.trim()
.to_string();
if constructor_text.is_empty() {
return Ok(None);
}
let constructor_simple = simple_name(&constructor_text);
let source_id = ensure_caller_node(helper, call_context);
let new_site_span = span_from_node(new_node);
let target_id =
helper.ensure_callee(constructor_simple, new_site_span, CalleeKindHint::Function);
let span = Some(new_site_span);
let argument_count = u8::try_from(count_arguments(new_node)).unwrap_or(u8::MAX);
Ok(Some((source_id, target_id, argument_count, span)))
}
fn build_import_edge_with_helper(
import_node: Node<'_>,
content: &[u8],
file: &Arc<str>,
helper: &mut GraphBuildHelper,
) -> GraphResult<
Option<(
sqry_core::graph::unified::NodeId,
sqry_core::graph::unified::NodeId,
)>,
> {
let Some(source_node) = import_node.child_by_field_name("source") else {
return Ok(None);
};
let source_text = source_node
.utf8_text(content)
.map_err(|_| GraphBuilderError::ParseError {
span: span_from_node(import_node),
reason: "failed to read import source".to_string(),
})?
.trim()
.trim_matches(|c| c == '"' || c == '\'')
.to_string();
if source_text.is_empty() {
return Ok(None);
}
let resolved_path =
sqry_core::graph::resolve_import_path(std::path::Path::new(file.as_ref()), &source_text)?;
let from_id = helper.add_module("<module>", None);
let to_id = helper.add_import(&resolved_path, Some(span_from_node(import_node)));
Ok(Some((from_id, to_id)))
}
#[allow(clippy::too_many_lines)]
fn build_export_edges_with_helper(
export_node: Node<'_>,
content: &[u8],
file: &Arc<str>,
helper: &mut GraphBuildHelper,
) {
let module_id = helper.add_module("<module>", None);
let source_node = export_node.child_by_field_name("source");
let is_reexport = source_node.is_some();
let has_default = export_node
.children(&mut export_node.walk())
.any(|child| child.kind() == "default");
let namespace_export = export_node
.children(&mut export_node.walk())
.find(|child| child.kind() == "namespace_export");
let has_wildcard = export_node
.children(&mut export_node.walk())
.any(|child| child.kind() == "*");
let export_clause = export_node
.children(&mut export_node.walk())
.find(|child| child.kind() == "export_clause");
let declaration = export_node.children(&mut export_node.walk()).find(|child| {
matches!(
child.kind(),
"function_declaration"
| "class_declaration"
| "lexical_declaration"
| "variable_declaration"
| "generator_function_declaration"
)
});
if has_default {
let exported_name = if let Some(ref decl) = declaration {
decl.child_by_field_name("name")
.and_then(|n| n.utf8_text(content).ok())
.map_or_else(|| "default".to_string(), |s| s.trim().to_string())
} else {
export_node
.children(&mut export_node.walk())
.find(|child| child.kind() == "identifier")
.and_then(|n| n.utf8_text(content).ok())
.map_or_else(|| "default".to_string(), |s| s.trim().to_string())
};
let exported_id = helper.add_function(&exported_name, None, false, false);
helper.add_export_edge_full(module_id, exported_id, ExportKind::Default, None);
} else if let Some(ns_export) = namespace_export {
let alias = ns_export
.children(&mut ns_export.walk())
.find(|child| child.kind() == "identifier")
.and_then(|n| n.utf8_text(content).ok())
.map(|s| s.trim().to_string());
let source_path = source_node
.and_then(|s| s.utf8_text(content).ok())
.map_or_else(
|| "<unknown>".to_string(),
|s| s.trim().trim_matches(|c| c == '"' || c == '\'').to_string(),
);
let resolved_path = sqry_core::graph::resolve_import_path(
std::path::Path::new(file.as_ref()),
&source_path,
)
.unwrap_or(source_path);
let source_module_id = helper.add_module(&resolved_path, None);
helper.add_export_edge_full(
module_id,
source_module_id,
ExportKind::Namespace,
alias.as_deref(),
);
} else if has_wildcard && is_reexport {
let source_path = source_node
.and_then(|s| s.utf8_text(content).ok())
.map_or_else(
|| "<unknown>".to_string(),
|s| s.trim().trim_matches(|c| c == '"' || c == '\'').to_string(),
);
let resolved_path = sqry_core::graph::resolve_import_path(
std::path::Path::new(file.as_ref()),
&source_path,
)
.unwrap_or(source_path);
let source_module_id = helper.add_module(&resolved_path, None);
helper.add_export_edge_full(module_id, source_module_id, ExportKind::Reexport, None);
} else if let Some(clause) = export_clause {
let mut cursor = clause.walk();
for child in clause.children(&mut cursor) {
if child.kind() == "export_specifier" {
let identifiers: Vec<_> = child
.children(&mut child.walk())
.filter(|n| n.kind() == "identifier")
.collect();
if let Some(first_ident) = identifiers.first() {
let local_name = first_ident
.utf8_text(content)
.ok()
.map(|s| s.trim().to_string())
.unwrap_or_default();
if local_name.is_empty() {
continue;
}
let alias = identifiers.get(1).and_then(|n| {
n.utf8_text(content)
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
});
let exported_id = helper.add_function(&local_name, None, false, false);
let kind = if is_reexport {
ExportKind::Reexport
} else {
ExportKind::Direct
};
helper.add_export_edge_full(module_id, exported_id, kind, alias.as_deref());
}
}
}
} else if let Some(decl) = declaration {
match decl.kind() {
"function_declaration" | "generator_function_declaration" => {
if let Some(name_node) = decl.child_by_field_name("name")
&& let Ok(name) = name_node.utf8_text(content)
{
let name = name.trim().to_string();
if !name.is_empty() {
let exported_id = helper.add_function(&name, None, false, false);
helper.add_export_edge_full(
module_id,
exported_id,
ExportKind::Direct,
None,
);
}
}
}
"class_declaration" => {
if let Some(name_node) = decl.child_by_field_name("name")
&& let Ok(name) = name_node.utf8_text(content)
{
let name = name.trim().to_string();
if !name.is_empty() {
let exported_id = helper.add_class(&name, None);
helper.add_export_edge_full(
module_id,
exported_id,
ExportKind::Direct,
None,
);
}
}
}
"lexical_declaration" | "variable_declaration" => {
let mut cursor = decl.walk();
for child in decl.children(&mut cursor) {
if child.kind() == "variable_declarator"
&& let Some(name_node) = child.child_by_field_name("name")
&& let Ok(name) = name_node.utf8_text(content)
{
let name = name.trim().to_string();
if !name.is_empty() {
let exported_id = helper.add_variable(&name, None);
helper.add_export_edge_full(
module_id,
exported_id,
ExportKind::Direct,
None,
);
}
}
}
}
_ => {}
}
}
}
fn build_commonjs_export_edges(
expr_stmt_node: Node<'_>,
content: &[u8],
helper: &mut GraphBuildHelper,
) {
let Some(assignment) = expr_stmt_node
.children(&mut expr_stmt_node.walk())
.find(|child| child.kind() == "assignment_expression")
else {
return;
};
let Some(left) = assignment.child_by_field_name("left") else {
return;
};
let Some(right) = assignment.child_by_field_name("right") else {
return;
};
let left_text = left.utf8_text(content).ok().map(|s| s.trim().to_string());
let Some(left_text) = left_text else {
return;
};
let module_id = helper.add_module("<module>", None);
if left_text == "module.exports" {
if right.kind() == "object" {
let mut cursor = right.walk();
for child in right.children(&mut cursor) {
if child.kind() == "shorthand_property_identifier" {
if let Ok(name) = child.utf8_text(content) {
let name = name.trim();
if !name.is_empty() {
let exported_id = helper.add_function(name, None, false, false);
helper.add_export_edge_full(
module_id,
exported_id,
ExportKind::Direct,
None,
);
}
}
} else if child.kind() == "pair" {
if let Some(key_node) = child.child_by_field_name("key")
&& let Ok(export_name) = key_node.utf8_text(content)
{
let export_name = export_name.trim();
if !export_name.is_empty() {
let exported_id = helper.add_function(export_name, None, false, false);
helper.add_export_edge_full(
module_id,
exported_id,
ExportKind::Direct,
None,
);
}
}
} else if child.kind() == "spread_element" {
}
}
} else if right.kind() == "identifier" || right.kind() == "member_expression" {
let export_name = right
.utf8_text(content)
.ok()
.map_or_else(|| "default".to_string(), |s| s.trim().to_string());
if !export_name.is_empty() {
let exported_id = helper.add_function(&export_name, None, false, false);
helper.add_export_edge_full(module_id, exported_id, ExportKind::Default, None);
}
} else if matches!(
right.kind(),
"function_expression"
| "arrow_function"
| "class"
| "call_expression"
| "new_expression"
) {
let exported_id = helper.add_function("default", None, false, false);
helper.add_export_edge_full(module_id, exported_id, ExportKind::Default, None);
}
}
else if left_text.starts_with("exports.") || left_text.starts_with("module.exports.") {
let export_name = if let Some(name) = left_text.strip_prefix("module.exports.") {
name
} else if let Some(name) = left_text.strip_prefix("exports.") {
name
} else {
return;
};
if !export_name.is_empty() {
let exported_id = helper.add_function(export_name, None, false, false);
helper.add_export_edge_full(module_id, exported_id, ExportKind::Direct, None);
}
}
}
fn build_inherits_edge_with_helper(
class_node: Node<'_>,
content: &[u8],
helper: &mut GraphBuildHelper,
) {
let heritage = class_node
.children(&mut class_node.walk())
.find(|child| child.kind() == "class_heritage");
let Some(heritage_node) = heritage else {
return; };
let class_name = if class_node.kind() == "class_declaration" {
class_node
.child_by_field_name("name")
.and_then(|n| n.utf8_text(content).ok())
.map(|s| s.trim().to_string())
} else {
class_node
.parent()
.filter(|p| p.kind() == "variable_declarator")
.and_then(|p| p.child_by_field_name("name"))
.and_then(|n| n.utf8_text(content).ok())
.map(|s| s.trim().to_string())
.or_else(|| {
Some(SyntheticNameBuilder::from_node_with_hash(
&class_node,
content,
"class",
))
})
};
let parent_name = extract_parent_class_name(heritage_node, content);
if let (Some(child_name), Some(parent_name)) = (class_name, parent_name)
&& !child_name.is_empty()
&& !parent_name.is_empty()
{
let child_id = helper.add_class(&child_name, None);
let parent_id = helper.add_class(&parent_name, None);
helper.add_inherits_edge(child_id, parent_id);
}
}
fn extract_parent_class_name(heritage_node: Node<'_>, content: &[u8]) -> Option<String> {
let mut cursor = heritage_node.walk();
for child in heritage_node.children(&mut cursor) {
match child.kind() {
"identifier" => {
return child.utf8_text(content).ok().map(|s| s.trim().to_string());
}
"member_expression" => {
return child.utf8_text(content).ok().map(|s| s.trim().to_string());
}
"call_expression" => {
return child.utf8_text(content).ok().map(|s| s.trim().to_string());
}
_ => {}
}
}
None
}
fn simple_name(name: &str) -> &str {
name.rsplit(['.', '/']).next().unwrap_or(name)
}
fn normalize_optional_chain(text: &str) -> String {
text.replace("?.", ".")
.trim()
.trim_end_matches('.')
.to_string()
}
fn check_uses_await(call_node: Node<'_>) -> bool {
let mut current = call_node;
for _ in 0..2 {
if let Some(parent) = current.parent() {
if parent.kind() == "await_expression" {
return true;
}
current = parent;
} else {
break;
}
}
false
}
fn count_arguments(node: Node<'_>) -> usize {
node.child_by_field_name("arguments").map_or(0, |args| {
let mut count = 0;
let mut cursor = args.walk();
for child in args.children(&mut cursor) {
if !matches!(child.kind(), "(" | ")" | ",") {
count += 1;
}
}
count
})
}
fn span_from_node(node: Node<'_>) -> Span {
let start = node.start_position();
let end = node.end_position();
Span::new(
Position::new(start.row, start.column),
Position::new(end.row, end.column),
)
}
fn extract_string_literal(node: &Node, content: &[u8]) -> Option<String> {
let text = node.utf8_text(content).ok()?;
let trimmed = text.trim();
trimmed
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.or_else(|| {
trimmed
.strip_prefix('\'')
.and_then(|s| s.strip_suffix('\''))
})
.or_else(|| trimmed.strip_prefix('`').and_then(|s| s.strip_suffix('`')))
.map(std::string::ToString::to_string)
}
#[derive(Debug, Clone)]
pub struct CallContext {
#[allow(dead_code)] pub name: Arc<str>,
pub qualified_name: String,
pub span: (usize, usize),
pub is_async: bool,
}
impl CallContext {
pub fn qualified_name(&self) -> &str {
&self.qualified_name
}
}
pub struct ASTGraph {
callable_map: HashMap<usize, usize>,
context_map: HashMap<usize, CallContext>,
}
impl ASTGraph {
pub fn from_tree(tree: &Tree, content: &[u8], max_scope_depth: usize) -> Result<Self, String> {
let mut builder = ASTGraphBuilder::new(content, max_scope_depth);
let recursion_limits = sqry_core::config::RecursionLimits::load_or_default()
.map_err(|e| format!("Failed to load recursion limits: {e}"))?;
let file_ops_depth = recursion_limits
.effective_file_ops_depth()
.map_err(|e| format!("Invalid file_ops_depth configuration: {e}"))?;
let mut guard = sqry_core::query::security::RecursionGuard::new(file_ops_depth)
.map_err(|e| format!("Failed to create recursion guard: {e}"))?;
builder
.visit(tree.root_node(), None, &mut guard)
.map_err(|e| format!("JavaScript AST traversal hit recursion limit: {e}"))?;
Ok(builder.build())
}
pub fn get_callable_context(&self, node_id: usize) -> Option<&CallContext> {
let callable_id = self.callable_map.get(&node_id)?;
self.context_map.get(callable_id)
}
pub fn contexts(&self) -> impl Iterator<Item = &CallContext> {
self.context_map.values()
}
}
struct ASTGraphBuilder<'a> {
content: &'a [u8],
max_scope_depth: usize,
callable_map: HashMap<usize, usize>,
context_map: HashMap<usize, CallContext>,
#[allow(dead_code)] current_callable: Option<usize>,
current_scope: Vec<Arc<str>>,
}
impl<'a> ASTGraphBuilder<'a> {
fn new(content: &'a [u8], max_scope_depth: usize) -> Self {
Self {
content,
max_scope_depth,
callable_map: HashMap::new(),
context_map: HashMap::new(),
current_callable: None,
current_scope: Vec::new(),
}
}
fn build(self) -> ASTGraph {
ASTGraph {
callable_map: self.callable_map,
context_map: self.context_map,
}
}
fn visit(
&mut self,
node: Node<'_>,
parent_callable: Option<usize>,
guard: &mut sqry_core::query::security::RecursionGuard,
) -> Result<(), sqry_core::query::security::RecursionError> {
guard.enter()?;
let node_id = node.id();
let callable_name = callable_node_name(node, self.content);
let new_callable = if let Some(name) = callable_name {
let start = node.start_byte();
let end = node.end_byte();
let is_async = is_async_function(node, self.content);
let qualified_name = if self.current_scope.is_empty() {
name.to_string()
} else if self.current_scope.len() <= self.max_scope_depth {
format!("{}.{}", self.current_scope.join("."), name)
} else {
let truncated = &self.current_scope[..self.max_scope_depth];
format!("{}.{}", truncated.join("."), name)
};
let context = CallContext {
name: Arc::from(name),
qualified_name,
span: (start, end),
is_async,
};
self.context_map.insert(node_id, context);
Some(node_id)
} else {
None
};
let effective_callable = new_callable.or(parent_callable);
if let Some(callable_id) = effective_callable {
self.callable_map.insert(node_id, callable_id);
}
let scope_name = scope_node_name(node, self.content);
let pushed_scope = if let Some(name) = scope_name {
self.current_scope.push(Arc::from(name));
true
} else {
false
};
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
self.visit(child, effective_callable, guard)?;
}
if pushed_scope {
self.current_scope.pop();
}
guard.exit();
Ok(())
}
}
fn callable_node_name(node: Node<'_>, content: &[u8]) -> Option<String> {
match node.kind() {
"function_declaration" | "generator_function_declaration" => node
.child_by_field_name("name")
.and_then(|child| child.utf8_text(content).ok().map(|s| s.trim().to_string())),
"function_expression" | "generator_function" => {
node.child_by_field_name("name")
.and_then(|child| child.utf8_text(content).ok().map(|s| s.trim().to_string()))
.or_else(|| {
Some(SyntheticNameBuilder::from_node_with_hash(
&node, content, "function",
))
})
}
"arrow_function" => {
if let Some(parent) = node.parent()
&& parent.kind() == "variable_declarator"
&& let Some(name_node) = parent.child_by_field_name("name")
&& let Ok(name) = name_node.utf8_text(content)
{
let trimmed = name.trim();
if !trimmed.is_empty() {
return Some(trimmed.to_string());
}
}
Some(SyntheticNameBuilder::from_node_with_hash(
&node, content, "arrow",
))
}
"method_definition" => node
.child_by_field_name("name")
.and_then(|child| child.utf8_text(content).ok().map(|s| s.trim().to_string())),
_ => None,
}
}
fn scope_node_name(node: Node<'_>, content: &[u8]) -> Option<String> {
match node.kind() {
"class_declaration" | "class" => node
.child_by_field_name("name")
.and_then(|child| child.utf8_text(content).ok().map(|s| s.trim().to_string()))
.or_else(|| {
Some(SyntheticNameBuilder::from_node_with_hash(
&node, content, "class",
))
}),
_ => None,
}
}
fn is_async_function(node: Node<'_>, _content: &[u8]) -> bool {
let mut cursor = node.walk();
node.children(&mut cursor)
.any(|child| child.kind() == "async")
}
fn process_jsdoc_annotations(
node: Node,
content: &[u8],
helper: &mut GraphBuildHelper,
) -> GraphResult<()> {
match node.kind() {
"function_declaration" | "generator_function_declaration" => {
process_function_jsdoc(node, content, helper)?;
}
"method_definition" => {
process_method_jsdoc(node, content, helper)?;
}
"lexical_declaration" | "variable_declaration" => {
process_variable_jsdoc(node, content, helper)?;
}
"class_declaration" | "class" => {
process_class_fields(node, content, helper)?;
process_constructor_this_assignments(node, content, helper)?;
}
_ => {}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
process_jsdoc_annotations(child, content, helper)?;
}
Ok(())
}
fn process_function_jsdoc(
func_node: Node,
content: &[u8],
helper: &mut GraphBuildHelper,
) -> GraphResult<()> {
let Some(jsdoc_text) = extract_jsdoc_comment(func_node, content) else {
return Ok(());
};
let tags = parse_jsdoc_tags(&jsdoc_text);
let Some(name_node) = func_node.child_by_field_name("name") else {
return Ok(());
};
let function_name = name_node
.utf8_text(content)
.map_err(|_| GraphBuilderError::ParseError {
span: span_from_node(func_node),
reason: "failed to read function name".to_string(),
})?
.trim()
.to_string();
if function_name.is_empty() {
return Ok(());
}
let func_node_id = helper.ensure_callee(
&function_name,
span_from_node(func_node),
CalleeKindHint::Function,
);
let ast_params = extract_ast_parameters(func_node, content);
let ast_param_map: HashMap<&str, usize> = ast_params
.iter()
.map(|(idx, name)| (name.as_str(), *idx))
.collect();
for param_tag in &tags.params {
let mut normalized_name = param_tag
.name
.trim_start_matches("...")
.trim_matches(|c| c == '[' || c == ']');
if let Some(base_name) = normalized_name.split('.').next() {
normalized_name = base_name;
}
let Some(&ast_index) = ast_param_map.get(normalized_name) else {
continue;
};
let canonical_type = canonical_type_string(¶m_tag.type_str);
let type_node_id = helper.add_type(&canonical_type, None);
helper.add_typeof_edge_with_context(
func_node_id,
type_node_id,
Some(TypeOfContext::Parameter),
ast_index.try_into().ok(), Some(¶m_tag.name),
);
let type_names = extract_type_names(¶m_tag.type_str);
for type_name in type_names {
let ref_type_id = helper.add_type(&type_name, None);
helper.add_reference_edge(func_node_id, ref_type_id);
}
}
if let Some(return_type) = &tags.returns {
let canonical_type = canonical_type_string(return_type);
let type_node_id = helper.add_type(&canonical_type, None);
helper.add_typeof_edge_with_context(
func_node_id,
type_node_id,
Some(TypeOfContext::Return),
Some(0),
None,
);
let type_names = extract_type_names(return_type);
for type_name in type_names {
let ref_type_id = helper.add_type(&type_name, None);
helper.add_reference_edge(func_node_id, ref_type_id);
}
}
Ok(())
}
fn process_method_jsdoc(
method_node: Node,
content: &[u8],
helper: &mut GraphBuildHelper,
) -> GraphResult<()> {
let Some(jsdoc_text) = extract_jsdoc_comment(method_node, content) else {
return Ok(());
};
let tags = parse_jsdoc_tags(&jsdoc_text);
let Some(name_node) = method_node.child_by_field_name("name") else {
return Ok(());
};
let method_name = name_node
.utf8_text(content)
.map_err(|_| GraphBuilderError::ParseError {
span: span_from_node(method_node),
reason: "failed to read method name".to_string(),
})?
.trim()
.to_string();
if method_name.is_empty() {
return Ok(());
}
let class_name = get_enclosing_class_name(method_node, content)?;
let Some(class_name) = class_name else {
return Ok(());
};
let qualified_name = format!("{class_name}.{method_name}");
let method_node_id = helper.ensure_method(&qualified_name, None, false, false);
let ast_params = extract_ast_parameters(method_node, content);
let ast_param_map: HashMap<&str, usize> = ast_params
.iter()
.map(|(idx, name)| (name.as_str(), *idx))
.collect();
for param_tag in &tags.params {
let mut normalized_name = param_tag
.name
.trim_start_matches("...")
.trim_matches(|c| c == '[' || c == ']');
if let Some(base_name) = normalized_name.split('.').next() {
normalized_name = base_name;
}
let Some(&ast_index) = ast_param_map.get(normalized_name) else {
continue;
};
let canonical_type = canonical_type_string(¶m_tag.type_str);
let type_node_id = helper.add_type(&canonical_type, None);
helper.add_typeof_edge_with_context(
method_node_id,
type_node_id,
Some(TypeOfContext::Parameter),
ast_index.try_into().ok(), Some(¶m_tag.name),
);
let type_names = extract_type_names(¶m_tag.type_str);
for type_name in type_names {
let ref_type_id = helper.add_type(&type_name, None);
helper.add_reference_edge(method_node_id, ref_type_id);
}
}
if let Some(return_type) = &tags.returns {
let canonical_type = canonical_type_string(return_type);
let type_node_id = helper.add_type(&canonical_type, None);
helper.add_typeof_edge_with_context(
method_node_id,
type_node_id,
Some(TypeOfContext::Return),
Some(0),
None,
);
let type_names = extract_type_names(return_type);
for type_name in type_names {
let ref_type_id = helper.add_type(&type_name, None);
helper.add_reference_edge(method_node_id, ref_type_id);
}
}
Ok(())
}
fn process_variable_jsdoc(
decl_node: Node,
content: &[u8],
helper: &mut GraphBuildHelper,
) -> GraphResult<()> {
if !is_top_level_variable(decl_node) {
return Ok(());
}
let Some(jsdoc_text) = extract_jsdoc_comment(decl_node, content) else {
return Ok(());
};
let tags = parse_jsdoc_tags(&jsdoc_text);
let Some(type_annotation) = &tags.type_annotation else {
return Ok(());
};
let mut cursor = decl_node.walk();
for child in decl_node.children(&mut cursor) {
if child.kind() == "variable_declarator"
&& let Some(name_node) = child.child_by_field_name("name")
{
let var_name = name_node
.utf8_text(content)
.map_err(|_| GraphBuilderError::ParseError {
span: span_from_node(child),
reason: "failed to read variable name".to_string(),
})?
.trim()
.to_string();
if !var_name.is_empty() {
let var_node_id = helper.add_variable(&var_name, None);
let canonical_type = canonical_type_string(type_annotation);
let type_node_id = helper.add_type(&canonical_type, None);
helper.add_typeof_edge_with_context(
var_node_id,
type_node_id,
Some(TypeOfContext::Variable),
None,
None,
);
let type_names = extract_type_names(type_annotation);
for type_name in type_names {
let ref_type_id = helper.add_type(&type_name, None);
helper.add_reference_edge(var_node_id, ref_type_id);
}
}
}
}
Ok(())
}
fn resolve_class_name_for_fields(
class_node: Node<'_>,
content: &[u8],
) -> GraphResult<Option<String>> {
if let Some(name_node) = class_node.child_by_field_name("name") {
let name = name_node
.utf8_text(content)
.map_err(|_| GraphBuilderError::ParseError {
span: span_from_node(class_node),
reason: "failed to read class name".to_string(),
})?
.trim()
.to_string();
if name.is_empty() {
return Ok(None);
}
return Ok(Some(name));
}
let Some(parent) = class_node.parent() else {
return Ok(None);
};
match parent.kind() {
"variable_declarator" => {
if let Some(name_node) = parent.child_by_field_name("name")
&& let Ok(var_name) = name_node.utf8_text(content)
{
let var_name = var_name.trim().to_string();
if var_name.is_empty() {
return Ok(None);
}
return Ok(Some(var_name));
}
Ok(None)
}
"assignment_expression" => {
if let Some(left) = parent.child_by_field_name("left")
&& let Ok(assign_name) = left.utf8_text(content)
{
let assign_name = assign_name.trim().to_string();
if assign_name.is_empty() {
return Ok(None);
}
return Ok(Some(assign_name));
}
Ok(None)
}
_ => Ok(None),
}
}
fn process_class_fields(
class_node: Node<'_>,
content: &[u8],
helper: &mut GraphBuildHelper,
) -> GraphResult<()> {
let Some(class_name) = resolve_class_name_for_fields(class_node, content)? else {
return Ok(());
};
let Some(body_node) = class_node.child_by_field_name("body") else {
return Ok(());
};
let mut cursor = body_node.walk();
for child in body_node.children(&mut cursor) {
if child.kind() != "field_definition" {
continue;
}
emit_class_field_node(child, content, helper, &class_name)?;
}
Ok(())
}
fn emit_class_field_node(
field_node: Node<'_>,
content: &[u8],
helper: &mut GraphBuildHelper,
class_name: &str,
) -> GraphResult<()> {
let Some(name_node) = field_node.child_by_field_name("property") else {
return Ok(());
};
let raw_name = name_node
.utf8_text(content)
.map_err(|_| GraphBuilderError::ParseError {
span: span_from_node(field_node),
reason: "failed to read field name".to_string(),
})?
.trim()
.to_string();
if raw_name.is_empty() {
return Ok(());
}
let is_hash_private = name_node.kind() == "private_property_identifier";
let mut is_static = false;
let mut mod_cursor = field_node.walk();
for modifier in field_node.children(&mut mod_cursor) {
if modifier.kind() == "static" {
is_static = true;
}
}
let visibility: Option<&str> = if is_hash_private {
Some("private")
} else {
Some("public")
};
let qualified_name = format!("{class_name}.{raw_name}");
let span = Some(span_from_node(field_node));
let field_id = helper.add_property_with_static_and_visibility(
&qualified_name,
span,
is_static,
visibility,
);
if let Some(jsdoc_text) = extract_jsdoc_comment(field_node, content) {
let tags = parse_jsdoc_tags(&jsdoc_text);
if let Some(type_annotation) = &tags.type_annotation {
let canonical_type = canonical_type_string(type_annotation);
let type_node_id = helper.add_type(&canonical_type, None);
helper.add_typeof_edge_with_context(
field_id,
type_node_id,
Some(TypeOfContext::Field),
None,
Some(&raw_name),
);
let type_names = extract_type_names(type_annotation);
for type_name in type_names {
let ref_type_id = helper.add_type(&type_name, None);
helper.add_reference_edge(field_id, ref_type_id);
}
}
}
Ok(())
}
fn process_constructor_this_assignments(
class_node: Node<'_>,
content: &[u8],
helper: &mut GraphBuildHelper,
) -> GraphResult<()> {
let Some(class_name) = resolve_class_name_for_fields(class_node, content)? else {
return Ok(());
};
let Some(body_node) = class_node.child_by_field_name("body") else {
return Ok(());
};
let mut cursor = body_node.walk();
for child in body_node.children(&mut cursor) {
if child.kind() != "method_definition" {
continue;
}
let Some(name_node) = child.child_by_field_name("name") else {
continue;
};
let Ok(method_name) = name_node.utf8_text(content) else {
continue;
};
if method_name.trim() != "constructor" {
continue;
}
let Some(method_body) = child.child_by_field_name("body") else {
continue;
};
walk_for_this_assignments(method_body, content, helper, &class_name);
}
Ok(())
}
fn walk_for_this_assignments(
node: Node<'_>,
content: &[u8],
helper: &mut GraphBuildHelper,
class_name: &str,
) {
if node.kind() == "assignment_expression"
&& let Some(left) = node.child_by_field_name("left")
&& left.kind() == "member_expression"
&& let Some(object) = left.child_by_field_name("object")
&& object.kind() == "this"
&& let Some(property) = left.child_by_field_name("property")
&& property.kind() == "property_identifier"
&& let Ok(field_name) = property.utf8_text(content)
{
let field_name = field_name.trim();
if !field_name.is_empty() {
let qualified_name = format!("{class_name}.{field_name}");
let _ = helper.add_property_with_static_and_visibility(
&qualified_name,
Some(span_from_node(left)),
false,
Some("public"),
);
}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
walk_for_this_assignments(child, content, helper, class_name);
}
}
fn get_enclosing_class_name(method_node: Node, content: &[u8]) -> GraphResult<Option<String>> {
let mut current = method_node;
while let Some(parent) = current.parent() {
if parent.kind() == "class_declaration" {
if let Some(name_node) = parent.child_by_field_name("name") {
let class_name = name_node
.utf8_text(content)
.map_err(|_| GraphBuilderError::ParseError {
span: span_from_node(parent),
reason: "failed to read class name".to_string(),
})?
.trim()
.to_string();
if !class_name.is_empty() {
return Ok(Some(class_name));
}
}
} else if parent.kind() == "class" {
if let Some(grandparent) = parent.parent() {
if grandparent.kind() == "variable_declarator" {
if let Some(name_node) = grandparent.child_by_field_name("name")
&& let Ok(var_name) = name_node.utf8_text(content)
{
let var_name = var_name.trim().to_string();
if !var_name.is_empty() {
return Ok(Some(var_name));
}
}
} else if grandparent.kind() == "assignment_expression" {
if let Some(left) = grandparent.child_by_field_name("left")
&& let Ok(assign_name) = left.utf8_text(content)
{
let assign_name = assign_name.trim().to_string();
if !assign_name.is_empty() {
return Ok(Some(assign_name));
}
}
}
}
return Ok(None);
}
current = parent;
}
Ok(None)
}
fn extract_ast_parameters(func_node: Node, content: &[u8]) -> Vec<(usize, String)> {
let Some(params_node) = func_node.child_by_field_name("parameters") else {
return Vec::new();
};
let mut cursor = params_node.walk();
params_node
.named_children(&mut cursor)
.enumerate()
.filter_map(|(ast_index, param)| {
let param_name = match param.kind() {
"identifier" => param
.utf8_text(content)
.ok()
.map(std::string::ToString::to_string),
"required_parameter" | "optional_parameter" => {
param
.child_by_field_name("pattern")
.and_then(|p| p.utf8_text(content).ok())
.map(std::string::ToString::to_string)
}
"rest_pattern" => {
param
.named_child(0)
.and_then(|n| n.utf8_text(content).ok())
.map(|s| s.trim_start_matches("...").to_string())
}
"assignment_pattern" => {
param
.child_by_field_name("left")
.filter(|left| left.kind() == "identifier")
.and_then(|left| left.utf8_text(content).ok())
.map(std::string::ToString::to_string)
}
_ => None,
};
param_name.map(|name| (ast_index, name))
})
.collect()
}
fn is_top_level_variable(decl_node: Node) -> bool {
let mut current = decl_node;
while let Some(parent) = current.parent() {
match parent.kind() {
"function_declaration"
| "generator_function_declaration"
| "function_expression"
| "arrow_function"
| "method_definition" => return false,
"statement_block" | "if_statement" | "for_statement" | "for_in_statement"
| "for_of_statement" | "while_statement" | "do_statement" | "try_statement"
| "catch_clause" | "finally_clause" | "switch_statement" | "switch_case"
| "switch_default" | "class_body" | "class_static_block" | "with_statement" => {
return false;
}
"program" | "export_statement" => return true,
_ => {}
}
current = parent;
}
true
}
fn build_ffi_call_edge(
ast_graph: &ASTGraph,
call_node: Node<'_>,
content: &[u8],
helper: &mut GraphBuildHelper,
) -> GraphResult<bool> {
let Some(callee_expr) = call_node.child_by_field_name("function") else {
return Ok(false);
};
let callee_text = callee_expr
.utf8_text(content)
.map_err(|_| GraphBuilderError::ParseError {
span: span_from_node(call_node),
reason: "failed to read call expression".to_string(),
})?
.trim();
if callee_text.starts_with("WebAssembly.") {
return Ok(build_webassembly_call_edge(
ast_graph,
call_node,
content,
callee_text,
helper,
));
}
if callee_text == "require" {
return Ok(build_require_ffi_edge(
ast_graph, call_node, content, helper,
));
}
if callee_text == "process.dlopen" {
return Ok(build_dlopen_edge(ast_graph, call_node, content, helper));
}
Ok(false)
}
fn build_ffi_new_edge(
ast_graph: &ASTGraph,
new_node: Node<'_>,
content: &[u8],
helper: &mut GraphBuildHelper,
) -> GraphResult<bool> {
let Some(constructor_expr) = new_node.child_by_field_name("constructor") else {
return Ok(false);
};
let constructor_text = constructor_expr
.utf8_text(content)
.map_err(|_| GraphBuilderError::ParseError {
span: span_from_node(new_node),
reason: "failed to read constructor expression".to_string(),
})?
.trim();
if constructor_text == "WebAssembly.Module" || constructor_text == "WebAssembly.Instance" {
return Ok(build_webassembly_constructor_edge(
ast_graph,
new_node,
content,
constructor_text,
helper,
));
}
Ok(false)
}
fn build_webassembly_call_edge(
ast_graph: &ASTGraph,
call_node: Node<'_>,
content: &[u8],
callee_text: &str,
helper: &mut GraphBuildHelper,
) -> bool {
let method_name = callee_text
.strip_prefix("WebAssembly.")
.unwrap_or(callee_text);
let is_wasm_load = matches!(
method_name,
"instantiate" | "instantiateStreaming" | "compile" | "compileStreaming" | "validate"
);
if !is_wasm_load {
return false;
}
let caller_id = get_caller_node_id(ast_graph, call_node, content, helper);
let wasm_module_name = extract_wasm_module_name(call_node, content)
.unwrap_or_else(|| format!("wasm::{method_name}"));
let wasm_node_id = helper.add_module(&wasm_module_name, Some(span_from_node(call_node)));
helper.add_webassembly_edge(caller_id, wasm_node_id);
true
}
fn build_webassembly_constructor_edge(
ast_graph: &ASTGraph,
new_node: Node<'_>,
content: &[u8],
constructor_text: &str,
helper: &mut GraphBuildHelper,
) -> bool {
let caller_id = get_caller_node_id(ast_graph, new_node, content, helper);
let type_name = constructor_text
.strip_prefix("WebAssembly.")
.unwrap_or(constructor_text);
let wasm_module_name = format!("wasm::{type_name}");
let wasm_node_id = helper.add_module(&wasm_module_name, Some(span_from_node(new_node)));
helper.add_webassembly_edge(caller_id, wasm_node_id);
true
}
fn build_require_ffi_edge(
ast_graph: &ASTGraph,
call_node: Node<'_>,
content: &[u8],
helper: &mut GraphBuildHelper,
) -> bool {
let Some(args) = call_node.child_by_field_name("arguments") else {
return false;
};
let mut cursor = args.walk();
let first_arg = args
.children(&mut cursor)
.find(|child| !matches!(child.kind(), "(" | ")" | ","));
let Some(arg_node) = first_arg else {
return false;
};
let module_path = extract_string_literal(&arg_node, content);
let Some(path) = module_path else {
return false;
};
let from_id = helper.add_module("<module>", None);
let resolved_path = if path.starts_with('.') {
sqry_core::graph::resolve_import_path(std::path::Path::new(helper.file_path()), &path)
.unwrap_or_else(|_| simple_name(&path).to_string())
} else {
simple_name(&path).to_string()
};
let to_id = helper.add_import(&resolved_path, Some(span_from_node(call_node)));
helper.add_import_edge(from_id, to_id);
let is_native_addon = std::path::Path::new(&path)
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("node"))
|| is_known_native_addon(&path);
if is_native_addon {
let caller_id = get_caller_node_id(ast_graph, call_node, content, helper);
let ffi_name = format!("native::{}", simple_name(&path));
let ffi_node_id = helper.add_module(&ffi_name, Some(span_from_node(call_node)));
helper.add_ffi_edge(caller_id, ffi_node_id, FfiConvention::C);
}
true
}
fn build_dlopen_edge(
ast_graph: &ASTGraph,
call_node: Node<'_>,
content: &[u8],
helper: &mut GraphBuildHelper,
) -> bool {
let caller_id = get_caller_node_id(ast_graph, call_node, content, helper);
let module_name = call_node
.child_by_field_name("arguments")
.and_then(|args| {
let mut cursor = args.walk();
args.children(&mut cursor)
.filter(|child| !matches!(child.kind(), "(" | ")" | ","))
.nth(1) })
.and_then(|node| extract_string_literal(&node, content))
.map_or_else(
|| "native::dlopen".to_string(),
|path| format!("native::{}", simple_name(&path)),
);
let ffi_node_id = helper.add_module(&module_name, Some(span_from_node(call_node)));
helper.add_ffi_edge(caller_id, ffi_node_id, FfiConvention::C);
true
}
fn get_caller_node_id(
ast_graph: &ASTGraph,
node: Node<'_>,
content: &[u8],
helper: &mut GraphBuildHelper,
) -> sqry_core::graph::unified::NodeId {
let module_context;
let call_context = if let Some(ctx) = ast_graph.get_callable_context(node.id()) {
ctx
} else {
module_context = CallContext {
name: Arc::from("<module>"),
qualified_name: "<module>".to_string(),
span: (0, content.len()),
is_async: false,
};
&module_context
};
ensure_caller_node(helper, call_context)
}
fn ensure_caller_node(
helper: &mut GraphBuildHelper,
call_context: &CallContext,
) -> sqry_core::graph::unified::NodeId {
let caller_span = Some(Span::from_bytes(call_context.span.0, call_context.span.1));
let qualified_name = call_context.qualified_name();
if qualified_name.contains('.') {
helper.ensure_method(qualified_name, caller_span, call_context.is_async, false)
} else {
helper.ensure_function(qualified_name, caller_span, call_context.is_async, false)
}
}
fn extract_wasm_module_name(call_node: Node<'_>, content: &[u8]) -> Option<String> {
let args = call_node.child_by_field_name("arguments")?;
let mut cursor = args.walk();
let first_arg = args
.children(&mut cursor)
.find(|child| !matches!(child.kind(), "(" | ")" | ","))?;
if first_arg.kind() == "call_expression"
&& let Some(func) = first_arg.child_by_field_name("function")
{
let func_text = func.utf8_text(content).ok()?.trim();
if func_text == "fetch" {
if let Some(fetch_args) = first_arg.child_by_field_name("arguments") {
let mut fetch_cursor = fetch_args.walk();
let url_arg = fetch_args
.children(&mut fetch_cursor)
.find(|child| !matches!(child.kind(), "(" | ")" | ","))?;
if let Some(url) = extract_string_literal(&url_arg, content) {
return Some(format!("wasm::{}", simple_name(&url)));
}
}
}
}
if let Some(path) = extract_string_literal(&first_arg, content) {
return Some(format!("wasm::{}", simple_name(&path)));
}
None
}
fn is_known_native_addon(package_name: &str) -> bool {
const NATIVE_PACKAGES: &[&str] = &[
"better-sqlite3",
"sqlite3",
"bcrypt",
"sharp",
"canvas",
"node-sass",
"leveldown",
"bufferutil",
"utf-8-validate",
"fsevents",
"cpu-features",
"node-gyp",
"node-pre-gyp",
"prebuild",
"nan",
"node-addon-api",
"ref-napi",
"ffi-napi",
];
NATIVE_PACKAGES
.iter()
.any(|&pkg| package_name.contains(pkg))
}