1use std::{collections::HashMap, path::Path, sync::Arc};
2
3use sqry_core::graph::unified::edge::kind::TypeOfContext;
4use sqry_core::graph::unified::edge::{ExportKind, FfiConvention, HttpMethod};
5use sqry_core::graph::unified::{GraphBuildHelper, NodeId, StagingGraph};
6use sqry_core::graph::{GraphBuilder, GraphBuilderError, GraphResult, Language, Position, Span};
7use sqry_core::relations::SyntheticNameBuilder;
8use tree_sitter::{Node, Tree};
9
10use super::jsdoc_parser::{extract_jsdoc_comment, parse_jsdoc_tags};
11use super::local_scopes;
12use super::type_extractor::{canonical_type_string, extract_type_names};
13
14const DEFAULT_SCOPE_DEPTH: usize = 4;
15type CallEdgeData = (NodeId, NodeId, u8, bool, Option<Span>);
16type ConstructorEdgeData = (NodeId, NodeId, u8, Option<Span>);
17
18#[derive(Debug, Clone, Copy)]
20pub struct JavaScriptGraphBuilder {
21 max_scope_depth: usize,
22}
23
24impl Default for JavaScriptGraphBuilder {
25 fn default() -> Self {
26 Self {
27 max_scope_depth: DEFAULT_SCOPE_DEPTH,
28 }
29 }
30}
31
32impl JavaScriptGraphBuilder {
33 #[must_use]
34 pub fn new(max_scope_depth: usize) -> Self {
35 Self { max_scope_depth }
36 }
37}
38
39fn infer_visibility(qualified_name: &str) -> &'static str {
42 let name_part = qualified_name.rsplit('.').next().unwrap_or(qualified_name);
44 if name_part.starts_with('_') {
45 "private"
46 } else {
47 "public"
48 }
49}
50
51impl GraphBuilder for JavaScriptGraphBuilder {
52 fn build_graph(
53 &self,
54 tree: &Tree,
55 content: &[u8],
56 file: &Path,
57 staging: &mut StagingGraph,
58 ) -> GraphResult<()> {
59 let mut helper = GraphBuildHelper::new(staging, file, Language::JavaScript);
61 let file_arc = Arc::from(file.to_string_lossy().to_string());
62
63 let ast_graph = ASTGraph::from_tree(tree, content, self.max_scope_depth).map_err(|e| {
65 GraphBuilderError::ParseError {
66 span: Span::default(),
67 reason: e,
68 }
69 })?;
70
71 for context in ast_graph.contexts() {
73 let span = Some(Span::from_bytes(context.span.0, context.span.1));
74 let visibility = infer_visibility(&context.qualified_name);
76
77 if context.qualified_name.contains('.') {
79 helper.add_method_with_visibility(
80 &context.qualified_name,
81 span,
82 context.is_async,
83 false, Some(visibility),
85 );
86 } else {
87 helper.add_function_with_visibility(
88 &context.qualified_name,
89 span,
90 context.is_async,
91 false, Some(visibility),
93 );
94 }
95 }
96
97 let mut scope_tree = local_scopes::build(tree.root_node(), content)?;
99
100 let mut cursor = tree.root_node().walk();
102 extract_edges_recursive(
103 tree.root_node(),
104 &mut cursor,
105 content,
106 &file_arc,
107 &ast_graph,
108 &mut helper,
109 &mut scope_tree,
110 )?;
111
112 process_jsdoc_annotations(tree.root_node(), content, &mut helper)?;
114
115 Ok(())
116 }
117
118 fn language(&self) -> Language {
119 Language::JavaScript
120 }
121}
122
123fn extract_edges_recursive<'a>(
125 node: Node<'a>,
126 cursor: &mut tree_sitter::TreeCursor<'a>,
127 content: &[u8],
128 file: &Arc<str>,
129 ast_graph: &ASTGraph,
130 helper: &mut GraphBuildHelper,
131 scope_tree: &mut local_scopes::JavaScriptScopeTree,
132) -> GraphResult<()> {
133 match node.kind() {
134 "call_expression" => {
135 let _ = build_http_request_edge(ast_graph, node, content, helper);
137 let _ = detect_route_endpoint(node, content, helper);
139 let is_ffi = build_ffi_call_edge(ast_graph, node, content, helper)?;
141 if !is_ffi {
142 if let Some((caller_id, callee_id, argument_count, is_async, span)) =
144 build_call_edge_with_helper(ast_graph, node, content, helper)?
145 {
146 helper.add_call_edge_full_with_span(
147 caller_id,
148 callee_id,
149 argument_count,
150 is_async,
151 span.into_iter().collect(),
152 );
153 }
154 }
155 }
156 "new_expression" => {
157 let is_ffi = build_ffi_new_edge(ast_graph, node, content, helper)?;
159 if !is_ffi {
160 if let Some((caller_id, callee_id, argument_count, span)) =
162 build_constructor_edge_with_helper(ast_graph, node, content, helper)?
163 {
164 helper.add_call_edge_full_with_span(
165 caller_id,
166 callee_id,
167 argument_count,
168 false,
169 span.into_iter().collect(),
170 );
171 }
172 }
173 }
174 "import_statement" => {
175 if let Some((from_id, to_id)) =
176 build_import_edge_with_helper(node, content, file, helper)?
177 {
178 helper.add_import_edge(from_id, to_id);
179 }
180 }
181 "export_statement" => {
182 build_export_edges_with_helper(node, content, file, helper);
183 }
184 "expression_statement" => {
185 build_commonjs_export_edges(node, content, helper);
187 }
188 "class_declaration" | "class" => {
189 build_inherits_edge_with_helper(node, content, helper);
190 }
191 "identifier" => {
192 local_scopes::handle_identifier_for_reference(node, content, scope_tree, helper);
193 }
194 _ => {}
195 }
196
197 let children: Vec<_> = node.children(cursor).collect();
200 for child in children {
201 let mut child_cursor = child.walk();
202 extract_edges_recursive(
203 child,
204 &mut child_cursor,
205 content,
206 file,
207 ast_graph,
208 helper,
209 scope_tree,
210 )?;
211 }
212
213 Ok(())
214}
215
216fn build_call_edge_with_helper(
218 ast_graph: &ASTGraph,
219 call_node: Node<'_>,
220 content: &[u8],
221 helper: &mut GraphBuildHelper,
222) -> GraphResult<Option<CallEdgeData>> {
223 let module_context;
225 let call_context = if let Some(ctx) = ast_graph.get_callable_context(call_node.id()) {
226 ctx
227 } else {
228 module_context = CallContext {
230 name: Arc::from("<module>"),
231 qualified_name: "<module>".to_string(),
232 span: (0, content.len()),
233 is_async: false,
234 };
235 &module_context
236 };
237
238 let Some(callee_expr) = call_node.child_by_field_name("function") else {
239 return Ok(None);
240 };
241
242 let raw_callee_text = callee_expr
243 .utf8_text(content)
244 .map_err(|_| GraphBuilderError::ParseError {
245 span: span_from_node(call_node),
246 reason: "failed to read call expression".to_string(),
247 })?
248 .trim()
249 .to_string();
250
251 let callee_text = if raw_callee_text.contains("?.") {
253 normalize_optional_chain(&raw_callee_text)
254 } else {
255 raw_callee_text
256 };
257
258 if callee_text.is_empty() {
259 return Ok(None);
260 }
261
262 let callee_simple = simple_name(&callee_text);
263 if callee_simple.is_empty() {
264 return Ok(None);
265 }
266
267 let caller_qname = call_context.qualified_name();
269 let target_qname = if let Some(method_name) = callee_text.strip_prefix("this.") {
270 if let Some(scope_idx) = caller_qname.rfind('.') {
272 let class_name = &caller_qname[..scope_idx];
273 format!("{}.{}", class_name, simple_name(method_name))
274 } else {
275 callee_text.clone()
276 }
277 } else if callee_text.starts_with("super.") || callee_text.contains('.') {
278 callee_text.clone()
279 } else {
280 callee_simple.to_string()
281 };
282
283 let source_id = ensure_caller_node(helper, call_context);
285 let target_id = helper.ensure_function(&target_qname, None, false, false);
286
287 let span = Some(span_from_node(call_node));
288 let argument_count = u8::try_from(count_arguments(call_node)).unwrap_or(u8::MAX);
289 let is_async = check_uses_await(call_node);
290
291 Ok(Some((source_id, target_id, argument_count, is_async, span)))
292}
293
294#[derive(Debug, Clone)]
295struct HttpRequestInfo {
296 method: HttpMethod,
297 url: Option<String>,
298}
299
300fn build_http_request_edge(
301 ast_graph: &ASTGraph,
302 call_node: Node<'_>,
303 content: &[u8],
304 helper: &mut GraphBuildHelper,
305) -> bool {
306 let Some(info) = extract_http_request_info(call_node, content) else {
307 return false;
308 };
309
310 let caller_id = get_caller_node_id(ast_graph, call_node, content, helper);
311 let target_name = info.url.as_ref().map_or_else(
312 || format!("http::{}", info.method.as_str()),
313 |url| format!("http::{url}"),
314 );
315 let target_id = helper.add_module(&target_name, Some(span_from_node(call_node)));
316
317 helper.add_http_request_edge(caller_id, target_id, info.method, info.url.as_deref());
318 true
319}
320
321fn detect_route_endpoint(
335 call_node: Node<'_>,
336 content: &[u8],
337 helper: &mut GraphBuildHelper,
338) -> bool {
339 let Some(callee) = call_node.child_by_field_name("function") else {
341 return false;
342 };
343
344 if callee.kind() != "member_expression" {
345 return false;
346 }
347
348 let Some(property) = callee.child_by_field_name("property") else {
350 return false;
351 };
352
353 let Ok(method_name) = property.utf8_text(content) else {
354 return false;
355 };
356 let method_name = method_name.trim();
357
358 let method_str = match method_name {
360 "get" => "GET",
361 "post" => "POST",
362 "put" => "PUT",
363 "delete" => "DELETE",
364 "patch" => "PATCH",
365 "all" => "ALL",
366 _ => return false,
367 };
368
369 let Some(args) = call_node.child_by_field_name("arguments") else {
371 return false;
372 };
373
374 let mut cursor = args.walk();
375 let first_arg = args
376 .children(&mut cursor)
377 .find(|child| !matches!(child.kind(), "(" | ")" | ","));
378
379 let Some(first_arg) = first_arg else {
380 return false;
381 };
382
383 let Some(path) = extract_string_literal(&first_arg, content) else {
385 return false;
386 };
387
388 let qualified_name = format!("route::{method_str}::{path}");
390
391 let endpoint_id = helper.add_endpoint(&qualified_name, Some(span_from_node(call_node)));
393
394 let mut handler_cursor = args.walk();
397 let handler_arg = args
398 .children(&mut handler_cursor)
399 .filter(|child| !matches!(child.kind(), "(" | ")" | ","))
400 .nth(1);
401
402 if let Some(handler_node) = handler_arg
403 && let Ok(handler_text) = handler_node.utf8_text(content)
404 {
405 let handler_name = handler_text.trim();
406 if !handler_name.is_empty()
407 && matches!(handler_node.kind(), "identifier" | "member_expression")
408 {
409 let handler_id = helper.ensure_function(handler_name, None, false, false);
410 helper.add_contains_edge(endpoint_id, handler_id);
411 }
412 }
413
414 true
415}
416
417fn extract_http_request_info(call_node: Node<'_>, content: &[u8]) -> Option<HttpRequestInfo> {
418 let callee = call_node.child_by_field_name("function")?;
419 let callee_text = callee.utf8_text(content).ok()?.trim().to_string();
420
421 if callee_text == "fetch" {
422 return Some(extract_fetch_http_info(call_node, content));
423 }
424
425 if callee_text == "axios" {
426 return extract_axios_http_info(call_node, content);
427 }
428
429 if let Some(method_name) = callee_text.strip_prefix("axios.") {
430 let method = http_method_from_name(method_name)?;
431 let url = extract_first_arg_url(call_node, content);
432 return Some(HttpRequestInfo { method, url });
433 }
434
435 None
436}
437
438fn extract_fetch_http_info(call_node: Node<'_>, content: &[u8]) -> HttpRequestInfo {
439 let url = extract_first_arg_url(call_node, content);
440 let method = extract_method_from_options(call_node, content).unwrap_or(HttpMethod::Get);
441 HttpRequestInfo { method, url }
442}
443
444fn extract_axios_http_info(call_node: Node<'_>, content: &[u8]) -> Option<HttpRequestInfo> {
445 let args = call_node.child_by_field_name("arguments")?;
446 let mut cursor = args.walk();
447 let mut non_trivia = args
448 .children(&mut cursor)
449 .filter(|child| !matches!(child.kind(), "(" | ")" | ","));
450
451 let first_arg = non_trivia.next()?;
452 let second_arg = non_trivia.next();
453
454 if first_arg.kind() == "object" {
455 let (method, url) = extract_method_and_url_from_object(first_arg, content);
456 return Some(HttpRequestInfo {
457 method: method.unwrap_or(HttpMethod::Get),
458 url,
459 });
460 }
461
462 let url = extract_string_literal(&first_arg, content);
463 let method = if let Some(config) = second_arg {
464 if config.kind() == "object" {
465 extract_method_from_object(config, content)
466 } else {
467 None
468 }
469 } else {
470 None
471 };
472
473 Some(HttpRequestInfo {
474 method: method.unwrap_or(HttpMethod::Get),
475 url,
476 })
477}
478
479fn extract_first_arg_url(call_node: Node<'_>, content: &[u8]) -> Option<String> {
480 let args = call_node.child_by_field_name("arguments")?;
481 let mut cursor = args.walk();
482 let first_arg = args
483 .children(&mut cursor)
484 .find(|child| !matches!(child.kind(), "(" | ")" | ","))?;
485 extract_string_literal(&first_arg, content)
486}
487
488fn extract_method_from_options(call_node: Node<'_>, content: &[u8]) -> Option<HttpMethod> {
489 let args = call_node.child_by_field_name("arguments")?;
490 let mut cursor = args.walk();
491 let mut non_trivia = args
492 .children(&mut cursor)
493 .filter(|child| !matches!(child.kind(), "(" | ")" | ","));
494
495 let _first_arg = non_trivia.next()?;
496 let second_arg = non_trivia.next()?;
497 if second_arg.kind() != "object" {
498 return None;
499 }
500
501 extract_method_from_object(second_arg, content)
502}
503
504fn extract_method_from_object(obj_node: Node<'_>, content: &[u8]) -> Option<HttpMethod> {
505 let (method, _url) = extract_method_and_url_from_object(obj_node, content);
506 method
507}
508
509fn extract_method_and_url_from_object(
510 obj_node: Node<'_>,
511 content: &[u8],
512) -> (Option<HttpMethod>, Option<String>) {
513 let mut method = None;
514 let mut url = None;
515 let mut cursor = obj_node.walk();
516
517 for child in obj_node.children(&mut cursor) {
518 if child.kind() != "pair" {
519 continue;
520 }
521
522 let Some(key_node) = child.child_by_field_name("key") else {
523 continue;
524 };
525 let key_text = extract_object_key_text(&key_node, content);
526
527 let Some(value_node) = child.child_by_field_name("value") else {
528 continue;
529 };
530
531 if key_text.as_deref() == Some("method") {
532 if let Some(value) = extract_string_literal(&value_node, content) {
533 method = http_method_from_name(&value);
534 }
535 } else if key_text.as_deref() == Some("url") {
536 url = extract_string_literal(&value_node, content);
537 }
538 }
539
540 (method, url)
541}
542
543fn extract_object_key_text(node: &Node<'_>, content: &[u8]) -> Option<String> {
544 let raw = node.utf8_text(content).ok()?.trim().to_string();
545 if let Some(value) = extract_string_literal(node, content) {
546 return Some(value);
547 }
548 if raw.is_empty() {
549 return None;
550 }
551 Some(raw)
552}
553
554fn http_method_from_name(name: &str) -> Option<HttpMethod> {
555 match name.trim().to_ascii_lowercase().as_str() {
556 "get" => Some(HttpMethod::Get),
557 "post" => Some(HttpMethod::Post),
558 "put" => Some(HttpMethod::Put),
559 "delete" => Some(HttpMethod::Delete),
560 "patch" => Some(HttpMethod::Patch),
561 "head" => Some(HttpMethod::Head),
562 "options" => Some(HttpMethod::Options),
563 _ => None,
564 }
565}
566
567fn build_constructor_edge_with_helper(
569 ast_graph: &ASTGraph,
570 new_node: Node<'_>,
571 content: &[u8],
572 helper: &mut GraphBuildHelper,
573) -> GraphResult<Option<ConstructorEdgeData>> {
574 let module_context;
576 let call_context = if let Some(ctx) = ast_graph.get_callable_context(new_node.id()) {
577 ctx
578 } else {
579 module_context = CallContext {
580 name: Arc::from("<module>"),
581 qualified_name: "<module>".to_string(),
582 span: (0, content.len()),
583 is_async: false,
584 };
585 &module_context
586 };
587
588 let Some(constructor_expr) = new_node.child_by_field_name("constructor") else {
589 return Ok(None);
590 };
591
592 let constructor_text = constructor_expr
593 .utf8_text(content)
594 .map_err(|_| GraphBuilderError::ParseError {
595 span: span_from_node(new_node),
596 reason: "failed to read constructor expression".to_string(),
597 })?
598 .trim()
599 .to_string();
600
601 if constructor_text.is_empty() {
602 return Ok(None);
603 }
604
605 let constructor_simple = simple_name(&constructor_text);
606 let source_id = ensure_caller_node(helper, call_context);
607 let target_id = helper.ensure_function(constructor_simple, None, false, false);
608
609 let span = Some(span_from_node(new_node));
610 let argument_count = u8::try_from(count_arguments(new_node)).unwrap_or(u8::MAX);
611
612 Ok(Some((source_id, target_id, argument_count, span)))
613}
614
615fn build_import_edge_with_helper(
617 import_node: Node<'_>,
618 content: &[u8],
619 file: &Arc<str>,
620 helper: &mut GraphBuildHelper,
621) -> GraphResult<
622 Option<(
623 sqry_core::graph::unified::NodeId,
624 sqry_core::graph::unified::NodeId,
625 )>,
626> {
627 let Some(source_node) = import_node.child_by_field_name("source") else {
628 return Ok(None);
629 };
630
631 let source_text = source_node
632 .utf8_text(content)
633 .map_err(|_| GraphBuilderError::ParseError {
634 span: span_from_node(import_node),
635 reason: "failed to read import source".to_string(),
636 })?
637 .trim()
638 .trim_matches(|c| c == '"' || c == '\'')
639 .to_string();
640
641 if source_text.is_empty() {
642 return Ok(None);
643 }
644
645 let resolved_path =
647 sqry_core::graph::resolve_import_path(std::path::Path::new(file.as_ref()), &source_text)?;
648
649 let from_id = helper.add_module("<module>", None);
651 let to_id = helper.add_import(&resolved_path, Some(span_from_node(import_node)));
652
653 Ok(Some((from_id, to_id)))
654}
655
656#[allow(clippy::too_many_lines)]
667fn build_export_edges_with_helper(
668 export_node: Node<'_>,
669 content: &[u8],
670 file: &Arc<str>,
671 helper: &mut GraphBuildHelper,
672) {
673 let module_id = helper.add_module("<module>", None);
675
676 let source_node = export_node.child_by_field_name("source");
678 let is_reexport = source_node.is_some();
679
680 let has_default = export_node
682 .children(&mut export_node.walk())
683 .any(|child| child.kind() == "default");
684
685 let namespace_export = export_node
687 .children(&mut export_node.walk())
688 .find(|child| child.kind() == "namespace_export");
689
690 let has_wildcard = export_node
692 .children(&mut export_node.walk())
693 .any(|child| child.kind() == "*");
694
695 let export_clause = export_node
697 .children(&mut export_node.walk())
698 .find(|child| child.kind() == "export_clause");
699
700 let declaration = export_node.children(&mut export_node.walk()).find(|child| {
702 matches!(
703 child.kind(),
704 "function_declaration"
705 | "class_declaration"
706 | "lexical_declaration"
707 | "variable_declaration"
708 | "generator_function_declaration"
709 )
710 });
711
712 if has_default {
713 let exported_name = if let Some(ref decl) = declaration {
716 decl.child_by_field_name("name")
718 .and_then(|n| n.utf8_text(content).ok())
719 .map_or_else(|| "default".to_string(), |s| s.trim().to_string())
720 } else {
721 export_node
723 .children(&mut export_node.walk())
724 .find(|child| child.kind() == "identifier")
725 .and_then(|n| n.utf8_text(content).ok())
726 .map_or_else(|| "default".to_string(), |s| s.trim().to_string())
727 };
728
729 let exported_id = helper.add_function(&exported_name, None, false, false);
730 helper.add_export_edge_full(module_id, exported_id, ExportKind::Default, None);
731 } else if let Some(ns_export) = namespace_export {
732 let alias = ns_export
735 .children(&mut ns_export.walk())
736 .find(|child| child.kind() == "identifier")
737 .and_then(|n| n.utf8_text(content).ok())
738 .map(|s| s.trim().to_string());
739
740 let source_path = source_node
742 .and_then(|s| s.utf8_text(content).ok())
743 .map_or_else(
744 || "<unknown>".to_string(),
745 |s| s.trim().trim_matches(|c| c == '"' || c == '\'').to_string(),
746 );
747
748 let resolved_path = sqry_core::graph::resolve_import_path(
749 std::path::Path::new(file.as_ref()),
750 &source_path,
751 )
752 .unwrap_or(source_path);
753
754 let source_module_id = helper.add_module(&resolved_path, None);
755 helper.add_export_edge_full(
756 module_id,
757 source_module_id,
758 ExportKind::Namespace,
759 alias.as_deref(),
760 );
761 } else if has_wildcard && is_reexport {
762 let source_path = source_node
764 .and_then(|s| s.utf8_text(content).ok())
765 .map_or_else(
766 || "<unknown>".to_string(),
767 |s| s.trim().trim_matches(|c| c == '"' || c == '\'').to_string(),
768 );
769
770 let resolved_path = sqry_core::graph::resolve_import_path(
771 std::path::Path::new(file.as_ref()),
772 &source_path,
773 )
774 .unwrap_or(source_path);
775
776 let source_module_id = helper.add_module(&resolved_path, None);
777 helper.add_export_edge_full(module_id, source_module_id, ExportKind::Reexport, None);
779 } else if let Some(clause) = export_clause {
780 let mut cursor = clause.walk();
782 for child in clause.children(&mut cursor) {
783 if child.kind() == "export_specifier" {
784 let identifiers: Vec<_> = child
787 .children(&mut child.walk())
788 .filter(|n| n.kind() == "identifier")
789 .collect();
790
791 if let Some(first_ident) = identifiers.first() {
792 let local_name = first_ident
793 .utf8_text(content)
794 .ok()
795 .map(|s| s.trim().to_string())
796 .unwrap_or_default();
797
798 if local_name.is_empty() {
799 continue;
800 }
801
802 let alias = identifiers.get(1).and_then(|n| {
804 n.utf8_text(content)
805 .ok()
806 .map(|s| s.trim().to_string())
807 .filter(|s| !s.is_empty())
808 });
809
810 let exported_id = helper.add_function(&local_name, None, false, false);
811
812 let kind = if is_reexport {
813 ExportKind::Reexport
814 } else {
815 ExportKind::Direct
816 };
817
818 helper.add_export_edge_full(module_id, exported_id, kind, alias.as_deref());
819 }
820 }
821 }
822 } else if let Some(decl) = declaration {
823 match decl.kind() {
825 "function_declaration" | "generator_function_declaration" => {
826 if let Some(name_node) = decl.child_by_field_name("name")
827 && let Ok(name) = name_node.utf8_text(content)
828 {
829 let name = name.trim().to_string();
830 if !name.is_empty() {
831 let exported_id = helper.add_function(&name, None, false, false);
832 helper.add_export_edge_full(
833 module_id,
834 exported_id,
835 ExportKind::Direct,
836 None,
837 );
838 }
839 }
840 }
841 "class_declaration" => {
842 if let Some(name_node) = decl.child_by_field_name("name")
843 && let Ok(name) = name_node.utf8_text(content)
844 {
845 let name = name.trim().to_string();
846 if !name.is_empty() {
847 let exported_id = helper.add_class(&name, None);
848 helper.add_export_edge_full(
849 module_id,
850 exported_id,
851 ExportKind::Direct,
852 None,
853 );
854 }
855 }
856 }
857 "lexical_declaration" | "variable_declaration" => {
858 let mut cursor = decl.walk();
860 for child in decl.children(&mut cursor) {
861 if child.kind() == "variable_declarator"
862 && let Some(name_node) = child.child_by_field_name("name")
863 && let Ok(name) = name_node.utf8_text(content)
864 {
865 let name = name.trim().to_string();
866 if !name.is_empty() {
867 let exported_id = helper.add_variable(&name, None);
868 helper.add_export_edge_full(
869 module_id,
870 exported_id,
871 ExportKind::Direct,
872 None,
873 );
874 }
875 }
876 }
877 }
878 _ => {}
879 }
880 }
881}
882
883fn build_commonjs_export_edges(
891 expr_stmt_node: Node<'_>,
892 content: &[u8],
893 helper: &mut GraphBuildHelper,
894) {
895 let Some(assignment) = expr_stmt_node
897 .children(&mut expr_stmt_node.walk())
898 .find(|child| child.kind() == "assignment_expression")
899 else {
900 return;
901 };
902
903 let Some(left) = assignment.child_by_field_name("left") else {
904 return;
905 };
906 let Some(right) = assignment.child_by_field_name("right") else {
907 return;
908 };
909
910 let left_text = left.utf8_text(content).ok().map(|s| s.trim().to_string());
911 let Some(left_text) = left_text else {
912 return;
913 };
914
915 let module_id = helper.add_module("<module>", None);
916
917 if left_text == "module.exports" {
919 if right.kind() == "object" {
920 let mut cursor = right.walk();
922 for child in right.children(&mut cursor) {
923 if child.kind() == "shorthand_property_identifier" {
924 if let Ok(name) = child.utf8_text(content) {
926 let name = name.trim();
927 if !name.is_empty() {
928 let exported_id = helper.add_function(name, None, false, false);
929 helper.add_export_edge_full(
930 module_id,
931 exported_id,
932 ExportKind::Direct,
933 None,
934 );
935 }
936 }
937 } else if child.kind() == "pair" {
938 if let Some(key_node) = child.child_by_field_name("key")
940 && let Ok(export_name) = key_node.utf8_text(content)
941 {
942 let export_name = export_name.trim();
943 if !export_name.is_empty() {
944 let exported_id = helper.add_function(export_name, None, false, false);
945 helper.add_export_edge_full(
946 module_id,
947 exported_id,
948 ExportKind::Direct,
949 None,
950 );
951 }
952 }
953 } else if child.kind() == "spread_element" {
954 }
956 }
957 } else if right.kind() == "identifier" || right.kind() == "member_expression" {
958 let export_name = right
960 .utf8_text(content)
961 .ok()
962 .map_or_else(|| "default".to_string(), |s| s.trim().to_string());
963
964 if !export_name.is_empty() {
965 let exported_id = helper.add_function(&export_name, None, false, false);
966 helper.add_export_edge_full(module_id, exported_id, ExportKind::Default, None);
967 }
968 } else if matches!(
969 right.kind(),
970 "function_expression"
971 | "arrow_function"
972 | "class"
973 | "call_expression"
974 | "new_expression"
975 ) {
976 let exported_id = helper.add_function("default", None, false, false);
978 helper.add_export_edge_full(module_id, exported_id, ExportKind::Default, None);
979 }
980 }
981 else if left_text.starts_with("exports.") || left_text.starts_with("module.exports.") {
983 let export_name = if let Some(name) = left_text.strip_prefix("module.exports.") {
985 name
986 } else if let Some(name) = left_text.strip_prefix("exports.") {
987 name
988 } else {
989 return;
990 };
991
992 if !export_name.is_empty() {
993 let exported_id = helper.add_function(export_name, None, false, false);
994 helper.add_export_edge_full(module_id, exported_id, ExportKind::Direct, None);
995 }
996 }
997}
998
999fn build_inherits_edge_with_helper(
1006 class_node: Node<'_>,
1007 content: &[u8],
1008 helper: &mut GraphBuildHelper,
1009) {
1010 let heritage = class_node
1012 .children(&mut class_node.walk())
1013 .find(|child| child.kind() == "class_heritage");
1014
1015 let Some(heritage_node) = heritage else {
1016 return; };
1018
1019 let class_name = if class_node.kind() == "class_declaration" {
1021 class_node
1022 .child_by_field_name("name")
1023 .and_then(|n| n.utf8_text(content).ok())
1024 .map(|s| s.trim().to_string())
1025 } else {
1026 class_node
1028 .parent()
1029 .filter(|p| p.kind() == "variable_declarator")
1030 .and_then(|p| p.child_by_field_name("name"))
1031 .and_then(|n| n.utf8_text(content).ok())
1032 .map(|s| s.trim().to_string())
1033 .or_else(|| {
1034 Some(SyntheticNameBuilder::from_node_with_hash(
1036 &class_node,
1037 content,
1038 "class",
1039 ))
1040 })
1041 };
1042
1043 let parent_name = extract_parent_class_name(heritage_node, content);
1046
1047 if let (Some(child_name), Some(parent_name)) = (class_name, parent_name)
1049 && !child_name.is_empty()
1050 && !parent_name.is_empty()
1051 {
1052 let child_id = helper.add_class(&child_name, None);
1053 let parent_id = helper.add_class(&parent_name, None);
1054 helper.add_inherits_edge(child_id, parent_id);
1055 }
1056}
1057
1058fn extract_parent_class_name(heritage_node: Node<'_>, content: &[u8]) -> Option<String> {
1073 let mut cursor = heritage_node.walk();
1074 for child in heritage_node.children(&mut cursor) {
1075 match child.kind() {
1076 "identifier" => {
1077 return child.utf8_text(content).ok().map(|s| s.trim().to_string());
1079 }
1080 "member_expression" => {
1081 return child.utf8_text(content).ok().map(|s| s.trim().to_string());
1084 }
1085 "call_expression" => {
1086 return child.utf8_text(content).ok().map(|s| s.trim().to_string());
1091 }
1092 _ => {}
1093 }
1094 }
1095 None
1096}
1097
1098fn simple_name(name: &str) -> &str {
1099 name.rsplit(['.', '/']).next().unwrap_or(name)
1102}
1103
1104fn normalize_optional_chain(text: &str) -> String {
1108 text.replace("?.", ".")
1109 .trim()
1110 .trim_end_matches('.')
1111 .to_string()
1112}
1113
1114fn check_uses_await(call_node: Node<'_>) -> bool {
1115 let mut current = call_node;
1117 for _ in 0..2 {
1118 if let Some(parent) = current.parent() {
1120 if parent.kind() == "await_expression" {
1121 return true;
1122 }
1123 current = parent;
1124 } else {
1125 break;
1126 }
1127 }
1128 false
1129}
1130
1131fn count_arguments(node: Node<'_>) -> usize {
1132 node.child_by_field_name("arguments").map_or(0, |args| {
1133 let mut count = 0;
1134 let mut cursor = args.walk();
1135 for child in args.children(&mut cursor) {
1136 if !matches!(child.kind(), "(" | ")" | ",") {
1137 count += 1;
1138 }
1139 }
1140 count
1141 })
1142}
1143
1144fn span_from_node(node: Node<'_>) -> Span {
1145 let start = node.start_position();
1146 let end = node.end_position();
1147 Span::new(
1148 Position::new(start.row, start.column),
1149 Position::new(end.row, end.column),
1150 )
1151}
1152
1153fn extract_string_literal(node: &Node, content: &[u8]) -> Option<String> {
1154 let text = node.utf8_text(content).ok()?;
1155 let trimmed = text.trim();
1156
1157 trimmed
1159 .strip_prefix('"')
1160 .and_then(|s| s.strip_suffix('"'))
1161 .or_else(|| {
1162 trimmed
1163 .strip_prefix('\'')
1164 .and_then(|s| s.strip_suffix('\''))
1165 })
1166 .or_else(|| trimmed.strip_prefix('`').and_then(|s| s.strip_suffix('`')))
1167 .map(std::string::ToString::to_string)
1168}
1169
1170#[derive(Debug, Clone)]
1173pub struct CallContext {
1174 #[allow(dead_code)] pub name: Arc<str>,
1176 pub qualified_name: String,
1177 pub span: (usize, usize),
1178 pub is_async: bool,
1179}
1180
1181impl CallContext {
1182 pub fn qualified_name(&self) -> &str {
1183 &self.qualified_name
1184 }
1185}
1186
1187pub struct ASTGraph {
1188 callable_map: HashMap<usize, usize>,
1190 context_map: HashMap<usize, CallContext>,
1192}
1193
1194impl ASTGraph {
1195 pub fn from_tree(tree: &Tree, content: &[u8], max_scope_depth: usize) -> Result<Self, String> {
1197 let mut builder = ASTGraphBuilder::new(content, max_scope_depth);
1198
1199 let recursion_limits = sqry_core::config::RecursionLimits::load_or_default()
1201 .map_err(|e| format!("Failed to load recursion limits: {e}"))?;
1202 let file_ops_depth = recursion_limits
1203 .effective_file_ops_depth()
1204 .map_err(|e| format!("Invalid file_ops_depth configuration: {e}"))?;
1205 let mut guard = sqry_core::query::security::RecursionGuard::new(file_ops_depth)
1206 .map_err(|e| format!("Failed to create recursion guard: {e}"))?;
1207
1208 builder
1209 .visit(tree.root_node(), None, &mut guard)
1210 .map_err(|e| format!("JavaScript AST traversal hit recursion limit: {e}"))?;
1211 Ok(builder.build())
1212 }
1213
1214 pub fn get_callable_context(&self, node_id: usize) -> Option<&CallContext> {
1216 let callable_id = self.callable_map.get(&node_id)?;
1217 self.context_map.get(callable_id)
1218 }
1219
1220 pub fn contexts(&self) -> impl Iterator<Item = &CallContext> {
1222 self.context_map.values()
1223 }
1224}
1225
1226struct ASTGraphBuilder<'a> {
1227 content: &'a [u8],
1228 max_scope_depth: usize,
1229 callable_map: HashMap<usize, usize>,
1230 context_map: HashMap<usize, CallContext>,
1231 #[allow(dead_code)] current_callable: Option<usize>,
1233 current_scope: Vec<Arc<str>>,
1234}
1235
1236impl<'a> ASTGraphBuilder<'a> {
1237 fn new(content: &'a [u8], max_scope_depth: usize) -> Self {
1238 Self {
1239 content,
1240 max_scope_depth,
1241 callable_map: HashMap::new(),
1242 context_map: HashMap::new(),
1243 current_callable: None,
1244 current_scope: Vec::new(),
1245 }
1246 }
1247
1248 fn build(self) -> ASTGraph {
1249 ASTGraph {
1250 callable_map: self.callable_map,
1251 context_map: self.context_map,
1252 }
1253 }
1254
1255 fn visit(
1259 &mut self,
1260 node: Node<'_>,
1261 parent_callable: Option<usize>,
1262 guard: &mut sqry_core::query::security::RecursionGuard,
1263 ) -> Result<(), sqry_core::query::security::RecursionError> {
1264 guard.enter()?;
1265
1266 let node_id = node.id();
1267
1268 let callable_name = callable_node_name(node, self.content);
1270
1271 let new_callable = if let Some(name) = callable_name {
1272 let start = node.start_byte();
1274 let end = node.end_byte();
1275 let is_async = is_async_function(node, self.content);
1276
1277 let qualified_name = if self.current_scope.is_empty() {
1278 name.to_string()
1279 } else if self.current_scope.len() <= self.max_scope_depth {
1280 format!("{}.{}", self.current_scope.join("."), name)
1281 } else {
1282 let truncated = &self.current_scope[..self.max_scope_depth];
1284 format!("{}.{}", truncated.join("."), name)
1285 };
1286
1287 let context = CallContext {
1288 name: Arc::from(name),
1289 qualified_name,
1290 span: (start, end),
1291 is_async,
1292 };
1293
1294 self.context_map.insert(node_id, context);
1295 Some(node_id)
1296 } else {
1297 None
1298 };
1299
1300 let effective_callable = new_callable.or(parent_callable);
1302
1303 if let Some(callable_id) = effective_callable {
1305 self.callable_map.insert(node_id, callable_id);
1306 }
1307
1308 let scope_name = scope_node_name(node, self.content);
1310 let pushed_scope = if let Some(name) = scope_name {
1311 self.current_scope.push(Arc::from(name));
1312 true
1313 } else {
1314 false
1315 };
1316
1317 let mut cursor = node.walk();
1319 for child in node.children(&mut cursor) {
1320 self.visit(child, effective_callable, guard)?;
1321 }
1322
1323 if pushed_scope {
1325 self.current_scope.pop();
1326 }
1327
1328 guard.exit();
1329 Ok(())
1330 }
1331}
1332
1333fn callable_node_name(node: Node<'_>, content: &[u8]) -> Option<String> {
1335 match node.kind() {
1336 "function_declaration" | "generator_function_declaration" => node
1337 .child_by_field_name("name")
1338 .and_then(|child| child.utf8_text(content).ok().map(|s| s.trim().to_string())),
1339 "function_expression" | "generator_function" => {
1340 node.child_by_field_name("name")
1342 .and_then(|child| child.utf8_text(content).ok().map(|s| s.trim().to_string()))
1343 .or_else(|| {
1344 Some(SyntheticNameBuilder::from_node_with_hash(
1345 &node, content, "function",
1346 ))
1347 })
1348 }
1349 "arrow_function" => {
1350 if let Some(parent) = node.parent()
1354 && parent.kind() == "variable_declarator"
1355 && let Some(name_node) = parent.child_by_field_name("name")
1356 && let Ok(name) = name_node.utf8_text(content)
1357 {
1358 let trimmed = name.trim();
1359 if !trimmed.is_empty() {
1360 return Some(trimmed.to_string());
1361 }
1362 }
1363 Some(SyntheticNameBuilder::from_node_with_hash(
1366 &node, content, "arrow",
1367 ))
1368 }
1369 "method_definition" => node
1370 .child_by_field_name("name")
1371 .and_then(|child| child.utf8_text(content).ok().map(|s| s.trim().to_string())),
1372 _ => None,
1373 }
1374}
1375
1376fn scope_node_name(node: Node<'_>, content: &[u8]) -> Option<String> {
1377 match node.kind() {
1378 "class_declaration" | "class" => node
1379 .child_by_field_name("name")
1380 .and_then(|child| child.utf8_text(content).ok().map(|s| s.trim().to_string()))
1381 .or_else(|| {
1382 Some(SyntheticNameBuilder::from_node_with_hash(
1383 &node, content, "class",
1384 ))
1385 }),
1386 _ => None,
1387 }
1388}
1389
1390fn is_async_function(node: Node<'_>, _content: &[u8]) -> bool {
1391 let mut cursor = node.walk();
1393 node.children(&mut cursor)
1394 .any(|child| child.kind() == "async")
1395}
1396
1397fn process_jsdoc_annotations(
1402 node: Node,
1403 content: &[u8],
1404 helper: &mut GraphBuildHelper,
1405) -> GraphResult<()> {
1406 match node.kind() {
1408 "function_declaration" | "generator_function_declaration" => {
1409 process_function_jsdoc(node, content, helper)?;
1410 }
1411 "method_definition" => {
1412 process_method_jsdoc(node, content, helper)?;
1413 }
1414 "lexical_declaration" | "variable_declaration" => {
1415 process_variable_jsdoc(node, content, helper)?;
1416 }
1417 "class_declaration" | "class" => {
1418 process_class_fields_jsdoc(node, content, helper)?;
1419 }
1420 _ => {}
1421 }
1422
1423 let mut cursor = node.walk();
1425 for child in node.children(&mut cursor) {
1426 process_jsdoc_annotations(child, content, helper)?;
1427 }
1428
1429 Ok(())
1430}
1431
1432fn process_function_jsdoc(
1434 func_node: Node,
1435 content: &[u8],
1436 helper: &mut GraphBuildHelper,
1437) -> GraphResult<()> {
1438 let Some(jsdoc_text) = extract_jsdoc_comment(func_node, content) else {
1440 return Ok(());
1441 };
1442
1443 let tags = parse_jsdoc_tags(&jsdoc_text);
1445
1446 let Some(name_node) = func_node.child_by_field_name("name") else {
1448 return Ok(());
1449 };
1450
1451 let function_name = name_node
1452 .utf8_text(content)
1453 .map_err(|_| GraphBuilderError::ParseError {
1454 span: span_from_node(func_node),
1455 reason: "failed to read function name".to_string(),
1456 })?
1457 .trim()
1458 .to_string();
1459
1460 if function_name.is_empty() {
1461 return Ok(());
1462 }
1463
1464 let func_node_id = helper.ensure_function(&function_name, None, false, false);
1466
1467 let ast_params = extract_ast_parameters(func_node, content);
1470 let ast_param_map: HashMap<&str, usize> = ast_params
1471 .iter()
1472 .map(|(idx, name)| (name.as_str(), *idx))
1473 .collect();
1474
1475 for param_tag in &tags.params {
1477 let mut normalized_name = param_tag
1480 .name
1481 .trim_start_matches("...")
1482 .trim_matches(|c| c == '[' || c == ']');
1483
1484 if let Some(base_name) = normalized_name.split('.').next() {
1487 normalized_name = base_name;
1488 }
1489
1490 let Some(&ast_index) = ast_param_map.get(normalized_name) else {
1491 continue;
1493 };
1494
1495 let canonical_type = canonical_type_string(¶m_tag.type_str);
1497 let type_node_id = helper.add_type(&canonical_type, None);
1498 helper.add_typeof_edge_with_context(
1499 func_node_id,
1500 type_node_id,
1501 Some(TypeOfContext::Parameter),
1502 ast_index.try_into().ok(), Some(¶m_tag.name),
1504 );
1505
1506 let type_names = extract_type_names(¶m_tag.type_str);
1508 for type_name in type_names {
1509 let ref_type_id = helper.add_type(&type_name, None);
1510 helper.add_reference_edge(func_node_id, ref_type_id);
1511 }
1512 }
1513
1514 if let Some(return_type) = &tags.returns {
1516 let canonical_type = canonical_type_string(return_type);
1517 let type_node_id = helper.add_type(&canonical_type, None);
1518 helper.add_typeof_edge_with_context(
1519 func_node_id,
1520 type_node_id,
1521 Some(TypeOfContext::Return),
1522 Some(0),
1523 None,
1524 );
1525
1526 let type_names = extract_type_names(return_type);
1528 for type_name in type_names {
1529 let ref_type_id = helper.add_type(&type_name, None);
1530 helper.add_reference_edge(func_node_id, ref_type_id);
1531 }
1532 }
1533
1534 Ok(())
1535}
1536
1537fn process_method_jsdoc(
1539 method_node: Node,
1540 content: &[u8],
1541 helper: &mut GraphBuildHelper,
1542) -> GraphResult<()> {
1543 let Some(jsdoc_text) = extract_jsdoc_comment(method_node, content) else {
1545 return Ok(());
1546 };
1547
1548 let tags = parse_jsdoc_tags(&jsdoc_text);
1550
1551 let Some(name_node) = method_node.child_by_field_name("name") else {
1553 return Ok(());
1554 };
1555
1556 let method_name = name_node
1557 .utf8_text(content)
1558 .map_err(|_| GraphBuilderError::ParseError {
1559 span: span_from_node(method_node),
1560 reason: "failed to read method name".to_string(),
1561 })?
1562 .trim()
1563 .to_string();
1564
1565 if method_name.is_empty() {
1566 return Ok(());
1567 }
1568
1569 let class_name = get_enclosing_class_name(method_node, content)?;
1571 let Some(class_name) = class_name else {
1572 return Ok(());
1573 };
1574
1575 let qualified_name = format!("{class_name}.{method_name}");
1577
1578 let method_node_id = helper.ensure_method(&qualified_name, None, false, false);
1581
1582 let ast_params = extract_ast_parameters(method_node, content);
1585 let ast_param_map: HashMap<&str, usize> = ast_params
1586 .iter()
1587 .map(|(idx, name)| (name.as_str(), *idx))
1588 .collect();
1589
1590 for param_tag in &tags.params {
1592 let mut normalized_name = param_tag
1595 .name
1596 .trim_start_matches("...")
1597 .trim_matches(|c| c == '[' || c == ']');
1598
1599 if let Some(base_name) = normalized_name.split('.').next() {
1602 normalized_name = base_name;
1603 }
1604
1605 let Some(&ast_index) = ast_param_map.get(normalized_name) else {
1606 continue;
1608 };
1609
1610 let canonical_type = canonical_type_string(¶m_tag.type_str);
1611 let type_node_id = helper.add_type(&canonical_type, None);
1612 helper.add_typeof_edge_with_context(
1613 method_node_id,
1614 type_node_id,
1615 Some(TypeOfContext::Parameter),
1616 ast_index.try_into().ok(), Some(¶m_tag.name),
1618 );
1619
1620 let type_names = extract_type_names(¶m_tag.type_str);
1622 for type_name in type_names {
1623 let ref_type_id = helper.add_type(&type_name, None);
1624 helper.add_reference_edge(method_node_id, ref_type_id);
1625 }
1626 }
1627
1628 if let Some(return_type) = &tags.returns {
1630 let canonical_type = canonical_type_string(return_type);
1631 let type_node_id = helper.add_type(&canonical_type, None);
1632 helper.add_typeof_edge_with_context(
1633 method_node_id,
1634 type_node_id,
1635 Some(TypeOfContext::Return),
1636 Some(0),
1637 None,
1638 );
1639
1640 let type_names = extract_type_names(return_type);
1642 for type_name in type_names {
1643 let ref_type_id = helper.add_type(&type_name, None);
1644 helper.add_reference_edge(method_node_id, ref_type_id);
1645 }
1646 }
1647
1648 Ok(())
1649}
1650
1651fn process_variable_jsdoc(
1653 decl_node: Node,
1654 content: &[u8],
1655 helper: &mut GraphBuildHelper,
1656) -> GraphResult<()> {
1657 if !is_top_level_variable(decl_node) {
1659 return Ok(());
1660 }
1661
1662 let Some(jsdoc_text) = extract_jsdoc_comment(decl_node, content) else {
1664 return Ok(());
1665 };
1666
1667 let tags = parse_jsdoc_tags(&jsdoc_text);
1669
1670 let Some(type_annotation) = &tags.type_annotation else {
1672 return Ok(());
1673 };
1674
1675 let mut cursor = decl_node.walk();
1677 for child in decl_node.children(&mut cursor) {
1678 if child.kind() == "variable_declarator"
1679 && let Some(name_node) = child.child_by_field_name("name")
1680 {
1681 let var_name = name_node
1682 .utf8_text(content)
1683 .map_err(|_| GraphBuilderError::ParseError {
1684 span: span_from_node(child),
1685 reason: "failed to read variable name".to_string(),
1686 })?
1687 .trim()
1688 .to_string();
1689
1690 if !var_name.is_empty() {
1691 let var_node_id = helper.add_variable(&var_name, None);
1693
1694 let canonical_type = canonical_type_string(type_annotation);
1696 let type_node_id = helper.add_type(&canonical_type, None);
1697 helper.add_typeof_edge_with_context(
1698 var_node_id,
1699 type_node_id,
1700 Some(TypeOfContext::Variable),
1701 None,
1702 None,
1703 );
1704
1705 let type_names = extract_type_names(type_annotation);
1707 for type_name in type_names {
1708 let ref_type_id = helper.add_type(&type_name, None);
1709 helper.add_reference_edge(var_node_id, ref_type_id);
1710 }
1711 }
1712 }
1713 }
1714
1715 Ok(())
1716}
1717
1718fn process_class_fields_jsdoc(
1720 class_node: Node,
1721 content: &[u8],
1722 helper: &mut GraphBuildHelper,
1723) -> GraphResult<()> {
1724 let class_name = if let Some(name_node) = class_node.child_by_field_name("name") {
1726 name_node
1728 .utf8_text(content)
1729 .map_err(|_| GraphBuilderError::ParseError {
1730 span: span_from_node(class_node),
1731 reason: "failed to read class name".to_string(),
1732 })?
1733 .trim()
1734 .to_string()
1735 } else {
1736 if let Some(parent) = class_node.parent() {
1739 if parent.kind() == "variable_declarator" {
1740 if let Some(name_node) = parent.child_by_field_name("name")
1742 && let Ok(var_name) = name_node.utf8_text(content)
1743 {
1744 let var_name = var_name.trim().to_string();
1745 if var_name.is_empty() {
1746 return Ok(());
1747 }
1748 var_name
1749 } else {
1750 return Ok(());
1751 }
1752 } else if parent.kind() == "assignment_expression" {
1753 if let Some(left) = parent.child_by_field_name("left")
1755 && let Ok(assign_name) = left.utf8_text(content)
1756 {
1757 let assign_name = assign_name.trim().to_string();
1758 if assign_name.is_empty() {
1759 return Ok(());
1760 }
1761 assign_name
1762 } else {
1763 return Ok(());
1764 }
1765 } else {
1766 return Ok(());
1768 }
1769 } else {
1770 return Ok(());
1771 }
1772 };
1773
1774 if class_name.is_empty() {
1775 return Ok(());
1776 }
1777
1778 let Some(body_node) = class_node.child_by_field_name("body") else {
1780 return Ok(());
1781 };
1782
1783 let mut cursor = body_node.walk();
1785 for child in body_node.children(&mut cursor) {
1786 if child.kind() == "field_definition" {
1787 if let Some(jsdoc_text) = extract_jsdoc_comment(child, content) {
1789 let tags = parse_jsdoc_tags(&jsdoc_text);
1790
1791 if let Some(type_annotation) = &tags.type_annotation {
1793 if let Some(name_node) = child.child_by_field_name("property") {
1795 let field_name = name_node
1796 .utf8_text(content)
1797 .map_err(|_| GraphBuilderError::ParseError {
1798 span: span_from_node(child),
1799 reason: "failed to read field name".to_string(),
1800 })?
1801 .trim()
1802 .to_string();
1803
1804 if !field_name.is_empty() {
1805 let qualified_name = format!("{class_name}.{field_name}");
1807
1808 let field_node_id = helper.add_variable(&qualified_name, None);
1810
1811 let canonical_type = canonical_type_string(type_annotation);
1813 let type_node_id = helper.add_type(&canonical_type, None);
1814 helper.add_typeof_edge_with_context(
1815 field_node_id,
1816 type_node_id,
1817 Some(TypeOfContext::Field),
1818 None,
1819 None,
1820 );
1821
1822 let type_names = extract_type_names(type_annotation);
1824 for type_name in type_names {
1825 let ref_type_id = helper.add_type(&type_name, None);
1826 helper.add_reference_edge(field_node_id, ref_type_id);
1827 }
1828 }
1829 }
1830 }
1831 }
1832 }
1833 }
1834
1835 Ok(())
1836}
1837
1838fn get_enclosing_class_name(method_node: Node, content: &[u8]) -> GraphResult<Option<String>> {
1842 let mut current = method_node;
1844 while let Some(parent) = current.parent() {
1845 if parent.kind() == "class_declaration" {
1846 if let Some(name_node) = parent.child_by_field_name("name") {
1848 let class_name = name_node
1849 .utf8_text(content)
1850 .map_err(|_| GraphBuilderError::ParseError {
1851 span: span_from_node(parent),
1852 reason: "failed to read class name".to_string(),
1853 })?
1854 .trim()
1855 .to_string();
1856
1857 if !class_name.is_empty() {
1858 return Ok(Some(class_name));
1859 }
1860 }
1861 } else if parent.kind() == "class" {
1862 if let Some(grandparent) = parent.parent() {
1865 if grandparent.kind() == "variable_declarator" {
1866 if let Some(name_node) = grandparent.child_by_field_name("name")
1868 && let Ok(var_name) = name_node.utf8_text(content)
1869 {
1870 let var_name = var_name.trim().to_string();
1871 if !var_name.is_empty() {
1872 return Ok(Some(var_name));
1873 }
1874 }
1875 } else if grandparent.kind() == "assignment_expression" {
1876 if let Some(left) = grandparent.child_by_field_name("left")
1878 && let Ok(assign_name) = left.utf8_text(content)
1879 {
1880 let assign_name = assign_name.trim().to_string();
1881 if !assign_name.is_empty() {
1882 return Ok(Some(assign_name));
1883 }
1884 }
1885 }
1886 }
1887 return Ok(None);
1890 }
1891 current = parent;
1892 }
1893 Ok(None)
1894}
1895
1896fn extract_ast_parameters(func_node: Node, content: &[u8]) -> Vec<(usize, String)> {
1899 let Some(params_node) = func_node.child_by_field_name("parameters") else {
1900 return Vec::new();
1901 };
1902
1903 let mut cursor = params_node.walk();
1904 params_node
1905 .named_children(&mut cursor)
1906 .enumerate()
1907 .filter_map(|(ast_index, param)| {
1908 let param_name = match param.kind() {
1910 "identifier" => param
1911 .utf8_text(content)
1912 .ok()
1913 .map(std::string::ToString::to_string),
1914 "required_parameter" | "optional_parameter" => {
1915 param
1917 .child_by_field_name("pattern")
1918 .and_then(|p| p.utf8_text(content).ok())
1919 .map(std::string::ToString::to_string)
1920 }
1921 "rest_pattern" => {
1922 param
1925 .named_child(0)
1926 .and_then(|n| n.utf8_text(content).ok())
1927 .map(|s| s.trim_start_matches("...").to_string())
1928 }
1929 "assignment_pattern" => {
1930 param
1933 .child_by_field_name("left")
1934 .filter(|left| left.kind() == "identifier")
1935 .and_then(|left| left.utf8_text(content).ok())
1936 .map(std::string::ToString::to_string)
1937 }
1938 _ => None,
1939 };
1940
1941 param_name.map(|name| (ast_index, name))
1942 })
1943 .collect()
1944}
1945
1946fn is_top_level_variable(decl_node: Node) -> bool {
1949 let mut current = decl_node;
1950 while let Some(parent) = current.parent() {
1951 match parent.kind() {
1952 "function_declaration"
1954 | "generator_function_declaration"
1955 | "function_expression"
1956 | "arrow_function"
1957 | "method_definition" => return false,
1958
1959 "statement_block" | "if_statement" | "for_statement" | "for_in_statement"
1961 | "for_of_statement" | "while_statement" | "do_statement" | "try_statement"
1962 | "catch_clause" | "finally_clause" | "switch_statement" | "switch_case"
1963 | "switch_default" | "class_body" | "class_static_block" | "with_statement" => {
1964 return false;
1965 }
1966
1967 "program" | "export_statement" => return true,
1970
1971 _ => {}
1972 }
1973 current = parent;
1974 }
1975 true
1976}
1977
1978fn build_ffi_call_edge(
1990 ast_graph: &ASTGraph,
1991 call_node: Node<'_>,
1992 content: &[u8],
1993 helper: &mut GraphBuildHelper,
1994) -> GraphResult<bool> {
1995 let Some(callee_expr) = call_node.child_by_field_name("function") else {
1996 return Ok(false);
1997 };
1998
1999 let callee_text = callee_expr
2000 .utf8_text(content)
2001 .map_err(|_| GraphBuilderError::ParseError {
2002 span: span_from_node(call_node),
2003 reason: "failed to read call expression".to_string(),
2004 })?
2005 .trim();
2006
2007 if callee_text.starts_with("WebAssembly.") {
2009 return Ok(build_webassembly_call_edge(
2010 ast_graph,
2011 call_node,
2012 content,
2013 callee_text,
2014 helper,
2015 ));
2016 }
2017
2018 if callee_text == "require" {
2020 return Ok(build_require_ffi_edge(
2021 ast_graph, call_node, content, helper,
2022 ));
2023 }
2024
2025 if callee_text == "process.dlopen" {
2027 return Ok(build_dlopen_edge(ast_graph, call_node, content, helper));
2028 }
2029
2030 Ok(false)
2031}
2032
2033fn build_ffi_new_edge(
2041 ast_graph: &ASTGraph,
2042 new_node: Node<'_>,
2043 content: &[u8],
2044 helper: &mut GraphBuildHelper,
2045) -> GraphResult<bool> {
2046 let Some(constructor_expr) = new_node.child_by_field_name("constructor") else {
2047 return Ok(false);
2048 };
2049
2050 let constructor_text = constructor_expr
2051 .utf8_text(content)
2052 .map_err(|_| GraphBuilderError::ParseError {
2053 span: span_from_node(new_node),
2054 reason: "failed to read constructor expression".to_string(),
2055 })?
2056 .trim();
2057
2058 if constructor_text == "WebAssembly.Module" || constructor_text == "WebAssembly.Instance" {
2060 return Ok(build_webassembly_constructor_edge(
2061 ast_graph,
2062 new_node,
2063 content,
2064 constructor_text,
2065 helper,
2066 ));
2067 }
2068
2069 Ok(false)
2070}
2071
2072fn build_webassembly_call_edge(
2074 ast_graph: &ASTGraph,
2075 call_node: Node<'_>,
2076 content: &[u8],
2077 callee_text: &str,
2078 helper: &mut GraphBuildHelper,
2079) -> bool {
2080 let method_name = callee_text
2082 .strip_prefix("WebAssembly.")
2083 .unwrap_or(callee_text);
2084
2085 let is_wasm_load = matches!(
2087 method_name,
2088 "instantiate" | "instantiateStreaming" | "compile" | "compileStreaming" | "validate"
2089 );
2090
2091 if !is_wasm_load {
2092 return false;
2093 }
2094
2095 let caller_id = get_caller_node_id(ast_graph, call_node, content, helper);
2097
2098 let wasm_module_name = extract_wasm_module_name(call_node, content)
2100 .unwrap_or_else(|| format!("wasm::{method_name}"));
2101
2102 let wasm_node_id = helper.add_module(&wasm_module_name, Some(span_from_node(call_node)));
2104
2105 helper.add_webassembly_edge(caller_id, wasm_node_id);
2107
2108 true
2109}
2110
2111fn build_webassembly_constructor_edge(
2113 ast_graph: &ASTGraph,
2114 new_node: Node<'_>,
2115 content: &[u8],
2116 constructor_text: &str,
2117 helper: &mut GraphBuildHelper,
2118) -> bool {
2119 let caller_id = get_caller_node_id(ast_graph, new_node, content, helper);
2121
2122 let type_name = constructor_text
2124 .strip_prefix("WebAssembly.")
2125 .unwrap_or(constructor_text);
2126 let wasm_module_name = format!("wasm::{type_name}");
2127
2128 let wasm_node_id = helper.add_module(&wasm_module_name, Some(span_from_node(new_node)));
2130
2131 helper.add_webassembly_edge(caller_id, wasm_node_id);
2133
2134 true
2135}
2136
2137fn build_require_ffi_edge(
2142 ast_graph: &ASTGraph,
2143 call_node: Node<'_>,
2144 content: &[u8],
2145 helper: &mut GraphBuildHelper,
2146) -> bool {
2147 let Some(args) = call_node.child_by_field_name("arguments") else {
2149 return false;
2150 };
2151
2152 let mut cursor = args.walk();
2153 let first_arg = args
2154 .children(&mut cursor)
2155 .find(|child| !matches!(child.kind(), "(" | ")" | ","));
2156
2157 let Some(arg_node) = first_arg else {
2158 return false;
2159 };
2160
2161 let module_path = extract_string_literal(&arg_node, content);
2163 let Some(path) = module_path else {
2164 return false;
2165 };
2166
2167 let from_id = helper.add_module("<module>", None);
2169
2170 let resolved_path = if path.starts_with('.') {
2172 sqry_core::graph::resolve_import_path(std::path::Path::new(helper.file_path()), &path)
2174 .unwrap_or_else(|_| simple_name(&path).to_string())
2175 } else {
2176 simple_name(&path).to_string()
2178 };
2179
2180 let to_id = helper.add_import(&resolved_path, Some(span_from_node(call_node)));
2181 helper.add_import_edge(from_id, to_id);
2182
2183 let is_native_addon = std::path::Path::new(&path)
2185 .extension()
2186 .is_some_and(|ext| ext.eq_ignore_ascii_case("node"))
2187 || is_known_native_addon(&path);
2188
2189 if is_native_addon {
2190 let caller_id = get_caller_node_id(ast_graph, call_node, content, helper);
2192
2193 let ffi_name = format!("native::{}", simple_name(&path));
2195 let ffi_node_id = helper.add_module(&ffi_name, Some(span_from_node(call_node)));
2196
2197 helper.add_ffi_edge(caller_id, ffi_node_id, FfiConvention::C);
2199 }
2200
2201 true
2202}
2203
2204fn build_dlopen_edge(
2206 ast_graph: &ASTGraph,
2207 call_node: Node<'_>,
2208 content: &[u8],
2209 helper: &mut GraphBuildHelper,
2210) -> bool {
2211 let caller_id = get_caller_node_id(ast_graph, call_node, content, helper);
2213
2214 let module_name = call_node
2216 .child_by_field_name("arguments")
2217 .and_then(|args| {
2218 let mut cursor = args.walk();
2219 args.children(&mut cursor)
2220 .filter(|child| !matches!(child.kind(), "(" | ")" | ","))
2221 .nth(1) })
2223 .and_then(|node| extract_string_literal(&node, content))
2224 .map_or_else(
2225 || "native::dlopen".to_string(),
2226 |path| format!("native::{}", simple_name(&path)),
2227 );
2228
2229 let ffi_node_id = helper.add_module(&module_name, Some(span_from_node(call_node)));
2231
2232 helper.add_ffi_edge(caller_id, ffi_node_id, FfiConvention::C);
2234
2235 true
2236}
2237
2238fn get_caller_node_id(
2240 ast_graph: &ASTGraph,
2241 node: Node<'_>,
2242 content: &[u8],
2243 helper: &mut GraphBuildHelper,
2244) -> sqry_core::graph::unified::NodeId {
2245 let module_context;
2246 let call_context = if let Some(ctx) = ast_graph.get_callable_context(node.id()) {
2247 ctx
2248 } else {
2249 module_context = CallContext {
2250 name: Arc::from("<module>"),
2251 qualified_name: "<module>".to_string(),
2252 span: (0, content.len()),
2253 is_async: false,
2254 };
2255 &module_context
2256 };
2257
2258 ensure_caller_node(helper, call_context)
2259}
2260
2261fn ensure_caller_node(
2262 helper: &mut GraphBuildHelper,
2263 call_context: &CallContext,
2264) -> sqry_core::graph::unified::NodeId {
2265 let caller_span = Some(Span::from_bytes(call_context.span.0, call_context.span.1));
2266 let qualified_name = call_context.qualified_name();
2267 if qualified_name.contains('.') {
2268 helper.ensure_method(qualified_name, caller_span, call_context.is_async, false)
2269 } else {
2270 helper.ensure_function(qualified_name, caller_span, call_context.is_async, false)
2271 }
2272}
2273
2274fn extract_wasm_module_name(call_node: Node<'_>, content: &[u8]) -> Option<String> {
2280 let args = call_node.child_by_field_name("arguments")?;
2281
2282 let mut cursor = args.walk();
2283 let first_arg = args
2284 .children(&mut cursor)
2285 .find(|child| !matches!(child.kind(), "(" | ")" | ","))?;
2286
2287 if first_arg.kind() == "call_expression"
2289 && let Some(func) = first_arg.child_by_field_name("function")
2290 {
2291 let func_text = func.utf8_text(content).ok()?.trim();
2292 if func_text == "fetch" {
2293 if let Some(fetch_args) = first_arg.child_by_field_name("arguments") {
2295 let mut fetch_cursor = fetch_args.walk();
2296 let url_arg = fetch_args
2297 .children(&mut fetch_cursor)
2298 .find(|child| !matches!(child.kind(), "(" | ")" | ","))?;
2299
2300 if let Some(url) = extract_string_literal(&url_arg, content) {
2301 return Some(format!("wasm::{}", simple_name(&url)));
2302 }
2303 }
2304 }
2305 }
2306
2307 if let Some(path) = extract_string_literal(&first_arg, content) {
2309 return Some(format!("wasm::{}", simple_name(&path)));
2310 }
2311
2312 None
2313}
2314
2315fn is_known_native_addon(package_name: &str) -> bool {
2317 const NATIVE_PACKAGES: &[&str] = &[
2319 "better-sqlite3",
2320 "sqlite3",
2321 "bcrypt",
2322 "sharp",
2323 "canvas",
2324 "node-sass",
2325 "leveldown",
2326 "bufferutil",
2327 "utf-8-validate",
2328 "fsevents",
2329 "cpu-features",
2330 "node-gyp",
2331 "node-pre-gyp",
2332 "prebuild",
2333 "nan",
2334 "node-addon-api",
2335 "ref-napi",
2336 "ffi-napi",
2337 ];
2338
2339 NATIVE_PACKAGES
2340 .iter()
2341 .any(|&pkg| package_name.contains(pkg))
2342}