1use std::{
2 collections::HashMap,
3 path::Path,
4 sync::{Arc, OnceLock},
5};
6
7use sqry_core::graph::unified::build::helper::CalleeKindHint;
8use sqry_core::graph::unified::build::shape::{CfBucket, ShapeMapping};
9use sqry_core::graph::unified::edge::kind::TypeOfContext;
10use sqry_core::graph::unified::edge::{ExportKind, FfiConvention, HttpMethod};
11use sqry_core::graph::unified::storage::shape::SignatureShape;
12use sqry_core::graph::unified::{GraphBuildHelper, NodeId, NodeKind, StagingGraph};
13use sqry_core::graph::{GraphBuilder, GraphBuilderError, GraphResult, Language, Position, Span};
14use sqry_core::relations::SyntheticNameBuilder;
15use tree_sitter::{Node, Tree};
16
17use super::jsdoc_parser::{extract_jsdoc_comment, parse_jsdoc_tags};
18use super::local_scopes;
19use super::type_extractor::{canonical_type_string, extract_type_names};
20
21const DEFAULT_SCOPE_DEPTH: usize = 4;
22type CallEdgeData = (NodeId, NodeId, u8, bool, Option<Span>);
23type ConstructorEdgeData = (NodeId, NodeId, u8, Option<Span>);
24
25#[derive(Debug, Clone, Copy)]
27pub struct JavaScriptGraphBuilder {
28 max_scope_depth: usize,
29}
30
31impl Default for JavaScriptGraphBuilder {
32 fn default() -> Self {
33 Self {
34 max_scope_depth: DEFAULT_SCOPE_DEPTH,
35 }
36 }
37}
38
39impl JavaScriptGraphBuilder {
40 #[must_use]
41 pub fn new(max_scope_depth: usize) -> Self {
42 Self { max_scope_depth }
43 }
44}
45
46fn infer_visibility(qualified_name: &str) -> &'static str {
49 let name_part = qualified_name.rsplit('.').next().unwrap_or(qualified_name);
51 if name_part.starts_with('_') {
52 "private"
53 } else {
54 "public"
55 }
56}
57
58impl GraphBuilder for JavaScriptGraphBuilder {
59 fn build_graph(
60 &self,
61 tree: &Tree,
62 content: &[u8],
63 file: &Path,
64 staging: &mut StagingGraph,
65 ) -> GraphResult<()> {
66 let mut helper = GraphBuildHelper::new(staging, file, Language::JavaScript);
68 let file_arc = Arc::from(file.to_string_lossy().to_string());
69
70 let ast_graph = ASTGraph::from_tree(tree, content, self.max_scope_depth).map_err(|e| {
72 GraphBuilderError::ParseError {
73 span: Span::default(),
74 reason: e,
75 }
76 })?;
77
78 for context in ast_graph.contexts() {
80 let span = Some(context.decl_span);
81 let visibility = infer_visibility(&context.qualified_name);
83
84 if context.qualified_name.contains('.') {
86 helper.add_method_with_visibility(
87 &context.qualified_name,
88 span,
89 context.is_async,
90 false, Some(visibility),
92 );
93 } else {
94 helper.add_function_with_visibility(
95 &context.qualified_name,
96 span,
97 context.is_async,
98 false, Some(visibility),
100 );
101 }
102 }
103
104 let mut scope_tree = local_scopes::build(tree.root_node(), content)?;
106
107 let mut cursor = tree.root_node().walk();
109 extract_edges_recursive(
110 tree.root_node(),
111 &mut cursor,
112 content,
113 &file_arc,
114 &ast_graph,
115 &mut helper,
116 &mut scope_tree,
117 )?;
118
119 process_jsdoc_annotations(tree.root_node(), content, &mut helper)?;
121
122 Ok(())
123 }
124
125 fn language(&self) -> Language {
126 Language::JavaScript
127 }
128
129 fn shape_mapping(&self) -> Option<&dyn ShapeMapping> {
130 Some(javascript_shape_mapping())
131 }
132}
133
134pub struct JavaScriptShapeMapping {
141 cf_by_kind_id: Vec<Option<CfBucket>>,
142}
143
144impl JavaScriptShapeMapping {
145 fn build() -> Self {
147 let lang: tree_sitter::Language = tree_sitter_javascript::LANGUAGE.into();
148 let count = lang.node_kind_count();
149 let mut cf_by_kind_id = vec![None; count];
150 for (id, slot) in cf_by_kind_id.iter_mut().enumerate() {
151 let Ok(kind_id) = u16::try_from(id) else {
152 break;
153 };
154 if !lang.node_kind_is_named(kind_id) {
155 continue;
156 }
157 if let Some(name) = lang.node_kind_for_id(kind_id) {
158 *slot = cf_bucket_for_javascript_kind(name);
159 }
160 }
161 Self { cf_by_kind_id }
162 }
163}
164
165impl ShapeMapping for JavaScriptShapeMapping {
166 fn cf_bucket(&self, ts_node_kind_id: u16) -> Option<CfBucket> {
167 self.cf_by_kind_id
168 .get(ts_node_kind_id as usize)
169 .copied()
170 .flatten()
171 }
172
173 fn signature_shape(&self, fn_node: Node, _src: &[u8]) -> SignatureShape {
174 let mut shape = SignatureShape::default();
175 if let Some(params) = fn_node.child_by_field_name("parameters") {
176 let mut cursor = params.walk();
177 for child in params.named_children(&mut cursor) {
178 match child.kind() {
179 "identifier" | "object_pattern" | "array_pattern" => {
181 shape.arity_positional = shape.arity_positional.saturating_add(1);
182 }
183 "assignment_pattern" => {
185 shape.arity_positional = shape.arity_positional.saturating_add(1);
186 shape.has_defaults = true;
187 }
188 "rest_pattern" => shape.has_varargs = true,
190 _ => {}
191 }
192 }
193 }
194 shape
196 }
197}
198
199fn cf_bucket_for_javascript_kind(name: &str) -> Option<CfBucket> {
202 let bucket = match name {
203 "if_statement" | "ternary_expression" => CfBucket::Branch,
204 "for_statement" | "for_in_statement" | "while_statement" | "do_statement" => CfBucket::Loop,
205 "switch_statement" | "switch_case" | "switch_default" => CfBucket::Match,
206 "try_statement" => CfBucket::Try,
207 "catch_clause" => CfBucket::Catch,
208 "throw_statement" => CfBucket::Throw,
209 "return_statement" => CfBucket::Return,
210 "yield_expression" => CfBucket::Yield,
211 "await_expression" => CfBucket::Await,
212 "break_statement" | "continue_statement" => CfBucket::BreakContinue,
213 "call_expression" | "new_expression" => CfBucket::Call,
214 "lexical_declaration"
215 | "variable_declaration"
216 | "assignment_expression"
217 | "augmented_assignment_expression" => CfBucket::Assign,
218 "arrow_function" | "function_expression" => CfBucket::Closure,
219 _ => return None,
220 };
221 Some(bucket)
222}
223
224#[must_use]
226pub fn javascript_shape_mapping() -> &'static JavaScriptShapeMapping {
227 static MAPPING: OnceLock<JavaScriptShapeMapping> = OnceLock::new();
228 MAPPING.get_or_init(JavaScriptShapeMapping::build)
229}
230
231fn extract_edges_recursive<'a>(
233 node: Node<'a>,
234 cursor: &mut tree_sitter::TreeCursor<'a>,
235 content: &[u8],
236 file: &Arc<str>,
237 ast_graph: &ASTGraph,
238 helper: &mut GraphBuildHelper,
239 scope_tree: &mut local_scopes::JavaScriptScopeTree,
240) -> GraphResult<()> {
241 match node.kind() {
242 "call_expression" => {
243 let _ = build_http_request_edge(ast_graph, node, content, helper);
245 let _ = detect_route_endpoint(node, content, helper);
247 let is_ffi = build_ffi_call_edge(ast_graph, node, content, helper)?;
249 if !is_ffi {
250 if let Some((caller_id, callee_id, argument_count, is_async, span)) =
252 build_call_edge_with_helper(ast_graph, node, content, helper)?
253 {
254 helper.add_call_edge_full_with_span(
255 caller_id,
256 callee_id,
257 argument_count,
258 is_async,
259 span.into_iter().collect(),
260 );
261 }
262 }
263 }
264 "new_expression" => {
265 let is_ffi = build_ffi_new_edge(ast_graph, node, content, helper)?;
267 if !is_ffi {
268 if let Some((caller_id, callee_id, argument_count, span)) =
270 build_constructor_edge_with_helper(ast_graph, node, content, helper)?
271 {
272 helper.add_call_edge_full_with_span(
273 caller_id,
274 callee_id,
275 argument_count,
276 false,
277 span.into_iter().collect(),
278 );
279 }
280 }
281 }
282 "import_statement" => {
283 if let Some((from_id, to_id)) =
284 build_import_edge_with_helper(node, content, file, helper)?
285 {
286 helper.add_import_edge(from_id, to_id);
287 }
288 }
289 "export_statement" => {
290 build_export_edges_with_helper(node, content, file, helper);
291 }
292 "expression_statement" => {
293 build_commonjs_export_edges(node, content, helper);
295 }
296 "class_declaration" | "class" => {
297 build_inherits_edge_with_helper(node, content, helper);
298 }
299 "identifier" => {
300 local_scopes::handle_identifier_for_reference(node, content, scope_tree, helper);
301 }
302 _ => {}
303 }
304
305 let children: Vec<_> = node.children(cursor).collect();
308 for child in children {
309 let mut child_cursor = child.walk();
310 extract_edges_recursive(
311 child,
312 &mut child_cursor,
313 content,
314 file,
315 ast_graph,
316 helper,
317 scope_tree,
318 )?;
319 }
320
321 Ok(())
322}
323
324fn build_call_edge_with_helper(
326 ast_graph: &ASTGraph,
327 call_node: Node<'_>,
328 content: &[u8],
329 helper: &mut GraphBuildHelper,
330) -> GraphResult<Option<CallEdgeData>> {
331 let module_context;
333 let call_context = if let Some(ctx) = ast_graph.get_callable_context(call_node.id()) {
334 ctx
335 } else {
336 module_context = CallContext {
338 qualified_name: "<module>".to_string(),
339 decl_span: Span::default(),
348 is_async: false,
349 };
350 &module_context
351 };
352
353 let Some(callee_expr) = call_node.child_by_field_name("function") else {
354 return Ok(None);
355 };
356
357 let raw_callee_text = callee_expr
358 .utf8_text(content)
359 .map_err(|_| GraphBuilderError::ParseError {
360 span: span_from_node(call_node),
361 reason: "failed to read call expression".to_string(),
362 })?
363 .trim()
364 .to_string();
365
366 let callee_text = if raw_callee_text.contains("?.") {
368 normalize_optional_chain(&raw_callee_text)
369 } else {
370 raw_callee_text
371 };
372
373 if callee_text.is_empty() {
374 return Ok(None);
375 }
376
377 let callee_simple = simple_name(&callee_text);
378 if callee_simple.is_empty() {
379 return Ok(None);
380 }
381
382 let caller_qname = call_context.qualified_name();
384 let target_qname = if let Some(method_name) = callee_text.strip_prefix("this.") {
385 if let Some(scope_idx) = caller_qname.rfind('.') {
387 let class_name = &caller_qname[..scope_idx];
388 format!("{}.{}", class_name, simple_name(method_name))
389 } else {
390 callee_text.clone()
391 }
392 } else if callee_text.starts_with("super.") || callee_text.contains('.') {
393 callee_text.clone()
394 } else {
395 callee_simple.to_string()
396 };
397
398 let source_id = ensure_caller_node(helper, call_context);
400 let call_site_span = span_from_node(call_node);
401 let target_id = helper.ensure_callee(&target_qname, call_site_span, CalleeKindHint::Function);
402
403 let span = Some(call_site_span);
404 let argument_count = u8::try_from(count_arguments(call_node)).unwrap_or(u8::MAX);
405 let is_async = check_uses_await(call_node);
406
407 Ok(Some((source_id, target_id, argument_count, is_async, span)))
408}
409
410#[derive(Debug, Clone)]
411struct HttpRequestInfo {
412 method: HttpMethod,
413 url: Option<String>,
414}
415
416fn build_http_request_edge(
417 ast_graph: &ASTGraph,
418 call_node: Node<'_>,
419 content: &[u8],
420 helper: &mut GraphBuildHelper,
421) -> bool {
422 let Some(info) = extract_http_request_info(call_node, content) else {
423 return false;
424 };
425
426 let caller_id = get_caller_node_id(ast_graph, call_node, helper);
427 let target_name = info.url.as_ref().map_or_else(
428 || format!("http::{}", info.method.as_str()),
429 |url| format!("http::{url}"),
430 );
431 let target_id =
432 helper.add_call_site_node(&target_name, span_from_node(call_node), NodeKind::Module);
433
434 helper.add_http_request_edge(caller_id, target_id, info.method, info.url.as_deref());
435 true
436}
437
438fn detect_route_endpoint(
452 call_node: Node<'_>,
453 content: &[u8],
454 helper: &mut GraphBuildHelper,
455) -> bool {
456 let Some(callee) = call_node.child_by_field_name("function") else {
458 return false;
459 };
460
461 if callee.kind() != "member_expression" {
462 return false;
463 }
464
465 let Some(property) = callee.child_by_field_name("property") else {
467 return false;
468 };
469
470 let Ok(method_name) = property.utf8_text(content) else {
471 return false;
472 };
473 let method_name = method_name.trim();
474
475 let method_str = match method_name {
477 "get" => "GET",
478 "post" => "POST",
479 "put" => "PUT",
480 "delete" => "DELETE",
481 "patch" => "PATCH",
482 "all" => "ALL",
483 _ => return false,
484 };
485
486 let Some(args) = call_node.child_by_field_name("arguments") else {
488 return false;
489 };
490
491 let mut cursor = args.walk();
492 let first_arg = args
493 .children(&mut cursor)
494 .find(|child| !matches!(child.kind(), "(" | ")" | ","));
495
496 let Some(first_arg) = first_arg else {
497 return false;
498 };
499
500 let Some(path) = extract_string_literal(&first_arg, content) else {
502 return false;
503 };
504
505 let qualified_name = format!("route::{method_str}::{path}");
507
508 let endpoint_id = helper.add_endpoint(&qualified_name, Some(span_from_node(call_node)));
510
511 let mut handler_cursor = args.walk();
514 let handler_arg = args
515 .children(&mut handler_cursor)
516 .filter(|child| !matches!(child.kind(), "(" | ")" | ","))
517 .nth(1);
518
519 if let Some(handler_node) = handler_arg
520 && let Ok(handler_text) = handler_node.utf8_text(content)
521 {
522 let handler_name = handler_text.trim();
523 if !handler_name.is_empty()
524 && matches!(handler_node.kind(), "identifier" | "member_expression")
525 {
526 let handler_id = helper.ensure_callee(
527 handler_name,
528 span_from_node(handler_node),
529 CalleeKindHint::Function,
530 );
531 helper.add_contains_edge(endpoint_id, handler_id);
532 }
533 }
534
535 true
536}
537
538fn extract_http_request_info(call_node: Node<'_>, content: &[u8]) -> Option<HttpRequestInfo> {
539 let callee = call_node.child_by_field_name("function")?;
540 let callee_text = callee.utf8_text(content).ok()?.trim().to_string();
541
542 if callee_text == "fetch" {
543 return Some(extract_fetch_http_info(call_node, content));
544 }
545
546 if callee_text == "axios" {
547 return extract_axios_http_info(call_node, content);
548 }
549
550 if let Some(method_name) = callee_text.strip_prefix("axios.") {
551 let method = http_method_from_name(method_name)?;
552 let url = extract_first_arg_url(call_node, content);
553 return Some(HttpRequestInfo { method, url });
554 }
555
556 None
557}
558
559fn extract_fetch_http_info(call_node: Node<'_>, content: &[u8]) -> HttpRequestInfo {
560 let url = extract_first_arg_url(call_node, content);
561 let method = extract_method_from_options(call_node, content).unwrap_or(HttpMethod::Get);
562 HttpRequestInfo { method, url }
563}
564
565fn extract_axios_http_info(call_node: Node<'_>, content: &[u8]) -> Option<HttpRequestInfo> {
566 let args = call_node.child_by_field_name("arguments")?;
567 let mut cursor = args.walk();
568 let mut non_trivia = args
569 .children(&mut cursor)
570 .filter(|child| !matches!(child.kind(), "(" | ")" | ","));
571
572 let first_arg = non_trivia.next()?;
573 let second_arg = non_trivia.next();
574
575 if first_arg.kind() == "object" {
576 let (method, url) = extract_method_and_url_from_object(first_arg, content);
577 return Some(HttpRequestInfo {
578 method: method.unwrap_or(HttpMethod::Get),
579 url,
580 });
581 }
582
583 let url = extract_string_literal(&first_arg, content);
584 let method = if let Some(config) = second_arg {
585 if config.kind() == "object" {
586 extract_method_from_object(config, content)
587 } else {
588 None
589 }
590 } else {
591 None
592 };
593
594 Some(HttpRequestInfo {
595 method: method.unwrap_or(HttpMethod::Get),
596 url,
597 })
598}
599
600fn extract_first_arg_url(call_node: Node<'_>, content: &[u8]) -> Option<String> {
601 let args = call_node.child_by_field_name("arguments")?;
602 let mut cursor = args.walk();
603 let first_arg = args
604 .children(&mut cursor)
605 .find(|child| !matches!(child.kind(), "(" | ")" | ","))?;
606 extract_string_literal(&first_arg, content)
607}
608
609fn extract_method_from_options(call_node: Node<'_>, content: &[u8]) -> Option<HttpMethod> {
610 let args = call_node.child_by_field_name("arguments")?;
611 let mut cursor = args.walk();
612 let mut non_trivia = args
613 .children(&mut cursor)
614 .filter(|child| !matches!(child.kind(), "(" | ")" | ","));
615
616 let _first_arg = non_trivia.next()?;
617 let second_arg = non_trivia.next()?;
618 if second_arg.kind() != "object" {
619 return None;
620 }
621
622 extract_method_from_object(second_arg, content)
623}
624
625fn extract_method_from_object(obj_node: Node<'_>, content: &[u8]) -> Option<HttpMethod> {
626 let (method, _url) = extract_method_and_url_from_object(obj_node, content);
627 method
628}
629
630fn extract_method_and_url_from_object(
631 obj_node: Node<'_>,
632 content: &[u8],
633) -> (Option<HttpMethod>, Option<String>) {
634 let mut method = None;
635 let mut url = None;
636 let mut cursor = obj_node.walk();
637
638 for child in obj_node.children(&mut cursor) {
639 if child.kind() != "pair" {
640 continue;
641 }
642
643 let Some(key_node) = child.child_by_field_name("key") else {
644 continue;
645 };
646 let key_text = extract_object_key_text(&key_node, content);
647
648 let Some(value_node) = child.child_by_field_name("value") else {
649 continue;
650 };
651
652 if key_text.as_deref() == Some("method") {
653 if let Some(value) = extract_string_literal(&value_node, content) {
654 method = http_method_from_name(&value);
655 }
656 } else if key_text.as_deref() == Some("url") {
657 url = extract_string_literal(&value_node, content);
658 }
659 }
660
661 (method, url)
662}
663
664fn extract_object_key_text(node: &Node<'_>, content: &[u8]) -> Option<String> {
665 let raw = node.utf8_text(content).ok()?.trim().to_string();
666 if let Some(value) = extract_string_literal(node, content) {
667 return Some(value);
668 }
669 if raw.is_empty() {
670 return None;
671 }
672 Some(raw)
673}
674
675fn http_method_from_name(name: &str) -> Option<HttpMethod> {
676 match name.trim().to_ascii_lowercase().as_str() {
677 "get" => Some(HttpMethod::Get),
678 "post" => Some(HttpMethod::Post),
679 "put" => Some(HttpMethod::Put),
680 "delete" => Some(HttpMethod::Delete),
681 "patch" => Some(HttpMethod::Patch),
682 "head" => Some(HttpMethod::Head),
683 "options" => Some(HttpMethod::Options),
684 _ => None,
685 }
686}
687
688fn build_constructor_edge_with_helper(
690 ast_graph: &ASTGraph,
691 new_node: Node<'_>,
692 content: &[u8],
693 helper: &mut GraphBuildHelper,
694) -> GraphResult<Option<ConstructorEdgeData>> {
695 let module_context;
697 let call_context = if let Some(ctx) = ast_graph.get_callable_context(new_node.id()) {
698 ctx
699 } else {
700 module_context = CallContext {
701 qualified_name: "<module>".to_string(),
702 decl_span: Span::default(),
711 is_async: false,
712 };
713 &module_context
714 };
715
716 let Some(constructor_expr) = new_node.child_by_field_name("constructor") else {
717 return Ok(None);
718 };
719
720 let constructor_text = constructor_expr
721 .utf8_text(content)
722 .map_err(|_| GraphBuilderError::ParseError {
723 span: span_from_node(new_node),
724 reason: "failed to read constructor expression".to_string(),
725 })?
726 .trim()
727 .to_string();
728
729 if constructor_text.is_empty() {
730 return Ok(None);
731 }
732
733 let constructor_simple = simple_name(&constructor_text);
734 let source_id = ensure_caller_node(helper, call_context);
735 let new_site_span = span_from_node(new_node);
736 let target_id =
737 helper.ensure_callee(constructor_simple, new_site_span, CalleeKindHint::Function);
738
739 let span = Some(new_site_span);
740 let argument_count = u8::try_from(count_arguments(new_node)).unwrap_or(u8::MAX);
741
742 Ok(Some((source_id, target_id, argument_count, span)))
743}
744
745fn build_import_edge_with_helper(
747 import_node: Node<'_>,
748 content: &[u8],
749 file: &Arc<str>,
750 helper: &mut GraphBuildHelper,
751) -> GraphResult<
752 Option<(
753 sqry_core::graph::unified::NodeId,
754 sqry_core::graph::unified::NodeId,
755 )>,
756> {
757 let Some(source_node) = import_node.child_by_field_name("source") else {
758 return Ok(None);
759 };
760
761 let source_text = source_node
762 .utf8_text(content)
763 .map_err(|_| GraphBuilderError::ParseError {
764 span: span_from_node(import_node),
765 reason: "failed to read import source".to_string(),
766 })?
767 .trim()
768 .trim_matches(|c| c == '"' || c == '\'')
769 .to_string();
770
771 if source_text.is_empty() {
772 return Ok(None);
773 }
774
775 let resolved_path =
777 sqry_core::graph::resolve_import_path(std::path::Path::new(file.as_ref()), &source_text)?;
778
779 let from_id = helper.add_module("<module>", None);
781 let to_id = helper.add_import(&resolved_path, Some(span_from_node(import_node)));
782
783 Ok(Some((from_id, to_id)))
784}
785
786#[allow(clippy::too_many_lines)]
797fn build_export_edges_with_helper(
798 export_node: Node<'_>,
799 content: &[u8],
800 file: &Arc<str>,
801 helper: &mut GraphBuildHelper,
802) {
803 let module_id = helper.add_module("<module>", None);
805
806 let source_node = export_node.child_by_field_name("source");
808 let is_reexport = source_node.is_some();
809
810 let has_default = export_node
812 .children(&mut export_node.walk())
813 .any(|child| child.kind() == "default");
814
815 let namespace_export = export_node
817 .children(&mut export_node.walk())
818 .find(|child| child.kind() == "namespace_export");
819
820 let has_wildcard = export_node
822 .children(&mut export_node.walk())
823 .any(|child| child.kind() == "*");
824
825 let export_clause = export_node
827 .children(&mut export_node.walk())
828 .find(|child| child.kind() == "export_clause");
829
830 let declaration = export_node.children(&mut export_node.walk()).find(|child| {
832 matches!(
833 child.kind(),
834 "function_declaration"
835 | "class_declaration"
836 | "lexical_declaration"
837 | "variable_declaration"
838 | "generator_function_declaration"
839 )
840 });
841
842 if has_default {
843 let exported_name = if let Some(ref decl) = declaration {
846 decl.child_by_field_name("name")
848 .and_then(|n| n.utf8_text(content).ok())
849 .map_or_else(|| "default".to_string(), |s| s.trim().to_string())
850 } else {
851 export_node
853 .children(&mut export_node.walk())
854 .find(|child| child.kind() == "identifier")
855 .and_then(|n| n.utf8_text(content).ok())
856 .map_or_else(|| "default".to_string(), |s| s.trim().to_string())
857 };
858
859 let exported_id = helper.add_function(&exported_name, None, false, false);
860 if declaration
861 .as_ref()
862 .is_some_and(|decl| matches!(decl.kind(), "function_declaration" | "class_declaration"))
863 {
864 helper.mark_definition(exported_id);
865 }
866 helper.add_export_edge_full(module_id, exported_id, ExportKind::Default, None);
867 } else if let Some(ns_export) = namespace_export {
868 let alias = ns_export
871 .children(&mut ns_export.walk())
872 .find(|child| child.kind() == "identifier")
873 .and_then(|n| n.utf8_text(content).ok())
874 .map(|s| s.trim().to_string());
875
876 let source_path = source_node
878 .and_then(|s| s.utf8_text(content).ok())
879 .map_or_else(
880 || "<unknown>".to_string(),
881 |s| s.trim().trim_matches(|c| c == '"' || c == '\'').to_string(),
882 );
883
884 let resolved_path = sqry_core::graph::resolve_import_path(
885 std::path::Path::new(file.as_ref()),
886 &source_path,
887 )
888 .unwrap_or(source_path);
889
890 let source_module_id = helper.add_module(&resolved_path, None);
891 helper.add_export_edge_full(
892 module_id,
893 source_module_id,
894 ExportKind::Namespace,
895 alias.as_deref(),
896 );
897 } else if has_wildcard && is_reexport {
898 let source_path = source_node
900 .and_then(|s| s.utf8_text(content).ok())
901 .map_or_else(
902 || "<unknown>".to_string(),
903 |s| s.trim().trim_matches(|c| c == '"' || c == '\'').to_string(),
904 );
905
906 let resolved_path = sqry_core::graph::resolve_import_path(
907 std::path::Path::new(file.as_ref()),
908 &source_path,
909 )
910 .unwrap_or(source_path);
911
912 let source_module_id = helper.add_module(&resolved_path, None);
913 helper.add_export_edge_full(module_id, source_module_id, ExportKind::Reexport, None);
915 } else if let Some(clause) = export_clause {
916 let mut cursor = clause.walk();
918 for child in clause.children(&mut cursor) {
919 if child.kind() == "export_specifier" {
920 let identifiers: Vec<_> = child
923 .children(&mut child.walk())
924 .filter(|n| n.kind() == "identifier")
925 .collect();
926
927 if let Some(first_ident) = identifiers.first() {
928 let local_name = first_ident
929 .utf8_text(content)
930 .ok()
931 .map(|s| s.trim().to_string())
932 .unwrap_or_default();
933
934 if local_name.is_empty() {
935 continue;
936 }
937
938 let alias = identifiers.get(1).and_then(|n| {
940 n.utf8_text(content)
941 .ok()
942 .map(|s| s.trim().to_string())
943 .filter(|s| !s.is_empty())
944 });
945
946 let exported_id = helper.add_function(&local_name, None, false, false);
947
948 let kind = if is_reexport {
949 ExportKind::Reexport
950 } else {
951 ExportKind::Direct
952 };
953
954 helper.add_export_edge_full(module_id, exported_id, kind, alias.as_deref());
955 }
956 }
957 }
958 } else if let Some(decl) = declaration {
959 match decl.kind() {
961 "function_declaration" | "generator_function_declaration" => {
962 if let Some(name_node) = decl.child_by_field_name("name")
963 && let Ok(name) = name_node.utf8_text(content)
964 {
965 let name = name.trim().to_string();
966 if !name.is_empty() {
967 let exported_id = helper.add_function(&name, None, false, false);
968 helper.mark_definition(exported_id);
969 helper.add_export_edge_full(
970 module_id,
971 exported_id,
972 ExportKind::Direct,
973 None,
974 );
975 }
976 }
977 }
978 "class_declaration" => {
979 if let Some(name_node) = decl.child_by_field_name("name")
980 && let Ok(name) = name_node.utf8_text(content)
981 {
982 let name = name.trim().to_string();
983 if !name.is_empty() {
984 let exported_id = helper.add_class(&name, None);
985 helper.mark_definition(exported_id);
986 helper.add_export_edge_full(
987 module_id,
988 exported_id,
989 ExportKind::Direct,
990 None,
991 );
992 }
993 }
994 }
995 "lexical_declaration" | "variable_declaration" => {
996 let mut cursor = decl.walk();
998 for child in decl.children(&mut cursor) {
999 if child.kind() == "variable_declarator"
1000 && let Some(name_node) = child.child_by_field_name("name")
1001 && let Ok(name) = name_node.utf8_text(content)
1002 {
1003 let name = name.trim().to_string();
1004 if !name.is_empty() {
1005 let exported_id = helper.add_variable(&name, None);
1006 helper.mark_definition(exported_id);
1007 helper.add_export_edge_full(
1008 module_id,
1009 exported_id,
1010 ExportKind::Direct,
1011 None,
1012 );
1013 }
1014 }
1015 }
1016 }
1017 _ => {}
1018 }
1019 }
1020}
1021
1022fn build_commonjs_export_edges(
1030 expr_stmt_node: Node<'_>,
1031 content: &[u8],
1032 helper: &mut GraphBuildHelper,
1033) {
1034 let Some(assignment) = expr_stmt_node
1036 .children(&mut expr_stmt_node.walk())
1037 .find(|child| child.kind() == "assignment_expression")
1038 else {
1039 return;
1040 };
1041
1042 let Some(left) = assignment.child_by_field_name("left") else {
1043 return;
1044 };
1045 let Some(right) = assignment.child_by_field_name("right") else {
1046 return;
1047 };
1048
1049 let left_text = left.utf8_text(content).ok().map(|s| s.trim().to_string());
1050 let Some(left_text) = left_text else {
1051 return;
1052 };
1053
1054 let module_id = helper.add_module("<module>", None);
1055
1056 if left_text == "module.exports" {
1058 if right.kind() == "object" {
1059 let mut cursor = right.walk();
1061 for child in right.children(&mut cursor) {
1062 if child.kind() == "shorthand_property_identifier" {
1063 if let Ok(name) = child.utf8_text(content) {
1065 let name = name.trim();
1066 if !name.is_empty() {
1067 let exported_id = helper.add_function(name, None, false, false);
1068 helper.add_export_edge_full(
1069 module_id,
1070 exported_id,
1071 ExportKind::Direct,
1072 None,
1073 );
1074 }
1075 }
1076 } else if child.kind() == "pair" {
1077 if let Some(key_node) = child.child_by_field_name("key")
1079 && let Ok(export_name) = key_node.utf8_text(content)
1080 {
1081 let export_name = export_name.trim();
1082 if !export_name.is_empty() {
1083 let exported_id = helper.add_function(export_name, None, false, false);
1084 helper.add_export_edge_full(
1085 module_id,
1086 exported_id,
1087 ExportKind::Direct,
1088 None,
1089 );
1090 }
1091 }
1092 } else if child.kind() == "spread_element" {
1093 }
1095 }
1096 } else if right.kind() == "identifier" || right.kind() == "member_expression" {
1097 let export_name = right
1099 .utf8_text(content)
1100 .ok()
1101 .map_or_else(|| "default".to_string(), |s| s.trim().to_string());
1102
1103 if !export_name.is_empty() {
1104 let exported_id = helper.add_function(&export_name, None, false, false);
1105 helper.add_export_edge_full(module_id, exported_id, ExportKind::Default, None);
1106 }
1107 } else if matches!(
1108 right.kind(),
1109 "function_expression"
1110 | "arrow_function"
1111 | "class"
1112 | "call_expression"
1113 | "new_expression"
1114 ) {
1115 let exported_id = helper.add_function("default", None, false, false);
1117 helper.mark_definition(exported_id);
1118 helper.add_export_edge_full(module_id, exported_id, ExportKind::Default, None);
1119 }
1120 }
1121 else if left_text.starts_with("exports.") || left_text.starts_with("module.exports.") {
1123 let export_name = if let Some(name) = left_text.strip_prefix("module.exports.") {
1125 name
1126 } else if let Some(name) = left_text.strip_prefix("exports.") {
1127 name
1128 } else {
1129 return;
1130 };
1131
1132 if !export_name.is_empty() {
1133 let exported_id = helper.add_function(export_name, None, false, false);
1134 helper.add_export_edge_full(module_id, exported_id, ExportKind::Direct, None);
1135 }
1136 }
1137}
1138
1139fn build_inherits_edge_with_helper(
1146 class_node: Node<'_>,
1147 content: &[u8],
1148 helper: &mut GraphBuildHelper,
1149) {
1150 let heritage = class_node
1152 .children(&mut class_node.walk())
1153 .find(|child| child.kind() == "class_heritage");
1154
1155 let Some(heritage_node) = heritage else {
1156 return; };
1158
1159 let class_name = if class_node.kind() == "class_declaration" {
1161 class_node
1162 .child_by_field_name("name")
1163 .and_then(|n| n.utf8_text(content).ok())
1164 .map(|s| s.trim().to_string())
1165 } else {
1166 class_node
1168 .parent()
1169 .filter(|p| p.kind() == "variable_declarator")
1170 .and_then(|p| p.child_by_field_name("name"))
1171 .and_then(|n| n.utf8_text(content).ok())
1172 .map(|s| s.trim().to_string())
1173 .or_else(|| {
1174 Some(SyntheticNameBuilder::from_node_with_hash(
1176 &class_node,
1177 content,
1178 "class",
1179 ))
1180 })
1181 };
1182
1183 let parent_name = extract_parent_class_name(heritage_node, content);
1186
1187 if let (Some(child_name), Some(parent_name)) = (class_name, parent_name)
1189 && !child_name.is_empty()
1190 && !parent_name.is_empty()
1191 {
1192 let child_id = helper.add_class(&child_name, None);
1193 let parent_id = helper.add_class(&parent_name, None);
1194 helper.add_inherits_edge(child_id, parent_id);
1195 }
1196}
1197
1198fn extract_parent_class_name(heritage_node: Node<'_>, content: &[u8]) -> Option<String> {
1213 let mut cursor = heritage_node.walk();
1214 for child in heritage_node.children(&mut cursor) {
1215 match child.kind() {
1216 "identifier" => {
1217 return child.utf8_text(content).ok().map(|s| s.trim().to_string());
1219 }
1220 "member_expression" => {
1221 return child.utf8_text(content).ok().map(|s| s.trim().to_string());
1224 }
1225 "call_expression" => {
1226 return child.utf8_text(content).ok().map(|s| s.trim().to_string());
1231 }
1232 _ => {}
1233 }
1234 }
1235 None
1236}
1237
1238fn simple_name(name: &str) -> &str {
1239 name.rsplit(['.', '/']).next().unwrap_or(name)
1242}
1243
1244fn normalize_optional_chain(text: &str) -> String {
1248 text.replace("?.", ".")
1249 .trim()
1250 .trim_end_matches('.')
1251 .to_string()
1252}
1253
1254fn check_uses_await(call_node: Node<'_>) -> bool {
1255 let mut current = call_node;
1257 for _ in 0..2 {
1258 if let Some(parent) = current.parent() {
1260 if parent.kind() == "await_expression" {
1261 return true;
1262 }
1263 current = parent;
1264 } else {
1265 break;
1266 }
1267 }
1268 false
1269}
1270
1271fn count_arguments(node: Node<'_>) -> usize {
1272 node.child_by_field_name("arguments").map_or(0, |args| {
1273 let mut count = 0;
1274 let mut cursor = args.walk();
1275 for child in args.children(&mut cursor) {
1276 if !matches!(child.kind(), "(" | ")" | ",") {
1277 count += 1;
1278 }
1279 }
1280 count
1281 })
1282}
1283
1284fn span_from_node(node: Node<'_>) -> Span {
1285 let start = node.start_position();
1286 let end = node.end_position();
1287 Span::new(
1288 Position::new(start.row, start.column),
1289 Position::new(end.row, end.column),
1290 )
1291}
1292
1293fn extract_string_literal(node: &Node, content: &[u8]) -> Option<String> {
1294 let text = node.utf8_text(content).ok()?;
1295 let trimmed = text.trim();
1296
1297 trimmed
1299 .strip_prefix('"')
1300 .and_then(|s| s.strip_suffix('"'))
1301 .or_else(|| {
1302 trimmed
1303 .strip_prefix('\'')
1304 .and_then(|s| s.strip_suffix('\''))
1305 })
1306 .or_else(|| trimmed.strip_prefix('`').and_then(|s| s.strip_suffix('`')))
1307 .map(std::string::ToString::to_string)
1308}
1309
1310#[derive(Debug, Clone)]
1313pub struct CallContext {
1314 pub qualified_name: String,
1315 pub decl_span: Span,
1317 pub is_async: bool,
1318}
1319
1320impl CallContext {
1321 pub fn qualified_name(&self) -> &str {
1322 &self.qualified_name
1323 }
1324}
1325
1326pub struct ASTGraph {
1327 callable_map: HashMap<usize, usize>,
1329 context_map: HashMap<usize, CallContext>,
1331}
1332
1333impl ASTGraph {
1334 pub fn from_tree(tree: &Tree, content: &[u8], max_scope_depth: usize) -> Result<Self, String> {
1336 let mut builder = ASTGraphBuilder::new(content, max_scope_depth);
1337
1338 let recursion_limits = sqry_core::config::RecursionLimits::load_or_default()
1340 .map_err(|e| format!("Failed to load recursion limits: {e}"))?;
1341 let file_ops_depth = recursion_limits
1342 .effective_file_ops_depth()
1343 .map_err(|e| format!("Invalid file_ops_depth configuration: {e}"))?;
1344 let mut guard = sqry_core::query::security::RecursionGuard::new(file_ops_depth)
1345 .map_err(|e| format!("Failed to create recursion guard: {e}"))?;
1346
1347 builder
1348 .visit(tree.root_node(), None, &mut guard)
1349 .map_err(|e| format!("JavaScript AST traversal hit recursion limit: {e}"))?;
1350 Ok(builder.build())
1351 }
1352
1353 pub fn get_callable_context(&self, node_id: usize) -> Option<&CallContext> {
1355 let callable_id = self.callable_map.get(&node_id)?;
1356 self.context_map.get(callable_id)
1357 }
1358
1359 pub fn contexts(&self) -> impl Iterator<Item = &CallContext> {
1361 self.context_map.values()
1362 }
1363}
1364
1365struct ASTGraphBuilder<'a> {
1366 content: &'a [u8],
1367 max_scope_depth: usize,
1368 callable_map: HashMap<usize, usize>,
1369 context_map: HashMap<usize, CallContext>,
1370 current_scope: Vec<Arc<str>>,
1371}
1372
1373impl<'a> ASTGraphBuilder<'a> {
1374 fn new(content: &'a [u8], max_scope_depth: usize) -> Self {
1375 Self {
1376 content,
1377 max_scope_depth,
1378 callable_map: HashMap::new(),
1379 context_map: HashMap::new(),
1380 current_scope: Vec::new(),
1381 }
1382 }
1383
1384 fn build(self) -> ASTGraph {
1385 ASTGraph {
1386 callable_map: self.callable_map,
1387 context_map: self.context_map,
1388 }
1389 }
1390
1391 fn visit(
1395 &mut self,
1396 node: Node<'_>,
1397 parent_callable: Option<usize>,
1398 guard: &mut sqry_core::query::security::RecursionGuard,
1399 ) -> Result<(), sqry_core::query::security::RecursionError> {
1400 guard.enter()?;
1401
1402 let node_id = node.id();
1403
1404 let callable_name = callable_node_name(node, self.content);
1406
1407 let new_callable = if let Some(name) = callable_name {
1408 let is_async = is_async_function(node, self.content);
1410
1411 let qualified_name = if self.current_scope.is_empty() {
1412 name.clone()
1413 } else if self.current_scope.len() <= self.max_scope_depth {
1414 format!("{}.{}", self.current_scope.join("."), name)
1415 } else {
1416 let truncated = &self.current_scope[..self.max_scope_depth];
1418 format!("{}.{}", truncated.join("."), name)
1419 };
1420
1421 let context = CallContext {
1422 qualified_name,
1423 decl_span: Span::from_node(&node),
1424 is_async,
1425 };
1426
1427 self.context_map.insert(node_id, context);
1428 Some(node_id)
1429 } else {
1430 None
1431 };
1432
1433 let effective_callable = new_callable.or(parent_callable);
1435
1436 if let Some(callable_id) = effective_callable {
1438 self.callable_map.insert(node_id, callable_id);
1439 }
1440
1441 let scope_name = scope_node_name(node, self.content);
1443 let pushed_scope = if let Some(name) = scope_name {
1444 self.current_scope.push(Arc::from(name));
1445 true
1446 } else {
1447 false
1448 };
1449
1450 let mut cursor = node.walk();
1452 for child in node.children(&mut cursor) {
1453 self.visit(child, effective_callable, guard)?;
1454 }
1455
1456 if pushed_scope {
1458 self.current_scope.pop();
1459 }
1460
1461 guard.exit();
1462 Ok(())
1463 }
1464}
1465
1466fn callable_node_name(node: Node<'_>, content: &[u8]) -> Option<String> {
1468 match node.kind() {
1469 "function_declaration" | "generator_function_declaration" => node
1470 .child_by_field_name("name")
1471 .and_then(|child| child.utf8_text(content).ok().map(|s| s.trim().to_string())),
1472 "function_expression" | "generator_function" => {
1473 node.child_by_field_name("name")
1475 .and_then(|child| child.utf8_text(content).ok().map(|s| s.trim().to_string()))
1476 .or_else(|| {
1477 Some(SyntheticNameBuilder::from_node_with_hash(
1478 &node, content, "function",
1479 ))
1480 })
1481 }
1482 "arrow_function" => {
1483 if let Some(parent) = node.parent()
1487 && parent.kind() == "variable_declarator"
1488 && let Some(name_node) = parent.child_by_field_name("name")
1489 && let Ok(name) = name_node.utf8_text(content)
1490 {
1491 let trimmed = name.trim();
1492 if !trimmed.is_empty() {
1493 return Some(trimmed.to_string());
1494 }
1495 }
1496 Some(SyntheticNameBuilder::from_node_with_hash(
1499 &node, content, "arrow",
1500 ))
1501 }
1502 "method_definition" => node
1503 .child_by_field_name("name")
1504 .and_then(|child| child.utf8_text(content).ok().map(|s| s.trim().to_string())),
1505 _ => None,
1506 }
1507}
1508
1509fn scope_node_name(node: Node<'_>, content: &[u8]) -> Option<String> {
1510 match node.kind() {
1511 "class_declaration" | "class" => node
1512 .child_by_field_name("name")
1513 .and_then(|child| child.utf8_text(content).ok().map(|s| s.trim().to_string()))
1514 .or_else(|| {
1515 Some(SyntheticNameBuilder::from_node_with_hash(
1516 &node, content, "class",
1517 ))
1518 }),
1519 _ => None,
1520 }
1521}
1522
1523fn is_async_function(node: Node<'_>, _content: &[u8]) -> bool {
1524 let mut cursor = node.walk();
1526 node.children(&mut cursor)
1527 .any(|child| child.kind() == "async")
1528}
1529
1530fn process_jsdoc_annotations(
1535 node: Node,
1536 content: &[u8],
1537 helper: &mut GraphBuildHelper,
1538) -> GraphResult<()> {
1539 match node.kind() {
1541 "function_declaration" | "generator_function_declaration" => {
1542 process_function_jsdoc(node, content, helper)?;
1543 }
1544 "method_definition" => {
1545 process_method_jsdoc(node, content, helper)?;
1546 }
1547 "lexical_declaration" | "variable_declaration" => {
1548 process_variable_jsdoc(node, content, helper)?;
1549 }
1550 "class_declaration" | "class" => {
1551 process_class_fields(node, content, helper)?;
1552 process_constructor_this_assignments(node, content, helper)?;
1553 }
1554 _ => {}
1555 }
1556
1557 let mut cursor = node.walk();
1559 for child in node.children(&mut cursor) {
1560 process_jsdoc_annotations(child, content, helper)?;
1561 }
1562
1563 Ok(())
1564}
1565
1566fn process_function_jsdoc(
1568 func_node: Node,
1569 content: &[u8],
1570 helper: &mut GraphBuildHelper,
1571) -> GraphResult<()> {
1572 let Some(jsdoc_text) = extract_jsdoc_comment(func_node, content) else {
1574 return Ok(());
1575 };
1576
1577 let tags = parse_jsdoc_tags(&jsdoc_text);
1579
1580 let Some(name_node) = func_node.child_by_field_name("name") else {
1582 return Ok(());
1583 };
1584
1585 let function_name = name_node
1586 .utf8_text(content)
1587 .map_err(|_| GraphBuilderError::ParseError {
1588 span: span_from_node(func_node),
1589 reason: "failed to read function name".to_string(),
1590 })?
1591 .trim()
1592 .to_string();
1593
1594 if function_name.is_empty() {
1595 return Ok(());
1596 }
1597
1598 let func_node_id = helper.ensure_callee(
1600 &function_name,
1601 span_from_node(func_node),
1602 CalleeKindHint::Function,
1603 );
1604
1605 let ast_params = extract_ast_parameters(func_node, content);
1608 let ast_param_map: HashMap<&str, usize> = ast_params
1609 .iter()
1610 .map(|(idx, name)| (name.as_str(), *idx))
1611 .collect();
1612
1613 for param_tag in &tags.params {
1615 let mut normalized_name = param_tag
1618 .name
1619 .trim_start_matches("...")
1620 .trim_matches(|c| c == '[' || c == ']');
1621
1622 if let Some(base_name) = normalized_name.split('.').next() {
1625 normalized_name = base_name;
1626 }
1627
1628 let Some(&ast_index) = ast_param_map.get(normalized_name) else {
1629 continue;
1631 };
1632
1633 let canonical_type = canonical_type_string(¶m_tag.type_str);
1635 let type_node_id = helper.add_type(&canonical_type, None);
1636 helper.add_typeof_edge_with_context(
1637 func_node_id,
1638 type_node_id,
1639 Some(TypeOfContext::Parameter),
1640 ast_index.try_into().ok(), Some(¶m_tag.name),
1642 );
1643
1644 let type_names = extract_type_names(¶m_tag.type_str);
1646 for type_name in type_names {
1647 let ref_type_id = helper.add_type(&type_name, None);
1648 helper.add_reference_edge(func_node_id, ref_type_id);
1649 }
1650 }
1651
1652 if let Some(return_type) = &tags.returns {
1654 let canonical_type = canonical_type_string(return_type);
1655 let type_node_id = helper.add_type(&canonical_type, None);
1656 helper.add_typeof_edge_with_context(
1657 func_node_id,
1658 type_node_id,
1659 Some(TypeOfContext::Return),
1660 Some(0),
1661 None,
1662 );
1663
1664 let type_names = extract_type_names(return_type);
1666 for type_name in type_names {
1667 let ref_type_id = helper.add_type(&type_name, None);
1668 helper.add_reference_edge(func_node_id, ref_type_id);
1669 }
1670 }
1671
1672 Ok(())
1673}
1674
1675fn process_method_jsdoc(
1677 method_node: Node,
1678 content: &[u8],
1679 helper: &mut GraphBuildHelper,
1680) -> GraphResult<()> {
1681 let Some(jsdoc_text) = extract_jsdoc_comment(method_node, content) else {
1683 return Ok(());
1684 };
1685
1686 let tags = parse_jsdoc_tags(&jsdoc_text);
1688
1689 let Some(name_node) = method_node.child_by_field_name("name") else {
1691 return Ok(());
1692 };
1693
1694 let method_name = name_node
1695 .utf8_text(content)
1696 .map_err(|_| GraphBuilderError::ParseError {
1697 span: span_from_node(method_node),
1698 reason: "failed to read method name".to_string(),
1699 })?
1700 .trim()
1701 .to_string();
1702
1703 if method_name.is_empty() {
1704 return Ok(());
1705 }
1706
1707 let class_name = get_enclosing_class_name(method_node, content)?;
1709 let Some(class_name) = class_name else {
1710 return Ok(());
1711 };
1712
1713 let qualified_name = format!("{class_name}.{method_name}");
1715
1716 let method_node_id = helper.ensure_method(&qualified_name, None, false, false);
1719
1720 let ast_params = extract_ast_parameters(method_node, content);
1723 let ast_param_map: HashMap<&str, usize> = ast_params
1724 .iter()
1725 .map(|(idx, name)| (name.as_str(), *idx))
1726 .collect();
1727
1728 for param_tag in &tags.params {
1730 let mut normalized_name = param_tag
1733 .name
1734 .trim_start_matches("...")
1735 .trim_matches(|c| c == '[' || c == ']');
1736
1737 if let Some(base_name) = normalized_name.split('.').next() {
1740 normalized_name = base_name;
1741 }
1742
1743 let Some(&ast_index) = ast_param_map.get(normalized_name) else {
1744 continue;
1746 };
1747
1748 let canonical_type = canonical_type_string(¶m_tag.type_str);
1749 let type_node_id = helper.add_type(&canonical_type, None);
1750 helper.add_typeof_edge_with_context(
1751 method_node_id,
1752 type_node_id,
1753 Some(TypeOfContext::Parameter),
1754 ast_index.try_into().ok(), Some(¶m_tag.name),
1756 );
1757
1758 let type_names = extract_type_names(¶m_tag.type_str);
1760 for type_name in type_names {
1761 let ref_type_id = helper.add_type(&type_name, None);
1762 helper.add_reference_edge(method_node_id, ref_type_id);
1763 }
1764 }
1765
1766 if let Some(return_type) = &tags.returns {
1768 let canonical_type = canonical_type_string(return_type);
1769 let type_node_id = helper.add_type(&canonical_type, None);
1770 helper.add_typeof_edge_with_context(
1771 method_node_id,
1772 type_node_id,
1773 Some(TypeOfContext::Return),
1774 Some(0),
1775 None,
1776 );
1777
1778 let type_names = extract_type_names(return_type);
1780 for type_name in type_names {
1781 let ref_type_id = helper.add_type(&type_name, None);
1782 helper.add_reference_edge(method_node_id, ref_type_id);
1783 }
1784 }
1785
1786 Ok(())
1787}
1788
1789fn process_variable_jsdoc(
1791 decl_node: Node,
1792 content: &[u8],
1793 helper: &mut GraphBuildHelper,
1794) -> GraphResult<()> {
1795 if !is_top_level_variable(decl_node) {
1797 return Ok(());
1798 }
1799
1800 let Some(jsdoc_text) = extract_jsdoc_comment(decl_node, content) else {
1802 return Ok(());
1803 };
1804
1805 let tags = parse_jsdoc_tags(&jsdoc_text);
1807
1808 let Some(type_annotation) = &tags.type_annotation else {
1810 return Ok(());
1811 };
1812
1813 let mut cursor = decl_node.walk();
1815 for child in decl_node.children(&mut cursor) {
1816 if child.kind() == "variable_declarator"
1817 && let Some(name_node) = child.child_by_field_name("name")
1818 {
1819 let var_name = name_node
1820 .utf8_text(content)
1821 .map_err(|_| GraphBuilderError::ParseError {
1822 span: span_from_node(child),
1823 reason: "failed to read variable name".to_string(),
1824 })?
1825 .trim()
1826 .to_string();
1827
1828 if !var_name.is_empty() {
1829 let var_node_id = helper.add_variable(&var_name, None);
1831
1832 let canonical_type = canonical_type_string(type_annotation);
1834 let type_node_id = helper.add_type(&canonical_type, None);
1835 helper.add_typeof_edge_with_context(
1836 var_node_id,
1837 type_node_id,
1838 Some(TypeOfContext::Variable),
1839 None,
1840 None,
1841 );
1842
1843 let type_names = extract_type_names(type_annotation);
1845 for type_name in type_names {
1846 let ref_type_id = helper.add_type(&type_name, None);
1847 helper.add_reference_edge(var_node_id, ref_type_id);
1848 }
1849 }
1850 }
1851 }
1852
1853 Ok(())
1854}
1855
1856fn resolve_class_name_for_fields(
1867 class_node: Node<'_>,
1868 content: &[u8],
1869) -> GraphResult<Option<String>> {
1870 if let Some(name_node) = class_node.child_by_field_name("name") {
1871 let name = name_node
1872 .utf8_text(content)
1873 .map_err(|_| GraphBuilderError::ParseError {
1874 span: span_from_node(class_node),
1875 reason: "failed to read class name".to_string(),
1876 })?
1877 .trim()
1878 .to_string();
1879 if name.is_empty() {
1880 return Ok(None);
1881 }
1882 return Ok(Some(name));
1883 }
1884
1885 let Some(parent) = class_node.parent() else {
1887 return Ok(None);
1888 };
1889
1890 match parent.kind() {
1891 "variable_declarator" => {
1892 if let Some(name_node) = parent.child_by_field_name("name")
1893 && let Ok(var_name) = name_node.utf8_text(content)
1894 {
1895 let var_name = var_name.trim().to_string();
1896 if var_name.is_empty() {
1897 return Ok(None);
1898 }
1899 return Ok(Some(var_name));
1900 }
1901 Ok(None)
1902 }
1903 "assignment_expression" => {
1904 if let Some(left) = parent.child_by_field_name("left")
1905 && let Ok(assign_name) = left.utf8_text(content)
1906 {
1907 let assign_name = assign_name.trim().to_string();
1908 if assign_name.is_empty() {
1909 return Ok(None);
1910 }
1911 return Ok(Some(assign_name));
1912 }
1913 Ok(None)
1914 }
1915 _ => Ok(None),
1916 }
1917}
1918
1919fn process_class_fields(
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() != "field_definition" {
1950 continue;
1951 }
1952 emit_class_field_node(child, content, helper, &class_name)?;
1953 }
1954
1955 Ok(())
1956}
1957
1958fn emit_class_field_node(
1961 field_node: Node<'_>,
1962 content: &[u8],
1963 helper: &mut GraphBuildHelper,
1964 class_name: &str,
1965) -> GraphResult<()> {
1966 let Some(name_node) = field_node.child_by_field_name("property") else {
1970 return Ok(());
1971 };
1972
1973 let raw_name = name_node
1974 .utf8_text(content)
1975 .map_err(|_| GraphBuilderError::ParseError {
1976 span: span_from_node(field_node),
1977 reason: "failed to read field name".to_string(),
1978 })?
1979 .trim()
1980 .to_string();
1981
1982 if raw_name.is_empty() {
1983 return Ok(());
1984 }
1985
1986 let is_hash_private = name_node.kind() == "private_property_identifier";
1987
1988 let mut is_static = false;
1993 let mut mod_cursor = field_node.walk();
1994 for modifier in field_node.children(&mut mod_cursor) {
1995 if modifier.kind() == "static" {
1996 is_static = true;
1997 }
1998 }
1999
2000 let visibility: Option<&str> = if is_hash_private {
2006 Some("private")
2007 } else {
2008 Some("public")
2009 };
2010
2011 let qualified_name = format!("{class_name}.{raw_name}");
2012 let span = Some(span_from_node(field_node));
2013
2014 let field_id = helper.add_property_with_static_and_visibility(
2015 &qualified_name,
2016 span,
2017 is_static,
2018 visibility,
2019 );
2020
2021 if let Some(jsdoc_text) = extract_jsdoc_comment(field_node, content) {
2025 let tags = parse_jsdoc_tags(&jsdoc_text);
2026 if let Some(type_annotation) = &tags.type_annotation {
2027 let canonical_type = canonical_type_string(type_annotation);
2028 let type_node_id = helper.add_type(&canonical_type, None);
2029 helper.add_typeof_edge_with_context(
2030 field_id,
2031 type_node_id,
2032 Some(TypeOfContext::Field),
2033 None,
2034 Some(&raw_name),
2035 );
2036
2037 let type_names = extract_type_names(type_annotation);
2038 for type_name in type_names {
2039 let ref_type_id = helper.add_type(&type_name, None);
2040 helper.add_reference_edge(field_id, ref_type_id);
2041 }
2042 }
2043 }
2044
2045 Ok(())
2046}
2047
2048fn process_constructor_this_assignments(
2060 class_node: Node<'_>,
2061 content: &[u8],
2062 helper: &mut GraphBuildHelper,
2063) -> GraphResult<()> {
2064 let Some(class_name) = resolve_class_name_for_fields(class_node, content)? else {
2065 return Ok(());
2066 };
2067
2068 let Some(body_node) = class_node.child_by_field_name("body") else {
2069 return Ok(());
2070 };
2071
2072 let mut cursor = body_node.walk();
2073 for child in body_node.children(&mut cursor) {
2074 if child.kind() != "method_definition" {
2075 continue;
2076 }
2077
2078 let Some(name_node) = child.child_by_field_name("name") else {
2083 continue;
2084 };
2085 let Ok(method_name) = name_node.utf8_text(content) else {
2086 continue;
2087 };
2088 if method_name.trim() != "constructor" {
2089 continue;
2090 }
2091
2092 let Some(method_body) = child.child_by_field_name("body") else {
2093 continue;
2094 };
2095
2096 walk_for_this_assignments(method_body, content, helper, &class_name);
2097 }
2098
2099 Ok(())
2100}
2101
2102fn walk_for_this_assignments(
2106 node: Node<'_>,
2107 content: &[u8],
2108 helper: &mut GraphBuildHelper,
2109 class_name: &str,
2110) {
2111 if node.kind() == "assignment_expression"
2112 && let Some(left) = node.child_by_field_name("left")
2113 && left.kind() == "member_expression"
2114 && let Some(object) = left.child_by_field_name("object")
2115 && object.kind() == "this"
2116 && let Some(property) = left.child_by_field_name("property")
2117 && property.kind() == "property_identifier"
2118 && let Ok(field_name) = property.utf8_text(content)
2119 {
2120 let field_name = field_name.trim();
2121 if !field_name.is_empty() {
2122 let qualified_name = format!("{class_name}.{field_name}");
2123 let _ = helper.add_property_with_static_and_visibility(
2133 &qualified_name,
2134 Some(span_from_node(left)),
2135 false,
2136 Some("public"),
2137 );
2138 }
2139 }
2140
2141 let mut cursor = node.walk();
2149 for child in node.children(&mut cursor) {
2150 walk_for_this_assignments(child, content, helper, class_name);
2151 }
2152}
2153
2154fn get_enclosing_class_name(method_node: Node, content: &[u8]) -> GraphResult<Option<String>> {
2158 let mut current = method_node;
2160 while let Some(parent) = current.parent() {
2161 if parent.kind() == "class_declaration" {
2162 if let Some(name_node) = parent.child_by_field_name("name") {
2164 let class_name = name_node
2165 .utf8_text(content)
2166 .map_err(|_| GraphBuilderError::ParseError {
2167 span: span_from_node(parent),
2168 reason: "failed to read class name".to_string(),
2169 })?
2170 .trim()
2171 .to_string();
2172
2173 if !class_name.is_empty() {
2174 return Ok(Some(class_name));
2175 }
2176 }
2177 } else if parent.kind() == "class" {
2178 if let Some(grandparent) = parent.parent() {
2181 if grandparent.kind() == "variable_declarator" {
2182 if let Some(name_node) = grandparent.child_by_field_name("name")
2184 && let Ok(var_name) = name_node.utf8_text(content)
2185 {
2186 let var_name = var_name.trim().to_string();
2187 if !var_name.is_empty() {
2188 return Ok(Some(var_name));
2189 }
2190 }
2191 } else if grandparent.kind() == "assignment_expression" {
2192 if let Some(left) = grandparent.child_by_field_name("left")
2194 && let Ok(assign_name) = left.utf8_text(content)
2195 {
2196 let assign_name = assign_name.trim().to_string();
2197 if !assign_name.is_empty() {
2198 return Ok(Some(assign_name));
2199 }
2200 }
2201 }
2202 }
2203 return Ok(None);
2206 }
2207 current = parent;
2208 }
2209 Ok(None)
2210}
2211
2212fn extract_ast_parameters(func_node: Node, content: &[u8]) -> Vec<(usize, String)> {
2215 let Some(params_node) = func_node.child_by_field_name("parameters") else {
2216 return Vec::new();
2217 };
2218
2219 let mut cursor = params_node.walk();
2220 params_node
2221 .named_children(&mut cursor)
2222 .enumerate()
2223 .filter_map(|(ast_index, param)| {
2224 let param_name = match param.kind() {
2226 "identifier" => param
2227 .utf8_text(content)
2228 .ok()
2229 .map(std::string::ToString::to_string),
2230 "required_parameter" | "optional_parameter" => {
2231 param
2233 .child_by_field_name("pattern")
2234 .and_then(|p| p.utf8_text(content).ok())
2235 .map(std::string::ToString::to_string)
2236 }
2237 "rest_pattern" => {
2238 param
2241 .named_child(0)
2242 .and_then(|n| n.utf8_text(content).ok())
2243 .map(|s| s.trim_start_matches("...").to_string())
2244 }
2245 "assignment_pattern" => {
2246 param
2249 .child_by_field_name("left")
2250 .filter(|left| left.kind() == "identifier")
2251 .and_then(|left| left.utf8_text(content).ok())
2252 .map(std::string::ToString::to_string)
2253 }
2254 _ => None,
2255 };
2256
2257 param_name.map(|name| (ast_index, name))
2258 })
2259 .collect()
2260}
2261
2262fn is_top_level_variable(decl_node: Node) -> bool {
2265 let mut current = decl_node;
2266 while let Some(parent) = current.parent() {
2267 match parent.kind() {
2268 "function_declaration"
2270 | "generator_function_declaration"
2271 | "function_expression"
2272 | "arrow_function"
2273 | "method_definition" => return false,
2274
2275 "statement_block" | "if_statement" | "for_statement" | "for_in_statement"
2277 | "for_of_statement" | "while_statement" | "do_statement" | "try_statement"
2278 | "catch_clause" | "finally_clause" | "switch_statement" | "switch_case"
2279 | "switch_default" | "class_body" | "class_static_block" | "with_statement" => {
2280 return false;
2281 }
2282
2283 "program" | "export_statement" => return true,
2286
2287 _ => {}
2288 }
2289 current = parent;
2290 }
2291 true
2292}
2293
2294fn build_ffi_call_edge(
2306 ast_graph: &ASTGraph,
2307 call_node: Node<'_>,
2308 content: &[u8],
2309 helper: &mut GraphBuildHelper,
2310) -> GraphResult<bool> {
2311 let Some(callee_expr) = call_node.child_by_field_name("function") else {
2312 return Ok(false);
2313 };
2314
2315 let callee_text = callee_expr
2316 .utf8_text(content)
2317 .map_err(|_| GraphBuilderError::ParseError {
2318 span: span_from_node(call_node),
2319 reason: "failed to read call expression".to_string(),
2320 })?
2321 .trim();
2322
2323 if callee_text.starts_with("WebAssembly.") {
2325 return Ok(build_webassembly_call_edge(
2326 ast_graph,
2327 call_node,
2328 content,
2329 callee_text,
2330 helper,
2331 ));
2332 }
2333
2334 if callee_text == "require" {
2336 return Ok(build_require_ffi_edge(
2337 ast_graph, call_node, content, helper,
2338 ));
2339 }
2340
2341 if callee_text == "process.dlopen" {
2343 return Ok(build_dlopen_edge(ast_graph, call_node, content, helper));
2344 }
2345
2346 Ok(false)
2347}
2348
2349fn build_ffi_new_edge(
2357 ast_graph: &ASTGraph,
2358 new_node: Node<'_>,
2359 content: &[u8],
2360 helper: &mut GraphBuildHelper,
2361) -> GraphResult<bool> {
2362 let Some(constructor_expr) = new_node.child_by_field_name("constructor") else {
2363 return Ok(false);
2364 };
2365
2366 let constructor_text = constructor_expr
2367 .utf8_text(content)
2368 .map_err(|_| GraphBuilderError::ParseError {
2369 span: span_from_node(new_node),
2370 reason: "failed to read constructor expression".to_string(),
2371 })?
2372 .trim();
2373
2374 if constructor_text == "WebAssembly.Module" || constructor_text == "WebAssembly.Instance" {
2376 return Ok(build_webassembly_constructor_edge(
2377 ast_graph,
2378 new_node,
2379 constructor_text,
2380 helper,
2381 ));
2382 }
2383
2384 Ok(false)
2385}
2386
2387fn build_webassembly_call_edge(
2389 ast_graph: &ASTGraph,
2390 call_node: Node<'_>,
2391 content: &[u8],
2392 callee_text: &str,
2393 helper: &mut GraphBuildHelper,
2394) -> bool {
2395 let method_name = callee_text
2397 .strip_prefix("WebAssembly.")
2398 .unwrap_or(callee_text);
2399
2400 let is_wasm_load = matches!(
2402 method_name,
2403 "instantiate" | "instantiateStreaming" | "compile" | "compileStreaming" | "validate"
2404 );
2405
2406 if !is_wasm_load {
2407 return false;
2408 }
2409
2410 let caller_id = get_caller_node_id(ast_graph, call_node, helper);
2412
2413 let wasm_module_name = extract_wasm_module_name(call_node, content)
2415 .unwrap_or_else(|| format!("wasm::{method_name}"));
2416
2417 let wasm_node_id = helper.add_call_site_node(
2419 &wasm_module_name,
2420 span_from_node(call_node),
2421 NodeKind::Module,
2422 );
2423
2424 helper.add_webassembly_edge(caller_id, wasm_node_id);
2426
2427 true
2428}
2429
2430fn build_webassembly_constructor_edge(
2432 ast_graph: &ASTGraph,
2433 new_node: Node<'_>,
2434 constructor_text: &str,
2435 helper: &mut GraphBuildHelper,
2436) -> bool {
2437 let caller_id = get_caller_node_id(ast_graph, new_node, helper);
2439
2440 let type_name = constructor_text
2442 .strip_prefix("WebAssembly.")
2443 .unwrap_or(constructor_text);
2444 let wasm_module_name = format!("wasm::{type_name}");
2445
2446 let wasm_node_id = helper.add_call_site_node(
2448 &wasm_module_name,
2449 span_from_node(new_node),
2450 NodeKind::Module,
2451 );
2452
2453 helper.add_webassembly_edge(caller_id, wasm_node_id);
2455
2456 true
2457}
2458
2459fn build_require_ffi_edge(
2464 ast_graph: &ASTGraph,
2465 call_node: Node<'_>,
2466 content: &[u8],
2467 helper: &mut GraphBuildHelper,
2468) -> bool {
2469 let Some(args) = call_node.child_by_field_name("arguments") else {
2471 return false;
2472 };
2473
2474 let mut cursor = args.walk();
2475 let first_arg = args
2476 .children(&mut cursor)
2477 .find(|child| !matches!(child.kind(), "(" | ")" | ","));
2478
2479 let Some(arg_node) = first_arg else {
2480 return false;
2481 };
2482
2483 let module_path = extract_string_literal(&arg_node, content);
2485 let Some(path) = module_path else {
2486 return false;
2487 };
2488
2489 let from_id = helper.add_module("<module>", None);
2491
2492 let resolved_path = if path.starts_with('.') {
2494 sqry_core::graph::resolve_import_path(std::path::Path::new(helper.file_path()), &path)
2496 .unwrap_or_else(|_| simple_name(&path).to_string())
2497 } else {
2498 simple_name(&path).to_string()
2500 };
2501
2502 let to_id = helper.add_import(&resolved_path, Some(span_from_node(call_node)));
2503 helper.add_import_edge(from_id, to_id);
2504
2505 let is_native_addon = std::path::Path::new(&path)
2507 .extension()
2508 .is_some_and(|ext| ext.eq_ignore_ascii_case("node"))
2509 || is_known_native_addon(&path);
2510
2511 if is_native_addon {
2512 let caller_id = get_caller_node_id(ast_graph, call_node, helper);
2514
2515 let ffi_name = format!("native::{}", simple_name(&path));
2517 let ffi_node_id =
2518 helper.add_call_site_node(&ffi_name, span_from_node(call_node), NodeKind::Module);
2519
2520 helper.add_ffi_edge(caller_id, ffi_node_id, FfiConvention::C);
2522 }
2523
2524 true
2525}
2526
2527fn build_dlopen_edge(
2529 ast_graph: &ASTGraph,
2530 call_node: Node<'_>,
2531 content: &[u8],
2532 helper: &mut GraphBuildHelper,
2533) -> bool {
2534 let caller_id = get_caller_node_id(ast_graph, call_node, helper);
2536
2537 let module_name = call_node
2539 .child_by_field_name("arguments")
2540 .and_then(|args| {
2541 let mut cursor = args.walk();
2542 args.children(&mut cursor)
2543 .filter(|child| !matches!(child.kind(), "(" | ")" | ","))
2544 .nth(1) })
2546 .and_then(|node| extract_string_literal(&node, content))
2547 .map_or_else(
2548 || "native::dlopen".to_string(),
2549 |path| format!("native::{}", simple_name(&path)),
2550 );
2551
2552 let ffi_node_id =
2554 helper.add_call_site_node(&module_name, span_from_node(call_node), NodeKind::Module);
2555
2556 helper.add_ffi_edge(caller_id, ffi_node_id, FfiConvention::C);
2558
2559 true
2560}
2561
2562fn get_caller_node_id(
2564 ast_graph: &ASTGraph,
2565 node: Node<'_>,
2566 helper: &mut GraphBuildHelper,
2567) -> sqry_core::graph::unified::NodeId {
2568 let module_context;
2569 let call_context = if let Some(ctx) = ast_graph.get_callable_context(node.id()) {
2570 ctx
2571 } else {
2572 module_context = CallContext {
2573 qualified_name: "<module>".to_string(),
2574 decl_span: Span::default(),
2583 is_async: false,
2584 };
2585 &module_context
2586 };
2587
2588 ensure_caller_node(helper, call_context)
2589}
2590
2591fn ensure_caller_node(
2592 helper: &mut GraphBuildHelper,
2593 call_context: &CallContext,
2594) -> sqry_core::graph::unified::NodeId {
2595 let caller_span = Some(call_context.decl_span);
2596 let qualified_name = call_context.qualified_name();
2597 if qualified_name.contains('.') {
2598 helper.ensure_method(qualified_name, caller_span, call_context.is_async, false)
2599 } else {
2600 helper.ensure_function(qualified_name, caller_span, call_context.is_async, false)
2601 }
2602}
2603
2604fn extract_wasm_module_name(call_node: Node<'_>, content: &[u8]) -> Option<String> {
2610 let args = call_node.child_by_field_name("arguments")?;
2611
2612 let mut cursor = args.walk();
2613 let first_arg = args
2614 .children(&mut cursor)
2615 .find(|child| !matches!(child.kind(), "(" | ")" | ","))?;
2616
2617 if first_arg.kind() == "call_expression"
2619 && let Some(func) = first_arg.child_by_field_name("function")
2620 {
2621 let func_text = func.utf8_text(content).ok()?.trim();
2622 if func_text == "fetch" {
2623 if let Some(fetch_args) = first_arg.child_by_field_name("arguments") {
2625 let mut fetch_cursor = fetch_args.walk();
2626 let url_arg = fetch_args
2627 .children(&mut fetch_cursor)
2628 .find(|child| !matches!(child.kind(), "(" | ")" | ","))?;
2629
2630 if let Some(url) = extract_string_literal(&url_arg, content) {
2631 return Some(format!("wasm::{}", simple_name(&url)));
2632 }
2633 }
2634 }
2635 }
2636
2637 if let Some(path) = extract_string_literal(&first_arg, content) {
2639 return Some(format!("wasm::{}", simple_name(&path)));
2640 }
2641
2642 None
2643}
2644
2645fn is_known_native_addon(package_name: &str) -> bool {
2647 const NATIVE_PACKAGES: &[&str] = &[
2649 "better-sqlite3",
2650 "sqlite3",
2651 "bcrypt",
2652 "sharp",
2653 "canvas",
2654 "node-sass",
2655 "leveldown",
2656 "bufferutil",
2657 "utf-8-validate",
2658 "fsevents",
2659 "cpu-features",
2660 "node-gyp",
2661 "node-pre-gyp",
2662 "prebuild",
2663 "nan",
2664 "node-addon-api",
2665 "ref-napi",
2666 "ffi-napi",
2667 ];
2668
2669 NATIVE_PACKAGES
2670 .iter()
2671 .any(|&pkg| package_name.contains(pkg))
2672}
2673
2674#[cfg(test)]
2675mod shape_tests {
2676 use super::{cf_bucket_for_javascript_kind, javascript_shape_mapping};
2677 use sqry_core::graph::unified::build::shape::{
2678 CfBucket, ShapeBudget, ShapeMapping, compute_shape_descriptor,
2679 };
2680
2681 const SAMPLE: &str = include_str!(concat!(
2682 env!("CARGO_MANIFEST_DIR"),
2683 "/../test-fixtures/shape/reference/sample.js"
2684 ));
2685
2686 fn parse(src: &str) -> tree_sitter::Tree {
2687 let lang: tree_sitter::Language = tree_sitter_javascript::LANGUAGE.into();
2688 let mut p = tree_sitter::Parser::new();
2689 p.set_language(&lang).expect("load javascript grammar");
2690 p.parse(src, None).expect("parse")
2691 }
2692
2693 fn function_named<'t>(tree: &'t tree_sitter::Tree, name: &str) -> tree_sitter::Node<'t> {
2694 let root = tree.root_node();
2695 let mut stack = vec![root];
2696 while let Some(node) = stack.pop() {
2697 if node.kind() == "function_declaration"
2698 && node
2699 .child_by_field_name("name")
2700 .and_then(|n| n.utf8_text(SAMPLE.as_bytes()).ok())
2701 == Some(name)
2702 {
2703 return node;
2704 }
2705 let mut c = node.walk();
2706 for ch in node.children(&mut c) {
2707 stack.push(ch);
2708 }
2709 }
2710 panic!("no function_declaration named {name}");
2711 }
2712
2713 #[test]
2714 fn cf_table_is_non_empty() {
2715 let mapping = javascript_shape_mapping();
2716 let lang: tree_sitter::Language = tree_sitter_javascript::LANGUAGE.into();
2717 let mut covered = 0;
2718 for id in 0..lang.node_kind_count() {
2719 if mapping.cf_bucket(id as u16).is_some() {
2720 covered += 1;
2721 }
2722 }
2723 assert!(
2724 covered >= 10,
2725 "expected many JS CF kinds mapped, got {covered}"
2726 );
2727 }
2728
2729 #[test]
2730 fn histogram_covers_real_control_flow() {
2731 let tree = parse(SAMPLE);
2732 let func = function_named(&tree, "classify");
2733 let d = compute_shape_descriptor(
2734 func,
2735 SAMPLE.as_bytes(),
2736 javascript_shape_mapping(),
2737 &ShapeBudget::default(),
2738 );
2739 assert!(!d.is_unhashable());
2740 for bucket in [
2741 CfBucket::Branch,
2742 CfBucket::Loop,
2743 CfBucket::Match,
2744 CfBucket::Try,
2745 CfBucket::Catch,
2746 CfBucket::Throw,
2747 CfBucket::Return,
2748 CfBucket::BreakContinue,
2749 CfBucket::Call,
2750 CfBucket::Assign,
2751 CfBucket::Closure,
2752 ] {
2753 assert!(
2754 d.cf_histogram[bucket.index()] >= 1,
2755 "classify must exercise {bucket:?}"
2756 );
2757 }
2758 }
2759
2760 #[test]
2761 fn async_body_covers_await() {
2762 let tree = parse(SAMPLE);
2763 let func = function_named(&tree, "fetchValue");
2764 let d = compute_shape_descriptor(
2765 func,
2766 SAMPLE.as_bytes(),
2767 javascript_shape_mapping(),
2768 &ShapeBudget::default(),
2769 );
2770 assert!(d.cf_histogram[CfBucket::Await.index()] >= 1, "await");
2771 }
2772
2773 #[test]
2774 fn signature_shape_reads_arity_defaults_varargs() {
2775 let tree = parse(SAMPLE);
2776 let func = function_named(&tree, "classify");
2777 let mapping = javascript_shape_mapping();
2778 let shape = mapping.signature_shape(func, SAMPLE.as_bytes());
2779 assert_eq!(shape.arity_positional, 2, "values + threshold = 0");
2781 assert!(shape.has_defaults, "threshold = 0");
2782 assert!(shape.has_varargs, "...extra");
2783 assert!(!shape.has_return_annotation, "JS has no return annotation");
2784 }
2785
2786 #[test]
2787 fn unknown_kind_maps_to_none() {
2788 assert!(cf_bucket_for_javascript_kind("program").is_none());
2789 assert!(cf_bucket_for_javascript_kind("identifier").is_none());
2790 }
2791}