1use std::{collections::HashMap, path::Path, sync::Arc};
2
3use sqry_core::graph::unified::build::helper::CalleeKindHint;
4use sqry_core::graph::unified::edge::kind::TypeOfContext;
5use sqry_core::graph::unified::edge::{ExportKind, FfiConvention, HttpMethod};
6use sqry_core::graph::unified::{GraphBuildHelper, NodeId, StagingGraph};
7use sqry_core::graph::{GraphBuilder, GraphBuilderError, GraphResult, Language, Position, Span};
8use sqry_core::relations::SyntheticNameBuilder;
9use tree_sitter::{Node, Tree};
10
11use super::jsdoc_parser::{extract_jsdoc_comment, parse_jsdoc_tags};
12use super::local_scopes;
13use super::type_extractor::{canonical_type_string, extract_type_names};
14
15const DEFAULT_SCOPE_DEPTH: usize = 4;
16type CallEdgeData = (NodeId, NodeId, u8, bool, Option<Span>);
17type ConstructorEdgeData = (NodeId, NodeId, u8, Option<Span>);
18
19#[derive(Debug, Clone, Copy)]
21pub struct JavaScriptGraphBuilder {
22 max_scope_depth: usize,
23}
24
25impl Default for JavaScriptGraphBuilder {
26 fn default() -> Self {
27 Self {
28 max_scope_depth: DEFAULT_SCOPE_DEPTH,
29 }
30 }
31}
32
33impl JavaScriptGraphBuilder {
34 #[must_use]
35 pub fn new(max_scope_depth: usize) -> Self {
36 Self { max_scope_depth }
37 }
38}
39
40fn infer_visibility(qualified_name: &str) -> &'static str {
43 let name_part = qualified_name.rsplit('.').next().unwrap_or(qualified_name);
45 if name_part.starts_with('_') {
46 "private"
47 } else {
48 "public"
49 }
50}
51
52impl GraphBuilder for JavaScriptGraphBuilder {
53 fn build_graph(
54 &self,
55 tree: &Tree,
56 content: &[u8],
57 file: &Path,
58 staging: &mut StagingGraph,
59 ) -> GraphResult<()> {
60 let mut helper = GraphBuildHelper::new(staging, file, Language::JavaScript);
62 let file_arc = Arc::from(file.to_string_lossy().to_string());
63
64 let ast_graph = ASTGraph::from_tree(tree, content, self.max_scope_depth).map_err(|e| {
66 GraphBuilderError::ParseError {
67 span: Span::default(),
68 reason: e,
69 }
70 })?;
71
72 for context in ast_graph.contexts() {
74 let span = Some(Span::from_bytes(context.span.0, context.span.1));
75 let visibility = infer_visibility(&context.qualified_name);
77
78 if context.qualified_name.contains('.') {
80 helper.add_method_with_visibility(
81 &context.qualified_name,
82 span,
83 context.is_async,
84 false, Some(visibility),
86 );
87 } else {
88 helper.add_function_with_visibility(
89 &context.qualified_name,
90 span,
91 context.is_async,
92 false, Some(visibility),
94 );
95 }
96 }
97
98 let mut scope_tree = local_scopes::build(tree.root_node(), content)?;
100
101 let mut cursor = tree.root_node().walk();
103 extract_edges_recursive(
104 tree.root_node(),
105 &mut cursor,
106 content,
107 &file_arc,
108 &ast_graph,
109 &mut helper,
110 &mut scope_tree,
111 )?;
112
113 process_jsdoc_annotations(tree.root_node(), content, &mut helper)?;
115
116 Ok(())
117 }
118
119 fn language(&self) -> Language {
120 Language::JavaScript
121 }
122}
123
124fn extract_edges_recursive<'a>(
126 node: Node<'a>,
127 cursor: &mut tree_sitter::TreeCursor<'a>,
128 content: &[u8],
129 file: &Arc<str>,
130 ast_graph: &ASTGraph,
131 helper: &mut GraphBuildHelper,
132 scope_tree: &mut local_scopes::JavaScriptScopeTree,
133) -> GraphResult<()> {
134 match node.kind() {
135 "call_expression" => {
136 let _ = build_http_request_edge(ast_graph, node, content, helper);
138 let _ = detect_route_endpoint(node, content, helper);
140 let is_ffi = build_ffi_call_edge(ast_graph, node, content, helper)?;
142 if !is_ffi {
143 if let Some((caller_id, callee_id, argument_count, is_async, span)) =
145 build_call_edge_with_helper(ast_graph, node, content, helper)?
146 {
147 helper.add_call_edge_full_with_span(
148 caller_id,
149 callee_id,
150 argument_count,
151 is_async,
152 span.into_iter().collect(),
153 );
154 }
155 }
156 }
157 "new_expression" => {
158 let is_ffi = build_ffi_new_edge(ast_graph, node, content, helper)?;
160 if !is_ffi {
161 if let Some((caller_id, callee_id, argument_count, span)) =
163 build_constructor_edge_with_helper(ast_graph, node, content, helper)?
164 {
165 helper.add_call_edge_full_with_span(
166 caller_id,
167 callee_id,
168 argument_count,
169 false,
170 span.into_iter().collect(),
171 );
172 }
173 }
174 }
175 "import_statement" => {
176 if let Some((from_id, to_id)) =
177 build_import_edge_with_helper(node, content, file, helper)?
178 {
179 helper.add_import_edge(from_id, to_id);
180 }
181 }
182 "export_statement" => {
183 build_export_edges_with_helper(node, content, file, helper);
184 }
185 "expression_statement" => {
186 build_commonjs_export_edges(node, content, helper);
188 }
189 "class_declaration" | "class" => {
190 build_inherits_edge_with_helper(node, content, helper);
191 }
192 "identifier" => {
193 local_scopes::handle_identifier_for_reference(node, content, scope_tree, helper);
194 }
195 _ => {}
196 }
197
198 let children: Vec<_> = node.children(cursor).collect();
201 for child in children {
202 let mut child_cursor = child.walk();
203 extract_edges_recursive(
204 child,
205 &mut child_cursor,
206 content,
207 file,
208 ast_graph,
209 helper,
210 scope_tree,
211 )?;
212 }
213
214 Ok(())
215}
216
217fn build_call_edge_with_helper(
219 ast_graph: &ASTGraph,
220 call_node: Node<'_>,
221 content: &[u8],
222 helper: &mut GraphBuildHelper,
223) -> GraphResult<Option<CallEdgeData>> {
224 let module_context;
226 let call_context = if let Some(ctx) = ast_graph.get_callable_context(call_node.id()) {
227 ctx
228 } else {
229 module_context = CallContext {
231 name: Arc::from("<module>"),
232 qualified_name: "<module>".to_string(),
233 span: (0, content.len()),
234 is_async: false,
235 };
236 &module_context
237 };
238
239 let Some(callee_expr) = call_node.child_by_field_name("function") else {
240 return Ok(None);
241 };
242
243 let raw_callee_text = callee_expr
244 .utf8_text(content)
245 .map_err(|_| GraphBuilderError::ParseError {
246 span: span_from_node(call_node),
247 reason: "failed to read call expression".to_string(),
248 })?
249 .trim()
250 .to_string();
251
252 let callee_text = if raw_callee_text.contains("?.") {
254 normalize_optional_chain(&raw_callee_text)
255 } else {
256 raw_callee_text
257 };
258
259 if callee_text.is_empty() {
260 return Ok(None);
261 }
262
263 let callee_simple = simple_name(&callee_text);
264 if callee_simple.is_empty() {
265 return Ok(None);
266 }
267
268 let caller_qname = call_context.qualified_name();
270 let target_qname = if let Some(method_name) = callee_text.strip_prefix("this.") {
271 if let Some(scope_idx) = caller_qname.rfind('.') {
273 let class_name = &caller_qname[..scope_idx];
274 format!("{}.{}", class_name, simple_name(method_name))
275 } else {
276 callee_text.clone()
277 }
278 } else if callee_text.starts_with("super.") || callee_text.contains('.') {
279 callee_text.clone()
280 } else {
281 callee_simple.to_string()
282 };
283
284 let source_id = ensure_caller_node(helper, call_context);
286 let call_site_span = span_from_node(call_node);
287 let target_id = helper.ensure_callee(&target_qname, call_site_span, CalleeKindHint::Function);
288
289 let span = Some(call_site_span);
290 let argument_count = u8::try_from(count_arguments(call_node)).unwrap_or(u8::MAX);
291 let is_async = check_uses_await(call_node);
292
293 Ok(Some((source_id, target_id, argument_count, is_async, span)))
294}
295
296#[derive(Debug, Clone)]
297struct HttpRequestInfo {
298 method: HttpMethod,
299 url: Option<String>,
300}
301
302fn build_http_request_edge(
303 ast_graph: &ASTGraph,
304 call_node: Node<'_>,
305 content: &[u8],
306 helper: &mut GraphBuildHelper,
307) -> bool {
308 let Some(info) = extract_http_request_info(call_node, content) else {
309 return false;
310 };
311
312 let caller_id = get_caller_node_id(ast_graph, call_node, content, helper);
313 let target_name = info.url.as_ref().map_or_else(
314 || format!("http::{}", info.method.as_str()),
315 |url| format!("http::{url}"),
316 );
317 let target_id = helper.add_module(&target_name, Some(span_from_node(call_node)));
318
319 helper.add_http_request_edge(caller_id, target_id, info.method, info.url.as_deref());
320 true
321}
322
323fn detect_route_endpoint(
337 call_node: Node<'_>,
338 content: &[u8],
339 helper: &mut GraphBuildHelper,
340) -> bool {
341 let Some(callee) = call_node.child_by_field_name("function") else {
343 return false;
344 };
345
346 if callee.kind() != "member_expression" {
347 return false;
348 }
349
350 let Some(property) = callee.child_by_field_name("property") else {
352 return false;
353 };
354
355 let Ok(method_name) = property.utf8_text(content) else {
356 return false;
357 };
358 let method_name = method_name.trim();
359
360 let method_str = match method_name {
362 "get" => "GET",
363 "post" => "POST",
364 "put" => "PUT",
365 "delete" => "DELETE",
366 "patch" => "PATCH",
367 "all" => "ALL",
368 _ => return false,
369 };
370
371 let Some(args) = call_node.child_by_field_name("arguments") else {
373 return false;
374 };
375
376 let mut cursor = args.walk();
377 let first_arg = args
378 .children(&mut cursor)
379 .find(|child| !matches!(child.kind(), "(" | ")" | ","));
380
381 let Some(first_arg) = first_arg else {
382 return false;
383 };
384
385 let Some(path) = extract_string_literal(&first_arg, content) else {
387 return false;
388 };
389
390 let qualified_name = format!("route::{method_str}::{path}");
392
393 let endpoint_id = helper.add_endpoint(&qualified_name, Some(span_from_node(call_node)));
395
396 let mut handler_cursor = args.walk();
399 let handler_arg = args
400 .children(&mut handler_cursor)
401 .filter(|child| !matches!(child.kind(), "(" | ")" | ","))
402 .nth(1);
403
404 if let Some(handler_node) = handler_arg
405 && let Ok(handler_text) = handler_node.utf8_text(content)
406 {
407 let handler_name = handler_text.trim();
408 if !handler_name.is_empty()
409 && matches!(handler_node.kind(), "identifier" | "member_expression")
410 {
411 let handler_id = helper.ensure_callee(
412 handler_name,
413 span_from_node(handler_node),
414 CalleeKindHint::Function,
415 );
416 helper.add_contains_edge(endpoint_id, handler_id);
417 }
418 }
419
420 true
421}
422
423fn extract_http_request_info(call_node: Node<'_>, content: &[u8]) -> Option<HttpRequestInfo> {
424 let callee = call_node.child_by_field_name("function")?;
425 let callee_text = callee.utf8_text(content).ok()?.trim().to_string();
426
427 if callee_text == "fetch" {
428 return Some(extract_fetch_http_info(call_node, content));
429 }
430
431 if callee_text == "axios" {
432 return extract_axios_http_info(call_node, content);
433 }
434
435 if let Some(method_name) = callee_text.strip_prefix("axios.") {
436 let method = http_method_from_name(method_name)?;
437 let url = extract_first_arg_url(call_node, content);
438 return Some(HttpRequestInfo { method, url });
439 }
440
441 None
442}
443
444fn extract_fetch_http_info(call_node: Node<'_>, content: &[u8]) -> HttpRequestInfo {
445 let url = extract_first_arg_url(call_node, content);
446 let method = extract_method_from_options(call_node, content).unwrap_or(HttpMethod::Get);
447 HttpRequestInfo { method, url }
448}
449
450fn extract_axios_http_info(call_node: Node<'_>, content: &[u8]) -> Option<HttpRequestInfo> {
451 let args = call_node.child_by_field_name("arguments")?;
452 let mut cursor = args.walk();
453 let mut non_trivia = args
454 .children(&mut cursor)
455 .filter(|child| !matches!(child.kind(), "(" | ")" | ","));
456
457 let first_arg = non_trivia.next()?;
458 let second_arg = non_trivia.next();
459
460 if first_arg.kind() == "object" {
461 let (method, url) = extract_method_and_url_from_object(first_arg, content);
462 return Some(HttpRequestInfo {
463 method: method.unwrap_or(HttpMethod::Get),
464 url,
465 });
466 }
467
468 let url = extract_string_literal(&first_arg, content);
469 let method = if let Some(config) = second_arg {
470 if config.kind() == "object" {
471 extract_method_from_object(config, content)
472 } else {
473 None
474 }
475 } else {
476 None
477 };
478
479 Some(HttpRequestInfo {
480 method: method.unwrap_or(HttpMethod::Get),
481 url,
482 })
483}
484
485fn extract_first_arg_url(call_node: Node<'_>, content: &[u8]) -> Option<String> {
486 let args = call_node.child_by_field_name("arguments")?;
487 let mut cursor = args.walk();
488 let first_arg = args
489 .children(&mut cursor)
490 .find(|child| !matches!(child.kind(), "(" | ")" | ","))?;
491 extract_string_literal(&first_arg, content)
492}
493
494fn extract_method_from_options(call_node: Node<'_>, content: &[u8]) -> Option<HttpMethod> {
495 let args = call_node.child_by_field_name("arguments")?;
496 let mut cursor = args.walk();
497 let mut non_trivia = args
498 .children(&mut cursor)
499 .filter(|child| !matches!(child.kind(), "(" | ")" | ","));
500
501 let _first_arg = non_trivia.next()?;
502 let second_arg = non_trivia.next()?;
503 if second_arg.kind() != "object" {
504 return None;
505 }
506
507 extract_method_from_object(second_arg, content)
508}
509
510fn extract_method_from_object(obj_node: Node<'_>, content: &[u8]) -> Option<HttpMethod> {
511 let (method, _url) = extract_method_and_url_from_object(obj_node, content);
512 method
513}
514
515fn extract_method_and_url_from_object(
516 obj_node: Node<'_>,
517 content: &[u8],
518) -> (Option<HttpMethod>, Option<String>) {
519 let mut method = None;
520 let mut url = None;
521 let mut cursor = obj_node.walk();
522
523 for child in obj_node.children(&mut cursor) {
524 if child.kind() != "pair" {
525 continue;
526 }
527
528 let Some(key_node) = child.child_by_field_name("key") else {
529 continue;
530 };
531 let key_text = extract_object_key_text(&key_node, content);
532
533 let Some(value_node) = child.child_by_field_name("value") else {
534 continue;
535 };
536
537 if key_text.as_deref() == Some("method") {
538 if let Some(value) = extract_string_literal(&value_node, content) {
539 method = http_method_from_name(&value);
540 }
541 } else if key_text.as_deref() == Some("url") {
542 url = extract_string_literal(&value_node, content);
543 }
544 }
545
546 (method, url)
547}
548
549fn extract_object_key_text(node: &Node<'_>, content: &[u8]) -> Option<String> {
550 let raw = node.utf8_text(content).ok()?.trim().to_string();
551 if let Some(value) = extract_string_literal(node, content) {
552 return Some(value);
553 }
554 if raw.is_empty() {
555 return None;
556 }
557 Some(raw)
558}
559
560fn http_method_from_name(name: &str) -> Option<HttpMethod> {
561 match name.trim().to_ascii_lowercase().as_str() {
562 "get" => Some(HttpMethod::Get),
563 "post" => Some(HttpMethod::Post),
564 "put" => Some(HttpMethod::Put),
565 "delete" => Some(HttpMethod::Delete),
566 "patch" => Some(HttpMethod::Patch),
567 "head" => Some(HttpMethod::Head),
568 "options" => Some(HttpMethod::Options),
569 _ => None,
570 }
571}
572
573fn build_constructor_edge_with_helper(
575 ast_graph: &ASTGraph,
576 new_node: Node<'_>,
577 content: &[u8],
578 helper: &mut GraphBuildHelper,
579) -> GraphResult<Option<ConstructorEdgeData>> {
580 let module_context;
582 let call_context = if let Some(ctx) = ast_graph.get_callable_context(new_node.id()) {
583 ctx
584 } else {
585 module_context = CallContext {
586 name: Arc::from("<module>"),
587 qualified_name: "<module>".to_string(),
588 span: (0, content.len()),
589 is_async: false,
590 };
591 &module_context
592 };
593
594 let Some(constructor_expr) = new_node.child_by_field_name("constructor") else {
595 return Ok(None);
596 };
597
598 let constructor_text = constructor_expr
599 .utf8_text(content)
600 .map_err(|_| GraphBuilderError::ParseError {
601 span: span_from_node(new_node),
602 reason: "failed to read constructor expression".to_string(),
603 })?
604 .trim()
605 .to_string();
606
607 if constructor_text.is_empty() {
608 return Ok(None);
609 }
610
611 let constructor_simple = simple_name(&constructor_text);
612 let source_id = ensure_caller_node(helper, call_context);
613 let new_site_span = span_from_node(new_node);
614 let target_id =
615 helper.ensure_callee(constructor_simple, new_site_span, CalleeKindHint::Function);
616
617 let span = Some(new_site_span);
618 let argument_count = u8::try_from(count_arguments(new_node)).unwrap_or(u8::MAX);
619
620 Ok(Some((source_id, target_id, argument_count, span)))
621}
622
623fn build_import_edge_with_helper(
625 import_node: Node<'_>,
626 content: &[u8],
627 file: &Arc<str>,
628 helper: &mut GraphBuildHelper,
629) -> GraphResult<
630 Option<(
631 sqry_core::graph::unified::NodeId,
632 sqry_core::graph::unified::NodeId,
633 )>,
634> {
635 let Some(source_node) = import_node.child_by_field_name("source") else {
636 return Ok(None);
637 };
638
639 let source_text = source_node
640 .utf8_text(content)
641 .map_err(|_| GraphBuilderError::ParseError {
642 span: span_from_node(import_node),
643 reason: "failed to read import source".to_string(),
644 })?
645 .trim()
646 .trim_matches(|c| c == '"' || c == '\'')
647 .to_string();
648
649 if source_text.is_empty() {
650 return Ok(None);
651 }
652
653 let resolved_path =
655 sqry_core::graph::resolve_import_path(std::path::Path::new(file.as_ref()), &source_text)?;
656
657 let from_id = helper.add_module("<module>", None);
659 let to_id = helper.add_import(&resolved_path, Some(span_from_node(import_node)));
660
661 Ok(Some((from_id, to_id)))
662}
663
664#[allow(clippy::too_many_lines)]
675fn build_export_edges_with_helper(
676 export_node: Node<'_>,
677 content: &[u8],
678 file: &Arc<str>,
679 helper: &mut GraphBuildHelper,
680) {
681 let module_id = helper.add_module("<module>", None);
683
684 let source_node = export_node.child_by_field_name("source");
686 let is_reexport = source_node.is_some();
687
688 let has_default = export_node
690 .children(&mut export_node.walk())
691 .any(|child| child.kind() == "default");
692
693 let namespace_export = export_node
695 .children(&mut export_node.walk())
696 .find(|child| child.kind() == "namespace_export");
697
698 let has_wildcard = export_node
700 .children(&mut export_node.walk())
701 .any(|child| child.kind() == "*");
702
703 let export_clause = export_node
705 .children(&mut export_node.walk())
706 .find(|child| child.kind() == "export_clause");
707
708 let declaration = export_node.children(&mut export_node.walk()).find(|child| {
710 matches!(
711 child.kind(),
712 "function_declaration"
713 | "class_declaration"
714 | "lexical_declaration"
715 | "variable_declaration"
716 | "generator_function_declaration"
717 )
718 });
719
720 if has_default {
721 let exported_name = if let Some(ref decl) = declaration {
724 decl.child_by_field_name("name")
726 .and_then(|n| n.utf8_text(content).ok())
727 .map_or_else(|| "default".to_string(), |s| s.trim().to_string())
728 } else {
729 export_node
731 .children(&mut export_node.walk())
732 .find(|child| child.kind() == "identifier")
733 .and_then(|n| n.utf8_text(content).ok())
734 .map_or_else(|| "default".to_string(), |s| s.trim().to_string())
735 };
736
737 let exported_id = helper.add_function(&exported_name, None, false, false);
738 helper.add_export_edge_full(module_id, exported_id, ExportKind::Default, None);
739 } else if let Some(ns_export) = namespace_export {
740 let alias = ns_export
743 .children(&mut ns_export.walk())
744 .find(|child| child.kind() == "identifier")
745 .and_then(|n| n.utf8_text(content).ok())
746 .map(|s| s.trim().to_string());
747
748 let source_path = source_node
750 .and_then(|s| s.utf8_text(content).ok())
751 .map_or_else(
752 || "<unknown>".to_string(),
753 |s| s.trim().trim_matches(|c| c == '"' || c == '\'').to_string(),
754 );
755
756 let resolved_path = sqry_core::graph::resolve_import_path(
757 std::path::Path::new(file.as_ref()),
758 &source_path,
759 )
760 .unwrap_or(source_path);
761
762 let source_module_id = helper.add_module(&resolved_path, None);
763 helper.add_export_edge_full(
764 module_id,
765 source_module_id,
766 ExportKind::Namespace,
767 alias.as_deref(),
768 );
769 } else if has_wildcard && is_reexport {
770 let source_path = source_node
772 .and_then(|s| s.utf8_text(content).ok())
773 .map_or_else(
774 || "<unknown>".to_string(),
775 |s| s.trim().trim_matches(|c| c == '"' || c == '\'').to_string(),
776 );
777
778 let resolved_path = sqry_core::graph::resolve_import_path(
779 std::path::Path::new(file.as_ref()),
780 &source_path,
781 )
782 .unwrap_or(source_path);
783
784 let source_module_id = helper.add_module(&resolved_path, None);
785 helper.add_export_edge_full(module_id, source_module_id, ExportKind::Reexport, None);
787 } else if let Some(clause) = export_clause {
788 let mut cursor = clause.walk();
790 for child in clause.children(&mut cursor) {
791 if child.kind() == "export_specifier" {
792 let identifiers: Vec<_> = child
795 .children(&mut child.walk())
796 .filter(|n| n.kind() == "identifier")
797 .collect();
798
799 if let Some(first_ident) = identifiers.first() {
800 let local_name = first_ident
801 .utf8_text(content)
802 .ok()
803 .map(|s| s.trim().to_string())
804 .unwrap_or_default();
805
806 if local_name.is_empty() {
807 continue;
808 }
809
810 let alias = identifiers.get(1).and_then(|n| {
812 n.utf8_text(content)
813 .ok()
814 .map(|s| s.trim().to_string())
815 .filter(|s| !s.is_empty())
816 });
817
818 let exported_id = helper.add_function(&local_name, None, false, false);
819
820 let kind = if is_reexport {
821 ExportKind::Reexport
822 } else {
823 ExportKind::Direct
824 };
825
826 helper.add_export_edge_full(module_id, exported_id, kind, alias.as_deref());
827 }
828 }
829 }
830 } else if let Some(decl) = declaration {
831 match decl.kind() {
833 "function_declaration" | "generator_function_declaration" => {
834 if let Some(name_node) = decl.child_by_field_name("name")
835 && let Ok(name) = name_node.utf8_text(content)
836 {
837 let name = name.trim().to_string();
838 if !name.is_empty() {
839 let exported_id = helper.add_function(&name, None, false, false);
840 helper.add_export_edge_full(
841 module_id,
842 exported_id,
843 ExportKind::Direct,
844 None,
845 );
846 }
847 }
848 }
849 "class_declaration" => {
850 if let Some(name_node) = decl.child_by_field_name("name")
851 && let Ok(name) = name_node.utf8_text(content)
852 {
853 let name = name.trim().to_string();
854 if !name.is_empty() {
855 let exported_id = helper.add_class(&name, None);
856 helper.add_export_edge_full(
857 module_id,
858 exported_id,
859 ExportKind::Direct,
860 None,
861 );
862 }
863 }
864 }
865 "lexical_declaration" | "variable_declaration" => {
866 let mut cursor = decl.walk();
868 for child in decl.children(&mut cursor) {
869 if child.kind() == "variable_declarator"
870 && let Some(name_node) = child.child_by_field_name("name")
871 && let Ok(name) = name_node.utf8_text(content)
872 {
873 let name = name.trim().to_string();
874 if !name.is_empty() {
875 let exported_id = helper.add_variable(&name, None);
876 helper.add_export_edge_full(
877 module_id,
878 exported_id,
879 ExportKind::Direct,
880 None,
881 );
882 }
883 }
884 }
885 }
886 _ => {}
887 }
888 }
889}
890
891fn build_commonjs_export_edges(
899 expr_stmt_node: Node<'_>,
900 content: &[u8],
901 helper: &mut GraphBuildHelper,
902) {
903 let Some(assignment) = expr_stmt_node
905 .children(&mut expr_stmt_node.walk())
906 .find(|child| child.kind() == "assignment_expression")
907 else {
908 return;
909 };
910
911 let Some(left) = assignment.child_by_field_name("left") else {
912 return;
913 };
914 let Some(right) = assignment.child_by_field_name("right") else {
915 return;
916 };
917
918 let left_text = left.utf8_text(content).ok().map(|s| s.trim().to_string());
919 let Some(left_text) = left_text else {
920 return;
921 };
922
923 let module_id = helper.add_module("<module>", None);
924
925 if left_text == "module.exports" {
927 if right.kind() == "object" {
928 let mut cursor = right.walk();
930 for child in right.children(&mut cursor) {
931 if child.kind() == "shorthand_property_identifier" {
932 if let Ok(name) = child.utf8_text(content) {
934 let name = name.trim();
935 if !name.is_empty() {
936 let exported_id = helper.add_function(name, None, false, false);
937 helper.add_export_edge_full(
938 module_id,
939 exported_id,
940 ExportKind::Direct,
941 None,
942 );
943 }
944 }
945 } else if child.kind() == "pair" {
946 if let Some(key_node) = child.child_by_field_name("key")
948 && let Ok(export_name) = key_node.utf8_text(content)
949 {
950 let export_name = export_name.trim();
951 if !export_name.is_empty() {
952 let exported_id = helper.add_function(export_name, None, false, false);
953 helper.add_export_edge_full(
954 module_id,
955 exported_id,
956 ExportKind::Direct,
957 None,
958 );
959 }
960 }
961 } else if child.kind() == "spread_element" {
962 }
964 }
965 } else if right.kind() == "identifier" || right.kind() == "member_expression" {
966 let export_name = right
968 .utf8_text(content)
969 .ok()
970 .map_or_else(|| "default".to_string(), |s| s.trim().to_string());
971
972 if !export_name.is_empty() {
973 let exported_id = helper.add_function(&export_name, None, false, false);
974 helper.add_export_edge_full(module_id, exported_id, ExportKind::Default, None);
975 }
976 } else if matches!(
977 right.kind(),
978 "function_expression"
979 | "arrow_function"
980 | "class"
981 | "call_expression"
982 | "new_expression"
983 ) {
984 let exported_id = helper.add_function("default", None, false, false);
986 helper.add_export_edge_full(module_id, exported_id, ExportKind::Default, None);
987 }
988 }
989 else if left_text.starts_with("exports.") || left_text.starts_with("module.exports.") {
991 let export_name = if let Some(name) = left_text.strip_prefix("module.exports.") {
993 name
994 } else if let Some(name) = left_text.strip_prefix("exports.") {
995 name
996 } else {
997 return;
998 };
999
1000 if !export_name.is_empty() {
1001 let exported_id = helper.add_function(export_name, None, false, false);
1002 helper.add_export_edge_full(module_id, exported_id, ExportKind::Direct, None);
1003 }
1004 }
1005}
1006
1007fn build_inherits_edge_with_helper(
1014 class_node: Node<'_>,
1015 content: &[u8],
1016 helper: &mut GraphBuildHelper,
1017) {
1018 let heritage = class_node
1020 .children(&mut class_node.walk())
1021 .find(|child| child.kind() == "class_heritage");
1022
1023 let Some(heritage_node) = heritage else {
1024 return; };
1026
1027 let class_name = if class_node.kind() == "class_declaration" {
1029 class_node
1030 .child_by_field_name("name")
1031 .and_then(|n| n.utf8_text(content).ok())
1032 .map(|s| s.trim().to_string())
1033 } else {
1034 class_node
1036 .parent()
1037 .filter(|p| p.kind() == "variable_declarator")
1038 .and_then(|p| p.child_by_field_name("name"))
1039 .and_then(|n| n.utf8_text(content).ok())
1040 .map(|s| s.trim().to_string())
1041 .or_else(|| {
1042 Some(SyntheticNameBuilder::from_node_with_hash(
1044 &class_node,
1045 content,
1046 "class",
1047 ))
1048 })
1049 };
1050
1051 let parent_name = extract_parent_class_name(heritage_node, content);
1054
1055 if let (Some(child_name), Some(parent_name)) = (class_name, parent_name)
1057 && !child_name.is_empty()
1058 && !parent_name.is_empty()
1059 {
1060 let child_id = helper.add_class(&child_name, None);
1061 let parent_id = helper.add_class(&parent_name, None);
1062 helper.add_inherits_edge(child_id, parent_id);
1063 }
1064}
1065
1066fn extract_parent_class_name(heritage_node: Node<'_>, content: &[u8]) -> Option<String> {
1081 let mut cursor = heritage_node.walk();
1082 for child in heritage_node.children(&mut cursor) {
1083 match child.kind() {
1084 "identifier" => {
1085 return child.utf8_text(content).ok().map(|s| s.trim().to_string());
1087 }
1088 "member_expression" => {
1089 return child.utf8_text(content).ok().map(|s| s.trim().to_string());
1092 }
1093 "call_expression" => {
1094 return child.utf8_text(content).ok().map(|s| s.trim().to_string());
1099 }
1100 _ => {}
1101 }
1102 }
1103 None
1104}
1105
1106fn simple_name(name: &str) -> &str {
1107 name.rsplit(['.', '/']).next().unwrap_or(name)
1110}
1111
1112fn normalize_optional_chain(text: &str) -> String {
1116 text.replace("?.", ".")
1117 .trim()
1118 .trim_end_matches('.')
1119 .to_string()
1120}
1121
1122fn check_uses_await(call_node: Node<'_>) -> bool {
1123 let mut current = call_node;
1125 for _ in 0..2 {
1126 if let Some(parent) = current.parent() {
1128 if parent.kind() == "await_expression" {
1129 return true;
1130 }
1131 current = parent;
1132 } else {
1133 break;
1134 }
1135 }
1136 false
1137}
1138
1139fn count_arguments(node: Node<'_>) -> usize {
1140 node.child_by_field_name("arguments").map_or(0, |args| {
1141 let mut count = 0;
1142 let mut cursor = args.walk();
1143 for child in args.children(&mut cursor) {
1144 if !matches!(child.kind(), "(" | ")" | ",") {
1145 count += 1;
1146 }
1147 }
1148 count
1149 })
1150}
1151
1152fn span_from_node(node: Node<'_>) -> Span {
1153 let start = node.start_position();
1154 let end = node.end_position();
1155 Span::new(
1156 Position::new(start.row, start.column),
1157 Position::new(end.row, end.column),
1158 )
1159}
1160
1161fn extract_string_literal(node: &Node, content: &[u8]) -> Option<String> {
1162 let text = node.utf8_text(content).ok()?;
1163 let trimmed = text.trim();
1164
1165 trimmed
1167 .strip_prefix('"')
1168 .and_then(|s| s.strip_suffix('"'))
1169 .or_else(|| {
1170 trimmed
1171 .strip_prefix('\'')
1172 .and_then(|s| s.strip_suffix('\''))
1173 })
1174 .or_else(|| trimmed.strip_prefix('`').and_then(|s| s.strip_suffix('`')))
1175 .map(std::string::ToString::to_string)
1176}
1177
1178#[derive(Debug, Clone)]
1181pub struct CallContext {
1182 #[allow(dead_code)] pub name: Arc<str>,
1184 pub qualified_name: String,
1185 pub span: (usize, usize),
1186 pub is_async: bool,
1187}
1188
1189impl CallContext {
1190 pub fn qualified_name(&self) -> &str {
1191 &self.qualified_name
1192 }
1193}
1194
1195pub struct ASTGraph {
1196 callable_map: HashMap<usize, usize>,
1198 context_map: HashMap<usize, CallContext>,
1200}
1201
1202impl ASTGraph {
1203 pub fn from_tree(tree: &Tree, content: &[u8], max_scope_depth: usize) -> Result<Self, String> {
1205 let mut builder = ASTGraphBuilder::new(content, max_scope_depth);
1206
1207 let recursion_limits = sqry_core::config::RecursionLimits::load_or_default()
1209 .map_err(|e| format!("Failed to load recursion limits: {e}"))?;
1210 let file_ops_depth = recursion_limits
1211 .effective_file_ops_depth()
1212 .map_err(|e| format!("Invalid file_ops_depth configuration: {e}"))?;
1213 let mut guard = sqry_core::query::security::RecursionGuard::new(file_ops_depth)
1214 .map_err(|e| format!("Failed to create recursion guard: {e}"))?;
1215
1216 builder
1217 .visit(tree.root_node(), None, &mut guard)
1218 .map_err(|e| format!("JavaScript AST traversal hit recursion limit: {e}"))?;
1219 Ok(builder.build())
1220 }
1221
1222 pub fn get_callable_context(&self, node_id: usize) -> Option<&CallContext> {
1224 let callable_id = self.callable_map.get(&node_id)?;
1225 self.context_map.get(callable_id)
1226 }
1227
1228 pub fn contexts(&self) -> impl Iterator<Item = &CallContext> {
1230 self.context_map.values()
1231 }
1232}
1233
1234struct ASTGraphBuilder<'a> {
1235 content: &'a [u8],
1236 max_scope_depth: usize,
1237 callable_map: HashMap<usize, usize>,
1238 context_map: HashMap<usize, CallContext>,
1239 #[allow(dead_code)] current_callable: Option<usize>,
1241 current_scope: Vec<Arc<str>>,
1242}
1243
1244impl<'a> ASTGraphBuilder<'a> {
1245 fn new(content: &'a [u8], max_scope_depth: usize) -> Self {
1246 Self {
1247 content,
1248 max_scope_depth,
1249 callable_map: HashMap::new(),
1250 context_map: HashMap::new(),
1251 current_callable: None,
1252 current_scope: Vec::new(),
1253 }
1254 }
1255
1256 fn build(self) -> ASTGraph {
1257 ASTGraph {
1258 callable_map: self.callable_map,
1259 context_map: self.context_map,
1260 }
1261 }
1262
1263 fn visit(
1267 &mut self,
1268 node: Node<'_>,
1269 parent_callable: Option<usize>,
1270 guard: &mut sqry_core::query::security::RecursionGuard,
1271 ) -> Result<(), sqry_core::query::security::RecursionError> {
1272 guard.enter()?;
1273
1274 let node_id = node.id();
1275
1276 let callable_name = callable_node_name(node, self.content);
1278
1279 let new_callable = if let Some(name) = callable_name {
1280 let start = node.start_byte();
1282 let end = node.end_byte();
1283 let is_async = is_async_function(node, self.content);
1284
1285 let qualified_name = if self.current_scope.is_empty() {
1286 name.to_string()
1287 } else if self.current_scope.len() <= self.max_scope_depth {
1288 format!("{}.{}", self.current_scope.join("."), name)
1289 } else {
1290 let truncated = &self.current_scope[..self.max_scope_depth];
1292 format!("{}.{}", truncated.join("."), name)
1293 };
1294
1295 let context = CallContext {
1296 name: Arc::from(name),
1297 qualified_name,
1298 span: (start, end),
1299 is_async,
1300 };
1301
1302 self.context_map.insert(node_id, context);
1303 Some(node_id)
1304 } else {
1305 None
1306 };
1307
1308 let effective_callable = new_callable.or(parent_callable);
1310
1311 if let Some(callable_id) = effective_callable {
1313 self.callable_map.insert(node_id, callable_id);
1314 }
1315
1316 let scope_name = scope_node_name(node, self.content);
1318 let pushed_scope = if let Some(name) = scope_name {
1319 self.current_scope.push(Arc::from(name));
1320 true
1321 } else {
1322 false
1323 };
1324
1325 let mut cursor = node.walk();
1327 for child in node.children(&mut cursor) {
1328 self.visit(child, effective_callable, guard)?;
1329 }
1330
1331 if pushed_scope {
1333 self.current_scope.pop();
1334 }
1335
1336 guard.exit();
1337 Ok(())
1338 }
1339}
1340
1341fn callable_node_name(node: Node<'_>, content: &[u8]) -> Option<String> {
1343 match node.kind() {
1344 "function_declaration" | "generator_function_declaration" => node
1345 .child_by_field_name("name")
1346 .and_then(|child| child.utf8_text(content).ok().map(|s| s.trim().to_string())),
1347 "function_expression" | "generator_function" => {
1348 node.child_by_field_name("name")
1350 .and_then(|child| child.utf8_text(content).ok().map(|s| s.trim().to_string()))
1351 .or_else(|| {
1352 Some(SyntheticNameBuilder::from_node_with_hash(
1353 &node, content, "function",
1354 ))
1355 })
1356 }
1357 "arrow_function" => {
1358 if let Some(parent) = node.parent()
1362 && parent.kind() == "variable_declarator"
1363 && let Some(name_node) = parent.child_by_field_name("name")
1364 && let Ok(name) = name_node.utf8_text(content)
1365 {
1366 let trimmed = name.trim();
1367 if !trimmed.is_empty() {
1368 return Some(trimmed.to_string());
1369 }
1370 }
1371 Some(SyntheticNameBuilder::from_node_with_hash(
1374 &node, content, "arrow",
1375 ))
1376 }
1377 "method_definition" => node
1378 .child_by_field_name("name")
1379 .and_then(|child| child.utf8_text(content).ok().map(|s| s.trim().to_string())),
1380 _ => None,
1381 }
1382}
1383
1384fn scope_node_name(node: Node<'_>, content: &[u8]) -> Option<String> {
1385 match node.kind() {
1386 "class_declaration" | "class" => node
1387 .child_by_field_name("name")
1388 .and_then(|child| child.utf8_text(content).ok().map(|s| s.trim().to_string()))
1389 .or_else(|| {
1390 Some(SyntheticNameBuilder::from_node_with_hash(
1391 &node, content, "class",
1392 ))
1393 }),
1394 _ => None,
1395 }
1396}
1397
1398fn is_async_function(node: Node<'_>, _content: &[u8]) -> bool {
1399 let mut cursor = node.walk();
1401 node.children(&mut cursor)
1402 .any(|child| child.kind() == "async")
1403}
1404
1405fn process_jsdoc_annotations(
1410 node: Node,
1411 content: &[u8],
1412 helper: &mut GraphBuildHelper,
1413) -> GraphResult<()> {
1414 match node.kind() {
1416 "function_declaration" | "generator_function_declaration" => {
1417 process_function_jsdoc(node, content, helper)?;
1418 }
1419 "method_definition" => {
1420 process_method_jsdoc(node, content, helper)?;
1421 }
1422 "lexical_declaration" | "variable_declaration" => {
1423 process_variable_jsdoc(node, content, helper)?;
1424 }
1425 "class_declaration" | "class" => {
1426 process_class_fields(node, content, helper)?;
1427 process_constructor_this_assignments(node, content, helper)?;
1428 }
1429 _ => {}
1430 }
1431
1432 let mut cursor = node.walk();
1434 for child in node.children(&mut cursor) {
1435 process_jsdoc_annotations(child, content, helper)?;
1436 }
1437
1438 Ok(())
1439}
1440
1441fn process_function_jsdoc(
1443 func_node: Node,
1444 content: &[u8],
1445 helper: &mut GraphBuildHelper,
1446) -> GraphResult<()> {
1447 let Some(jsdoc_text) = extract_jsdoc_comment(func_node, content) else {
1449 return Ok(());
1450 };
1451
1452 let tags = parse_jsdoc_tags(&jsdoc_text);
1454
1455 let Some(name_node) = func_node.child_by_field_name("name") else {
1457 return Ok(());
1458 };
1459
1460 let function_name = name_node
1461 .utf8_text(content)
1462 .map_err(|_| GraphBuilderError::ParseError {
1463 span: span_from_node(func_node),
1464 reason: "failed to read function name".to_string(),
1465 })?
1466 .trim()
1467 .to_string();
1468
1469 if function_name.is_empty() {
1470 return Ok(());
1471 }
1472
1473 let func_node_id = helper.ensure_callee(
1475 &function_name,
1476 span_from_node(func_node),
1477 CalleeKindHint::Function,
1478 );
1479
1480 let ast_params = extract_ast_parameters(func_node, content);
1483 let ast_param_map: HashMap<&str, usize> = ast_params
1484 .iter()
1485 .map(|(idx, name)| (name.as_str(), *idx))
1486 .collect();
1487
1488 for param_tag in &tags.params {
1490 let mut normalized_name = param_tag
1493 .name
1494 .trim_start_matches("...")
1495 .trim_matches(|c| c == '[' || c == ']');
1496
1497 if let Some(base_name) = normalized_name.split('.').next() {
1500 normalized_name = base_name;
1501 }
1502
1503 let Some(&ast_index) = ast_param_map.get(normalized_name) else {
1504 continue;
1506 };
1507
1508 let canonical_type = canonical_type_string(¶m_tag.type_str);
1510 let type_node_id = helper.add_type(&canonical_type, None);
1511 helper.add_typeof_edge_with_context(
1512 func_node_id,
1513 type_node_id,
1514 Some(TypeOfContext::Parameter),
1515 ast_index.try_into().ok(), Some(¶m_tag.name),
1517 );
1518
1519 let type_names = extract_type_names(¶m_tag.type_str);
1521 for type_name in type_names {
1522 let ref_type_id = helper.add_type(&type_name, None);
1523 helper.add_reference_edge(func_node_id, ref_type_id);
1524 }
1525 }
1526
1527 if let Some(return_type) = &tags.returns {
1529 let canonical_type = canonical_type_string(return_type);
1530 let type_node_id = helper.add_type(&canonical_type, None);
1531 helper.add_typeof_edge_with_context(
1532 func_node_id,
1533 type_node_id,
1534 Some(TypeOfContext::Return),
1535 Some(0),
1536 None,
1537 );
1538
1539 let type_names = extract_type_names(return_type);
1541 for type_name in type_names {
1542 let ref_type_id = helper.add_type(&type_name, None);
1543 helper.add_reference_edge(func_node_id, ref_type_id);
1544 }
1545 }
1546
1547 Ok(())
1548}
1549
1550fn process_method_jsdoc(
1552 method_node: Node,
1553 content: &[u8],
1554 helper: &mut GraphBuildHelper,
1555) -> GraphResult<()> {
1556 let Some(jsdoc_text) = extract_jsdoc_comment(method_node, content) else {
1558 return Ok(());
1559 };
1560
1561 let tags = parse_jsdoc_tags(&jsdoc_text);
1563
1564 let Some(name_node) = method_node.child_by_field_name("name") else {
1566 return Ok(());
1567 };
1568
1569 let method_name = name_node
1570 .utf8_text(content)
1571 .map_err(|_| GraphBuilderError::ParseError {
1572 span: span_from_node(method_node),
1573 reason: "failed to read method name".to_string(),
1574 })?
1575 .trim()
1576 .to_string();
1577
1578 if method_name.is_empty() {
1579 return Ok(());
1580 }
1581
1582 let class_name = get_enclosing_class_name(method_node, content)?;
1584 let Some(class_name) = class_name else {
1585 return Ok(());
1586 };
1587
1588 let qualified_name = format!("{class_name}.{method_name}");
1590
1591 let method_node_id = helper.ensure_method(&qualified_name, None, false, false);
1594
1595 let ast_params = extract_ast_parameters(method_node, content);
1598 let ast_param_map: HashMap<&str, usize> = ast_params
1599 .iter()
1600 .map(|(idx, name)| (name.as_str(), *idx))
1601 .collect();
1602
1603 for param_tag in &tags.params {
1605 let mut normalized_name = param_tag
1608 .name
1609 .trim_start_matches("...")
1610 .trim_matches(|c| c == '[' || c == ']');
1611
1612 if let Some(base_name) = normalized_name.split('.').next() {
1615 normalized_name = base_name;
1616 }
1617
1618 let Some(&ast_index) = ast_param_map.get(normalized_name) else {
1619 continue;
1621 };
1622
1623 let canonical_type = canonical_type_string(¶m_tag.type_str);
1624 let type_node_id = helper.add_type(&canonical_type, None);
1625 helper.add_typeof_edge_with_context(
1626 method_node_id,
1627 type_node_id,
1628 Some(TypeOfContext::Parameter),
1629 ast_index.try_into().ok(), Some(¶m_tag.name),
1631 );
1632
1633 let type_names = extract_type_names(¶m_tag.type_str);
1635 for type_name in type_names {
1636 let ref_type_id = helper.add_type(&type_name, None);
1637 helper.add_reference_edge(method_node_id, ref_type_id);
1638 }
1639 }
1640
1641 if let Some(return_type) = &tags.returns {
1643 let canonical_type = canonical_type_string(return_type);
1644 let type_node_id = helper.add_type(&canonical_type, None);
1645 helper.add_typeof_edge_with_context(
1646 method_node_id,
1647 type_node_id,
1648 Some(TypeOfContext::Return),
1649 Some(0),
1650 None,
1651 );
1652
1653 let type_names = extract_type_names(return_type);
1655 for type_name in type_names {
1656 let ref_type_id = helper.add_type(&type_name, None);
1657 helper.add_reference_edge(method_node_id, ref_type_id);
1658 }
1659 }
1660
1661 Ok(())
1662}
1663
1664fn process_variable_jsdoc(
1666 decl_node: Node,
1667 content: &[u8],
1668 helper: &mut GraphBuildHelper,
1669) -> GraphResult<()> {
1670 if !is_top_level_variable(decl_node) {
1672 return Ok(());
1673 }
1674
1675 let Some(jsdoc_text) = extract_jsdoc_comment(decl_node, content) else {
1677 return Ok(());
1678 };
1679
1680 let tags = parse_jsdoc_tags(&jsdoc_text);
1682
1683 let Some(type_annotation) = &tags.type_annotation else {
1685 return Ok(());
1686 };
1687
1688 let mut cursor = decl_node.walk();
1690 for child in decl_node.children(&mut cursor) {
1691 if child.kind() == "variable_declarator"
1692 && let Some(name_node) = child.child_by_field_name("name")
1693 {
1694 let var_name = name_node
1695 .utf8_text(content)
1696 .map_err(|_| GraphBuilderError::ParseError {
1697 span: span_from_node(child),
1698 reason: "failed to read variable name".to_string(),
1699 })?
1700 .trim()
1701 .to_string();
1702
1703 if !var_name.is_empty() {
1704 let var_node_id = helper.add_variable(&var_name, None);
1706
1707 let canonical_type = canonical_type_string(type_annotation);
1709 let type_node_id = helper.add_type(&canonical_type, None);
1710 helper.add_typeof_edge_with_context(
1711 var_node_id,
1712 type_node_id,
1713 Some(TypeOfContext::Variable),
1714 None,
1715 None,
1716 );
1717
1718 let type_names = extract_type_names(type_annotation);
1720 for type_name in type_names {
1721 let ref_type_id = helper.add_type(&type_name, None);
1722 helper.add_reference_edge(var_node_id, ref_type_id);
1723 }
1724 }
1725 }
1726 }
1727
1728 Ok(())
1729}
1730
1731fn resolve_class_name_for_fields(
1742 class_node: Node<'_>,
1743 content: &[u8],
1744) -> GraphResult<Option<String>> {
1745 if let Some(name_node) = class_node.child_by_field_name("name") {
1746 let name = name_node
1747 .utf8_text(content)
1748 .map_err(|_| GraphBuilderError::ParseError {
1749 span: span_from_node(class_node),
1750 reason: "failed to read class name".to_string(),
1751 })?
1752 .trim()
1753 .to_string();
1754 if name.is_empty() {
1755 return Ok(None);
1756 }
1757 return Ok(Some(name));
1758 }
1759
1760 let Some(parent) = class_node.parent() else {
1762 return Ok(None);
1763 };
1764
1765 match parent.kind() {
1766 "variable_declarator" => {
1767 if let Some(name_node) = parent.child_by_field_name("name")
1768 && let Ok(var_name) = name_node.utf8_text(content)
1769 {
1770 let var_name = var_name.trim().to_string();
1771 if var_name.is_empty() {
1772 return Ok(None);
1773 }
1774 return Ok(Some(var_name));
1775 }
1776 Ok(None)
1777 }
1778 "assignment_expression" => {
1779 if let Some(left) = parent.child_by_field_name("left")
1780 && let Ok(assign_name) = left.utf8_text(content)
1781 {
1782 let assign_name = assign_name.trim().to_string();
1783 if assign_name.is_empty() {
1784 return Ok(None);
1785 }
1786 return Ok(Some(assign_name));
1787 }
1788 Ok(None)
1789 }
1790 _ => Ok(None),
1791 }
1792}
1793
1794fn process_class_fields(
1810 class_node: Node<'_>,
1811 content: &[u8],
1812 helper: &mut GraphBuildHelper,
1813) -> GraphResult<()> {
1814 let Some(class_name) = resolve_class_name_for_fields(class_node, content)? else {
1815 return Ok(());
1816 };
1817
1818 let Some(body_node) = class_node.child_by_field_name("body") else {
1819 return Ok(());
1820 };
1821
1822 let mut cursor = body_node.walk();
1823 for child in body_node.children(&mut cursor) {
1824 if child.kind() != "field_definition" {
1825 continue;
1826 }
1827 emit_class_field_node(child, content, helper, &class_name)?;
1828 }
1829
1830 Ok(())
1831}
1832
1833fn emit_class_field_node(
1836 field_node: Node<'_>,
1837 content: &[u8],
1838 helper: &mut GraphBuildHelper,
1839 class_name: &str,
1840) -> GraphResult<()> {
1841 let Some(name_node) = field_node.child_by_field_name("property") else {
1845 return Ok(());
1846 };
1847
1848 let raw_name = name_node
1849 .utf8_text(content)
1850 .map_err(|_| GraphBuilderError::ParseError {
1851 span: span_from_node(field_node),
1852 reason: "failed to read field name".to_string(),
1853 })?
1854 .trim()
1855 .to_string();
1856
1857 if raw_name.is_empty() {
1858 return Ok(());
1859 }
1860
1861 let is_hash_private = name_node.kind() == "private_property_identifier";
1862
1863 let mut is_static = false;
1868 let mut mod_cursor = field_node.walk();
1869 for modifier in field_node.children(&mut mod_cursor) {
1870 if modifier.kind() == "static" {
1871 is_static = true;
1872 }
1873 }
1874
1875 let visibility: Option<&str> = if is_hash_private {
1881 Some("private")
1882 } else {
1883 Some("public")
1884 };
1885
1886 let qualified_name = format!("{class_name}.{raw_name}");
1887 let span = Some(span_from_node(field_node));
1888
1889 let field_id = helper.add_property_with_static_and_visibility(
1890 &qualified_name,
1891 span,
1892 is_static,
1893 visibility,
1894 );
1895
1896 if let Some(jsdoc_text) = extract_jsdoc_comment(field_node, content) {
1900 let tags = parse_jsdoc_tags(&jsdoc_text);
1901 if let Some(type_annotation) = &tags.type_annotation {
1902 let canonical_type = canonical_type_string(type_annotation);
1903 let type_node_id = helper.add_type(&canonical_type, None);
1904 helper.add_typeof_edge_with_context(
1905 field_id,
1906 type_node_id,
1907 Some(TypeOfContext::Field),
1908 None,
1909 Some(&raw_name),
1910 );
1911
1912 let type_names = extract_type_names(type_annotation);
1913 for type_name in type_names {
1914 let ref_type_id = helper.add_type(&type_name, None);
1915 helper.add_reference_edge(field_id, ref_type_id);
1916 }
1917 }
1918 }
1919
1920 Ok(())
1921}
1922
1923fn process_constructor_this_assignments(
1935 class_node: Node<'_>,
1936 content: &[u8],
1937 helper: &mut GraphBuildHelper,
1938) -> GraphResult<()> {
1939 let Some(class_name) = resolve_class_name_for_fields(class_node, content)? else {
1940 return Ok(());
1941 };
1942
1943 let Some(body_node) = class_node.child_by_field_name("body") else {
1944 return Ok(());
1945 };
1946
1947 let mut cursor = body_node.walk();
1948 for child in body_node.children(&mut cursor) {
1949 if child.kind() != "method_definition" {
1950 continue;
1951 }
1952
1953 let Some(name_node) = child.child_by_field_name("name") else {
1958 continue;
1959 };
1960 let Ok(method_name) = name_node.utf8_text(content) else {
1961 continue;
1962 };
1963 if method_name.trim() != "constructor" {
1964 continue;
1965 }
1966
1967 let Some(method_body) = child.child_by_field_name("body") else {
1968 continue;
1969 };
1970
1971 walk_for_this_assignments(method_body, content, helper, &class_name);
1972 }
1973
1974 Ok(())
1975}
1976
1977fn walk_for_this_assignments(
1981 node: Node<'_>,
1982 content: &[u8],
1983 helper: &mut GraphBuildHelper,
1984 class_name: &str,
1985) {
1986 if node.kind() == "assignment_expression"
1987 && let Some(left) = node.child_by_field_name("left")
1988 && left.kind() == "member_expression"
1989 && let Some(object) = left.child_by_field_name("object")
1990 && object.kind() == "this"
1991 && let Some(property) = left.child_by_field_name("property")
1992 && property.kind() == "property_identifier"
1993 && let Ok(field_name) = property.utf8_text(content)
1994 {
1995 let field_name = field_name.trim();
1996 if !field_name.is_empty() {
1997 let qualified_name = format!("{class_name}.{field_name}");
1998 let _ = helper.add_property_with_static_and_visibility(
2008 &qualified_name,
2009 Some(span_from_node(left)),
2010 false,
2011 Some("public"),
2012 );
2013 }
2014 }
2015
2016 let mut cursor = node.walk();
2024 for child in node.children(&mut cursor) {
2025 walk_for_this_assignments(child, content, helper, class_name);
2026 }
2027}
2028
2029fn get_enclosing_class_name(method_node: Node, content: &[u8]) -> GraphResult<Option<String>> {
2033 let mut current = method_node;
2035 while let Some(parent) = current.parent() {
2036 if parent.kind() == "class_declaration" {
2037 if let Some(name_node) = parent.child_by_field_name("name") {
2039 let class_name = name_node
2040 .utf8_text(content)
2041 .map_err(|_| GraphBuilderError::ParseError {
2042 span: span_from_node(parent),
2043 reason: "failed to read class name".to_string(),
2044 })?
2045 .trim()
2046 .to_string();
2047
2048 if !class_name.is_empty() {
2049 return Ok(Some(class_name));
2050 }
2051 }
2052 } else if parent.kind() == "class" {
2053 if let Some(grandparent) = parent.parent() {
2056 if grandparent.kind() == "variable_declarator" {
2057 if let Some(name_node) = grandparent.child_by_field_name("name")
2059 && let Ok(var_name) = name_node.utf8_text(content)
2060 {
2061 let var_name = var_name.trim().to_string();
2062 if !var_name.is_empty() {
2063 return Ok(Some(var_name));
2064 }
2065 }
2066 } else if grandparent.kind() == "assignment_expression" {
2067 if let Some(left) = grandparent.child_by_field_name("left")
2069 && let Ok(assign_name) = left.utf8_text(content)
2070 {
2071 let assign_name = assign_name.trim().to_string();
2072 if !assign_name.is_empty() {
2073 return Ok(Some(assign_name));
2074 }
2075 }
2076 }
2077 }
2078 return Ok(None);
2081 }
2082 current = parent;
2083 }
2084 Ok(None)
2085}
2086
2087fn extract_ast_parameters(func_node: Node, content: &[u8]) -> Vec<(usize, String)> {
2090 let Some(params_node) = func_node.child_by_field_name("parameters") else {
2091 return Vec::new();
2092 };
2093
2094 let mut cursor = params_node.walk();
2095 params_node
2096 .named_children(&mut cursor)
2097 .enumerate()
2098 .filter_map(|(ast_index, param)| {
2099 let param_name = match param.kind() {
2101 "identifier" => param
2102 .utf8_text(content)
2103 .ok()
2104 .map(std::string::ToString::to_string),
2105 "required_parameter" | "optional_parameter" => {
2106 param
2108 .child_by_field_name("pattern")
2109 .and_then(|p| p.utf8_text(content).ok())
2110 .map(std::string::ToString::to_string)
2111 }
2112 "rest_pattern" => {
2113 param
2116 .named_child(0)
2117 .and_then(|n| n.utf8_text(content).ok())
2118 .map(|s| s.trim_start_matches("...").to_string())
2119 }
2120 "assignment_pattern" => {
2121 param
2124 .child_by_field_name("left")
2125 .filter(|left| left.kind() == "identifier")
2126 .and_then(|left| left.utf8_text(content).ok())
2127 .map(std::string::ToString::to_string)
2128 }
2129 _ => None,
2130 };
2131
2132 param_name.map(|name| (ast_index, name))
2133 })
2134 .collect()
2135}
2136
2137fn is_top_level_variable(decl_node: Node) -> bool {
2140 let mut current = decl_node;
2141 while let Some(parent) = current.parent() {
2142 match parent.kind() {
2143 "function_declaration"
2145 | "generator_function_declaration"
2146 | "function_expression"
2147 | "arrow_function"
2148 | "method_definition" => return false,
2149
2150 "statement_block" | "if_statement" | "for_statement" | "for_in_statement"
2152 | "for_of_statement" | "while_statement" | "do_statement" | "try_statement"
2153 | "catch_clause" | "finally_clause" | "switch_statement" | "switch_case"
2154 | "switch_default" | "class_body" | "class_static_block" | "with_statement" => {
2155 return false;
2156 }
2157
2158 "program" | "export_statement" => return true,
2161
2162 _ => {}
2163 }
2164 current = parent;
2165 }
2166 true
2167}
2168
2169fn build_ffi_call_edge(
2181 ast_graph: &ASTGraph,
2182 call_node: Node<'_>,
2183 content: &[u8],
2184 helper: &mut GraphBuildHelper,
2185) -> GraphResult<bool> {
2186 let Some(callee_expr) = call_node.child_by_field_name("function") else {
2187 return Ok(false);
2188 };
2189
2190 let callee_text = callee_expr
2191 .utf8_text(content)
2192 .map_err(|_| GraphBuilderError::ParseError {
2193 span: span_from_node(call_node),
2194 reason: "failed to read call expression".to_string(),
2195 })?
2196 .trim();
2197
2198 if callee_text.starts_with("WebAssembly.") {
2200 return Ok(build_webassembly_call_edge(
2201 ast_graph,
2202 call_node,
2203 content,
2204 callee_text,
2205 helper,
2206 ));
2207 }
2208
2209 if callee_text == "require" {
2211 return Ok(build_require_ffi_edge(
2212 ast_graph, call_node, content, helper,
2213 ));
2214 }
2215
2216 if callee_text == "process.dlopen" {
2218 return Ok(build_dlopen_edge(ast_graph, call_node, content, helper));
2219 }
2220
2221 Ok(false)
2222}
2223
2224fn build_ffi_new_edge(
2232 ast_graph: &ASTGraph,
2233 new_node: Node<'_>,
2234 content: &[u8],
2235 helper: &mut GraphBuildHelper,
2236) -> GraphResult<bool> {
2237 let Some(constructor_expr) = new_node.child_by_field_name("constructor") else {
2238 return Ok(false);
2239 };
2240
2241 let constructor_text = constructor_expr
2242 .utf8_text(content)
2243 .map_err(|_| GraphBuilderError::ParseError {
2244 span: span_from_node(new_node),
2245 reason: "failed to read constructor expression".to_string(),
2246 })?
2247 .trim();
2248
2249 if constructor_text == "WebAssembly.Module" || constructor_text == "WebAssembly.Instance" {
2251 return Ok(build_webassembly_constructor_edge(
2252 ast_graph,
2253 new_node,
2254 content,
2255 constructor_text,
2256 helper,
2257 ));
2258 }
2259
2260 Ok(false)
2261}
2262
2263fn build_webassembly_call_edge(
2265 ast_graph: &ASTGraph,
2266 call_node: Node<'_>,
2267 content: &[u8],
2268 callee_text: &str,
2269 helper: &mut GraphBuildHelper,
2270) -> bool {
2271 let method_name = callee_text
2273 .strip_prefix("WebAssembly.")
2274 .unwrap_or(callee_text);
2275
2276 let is_wasm_load = matches!(
2278 method_name,
2279 "instantiate" | "instantiateStreaming" | "compile" | "compileStreaming" | "validate"
2280 );
2281
2282 if !is_wasm_load {
2283 return false;
2284 }
2285
2286 let caller_id = get_caller_node_id(ast_graph, call_node, content, helper);
2288
2289 let wasm_module_name = extract_wasm_module_name(call_node, content)
2291 .unwrap_or_else(|| format!("wasm::{method_name}"));
2292
2293 let wasm_node_id = helper.add_module(&wasm_module_name, Some(span_from_node(call_node)));
2295
2296 helper.add_webassembly_edge(caller_id, wasm_node_id);
2298
2299 true
2300}
2301
2302fn build_webassembly_constructor_edge(
2304 ast_graph: &ASTGraph,
2305 new_node: Node<'_>,
2306 content: &[u8],
2307 constructor_text: &str,
2308 helper: &mut GraphBuildHelper,
2309) -> bool {
2310 let caller_id = get_caller_node_id(ast_graph, new_node, content, helper);
2312
2313 let type_name = constructor_text
2315 .strip_prefix("WebAssembly.")
2316 .unwrap_or(constructor_text);
2317 let wasm_module_name = format!("wasm::{type_name}");
2318
2319 let wasm_node_id = helper.add_module(&wasm_module_name, Some(span_from_node(new_node)));
2321
2322 helper.add_webassembly_edge(caller_id, wasm_node_id);
2324
2325 true
2326}
2327
2328fn build_require_ffi_edge(
2333 ast_graph: &ASTGraph,
2334 call_node: Node<'_>,
2335 content: &[u8],
2336 helper: &mut GraphBuildHelper,
2337) -> bool {
2338 let Some(args) = call_node.child_by_field_name("arguments") else {
2340 return false;
2341 };
2342
2343 let mut cursor = args.walk();
2344 let first_arg = args
2345 .children(&mut cursor)
2346 .find(|child| !matches!(child.kind(), "(" | ")" | ","));
2347
2348 let Some(arg_node) = first_arg else {
2349 return false;
2350 };
2351
2352 let module_path = extract_string_literal(&arg_node, content);
2354 let Some(path) = module_path else {
2355 return false;
2356 };
2357
2358 let from_id = helper.add_module("<module>", None);
2360
2361 let resolved_path = if path.starts_with('.') {
2363 sqry_core::graph::resolve_import_path(std::path::Path::new(helper.file_path()), &path)
2365 .unwrap_or_else(|_| simple_name(&path).to_string())
2366 } else {
2367 simple_name(&path).to_string()
2369 };
2370
2371 let to_id = helper.add_import(&resolved_path, Some(span_from_node(call_node)));
2372 helper.add_import_edge(from_id, to_id);
2373
2374 let is_native_addon = std::path::Path::new(&path)
2376 .extension()
2377 .is_some_and(|ext| ext.eq_ignore_ascii_case("node"))
2378 || is_known_native_addon(&path);
2379
2380 if is_native_addon {
2381 let caller_id = get_caller_node_id(ast_graph, call_node, content, helper);
2383
2384 let ffi_name = format!("native::{}", simple_name(&path));
2386 let ffi_node_id = helper.add_module(&ffi_name, Some(span_from_node(call_node)));
2387
2388 helper.add_ffi_edge(caller_id, ffi_node_id, FfiConvention::C);
2390 }
2391
2392 true
2393}
2394
2395fn build_dlopen_edge(
2397 ast_graph: &ASTGraph,
2398 call_node: Node<'_>,
2399 content: &[u8],
2400 helper: &mut GraphBuildHelper,
2401) -> bool {
2402 let caller_id = get_caller_node_id(ast_graph, call_node, content, helper);
2404
2405 let module_name = call_node
2407 .child_by_field_name("arguments")
2408 .and_then(|args| {
2409 let mut cursor = args.walk();
2410 args.children(&mut cursor)
2411 .filter(|child| !matches!(child.kind(), "(" | ")" | ","))
2412 .nth(1) })
2414 .and_then(|node| extract_string_literal(&node, content))
2415 .map_or_else(
2416 || "native::dlopen".to_string(),
2417 |path| format!("native::{}", simple_name(&path)),
2418 );
2419
2420 let ffi_node_id = helper.add_module(&module_name, Some(span_from_node(call_node)));
2422
2423 helper.add_ffi_edge(caller_id, ffi_node_id, FfiConvention::C);
2425
2426 true
2427}
2428
2429fn get_caller_node_id(
2431 ast_graph: &ASTGraph,
2432 node: Node<'_>,
2433 content: &[u8],
2434 helper: &mut GraphBuildHelper,
2435) -> sqry_core::graph::unified::NodeId {
2436 let module_context;
2437 let call_context = if let Some(ctx) = ast_graph.get_callable_context(node.id()) {
2438 ctx
2439 } else {
2440 module_context = CallContext {
2441 name: Arc::from("<module>"),
2442 qualified_name: "<module>".to_string(),
2443 span: (0, content.len()),
2444 is_async: false,
2445 };
2446 &module_context
2447 };
2448
2449 ensure_caller_node(helper, call_context)
2450}
2451
2452fn ensure_caller_node(
2453 helper: &mut GraphBuildHelper,
2454 call_context: &CallContext,
2455) -> sqry_core::graph::unified::NodeId {
2456 let caller_span = Some(Span::from_bytes(call_context.span.0, call_context.span.1));
2457 let qualified_name = call_context.qualified_name();
2458 if qualified_name.contains('.') {
2459 helper.ensure_method(qualified_name, caller_span, call_context.is_async, false)
2460 } else {
2461 helper.ensure_function(qualified_name, caller_span, call_context.is_async, false)
2462 }
2463}
2464
2465fn extract_wasm_module_name(call_node: Node<'_>, content: &[u8]) -> Option<String> {
2471 let args = call_node.child_by_field_name("arguments")?;
2472
2473 let mut cursor = args.walk();
2474 let first_arg = args
2475 .children(&mut cursor)
2476 .find(|child| !matches!(child.kind(), "(" | ")" | ","))?;
2477
2478 if first_arg.kind() == "call_expression"
2480 && let Some(func) = first_arg.child_by_field_name("function")
2481 {
2482 let func_text = func.utf8_text(content).ok()?.trim();
2483 if func_text == "fetch" {
2484 if let Some(fetch_args) = first_arg.child_by_field_name("arguments") {
2486 let mut fetch_cursor = fetch_args.walk();
2487 let url_arg = fetch_args
2488 .children(&mut fetch_cursor)
2489 .find(|child| !matches!(child.kind(), "(" | ")" | ","))?;
2490
2491 if let Some(url) = extract_string_literal(&url_arg, content) {
2492 return Some(format!("wasm::{}", simple_name(&url)));
2493 }
2494 }
2495 }
2496 }
2497
2498 if let Some(path) = extract_string_literal(&first_arg, content) {
2500 return Some(format!("wasm::{}", simple_name(&path)));
2501 }
2502
2503 None
2504}
2505
2506fn is_known_native_addon(package_name: &str) -> bool {
2508 const NATIVE_PACKAGES: &[&str] = &[
2510 "better-sqlite3",
2511 "sqlite3",
2512 "bcrypt",
2513 "sharp",
2514 "canvas",
2515 "node-sass",
2516 "leveldown",
2517 "bufferutil",
2518 "utf-8-validate",
2519 "fsevents",
2520 "cpu-features",
2521 "node-gyp",
2522 "node-pre-gyp",
2523 "prebuild",
2524 "nan",
2525 "node-addon-api",
2526 "ref-napi",
2527 "ffi-napi",
2528 ];
2529
2530 NATIVE_PACKAGES
2531 .iter()
2532 .any(|&pkg| package_name.contains(pkg))
2533}