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