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, 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(Span::from_bytes(context.span.0, context.span.1));
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 span: (0, content.len()),
340 is_async: false,
341 };
342 &module_context
343 };
344
345 let Some(callee_expr) = call_node.child_by_field_name("function") else {
346 return Ok(None);
347 };
348
349 let raw_callee_text = callee_expr
350 .utf8_text(content)
351 .map_err(|_| GraphBuilderError::ParseError {
352 span: span_from_node(call_node),
353 reason: "failed to read call expression".to_string(),
354 })?
355 .trim()
356 .to_string();
357
358 let callee_text = if raw_callee_text.contains("?.") {
360 normalize_optional_chain(&raw_callee_text)
361 } else {
362 raw_callee_text
363 };
364
365 if callee_text.is_empty() {
366 return Ok(None);
367 }
368
369 let callee_simple = simple_name(&callee_text);
370 if callee_simple.is_empty() {
371 return Ok(None);
372 }
373
374 let caller_qname = call_context.qualified_name();
376 let target_qname = if let Some(method_name) = callee_text.strip_prefix("this.") {
377 if let Some(scope_idx) = caller_qname.rfind('.') {
379 let class_name = &caller_qname[..scope_idx];
380 format!("{}.{}", class_name, simple_name(method_name))
381 } else {
382 callee_text.clone()
383 }
384 } else if callee_text.starts_with("super.") || callee_text.contains('.') {
385 callee_text.clone()
386 } else {
387 callee_simple.to_string()
388 };
389
390 let source_id = ensure_caller_node(helper, call_context);
392 let call_site_span = span_from_node(call_node);
393 let target_id = helper.ensure_callee(&target_qname, call_site_span, CalleeKindHint::Function);
394
395 let span = Some(call_site_span);
396 let argument_count = u8::try_from(count_arguments(call_node)).unwrap_or(u8::MAX);
397 let is_async = check_uses_await(call_node);
398
399 Ok(Some((source_id, target_id, argument_count, is_async, span)))
400}
401
402#[derive(Debug, Clone)]
403struct HttpRequestInfo {
404 method: HttpMethod,
405 url: Option<String>,
406}
407
408fn build_http_request_edge(
409 ast_graph: &ASTGraph,
410 call_node: Node<'_>,
411 content: &[u8],
412 helper: &mut GraphBuildHelper,
413) -> bool {
414 let Some(info) = extract_http_request_info(call_node, content) else {
415 return false;
416 };
417
418 let caller_id = get_caller_node_id(ast_graph, call_node, content, helper);
419 let target_name = info.url.as_ref().map_or_else(
420 || format!("http::{}", info.method.as_str()),
421 |url| format!("http::{url}"),
422 );
423 let target_id = helper.add_module(&target_name, Some(span_from_node(call_node)));
424
425 helper.add_http_request_edge(caller_id, target_id, info.method, info.url.as_deref());
426 true
427}
428
429fn detect_route_endpoint(
443 call_node: Node<'_>,
444 content: &[u8],
445 helper: &mut GraphBuildHelper,
446) -> bool {
447 let Some(callee) = call_node.child_by_field_name("function") else {
449 return false;
450 };
451
452 if callee.kind() != "member_expression" {
453 return false;
454 }
455
456 let Some(property) = callee.child_by_field_name("property") else {
458 return false;
459 };
460
461 let Ok(method_name) = property.utf8_text(content) else {
462 return false;
463 };
464 let method_name = method_name.trim();
465
466 let method_str = match method_name {
468 "get" => "GET",
469 "post" => "POST",
470 "put" => "PUT",
471 "delete" => "DELETE",
472 "patch" => "PATCH",
473 "all" => "ALL",
474 _ => return false,
475 };
476
477 let Some(args) = call_node.child_by_field_name("arguments") else {
479 return false;
480 };
481
482 let mut cursor = args.walk();
483 let first_arg = args
484 .children(&mut cursor)
485 .find(|child| !matches!(child.kind(), "(" | ")" | ","));
486
487 let Some(first_arg) = first_arg else {
488 return false;
489 };
490
491 let Some(path) = extract_string_literal(&first_arg, content) else {
493 return false;
494 };
495
496 let qualified_name = format!("route::{method_str}::{path}");
498
499 let endpoint_id = helper.add_endpoint(&qualified_name, Some(span_from_node(call_node)));
501
502 let mut handler_cursor = args.walk();
505 let handler_arg = args
506 .children(&mut handler_cursor)
507 .filter(|child| !matches!(child.kind(), "(" | ")" | ","))
508 .nth(1);
509
510 if let Some(handler_node) = handler_arg
511 && let Ok(handler_text) = handler_node.utf8_text(content)
512 {
513 let handler_name = handler_text.trim();
514 if !handler_name.is_empty()
515 && matches!(handler_node.kind(), "identifier" | "member_expression")
516 {
517 let handler_id = helper.ensure_callee(
518 handler_name,
519 span_from_node(handler_node),
520 CalleeKindHint::Function,
521 );
522 helper.add_contains_edge(endpoint_id, handler_id);
523 }
524 }
525
526 true
527}
528
529fn extract_http_request_info(call_node: Node<'_>, content: &[u8]) -> Option<HttpRequestInfo> {
530 let callee = call_node.child_by_field_name("function")?;
531 let callee_text = callee.utf8_text(content).ok()?.trim().to_string();
532
533 if callee_text == "fetch" {
534 return Some(extract_fetch_http_info(call_node, content));
535 }
536
537 if callee_text == "axios" {
538 return extract_axios_http_info(call_node, content);
539 }
540
541 if let Some(method_name) = callee_text.strip_prefix("axios.") {
542 let method = http_method_from_name(method_name)?;
543 let url = extract_first_arg_url(call_node, content);
544 return Some(HttpRequestInfo { method, url });
545 }
546
547 None
548}
549
550fn extract_fetch_http_info(call_node: Node<'_>, content: &[u8]) -> HttpRequestInfo {
551 let url = extract_first_arg_url(call_node, content);
552 let method = extract_method_from_options(call_node, content).unwrap_or(HttpMethod::Get);
553 HttpRequestInfo { method, url }
554}
555
556fn extract_axios_http_info(call_node: Node<'_>, content: &[u8]) -> Option<HttpRequestInfo> {
557 let args = call_node.child_by_field_name("arguments")?;
558 let mut cursor = args.walk();
559 let mut non_trivia = args
560 .children(&mut cursor)
561 .filter(|child| !matches!(child.kind(), "(" | ")" | ","));
562
563 let first_arg = non_trivia.next()?;
564 let second_arg = non_trivia.next();
565
566 if first_arg.kind() == "object" {
567 let (method, url) = extract_method_and_url_from_object(first_arg, content);
568 return Some(HttpRequestInfo {
569 method: method.unwrap_or(HttpMethod::Get),
570 url,
571 });
572 }
573
574 let url = extract_string_literal(&first_arg, content);
575 let method = if let Some(config) = second_arg {
576 if config.kind() == "object" {
577 extract_method_from_object(config, content)
578 } else {
579 None
580 }
581 } else {
582 None
583 };
584
585 Some(HttpRequestInfo {
586 method: method.unwrap_or(HttpMethod::Get),
587 url,
588 })
589}
590
591fn extract_first_arg_url(call_node: Node<'_>, content: &[u8]) -> Option<String> {
592 let args = call_node.child_by_field_name("arguments")?;
593 let mut cursor = args.walk();
594 let first_arg = args
595 .children(&mut cursor)
596 .find(|child| !matches!(child.kind(), "(" | ")" | ","))?;
597 extract_string_literal(&first_arg, content)
598}
599
600fn extract_method_from_options(call_node: Node<'_>, content: &[u8]) -> Option<HttpMethod> {
601 let args = call_node.child_by_field_name("arguments")?;
602 let mut cursor = args.walk();
603 let mut non_trivia = args
604 .children(&mut cursor)
605 .filter(|child| !matches!(child.kind(), "(" | ")" | ","));
606
607 let _first_arg = non_trivia.next()?;
608 let second_arg = non_trivia.next()?;
609 if second_arg.kind() != "object" {
610 return None;
611 }
612
613 extract_method_from_object(second_arg, content)
614}
615
616fn extract_method_from_object(obj_node: Node<'_>, content: &[u8]) -> Option<HttpMethod> {
617 let (method, _url) = extract_method_and_url_from_object(obj_node, content);
618 method
619}
620
621fn extract_method_and_url_from_object(
622 obj_node: Node<'_>,
623 content: &[u8],
624) -> (Option<HttpMethod>, Option<String>) {
625 let mut method = None;
626 let mut url = None;
627 let mut cursor = obj_node.walk();
628
629 for child in obj_node.children(&mut cursor) {
630 if child.kind() != "pair" {
631 continue;
632 }
633
634 let Some(key_node) = child.child_by_field_name("key") else {
635 continue;
636 };
637 let key_text = extract_object_key_text(&key_node, content);
638
639 let Some(value_node) = child.child_by_field_name("value") else {
640 continue;
641 };
642
643 if key_text.as_deref() == Some("method") {
644 if let Some(value) = extract_string_literal(&value_node, content) {
645 method = http_method_from_name(&value);
646 }
647 } else if key_text.as_deref() == Some("url") {
648 url = extract_string_literal(&value_node, content);
649 }
650 }
651
652 (method, url)
653}
654
655fn extract_object_key_text(node: &Node<'_>, content: &[u8]) -> Option<String> {
656 let raw = node.utf8_text(content).ok()?.trim().to_string();
657 if let Some(value) = extract_string_literal(node, content) {
658 return Some(value);
659 }
660 if raw.is_empty() {
661 return None;
662 }
663 Some(raw)
664}
665
666fn http_method_from_name(name: &str) -> Option<HttpMethod> {
667 match name.trim().to_ascii_lowercase().as_str() {
668 "get" => Some(HttpMethod::Get),
669 "post" => Some(HttpMethod::Post),
670 "put" => Some(HttpMethod::Put),
671 "delete" => Some(HttpMethod::Delete),
672 "patch" => Some(HttpMethod::Patch),
673 "head" => Some(HttpMethod::Head),
674 "options" => Some(HttpMethod::Options),
675 _ => None,
676 }
677}
678
679fn build_constructor_edge_with_helper(
681 ast_graph: &ASTGraph,
682 new_node: Node<'_>,
683 content: &[u8],
684 helper: &mut GraphBuildHelper,
685) -> GraphResult<Option<ConstructorEdgeData>> {
686 let module_context;
688 let call_context = if let Some(ctx) = ast_graph.get_callable_context(new_node.id()) {
689 ctx
690 } else {
691 module_context = CallContext {
692 qualified_name: "<module>".to_string(),
693 span: (0, content.len()),
694 is_async: false,
695 };
696 &module_context
697 };
698
699 let Some(constructor_expr) = new_node.child_by_field_name("constructor") else {
700 return Ok(None);
701 };
702
703 let constructor_text = constructor_expr
704 .utf8_text(content)
705 .map_err(|_| GraphBuilderError::ParseError {
706 span: span_from_node(new_node),
707 reason: "failed to read constructor expression".to_string(),
708 })?
709 .trim()
710 .to_string();
711
712 if constructor_text.is_empty() {
713 return Ok(None);
714 }
715
716 let constructor_simple = simple_name(&constructor_text);
717 let source_id = ensure_caller_node(helper, call_context);
718 let new_site_span = span_from_node(new_node);
719 let target_id =
720 helper.ensure_callee(constructor_simple, new_site_span, CalleeKindHint::Function);
721
722 let span = Some(new_site_span);
723 let argument_count = u8::try_from(count_arguments(new_node)).unwrap_or(u8::MAX);
724
725 Ok(Some((source_id, target_id, argument_count, span)))
726}
727
728fn build_import_edge_with_helper(
730 import_node: Node<'_>,
731 content: &[u8],
732 file: &Arc<str>,
733 helper: &mut GraphBuildHelper,
734) -> GraphResult<
735 Option<(
736 sqry_core::graph::unified::NodeId,
737 sqry_core::graph::unified::NodeId,
738 )>,
739> {
740 let Some(source_node) = import_node.child_by_field_name("source") else {
741 return Ok(None);
742 };
743
744 let source_text = source_node
745 .utf8_text(content)
746 .map_err(|_| GraphBuilderError::ParseError {
747 span: span_from_node(import_node),
748 reason: "failed to read import source".to_string(),
749 })?
750 .trim()
751 .trim_matches(|c| c == '"' || c == '\'')
752 .to_string();
753
754 if source_text.is_empty() {
755 return Ok(None);
756 }
757
758 let resolved_path =
760 sqry_core::graph::resolve_import_path(std::path::Path::new(file.as_ref()), &source_text)?;
761
762 let from_id = helper.add_module("<module>", None);
764 let to_id = helper.add_import(&resolved_path, Some(span_from_node(import_node)));
765
766 Ok(Some((from_id, to_id)))
767}
768
769#[allow(clippy::too_many_lines)]
780fn build_export_edges_with_helper(
781 export_node: Node<'_>,
782 content: &[u8],
783 file: &Arc<str>,
784 helper: &mut GraphBuildHelper,
785) {
786 let module_id = helper.add_module("<module>", None);
788
789 let source_node = export_node.child_by_field_name("source");
791 let is_reexport = source_node.is_some();
792
793 let has_default = export_node
795 .children(&mut export_node.walk())
796 .any(|child| child.kind() == "default");
797
798 let namespace_export = export_node
800 .children(&mut export_node.walk())
801 .find(|child| child.kind() == "namespace_export");
802
803 let has_wildcard = export_node
805 .children(&mut export_node.walk())
806 .any(|child| child.kind() == "*");
807
808 let export_clause = export_node
810 .children(&mut export_node.walk())
811 .find(|child| child.kind() == "export_clause");
812
813 let declaration = export_node.children(&mut export_node.walk()).find(|child| {
815 matches!(
816 child.kind(),
817 "function_declaration"
818 | "class_declaration"
819 | "lexical_declaration"
820 | "variable_declaration"
821 | "generator_function_declaration"
822 )
823 });
824
825 if has_default {
826 let exported_name = if let Some(ref decl) = declaration {
829 decl.child_by_field_name("name")
831 .and_then(|n| n.utf8_text(content).ok())
832 .map_or_else(|| "default".to_string(), |s| s.trim().to_string())
833 } else {
834 export_node
836 .children(&mut export_node.walk())
837 .find(|child| child.kind() == "identifier")
838 .and_then(|n| n.utf8_text(content).ok())
839 .map_or_else(|| "default".to_string(), |s| s.trim().to_string())
840 };
841
842 let exported_id = helper.add_function(&exported_name, None, false, false);
843 if declaration
844 .as_ref()
845 .is_some_and(|decl| matches!(decl.kind(), "function_declaration" | "class_declaration"))
846 {
847 helper.mark_definition(exported_id);
848 }
849 helper.add_export_edge_full(module_id, exported_id, ExportKind::Default, None);
850 } else if let Some(ns_export) = namespace_export {
851 let alias = ns_export
854 .children(&mut ns_export.walk())
855 .find(|child| child.kind() == "identifier")
856 .and_then(|n| n.utf8_text(content).ok())
857 .map(|s| s.trim().to_string());
858
859 let source_path = source_node
861 .and_then(|s| s.utf8_text(content).ok())
862 .map_or_else(
863 || "<unknown>".to_string(),
864 |s| s.trim().trim_matches(|c| c == '"' || c == '\'').to_string(),
865 );
866
867 let resolved_path = sqry_core::graph::resolve_import_path(
868 std::path::Path::new(file.as_ref()),
869 &source_path,
870 )
871 .unwrap_or(source_path);
872
873 let source_module_id = helper.add_module(&resolved_path, None);
874 helper.add_export_edge_full(
875 module_id,
876 source_module_id,
877 ExportKind::Namespace,
878 alias.as_deref(),
879 );
880 } else if has_wildcard && is_reexport {
881 let source_path = source_node
883 .and_then(|s| s.utf8_text(content).ok())
884 .map_or_else(
885 || "<unknown>".to_string(),
886 |s| s.trim().trim_matches(|c| c == '"' || c == '\'').to_string(),
887 );
888
889 let resolved_path = sqry_core::graph::resolve_import_path(
890 std::path::Path::new(file.as_ref()),
891 &source_path,
892 )
893 .unwrap_or(source_path);
894
895 let source_module_id = helper.add_module(&resolved_path, None);
896 helper.add_export_edge_full(module_id, source_module_id, ExportKind::Reexport, None);
898 } else if let Some(clause) = export_clause {
899 let mut cursor = clause.walk();
901 for child in clause.children(&mut cursor) {
902 if child.kind() == "export_specifier" {
903 let identifiers: Vec<_> = child
906 .children(&mut child.walk())
907 .filter(|n| n.kind() == "identifier")
908 .collect();
909
910 if let Some(first_ident) = identifiers.first() {
911 let local_name = first_ident
912 .utf8_text(content)
913 .ok()
914 .map(|s| s.trim().to_string())
915 .unwrap_or_default();
916
917 if local_name.is_empty() {
918 continue;
919 }
920
921 let alias = identifiers.get(1).and_then(|n| {
923 n.utf8_text(content)
924 .ok()
925 .map(|s| s.trim().to_string())
926 .filter(|s| !s.is_empty())
927 });
928
929 let exported_id = helper.add_function(&local_name, None, false, false);
930
931 let kind = if is_reexport {
932 ExportKind::Reexport
933 } else {
934 ExportKind::Direct
935 };
936
937 helper.add_export_edge_full(module_id, exported_id, kind, alias.as_deref());
938 }
939 }
940 }
941 } else if let Some(decl) = declaration {
942 match decl.kind() {
944 "function_declaration" | "generator_function_declaration" => {
945 if let Some(name_node) = decl.child_by_field_name("name")
946 && let Ok(name) = name_node.utf8_text(content)
947 {
948 let name = name.trim().to_string();
949 if !name.is_empty() {
950 let exported_id = helper.add_function(&name, None, false, false);
951 helper.mark_definition(exported_id);
952 helper.add_export_edge_full(
953 module_id,
954 exported_id,
955 ExportKind::Direct,
956 None,
957 );
958 }
959 }
960 }
961 "class_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_class(&name, None);
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 "lexical_declaration" | "variable_declaration" => {
979 let mut cursor = decl.walk();
981 for child in decl.children(&mut cursor) {
982 if child.kind() == "variable_declarator"
983 && let Some(name_node) = child.child_by_field_name("name")
984 && let Ok(name) = name_node.utf8_text(content)
985 {
986 let name = name.trim().to_string();
987 if !name.is_empty() {
988 let exported_id = helper.add_variable(&name, None);
989 helper.mark_definition(exported_id);
990 helper.add_export_edge_full(
991 module_id,
992 exported_id,
993 ExportKind::Direct,
994 None,
995 );
996 }
997 }
998 }
999 }
1000 _ => {}
1001 }
1002 }
1003}
1004
1005fn build_commonjs_export_edges(
1013 expr_stmt_node: Node<'_>,
1014 content: &[u8],
1015 helper: &mut GraphBuildHelper,
1016) {
1017 let Some(assignment) = expr_stmt_node
1019 .children(&mut expr_stmt_node.walk())
1020 .find(|child| child.kind() == "assignment_expression")
1021 else {
1022 return;
1023 };
1024
1025 let Some(left) = assignment.child_by_field_name("left") else {
1026 return;
1027 };
1028 let Some(right) = assignment.child_by_field_name("right") else {
1029 return;
1030 };
1031
1032 let left_text = left.utf8_text(content).ok().map(|s| s.trim().to_string());
1033 let Some(left_text) = left_text else {
1034 return;
1035 };
1036
1037 let module_id = helper.add_module("<module>", None);
1038
1039 if left_text == "module.exports" {
1041 if right.kind() == "object" {
1042 let mut cursor = right.walk();
1044 for child in right.children(&mut cursor) {
1045 if child.kind() == "shorthand_property_identifier" {
1046 if let Ok(name) = child.utf8_text(content) {
1048 let name = name.trim();
1049 if !name.is_empty() {
1050 let exported_id = helper.add_function(name, None, false, false);
1051 helper.add_export_edge_full(
1052 module_id,
1053 exported_id,
1054 ExportKind::Direct,
1055 None,
1056 );
1057 }
1058 }
1059 } else if child.kind() == "pair" {
1060 if let Some(key_node) = child.child_by_field_name("key")
1062 && let Ok(export_name) = key_node.utf8_text(content)
1063 {
1064 let export_name = export_name.trim();
1065 if !export_name.is_empty() {
1066 let exported_id = helper.add_function(export_name, None, false, false);
1067 helper.add_export_edge_full(
1068 module_id,
1069 exported_id,
1070 ExportKind::Direct,
1071 None,
1072 );
1073 }
1074 }
1075 } else if child.kind() == "spread_element" {
1076 }
1078 }
1079 } else if right.kind() == "identifier" || right.kind() == "member_expression" {
1080 let export_name = right
1082 .utf8_text(content)
1083 .ok()
1084 .map_or_else(|| "default".to_string(), |s| s.trim().to_string());
1085
1086 if !export_name.is_empty() {
1087 let exported_id = helper.add_function(&export_name, None, false, false);
1088 helper.add_export_edge_full(module_id, exported_id, ExportKind::Default, None);
1089 }
1090 } else if matches!(
1091 right.kind(),
1092 "function_expression"
1093 | "arrow_function"
1094 | "class"
1095 | "call_expression"
1096 | "new_expression"
1097 ) {
1098 let exported_id = helper.add_function("default", None, false, false);
1100 helper.mark_definition(exported_id);
1101 helper.add_export_edge_full(module_id, exported_id, ExportKind::Default, None);
1102 }
1103 }
1104 else if left_text.starts_with("exports.") || left_text.starts_with("module.exports.") {
1106 let export_name = if let Some(name) = left_text.strip_prefix("module.exports.") {
1108 name
1109 } else if let Some(name) = left_text.strip_prefix("exports.") {
1110 name
1111 } else {
1112 return;
1113 };
1114
1115 if !export_name.is_empty() {
1116 let exported_id = helper.add_function(export_name, None, false, false);
1117 helper.add_export_edge_full(module_id, exported_id, ExportKind::Direct, None);
1118 }
1119 }
1120}
1121
1122fn build_inherits_edge_with_helper(
1129 class_node: Node<'_>,
1130 content: &[u8],
1131 helper: &mut GraphBuildHelper,
1132) {
1133 let heritage = class_node
1135 .children(&mut class_node.walk())
1136 .find(|child| child.kind() == "class_heritage");
1137
1138 let Some(heritage_node) = heritage else {
1139 return; };
1141
1142 let class_name = if class_node.kind() == "class_declaration" {
1144 class_node
1145 .child_by_field_name("name")
1146 .and_then(|n| n.utf8_text(content).ok())
1147 .map(|s| s.trim().to_string())
1148 } else {
1149 class_node
1151 .parent()
1152 .filter(|p| p.kind() == "variable_declarator")
1153 .and_then(|p| p.child_by_field_name("name"))
1154 .and_then(|n| n.utf8_text(content).ok())
1155 .map(|s| s.trim().to_string())
1156 .or_else(|| {
1157 Some(SyntheticNameBuilder::from_node_with_hash(
1159 &class_node,
1160 content,
1161 "class",
1162 ))
1163 })
1164 };
1165
1166 let parent_name = extract_parent_class_name(heritage_node, content);
1169
1170 if let (Some(child_name), Some(parent_name)) = (class_name, parent_name)
1172 && !child_name.is_empty()
1173 && !parent_name.is_empty()
1174 {
1175 let child_id = helper.add_class(&child_name, None);
1176 let parent_id = helper.add_class(&parent_name, None);
1177 helper.add_inherits_edge(child_id, parent_id);
1178 }
1179}
1180
1181fn extract_parent_class_name(heritage_node: Node<'_>, content: &[u8]) -> Option<String> {
1196 let mut cursor = heritage_node.walk();
1197 for child in heritage_node.children(&mut cursor) {
1198 match child.kind() {
1199 "identifier" => {
1200 return child.utf8_text(content).ok().map(|s| s.trim().to_string());
1202 }
1203 "member_expression" => {
1204 return child.utf8_text(content).ok().map(|s| s.trim().to_string());
1207 }
1208 "call_expression" => {
1209 return child.utf8_text(content).ok().map(|s| s.trim().to_string());
1214 }
1215 _ => {}
1216 }
1217 }
1218 None
1219}
1220
1221fn simple_name(name: &str) -> &str {
1222 name.rsplit(['.', '/']).next().unwrap_or(name)
1225}
1226
1227fn normalize_optional_chain(text: &str) -> String {
1231 text.replace("?.", ".")
1232 .trim()
1233 .trim_end_matches('.')
1234 .to_string()
1235}
1236
1237fn check_uses_await(call_node: Node<'_>) -> bool {
1238 let mut current = call_node;
1240 for _ in 0..2 {
1241 if let Some(parent) = current.parent() {
1243 if parent.kind() == "await_expression" {
1244 return true;
1245 }
1246 current = parent;
1247 } else {
1248 break;
1249 }
1250 }
1251 false
1252}
1253
1254fn count_arguments(node: Node<'_>) -> usize {
1255 node.child_by_field_name("arguments").map_or(0, |args| {
1256 let mut count = 0;
1257 let mut cursor = args.walk();
1258 for child in args.children(&mut cursor) {
1259 if !matches!(child.kind(), "(" | ")" | ",") {
1260 count += 1;
1261 }
1262 }
1263 count
1264 })
1265}
1266
1267fn span_from_node(node: Node<'_>) -> Span {
1268 let start = node.start_position();
1269 let end = node.end_position();
1270 Span::new(
1271 Position::new(start.row, start.column),
1272 Position::new(end.row, end.column),
1273 )
1274}
1275
1276fn extract_string_literal(node: &Node, content: &[u8]) -> Option<String> {
1277 let text = node.utf8_text(content).ok()?;
1278 let trimmed = text.trim();
1279
1280 trimmed
1282 .strip_prefix('"')
1283 .and_then(|s| s.strip_suffix('"'))
1284 .or_else(|| {
1285 trimmed
1286 .strip_prefix('\'')
1287 .and_then(|s| s.strip_suffix('\''))
1288 })
1289 .or_else(|| trimmed.strip_prefix('`').and_then(|s| s.strip_suffix('`')))
1290 .map(std::string::ToString::to_string)
1291}
1292
1293#[derive(Debug, Clone)]
1296pub struct CallContext {
1297 pub qualified_name: String,
1298 pub span: (usize, usize),
1299 pub is_async: bool,
1300}
1301
1302impl CallContext {
1303 pub fn qualified_name(&self) -> &str {
1304 &self.qualified_name
1305 }
1306}
1307
1308pub struct ASTGraph {
1309 callable_map: HashMap<usize, usize>,
1311 context_map: HashMap<usize, CallContext>,
1313}
1314
1315impl ASTGraph {
1316 pub fn from_tree(tree: &Tree, content: &[u8], max_scope_depth: usize) -> Result<Self, String> {
1318 let mut builder = ASTGraphBuilder::new(content, max_scope_depth);
1319
1320 let recursion_limits = sqry_core::config::RecursionLimits::load_or_default()
1322 .map_err(|e| format!("Failed to load recursion limits: {e}"))?;
1323 let file_ops_depth = recursion_limits
1324 .effective_file_ops_depth()
1325 .map_err(|e| format!("Invalid file_ops_depth configuration: {e}"))?;
1326 let mut guard = sqry_core::query::security::RecursionGuard::new(file_ops_depth)
1327 .map_err(|e| format!("Failed to create recursion guard: {e}"))?;
1328
1329 builder
1330 .visit(tree.root_node(), None, &mut guard)
1331 .map_err(|e| format!("JavaScript AST traversal hit recursion limit: {e}"))?;
1332 Ok(builder.build())
1333 }
1334
1335 pub fn get_callable_context(&self, node_id: usize) -> Option<&CallContext> {
1337 let callable_id = self.callable_map.get(&node_id)?;
1338 self.context_map.get(callable_id)
1339 }
1340
1341 pub fn contexts(&self) -> impl Iterator<Item = &CallContext> {
1343 self.context_map.values()
1344 }
1345}
1346
1347struct ASTGraphBuilder<'a> {
1348 content: &'a [u8],
1349 max_scope_depth: usize,
1350 callable_map: HashMap<usize, usize>,
1351 context_map: HashMap<usize, CallContext>,
1352 current_scope: Vec<Arc<str>>,
1353}
1354
1355impl<'a> ASTGraphBuilder<'a> {
1356 fn new(content: &'a [u8], max_scope_depth: usize) -> Self {
1357 Self {
1358 content,
1359 max_scope_depth,
1360 callable_map: HashMap::new(),
1361 context_map: HashMap::new(),
1362 current_scope: Vec::new(),
1363 }
1364 }
1365
1366 fn build(self) -> ASTGraph {
1367 ASTGraph {
1368 callable_map: self.callable_map,
1369 context_map: self.context_map,
1370 }
1371 }
1372
1373 fn visit(
1377 &mut self,
1378 node: Node<'_>,
1379 parent_callable: Option<usize>,
1380 guard: &mut sqry_core::query::security::RecursionGuard,
1381 ) -> Result<(), sqry_core::query::security::RecursionError> {
1382 guard.enter()?;
1383
1384 let node_id = node.id();
1385
1386 let callable_name = callable_node_name(node, self.content);
1388
1389 let new_callable = if let Some(name) = callable_name {
1390 let start = node.start_byte();
1392 let end = node.end_byte();
1393 let is_async = is_async_function(node, self.content);
1394
1395 let qualified_name = if self.current_scope.is_empty() {
1396 name.clone()
1397 } else if self.current_scope.len() <= self.max_scope_depth {
1398 format!("{}.{}", self.current_scope.join("."), name)
1399 } else {
1400 let truncated = &self.current_scope[..self.max_scope_depth];
1402 format!("{}.{}", truncated.join("."), name)
1403 };
1404
1405 let context = CallContext {
1406 qualified_name,
1407 span: (start, end),
1408 is_async,
1409 };
1410
1411 self.context_map.insert(node_id, context);
1412 Some(node_id)
1413 } else {
1414 None
1415 };
1416
1417 let effective_callable = new_callable.or(parent_callable);
1419
1420 if let Some(callable_id) = effective_callable {
1422 self.callable_map.insert(node_id, callable_id);
1423 }
1424
1425 let scope_name = scope_node_name(node, self.content);
1427 let pushed_scope = if let Some(name) = scope_name {
1428 self.current_scope.push(Arc::from(name));
1429 true
1430 } else {
1431 false
1432 };
1433
1434 let mut cursor = node.walk();
1436 for child in node.children(&mut cursor) {
1437 self.visit(child, effective_callable, guard)?;
1438 }
1439
1440 if pushed_scope {
1442 self.current_scope.pop();
1443 }
1444
1445 guard.exit();
1446 Ok(())
1447 }
1448}
1449
1450fn callable_node_name(node: Node<'_>, content: &[u8]) -> Option<String> {
1452 match node.kind() {
1453 "function_declaration" | "generator_function_declaration" => node
1454 .child_by_field_name("name")
1455 .and_then(|child| child.utf8_text(content).ok().map(|s| s.trim().to_string())),
1456 "function_expression" | "generator_function" => {
1457 node.child_by_field_name("name")
1459 .and_then(|child| child.utf8_text(content).ok().map(|s| s.trim().to_string()))
1460 .or_else(|| {
1461 Some(SyntheticNameBuilder::from_node_with_hash(
1462 &node, content, "function",
1463 ))
1464 })
1465 }
1466 "arrow_function" => {
1467 if let Some(parent) = node.parent()
1471 && parent.kind() == "variable_declarator"
1472 && let Some(name_node) = parent.child_by_field_name("name")
1473 && let Ok(name) = name_node.utf8_text(content)
1474 {
1475 let trimmed = name.trim();
1476 if !trimmed.is_empty() {
1477 return Some(trimmed.to_string());
1478 }
1479 }
1480 Some(SyntheticNameBuilder::from_node_with_hash(
1483 &node, content, "arrow",
1484 ))
1485 }
1486 "method_definition" => node
1487 .child_by_field_name("name")
1488 .and_then(|child| child.utf8_text(content).ok().map(|s| s.trim().to_string())),
1489 _ => None,
1490 }
1491}
1492
1493fn scope_node_name(node: Node<'_>, content: &[u8]) -> Option<String> {
1494 match node.kind() {
1495 "class_declaration" | "class" => node
1496 .child_by_field_name("name")
1497 .and_then(|child| child.utf8_text(content).ok().map(|s| s.trim().to_string()))
1498 .or_else(|| {
1499 Some(SyntheticNameBuilder::from_node_with_hash(
1500 &node, content, "class",
1501 ))
1502 }),
1503 _ => None,
1504 }
1505}
1506
1507fn is_async_function(node: Node<'_>, _content: &[u8]) -> bool {
1508 let mut cursor = node.walk();
1510 node.children(&mut cursor)
1511 .any(|child| child.kind() == "async")
1512}
1513
1514fn process_jsdoc_annotations(
1519 node: Node,
1520 content: &[u8],
1521 helper: &mut GraphBuildHelper,
1522) -> GraphResult<()> {
1523 match node.kind() {
1525 "function_declaration" | "generator_function_declaration" => {
1526 process_function_jsdoc(node, content, helper)?;
1527 }
1528 "method_definition" => {
1529 process_method_jsdoc(node, content, helper)?;
1530 }
1531 "lexical_declaration" | "variable_declaration" => {
1532 process_variable_jsdoc(node, content, helper)?;
1533 }
1534 "class_declaration" | "class" => {
1535 process_class_fields(node, content, helper)?;
1536 process_constructor_this_assignments(node, content, helper)?;
1537 }
1538 _ => {}
1539 }
1540
1541 let mut cursor = node.walk();
1543 for child in node.children(&mut cursor) {
1544 process_jsdoc_annotations(child, content, helper)?;
1545 }
1546
1547 Ok(())
1548}
1549
1550fn process_function_jsdoc(
1552 func_node: Node,
1553 content: &[u8],
1554 helper: &mut GraphBuildHelper,
1555) -> GraphResult<()> {
1556 let Some(jsdoc_text) = extract_jsdoc_comment(func_node, content) else {
1558 return Ok(());
1559 };
1560
1561 let tags = parse_jsdoc_tags(&jsdoc_text);
1563
1564 let Some(name_node) = func_node.child_by_field_name("name") else {
1566 return Ok(());
1567 };
1568
1569 let function_name = name_node
1570 .utf8_text(content)
1571 .map_err(|_| GraphBuilderError::ParseError {
1572 span: span_from_node(func_node),
1573 reason: "failed to read function name".to_string(),
1574 })?
1575 .trim()
1576 .to_string();
1577
1578 if function_name.is_empty() {
1579 return Ok(());
1580 }
1581
1582 let func_node_id = helper.ensure_callee(
1584 &function_name,
1585 span_from_node(func_node),
1586 CalleeKindHint::Function,
1587 );
1588
1589 let ast_params = extract_ast_parameters(func_node, content);
1592 let ast_param_map: HashMap<&str, usize> = ast_params
1593 .iter()
1594 .map(|(idx, name)| (name.as_str(), *idx))
1595 .collect();
1596
1597 for param_tag in &tags.params {
1599 let mut normalized_name = param_tag
1602 .name
1603 .trim_start_matches("...")
1604 .trim_matches(|c| c == '[' || c == ']');
1605
1606 if let Some(base_name) = normalized_name.split('.').next() {
1609 normalized_name = base_name;
1610 }
1611
1612 let Some(&ast_index) = ast_param_map.get(normalized_name) else {
1613 continue;
1615 };
1616
1617 let canonical_type = canonical_type_string(¶m_tag.type_str);
1619 let type_node_id = helper.add_type(&canonical_type, None);
1620 helper.add_typeof_edge_with_context(
1621 func_node_id,
1622 type_node_id,
1623 Some(TypeOfContext::Parameter),
1624 ast_index.try_into().ok(), Some(¶m_tag.name),
1626 );
1627
1628 let type_names = extract_type_names(¶m_tag.type_str);
1630 for type_name in type_names {
1631 let ref_type_id = helper.add_type(&type_name, None);
1632 helper.add_reference_edge(func_node_id, ref_type_id);
1633 }
1634 }
1635
1636 if let Some(return_type) = &tags.returns {
1638 let canonical_type = canonical_type_string(return_type);
1639 let type_node_id = helper.add_type(&canonical_type, None);
1640 helper.add_typeof_edge_with_context(
1641 func_node_id,
1642 type_node_id,
1643 Some(TypeOfContext::Return),
1644 Some(0),
1645 None,
1646 );
1647
1648 let type_names = extract_type_names(return_type);
1650 for type_name in type_names {
1651 let ref_type_id = helper.add_type(&type_name, None);
1652 helper.add_reference_edge(func_node_id, ref_type_id);
1653 }
1654 }
1655
1656 Ok(())
1657}
1658
1659fn process_method_jsdoc(
1661 method_node: Node,
1662 content: &[u8],
1663 helper: &mut GraphBuildHelper,
1664) -> GraphResult<()> {
1665 let Some(jsdoc_text) = extract_jsdoc_comment(method_node, content) else {
1667 return Ok(());
1668 };
1669
1670 let tags = parse_jsdoc_tags(&jsdoc_text);
1672
1673 let Some(name_node) = method_node.child_by_field_name("name") else {
1675 return Ok(());
1676 };
1677
1678 let method_name = name_node
1679 .utf8_text(content)
1680 .map_err(|_| GraphBuilderError::ParseError {
1681 span: span_from_node(method_node),
1682 reason: "failed to read method name".to_string(),
1683 })?
1684 .trim()
1685 .to_string();
1686
1687 if method_name.is_empty() {
1688 return Ok(());
1689 }
1690
1691 let class_name = get_enclosing_class_name(method_node, content)?;
1693 let Some(class_name) = class_name else {
1694 return Ok(());
1695 };
1696
1697 let qualified_name = format!("{class_name}.{method_name}");
1699
1700 let method_node_id = helper.ensure_method(&qualified_name, None, false, false);
1703
1704 let ast_params = extract_ast_parameters(method_node, content);
1707 let ast_param_map: HashMap<&str, usize> = ast_params
1708 .iter()
1709 .map(|(idx, name)| (name.as_str(), *idx))
1710 .collect();
1711
1712 for param_tag in &tags.params {
1714 let mut normalized_name = param_tag
1717 .name
1718 .trim_start_matches("...")
1719 .trim_matches(|c| c == '[' || c == ']');
1720
1721 if let Some(base_name) = normalized_name.split('.').next() {
1724 normalized_name = base_name;
1725 }
1726
1727 let Some(&ast_index) = ast_param_map.get(normalized_name) else {
1728 continue;
1730 };
1731
1732 let canonical_type = canonical_type_string(¶m_tag.type_str);
1733 let type_node_id = helper.add_type(&canonical_type, None);
1734 helper.add_typeof_edge_with_context(
1735 method_node_id,
1736 type_node_id,
1737 Some(TypeOfContext::Parameter),
1738 ast_index.try_into().ok(), Some(¶m_tag.name),
1740 );
1741
1742 let type_names = extract_type_names(¶m_tag.type_str);
1744 for type_name in type_names {
1745 let ref_type_id = helper.add_type(&type_name, None);
1746 helper.add_reference_edge(method_node_id, ref_type_id);
1747 }
1748 }
1749
1750 if let Some(return_type) = &tags.returns {
1752 let canonical_type = canonical_type_string(return_type);
1753 let type_node_id = helper.add_type(&canonical_type, None);
1754 helper.add_typeof_edge_with_context(
1755 method_node_id,
1756 type_node_id,
1757 Some(TypeOfContext::Return),
1758 Some(0),
1759 None,
1760 );
1761
1762 let type_names = extract_type_names(return_type);
1764 for type_name in type_names {
1765 let ref_type_id = helper.add_type(&type_name, None);
1766 helper.add_reference_edge(method_node_id, ref_type_id);
1767 }
1768 }
1769
1770 Ok(())
1771}
1772
1773fn process_variable_jsdoc(
1775 decl_node: Node,
1776 content: &[u8],
1777 helper: &mut GraphBuildHelper,
1778) -> GraphResult<()> {
1779 if !is_top_level_variable(decl_node) {
1781 return Ok(());
1782 }
1783
1784 let Some(jsdoc_text) = extract_jsdoc_comment(decl_node, content) else {
1786 return Ok(());
1787 };
1788
1789 let tags = parse_jsdoc_tags(&jsdoc_text);
1791
1792 let Some(type_annotation) = &tags.type_annotation else {
1794 return Ok(());
1795 };
1796
1797 let mut cursor = decl_node.walk();
1799 for child in decl_node.children(&mut cursor) {
1800 if child.kind() == "variable_declarator"
1801 && let Some(name_node) = child.child_by_field_name("name")
1802 {
1803 let var_name = name_node
1804 .utf8_text(content)
1805 .map_err(|_| GraphBuilderError::ParseError {
1806 span: span_from_node(child),
1807 reason: "failed to read variable name".to_string(),
1808 })?
1809 .trim()
1810 .to_string();
1811
1812 if !var_name.is_empty() {
1813 let var_node_id = helper.add_variable(&var_name, None);
1815
1816 let canonical_type = canonical_type_string(type_annotation);
1818 let type_node_id = helper.add_type(&canonical_type, None);
1819 helper.add_typeof_edge_with_context(
1820 var_node_id,
1821 type_node_id,
1822 Some(TypeOfContext::Variable),
1823 None,
1824 None,
1825 );
1826
1827 let type_names = extract_type_names(type_annotation);
1829 for type_name in type_names {
1830 let ref_type_id = helper.add_type(&type_name, None);
1831 helper.add_reference_edge(var_node_id, ref_type_id);
1832 }
1833 }
1834 }
1835 }
1836
1837 Ok(())
1838}
1839
1840fn resolve_class_name_for_fields(
1851 class_node: Node<'_>,
1852 content: &[u8],
1853) -> GraphResult<Option<String>> {
1854 if let Some(name_node) = class_node.child_by_field_name("name") {
1855 let name = name_node
1856 .utf8_text(content)
1857 .map_err(|_| GraphBuilderError::ParseError {
1858 span: span_from_node(class_node),
1859 reason: "failed to read class name".to_string(),
1860 })?
1861 .trim()
1862 .to_string();
1863 if name.is_empty() {
1864 return Ok(None);
1865 }
1866 return Ok(Some(name));
1867 }
1868
1869 let Some(parent) = class_node.parent() else {
1871 return Ok(None);
1872 };
1873
1874 match parent.kind() {
1875 "variable_declarator" => {
1876 if let Some(name_node) = parent.child_by_field_name("name")
1877 && let Ok(var_name) = name_node.utf8_text(content)
1878 {
1879 let var_name = var_name.trim().to_string();
1880 if var_name.is_empty() {
1881 return Ok(None);
1882 }
1883 return Ok(Some(var_name));
1884 }
1885 Ok(None)
1886 }
1887 "assignment_expression" => {
1888 if let Some(left) = parent.child_by_field_name("left")
1889 && let Ok(assign_name) = left.utf8_text(content)
1890 {
1891 let assign_name = assign_name.trim().to_string();
1892 if assign_name.is_empty() {
1893 return Ok(None);
1894 }
1895 return Ok(Some(assign_name));
1896 }
1897 Ok(None)
1898 }
1899 _ => Ok(None),
1900 }
1901}
1902
1903fn process_class_fields(
1919 class_node: Node<'_>,
1920 content: &[u8],
1921 helper: &mut GraphBuildHelper,
1922) -> GraphResult<()> {
1923 let Some(class_name) = resolve_class_name_for_fields(class_node, content)? else {
1924 return Ok(());
1925 };
1926
1927 let Some(body_node) = class_node.child_by_field_name("body") else {
1928 return Ok(());
1929 };
1930
1931 let mut cursor = body_node.walk();
1932 for child in body_node.children(&mut cursor) {
1933 if child.kind() != "field_definition" {
1934 continue;
1935 }
1936 emit_class_field_node(child, content, helper, &class_name)?;
1937 }
1938
1939 Ok(())
1940}
1941
1942fn emit_class_field_node(
1945 field_node: Node<'_>,
1946 content: &[u8],
1947 helper: &mut GraphBuildHelper,
1948 class_name: &str,
1949) -> GraphResult<()> {
1950 let Some(name_node) = field_node.child_by_field_name("property") else {
1954 return Ok(());
1955 };
1956
1957 let raw_name = name_node
1958 .utf8_text(content)
1959 .map_err(|_| GraphBuilderError::ParseError {
1960 span: span_from_node(field_node),
1961 reason: "failed to read field name".to_string(),
1962 })?
1963 .trim()
1964 .to_string();
1965
1966 if raw_name.is_empty() {
1967 return Ok(());
1968 }
1969
1970 let is_hash_private = name_node.kind() == "private_property_identifier";
1971
1972 let mut is_static = false;
1977 let mut mod_cursor = field_node.walk();
1978 for modifier in field_node.children(&mut mod_cursor) {
1979 if modifier.kind() == "static" {
1980 is_static = true;
1981 }
1982 }
1983
1984 let visibility: Option<&str> = if is_hash_private {
1990 Some("private")
1991 } else {
1992 Some("public")
1993 };
1994
1995 let qualified_name = format!("{class_name}.{raw_name}");
1996 let span = Some(span_from_node(field_node));
1997
1998 let field_id = helper.add_property_with_static_and_visibility(
1999 &qualified_name,
2000 span,
2001 is_static,
2002 visibility,
2003 );
2004
2005 if let Some(jsdoc_text) = extract_jsdoc_comment(field_node, content) {
2009 let tags = parse_jsdoc_tags(&jsdoc_text);
2010 if let Some(type_annotation) = &tags.type_annotation {
2011 let canonical_type = canonical_type_string(type_annotation);
2012 let type_node_id = helper.add_type(&canonical_type, None);
2013 helper.add_typeof_edge_with_context(
2014 field_id,
2015 type_node_id,
2016 Some(TypeOfContext::Field),
2017 None,
2018 Some(&raw_name),
2019 );
2020
2021 let type_names = extract_type_names(type_annotation);
2022 for type_name in type_names {
2023 let ref_type_id = helper.add_type(&type_name, None);
2024 helper.add_reference_edge(field_id, ref_type_id);
2025 }
2026 }
2027 }
2028
2029 Ok(())
2030}
2031
2032fn process_constructor_this_assignments(
2044 class_node: Node<'_>,
2045 content: &[u8],
2046 helper: &mut GraphBuildHelper,
2047) -> GraphResult<()> {
2048 let Some(class_name) = resolve_class_name_for_fields(class_node, content)? else {
2049 return Ok(());
2050 };
2051
2052 let Some(body_node) = class_node.child_by_field_name("body") else {
2053 return Ok(());
2054 };
2055
2056 let mut cursor = body_node.walk();
2057 for child in body_node.children(&mut cursor) {
2058 if child.kind() != "method_definition" {
2059 continue;
2060 }
2061
2062 let Some(name_node) = child.child_by_field_name("name") else {
2067 continue;
2068 };
2069 let Ok(method_name) = name_node.utf8_text(content) else {
2070 continue;
2071 };
2072 if method_name.trim() != "constructor" {
2073 continue;
2074 }
2075
2076 let Some(method_body) = child.child_by_field_name("body") else {
2077 continue;
2078 };
2079
2080 walk_for_this_assignments(method_body, content, helper, &class_name);
2081 }
2082
2083 Ok(())
2084}
2085
2086fn walk_for_this_assignments(
2090 node: Node<'_>,
2091 content: &[u8],
2092 helper: &mut GraphBuildHelper,
2093 class_name: &str,
2094) {
2095 if node.kind() == "assignment_expression"
2096 && let Some(left) = node.child_by_field_name("left")
2097 && left.kind() == "member_expression"
2098 && let Some(object) = left.child_by_field_name("object")
2099 && object.kind() == "this"
2100 && let Some(property) = left.child_by_field_name("property")
2101 && property.kind() == "property_identifier"
2102 && let Ok(field_name) = property.utf8_text(content)
2103 {
2104 let field_name = field_name.trim();
2105 if !field_name.is_empty() {
2106 let qualified_name = format!("{class_name}.{field_name}");
2107 let _ = helper.add_property_with_static_and_visibility(
2117 &qualified_name,
2118 Some(span_from_node(left)),
2119 false,
2120 Some("public"),
2121 );
2122 }
2123 }
2124
2125 let mut cursor = node.walk();
2133 for child in node.children(&mut cursor) {
2134 walk_for_this_assignments(child, content, helper, class_name);
2135 }
2136}
2137
2138fn get_enclosing_class_name(method_node: Node, content: &[u8]) -> GraphResult<Option<String>> {
2142 let mut current = method_node;
2144 while let Some(parent) = current.parent() {
2145 if parent.kind() == "class_declaration" {
2146 if let Some(name_node) = parent.child_by_field_name("name") {
2148 let class_name = name_node
2149 .utf8_text(content)
2150 .map_err(|_| GraphBuilderError::ParseError {
2151 span: span_from_node(parent),
2152 reason: "failed to read class name".to_string(),
2153 })?
2154 .trim()
2155 .to_string();
2156
2157 if !class_name.is_empty() {
2158 return Ok(Some(class_name));
2159 }
2160 }
2161 } else if parent.kind() == "class" {
2162 if let Some(grandparent) = parent.parent() {
2165 if grandparent.kind() == "variable_declarator" {
2166 if let Some(name_node) = grandparent.child_by_field_name("name")
2168 && let Ok(var_name) = name_node.utf8_text(content)
2169 {
2170 let var_name = var_name.trim().to_string();
2171 if !var_name.is_empty() {
2172 return Ok(Some(var_name));
2173 }
2174 }
2175 } else if grandparent.kind() == "assignment_expression" {
2176 if let Some(left) = grandparent.child_by_field_name("left")
2178 && let Ok(assign_name) = left.utf8_text(content)
2179 {
2180 let assign_name = assign_name.trim().to_string();
2181 if !assign_name.is_empty() {
2182 return Ok(Some(assign_name));
2183 }
2184 }
2185 }
2186 }
2187 return Ok(None);
2190 }
2191 current = parent;
2192 }
2193 Ok(None)
2194}
2195
2196fn extract_ast_parameters(func_node: Node, content: &[u8]) -> Vec<(usize, String)> {
2199 let Some(params_node) = func_node.child_by_field_name("parameters") else {
2200 return Vec::new();
2201 };
2202
2203 let mut cursor = params_node.walk();
2204 params_node
2205 .named_children(&mut cursor)
2206 .enumerate()
2207 .filter_map(|(ast_index, param)| {
2208 let param_name = match param.kind() {
2210 "identifier" => param
2211 .utf8_text(content)
2212 .ok()
2213 .map(std::string::ToString::to_string),
2214 "required_parameter" | "optional_parameter" => {
2215 param
2217 .child_by_field_name("pattern")
2218 .and_then(|p| p.utf8_text(content).ok())
2219 .map(std::string::ToString::to_string)
2220 }
2221 "rest_pattern" => {
2222 param
2225 .named_child(0)
2226 .and_then(|n| n.utf8_text(content).ok())
2227 .map(|s| s.trim_start_matches("...").to_string())
2228 }
2229 "assignment_pattern" => {
2230 param
2233 .child_by_field_name("left")
2234 .filter(|left| left.kind() == "identifier")
2235 .and_then(|left| left.utf8_text(content).ok())
2236 .map(std::string::ToString::to_string)
2237 }
2238 _ => None,
2239 };
2240
2241 param_name.map(|name| (ast_index, name))
2242 })
2243 .collect()
2244}
2245
2246fn is_top_level_variable(decl_node: Node) -> bool {
2249 let mut current = decl_node;
2250 while let Some(parent) = current.parent() {
2251 match parent.kind() {
2252 "function_declaration"
2254 | "generator_function_declaration"
2255 | "function_expression"
2256 | "arrow_function"
2257 | "method_definition" => return false,
2258
2259 "statement_block" | "if_statement" | "for_statement" | "for_in_statement"
2261 | "for_of_statement" | "while_statement" | "do_statement" | "try_statement"
2262 | "catch_clause" | "finally_clause" | "switch_statement" | "switch_case"
2263 | "switch_default" | "class_body" | "class_static_block" | "with_statement" => {
2264 return false;
2265 }
2266
2267 "program" | "export_statement" => return true,
2270
2271 _ => {}
2272 }
2273 current = parent;
2274 }
2275 true
2276}
2277
2278fn build_ffi_call_edge(
2290 ast_graph: &ASTGraph,
2291 call_node: Node<'_>,
2292 content: &[u8],
2293 helper: &mut GraphBuildHelper,
2294) -> GraphResult<bool> {
2295 let Some(callee_expr) = call_node.child_by_field_name("function") else {
2296 return Ok(false);
2297 };
2298
2299 let callee_text = callee_expr
2300 .utf8_text(content)
2301 .map_err(|_| GraphBuilderError::ParseError {
2302 span: span_from_node(call_node),
2303 reason: "failed to read call expression".to_string(),
2304 })?
2305 .trim();
2306
2307 if callee_text.starts_with("WebAssembly.") {
2309 return Ok(build_webassembly_call_edge(
2310 ast_graph,
2311 call_node,
2312 content,
2313 callee_text,
2314 helper,
2315 ));
2316 }
2317
2318 if callee_text == "require" {
2320 return Ok(build_require_ffi_edge(
2321 ast_graph, call_node, content, helper,
2322 ));
2323 }
2324
2325 if callee_text == "process.dlopen" {
2327 return Ok(build_dlopen_edge(ast_graph, call_node, content, helper));
2328 }
2329
2330 Ok(false)
2331}
2332
2333fn build_ffi_new_edge(
2341 ast_graph: &ASTGraph,
2342 new_node: Node<'_>,
2343 content: &[u8],
2344 helper: &mut GraphBuildHelper,
2345) -> GraphResult<bool> {
2346 let Some(constructor_expr) = new_node.child_by_field_name("constructor") else {
2347 return Ok(false);
2348 };
2349
2350 let constructor_text = constructor_expr
2351 .utf8_text(content)
2352 .map_err(|_| GraphBuilderError::ParseError {
2353 span: span_from_node(new_node),
2354 reason: "failed to read constructor expression".to_string(),
2355 })?
2356 .trim();
2357
2358 if constructor_text == "WebAssembly.Module" || constructor_text == "WebAssembly.Instance" {
2360 return Ok(build_webassembly_constructor_edge(
2361 ast_graph,
2362 new_node,
2363 content,
2364 constructor_text,
2365 helper,
2366 ));
2367 }
2368
2369 Ok(false)
2370}
2371
2372fn build_webassembly_call_edge(
2374 ast_graph: &ASTGraph,
2375 call_node: Node<'_>,
2376 content: &[u8],
2377 callee_text: &str,
2378 helper: &mut GraphBuildHelper,
2379) -> bool {
2380 let method_name = callee_text
2382 .strip_prefix("WebAssembly.")
2383 .unwrap_or(callee_text);
2384
2385 let is_wasm_load = matches!(
2387 method_name,
2388 "instantiate" | "instantiateStreaming" | "compile" | "compileStreaming" | "validate"
2389 );
2390
2391 if !is_wasm_load {
2392 return false;
2393 }
2394
2395 let caller_id = get_caller_node_id(ast_graph, call_node, content, helper);
2397
2398 let wasm_module_name = extract_wasm_module_name(call_node, content)
2400 .unwrap_or_else(|| format!("wasm::{method_name}"));
2401
2402 let wasm_node_id = helper.add_module(&wasm_module_name, Some(span_from_node(call_node)));
2404
2405 helper.add_webassembly_edge(caller_id, wasm_node_id);
2407
2408 true
2409}
2410
2411fn build_webassembly_constructor_edge(
2413 ast_graph: &ASTGraph,
2414 new_node: Node<'_>,
2415 content: &[u8],
2416 constructor_text: &str,
2417 helper: &mut GraphBuildHelper,
2418) -> bool {
2419 let caller_id = get_caller_node_id(ast_graph, new_node, content, helper);
2421
2422 let type_name = constructor_text
2424 .strip_prefix("WebAssembly.")
2425 .unwrap_or(constructor_text);
2426 let wasm_module_name = format!("wasm::{type_name}");
2427
2428 let wasm_node_id = helper.add_module(&wasm_module_name, Some(span_from_node(new_node)));
2430
2431 helper.add_webassembly_edge(caller_id, wasm_node_id);
2433
2434 true
2435}
2436
2437fn build_require_ffi_edge(
2442 ast_graph: &ASTGraph,
2443 call_node: Node<'_>,
2444 content: &[u8],
2445 helper: &mut GraphBuildHelper,
2446) -> bool {
2447 let Some(args) = call_node.child_by_field_name("arguments") else {
2449 return false;
2450 };
2451
2452 let mut cursor = args.walk();
2453 let first_arg = args
2454 .children(&mut cursor)
2455 .find(|child| !matches!(child.kind(), "(" | ")" | ","));
2456
2457 let Some(arg_node) = first_arg else {
2458 return false;
2459 };
2460
2461 let module_path = extract_string_literal(&arg_node, content);
2463 let Some(path) = module_path else {
2464 return false;
2465 };
2466
2467 let from_id = helper.add_module("<module>", None);
2469
2470 let resolved_path = if path.starts_with('.') {
2472 sqry_core::graph::resolve_import_path(std::path::Path::new(helper.file_path()), &path)
2474 .unwrap_or_else(|_| simple_name(&path).to_string())
2475 } else {
2476 simple_name(&path).to_string()
2478 };
2479
2480 let to_id = helper.add_import(&resolved_path, Some(span_from_node(call_node)));
2481 helper.add_import_edge(from_id, to_id);
2482
2483 let is_native_addon = std::path::Path::new(&path)
2485 .extension()
2486 .is_some_and(|ext| ext.eq_ignore_ascii_case("node"))
2487 || is_known_native_addon(&path);
2488
2489 if is_native_addon {
2490 let caller_id = get_caller_node_id(ast_graph, call_node, content, helper);
2492
2493 let ffi_name = format!("native::{}", simple_name(&path));
2495 let ffi_node_id = helper.add_module(&ffi_name, Some(span_from_node(call_node)));
2496
2497 helper.add_ffi_edge(caller_id, ffi_node_id, FfiConvention::C);
2499 }
2500
2501 true
2502}
2503
2504fn build_dlopen_edge(
2506 ast_graph: &ASTGraph,
2507 call_node: Node<'_>,
2508 content: &[u8],
2509 helper: &mut GraphBuildHelper,
2510) -> bool {
2511 let caller_id = get_caller_node_id(ast_graph, call_node, content, helper);
2513
2514 let module_name = call_node
2516 .child_by_field_name("arguments")
2517 .and_then(|args| {
2518 let mut cursor = args.walk();
2519 args.children(&mut cursor)
2520 .filter(|child| !matches!(child.kind(), "(" | ")" | ","))
2521 .nth(1) })
2523 .and_then(|node| extract_string_literal(&node, content))
2524 .map_or_else(
2525 || "native::dlopen".to_string(),
2526 |path| format!("native::{}", simple_name(&path)),
2527 );
2528
2529 let ffi_node_id = helper.add_module(&module_name, Some(span_from_node(call_node)));
2531
2532 helper.add_ffi_edge(caller_id, ffi_node_id, FfiConvention::C);
2534
2535 true
2536}
2537
2538fn get_caller_node_id(
2540 ast_graph: &ASTGraph,
2541 node: Node<'_>,
2542 content: &[u8],
2543 helper: &mut GraphBuildHelper,
2544) -> sqry_core::graph::unified::NodeId {
2545 let module_context;
2546 let call_context = if let Some(ctx) = ast_graph.get_callable_context(node.id()) {
2547 ctx
2548 } else {
2549 module_context = CallContext {
2550 qualified_name: "<module>".to_string(),
2551 span: (0, content.len()),
2552 is_async: false,
2553 };
2554 &module_context
2555 };
2556
2557 ensure_caller_node(helper, call_context)
2558}
2559
2560fn ensure_caller_node(
2561 helper: &mut GraphBuildHelper,
2562 call_context: &CallContext,
2563) -> sqry_core::graph::unified::NodeId {
2564 let caller_span = Some(Span::from_bytes(call_context.span.0, call_context.span.1));
2565 let qualified_name = call_context.qualified_name();
2566 if qualified_name.contains('.') {
2567 helper.ensure_method(qualified_name, caller_span, call_context.is_async, false)
2568 } else {
2569 helper.ensure_function(qualified_name, caller_span, call_context.is_async, false)
2570 }
2571}
2572
2573fn extract_wasm_module_name(call_node: Node<'_>, content: &[u8]) -> Option<String> {
2579 let args = call_node.child_by_field_name("arguments")?;
2580
2581 let mut cursor = args.walk();
2582 let first_arg = args
2583 .children(&mut cursor)
2584 .find(|child| !matches!(child.kind(), "(" | ")" | ","))?;
2585
2586 if first_arg.kind() == "call_expression"
2588 && let Some(func) = first_arg.child_by_field_name("function")
2589 {
2590 let func_text = func.utf8_text(content).ok()?.trim();
2591 if func_text == "fetch" {
2592 if let Some(fetch_args) = first_arg.child_by_field_name("arguments") {
2594 let mut fetch_cursor = fetch_args.walk();
2595 let url_arg = fetch_args
2596 .children(&mut fetch_cursor)
2597 .find(|child| !matches!(child.kind(), "(" | ")" | ","))?;
2598
2599 if let Some(url) = extract_string_literal(&url_arg, content) {
2600 return Some(format!("wasm::{}", simple_name(&url)));
2601 }
2602 }
2603 }
2604 }
2605
2606 if let Some(path) = extract_string_literal(&first_arg, content) {
2608 return Some(format!("wasm::{}", simple_name(&path)));
2609 }
2610
2611 None
2612}
2613
2614fn is_known_native_addon(package_name: &str) -> bool {
2616 const NATIVE_PACKAGES: &[&str] = &[
2618 "better-sqlite3",
2619 "sqlite3",
2620 "bcrypt",
2621 "sharp",
2622 "canvas",
2623 "node-sass",
2624 "leveldown",
2625 "bufferutil",
2626 "utf-8-validate",
2627 "fsevents",
2628 "cpu-features",
2629 "node-gyp",
2630 "node-pre-gyp",
2631 "prebuild",
2632 "nan",
2633 "node-addon-api",
2634 "ref-napi",
2635 "ffi-napi",
2636 ];
2637
2638 NATIVE_PACKAGES
2639 .iter()
2640 .any(|&pkg| package_name.contains(pkg))
2641}
2642
2643#[cfg(test)]
2644mod shape_tests {
2645 use super::{cf_bucket_for_javascript_kind, javascript_shape_mapping};
2646 use sqry_core::graph::unified::build::shape::{
2647 CfBucket, ShapeBudget, ShapeMapping, compute_shape_descriptor,
2648 };
2649
2650 const SAMPLE: &str = include_str!(concat!(
2651 env!("CARGO_MANIFEST_DIR"),
2652 "/../test-fixtures/shape/reference/sample.js"
2653 ));
2654
2655 fn parse(src: &str) -> tree_sitter::Tree {
2656 let lang: tree_sitter::Language = tree_sitter_javascript::LANGUAGE.into();
2657 let mut p = tree_sitter::Parser::new();
2658 p.set_language(&lang).expect("load javascript grammar");
2659 p.parse(src, None).expect("parse")
2660 }
2661
2662 fn function_named<'t>(tree: &'t tree_sitter::Tree, name: &str) -> tree_sitter::Node<'t> {
2663 let root = tree.root_node();
2664 let mut stack = vec![root];
2665 while let Some(node) = stack.pop() {
2666 if node.kind() == "function_declaration"
2667 && node
2668 .child_by_field_name("name")
2669 .and_then(|n| n.utf8_text(SAMPLE.as_bytes()).ok())
2670 == Some(name)
2671 {
2672 return node;
2673 }
2674 let mut c = node.walk();
2675 for ch in node.children(&mut c) {
2676 stack.push(ch);
2677 }
2678 }
2679 panic!("no function_declaration named {name}");
2680 }
2681
2682 #[test]
2683 fn cf_table_is_non_empty() {
2684 let mapping = javascript_shape_mapping();
2685 let lang: tree_sitter::Language = tree_sitter_javascript::LANGUAGE.into();
2686 let mut covered = 0;
2687 for id in 0..lang.node_kind_count() {
2688 if mapping.cf_bucket(id as u16).is_some() {
2689 covered += 1;
2690 }
2691 }
2692 assert!(
2693 covered >= 10,
2694 "expected many JS CF kinds mapped, got {covered}"
2695 );
2696 }
2697
2698 #[test]
2699 fn histogram_covers_real_control_flow() {
2700 let tree = parse(SAMPLE);
2701 let func = function_named(&tree, "classify");
2702 let d = compute_shape_descriptor(
2703 func,
2704 SAMPLE.as_bytes(),
2705 javascript_shape_mapping(),
2706 &ShapeBudget::default(),
2707 );
2708 assert!(!d.is_unhashable());
2709 for bucket in [
2710 CfBucket::Branch,
2711 CfBucket::Loop,
2712 CfBucket::Match,
2713 CfBucket::Try,
2714 CfBucket::Catch,
2715 CfBucket::Throw,
2716 CfBucket::Return,
2717 CfBucket::BreakContinue,
2718 CfBucket::Call,
2719 CfBucket::Assign,
2720 CfBucket::Closure,
2721 ] {
2722 assert!(
2723 d.cf_histogram[bucket.index()] >= 1,
2724 "classify must exercise {bucket:?}"
2725 );
2726 }
2727 }
2728
2729 #[test]
2730 fn async_body_covers_await() {
2731 let tree = parse(SAMPLE);
2732 let func = function_named(&tree, "fetchValue");
2733 let d = compute_shape_descriptor(
2734 func,
2735 SAMPLE.as_bytes(),
2736 javascript_shape_mapping(),
2737 &ShapeBudget::default(),
2738 );
2739 assert!(d.cf_histogram[CfBucket::Await.index()] >= 1, "await");
2740 }
2741
2742 #[test]
2743 fn signature_shape_reads_arity_defaults_varargs() {
2744 let tree = parse(SAMPLE);
2745 let func = function_named(&tree, "classify");
2746 let mapping = javascript_shape_mapping();
2747 let shape = mapping.signature_shape(func, SAMPLE.as_bytes());
2748 assert_eq!(shape.arity_positional, 2, "values + threshold = 0");
2750 assert!(shape.has_defaults, "threshold = 0");
2751 assert!(shape.has_varargs, "...extra");
2752 assert!(!shape.has_return_annotation, "JS has no return annotation");
2753 }
2754
2755 #[test]
2756 fn unknown_kind_maps_to_none() {
2757 assert!(cf_bucket_for_javascript_kind("program").is_none());
2758 assert!(cf_bucket_for_javascript_kind("identifier").is_none());
2759 }
2760}