1use std::{collections::HashMap, path::Path, sync::OnceLock};
2
3use crate::relations::java_common::{PackageResolver, build_member_symbol, build_symbol};
4use crate::relations::local_scopes::{self, JavaScopeTree, ResolutionOutcome};
5use sqry_core::graph::unified::StagingGraph;
6use sqry_core::graph::unified::build::helper::GraphBuildHelper;
7use sqry_core::graph::unified::build::shape::{CfBucket, ShapeMapping};
8use sqry_core::graph::unified::edge::FfiConvention;
9use sqry_core::graph::unified::edge::kind::TypeOfContext;
10use sqry_core::graph::unified::storage::shape::SignatureShape;
11use sqry_core::graph::{GraphBuilder, GraphBuilderError, GraphResult, Language, Span};
12use tree_sitter::{Node, Tree};
13
14const DEFAULT_SCOPE_DEPTH: usize = 4;
15
16const FILE_MODULE_NAME: &str = "<file_module>";
19
20#[derive(Debug, Clone, Copy)]
41pub struct JavaGraphBuilder {
42 max_scope_depth: usize,
43}
44
45impl Default for JavaGraphBuilder {
46 fn default() -> Self {
47 Self {
48 max_scope_depth: DEFAULT_SCOPE_DEPTH,
49 }
50 }
51}
52
53impl JavaGraphBuilder {
54 #[must_use]
55 pub fn new(max_scope_depth: usize) -> Self {
56 Self { max_scope_depth }
57 }
58}
59
60impl GraphBuilder for JavaGraphBuilder {
61 fn build_graph(
62 &self,
63 tree: &Tree,
64 content: &[u8],
65 file: &Path,
66 staging: &mut StagingGraph,
67 ) -> GraphResult<()> {
68 let mut helper = GraphBuildHelper::new(staging, file, Language::Java);
69
70 let ast_graph = ASTGraph::from_tree(tree, content, self.max_scope_depth);
72 let mut scope_tree = local_scopes::build(tree.root_node(), content, Some(file))?;
73
74 for context in ast_graph.contexts() {
76 let qualified_name = context.qualified_name();
77 let span = Span::from_bytes(context.span.0, context.span.1);
78
79 if context.is_constructor {
80 helper.add_method_with_visibility(
81 qualified_name,
82 Some(span),
83 false,
84 false,
85 context.visibility.as_deref(),
86 );
87 } else {
88 let method_id = helper.add_method_with_signature(
90 qualified_name,
91 Some(span),
92 false,
93 context.is_static,
94 context.visibility.as_deref(),
95 context.return_type.as_deref(),
96 );
97
98 if let Some(return_type_text) = context.return_type.as_deref()
107 && return_type_text.trim() != "void"
108 {
109 let type_id = helper.add_type(return_type_text, None);
110 let method_simple_name = qualified_name
111 .rsplit_once('.')
112 .map_or(qualified_name, |(_, simple)| simple);
113 helper.add_typeof_edge_with_context(
114 method_id,
115 type_id,
116 Some(TypeOfContext::Return),
117 Some(0),
118 Some(method_simple_name),
119 );
120 }
121
122 if context.is_native {
124 build_jni_native_method_edge(context, &mut helper);
125 }
126 }
127 }
128
129 add_field_typeof_edges(&ast_graph, &mut helper);
131
132 let root = tree.root_node();
134 walk_tree_for_edges(
135 root,
136 content,
137 &ast_graph,
138 &mut scope_tree,
139 &mut helper,
140 tree,
141 )?;
142
143 Ok(())
144 }
145
146 fn language(&self) -> Language {
147 Language::Java
148 }
149
150 fn shape_mapping(&self) -> Option<&dyn ShapeMapping> {
151 Some(java_shape_mapping())
152 }
153}
154
155pub struct JavaShapeMapping {
162 cf_by_kind_id: Vec<Option<CfBucket>>,
163}
164
165impl JavaShapeMapping {
166 fn build() -> Self {
168 let lang: tree_sitter::Language = tree_sitter_java::LANGUAGE.into();
169 let count = lang.node_kind_count();
170 let mut cf_by_kind_id = vec![None; count];
171 for (id, slot) in cf_by_kind_id.iter_mut().enumerate() {
172 let Ok(kind_id) = u16::try_from(id) else {
173 break;
174 };
175 if !lang.node_kind_is_named(kind_id) {
176 continue;
177 }
178 if let Some(name) = lang.node_kind_for_id(kind_id) {
179 *slot = cf_bucket_for_java_kind(name);
180 }
181 }
182 Self { cf_by_kind_id }
183 }
184}
185
186impl ShapeMapping for JavaShapeMapping {
187 fn cf_bucket(&self, ts_node_kind_id: u16) -> Option<CfBucket> {
188 self.cf_by_kind_id
189 .get(ts_node_kind_id as usize)
190 .copied()
191 .flatten()
192 }
193
194 fn signature_shape(&self, fn_node: Node, _src: &[u8]) -> SignatureShape {
195 let mut shape = SignatureShape::default();
196 if let Some(params) = fn_node.child_by_field_name("parameters") {
197 let mut cursor = params.walk();
198 for child in params.named_children(&mut cursor) {
199 match child.kind() {
200 "formal_parameter" => {
201 shape.arity_positional = shape.arity_positional.saturating_add(1);
202 }
203 "spread_parameter" => {
205 shape.has_varargs = true;
206 shape.arity_positional = shape.arity_positional.saturating_add(1);
207 }
208 _ => {}
209 }
210 }
211 }
212 shape.has_return_annotation = fn_node.child_by_field_name("type").is_some();
215 shape
216 }
217}
218
219fn cf_bucket_for_java_kind(name: &str) -> Option<CfBucket> {
222 let bucket = match name {
223 "if_statement" | "ternary_expression" => CfBucket::Branch,
224 "for_statement" | "enhanced_for_statement" | "while_statement" | "do_statement" => {
225 CfBucket::Loop
226 }
227 "switch_expression" | "switch_block_statement_group" | "switch_label" | "switch_rule" => {
228 CfBucket::Match
229 }
230 "try_statement" => CfBucket::Try,
231 "catch_clause" => CfBucket::Catch,
232 "throw_statement" => CfBucket::Throw,
233 "try_with_resources_statement" | "synchronized_statement" => CfBucket::Resource,
235 "return_statement" => CfBucket::Return,
236 "yield_statement" => CfBucket::Yield,
237 "break_statement" | "continue_statement" => CfBucket::BreakContinue,
238 "method_invocation" | "object_creation_expression" | "explicit_constructor_invocation" => {
239 CfBucket::Call
240 }
241 "local_variable_declaration" | "assignment_expression" => CfBucket::Assign,
242 "lambda_expression" => CfBucket::Closure,
243 _ => return None,
244 };
245 Some(bucket)
246}
247
248#[must_use]
250pub fn java_shape_mapping() -> &'static JavaShapeMapping {
251 static MAPPING: OnceLock<JavaShapeMapping> = OnceLock::new();
252 MAPPING.get_or_init(JavaShapeMapping::build)
253}
254
255#[derive(Debug)]
260struct ASTGraph {
261 contexts: Vec<MethodContext>,
262 field_types: HashMap<String, (String, bool, Option<sqry_core::schema::Visibility>, bool)>,
272 import_map: HashMap<String, String>,
276 has_jna_import: bool,
278 has_panama_import: bool,
280 jna_library_interfaces: Vec<String>,
282}
283
284impl ASTGraph {
285 fn from_tree(tree: &Tree, content: &[u8], max_depth: usize) -> Self {
286 let package_name = PackageResolver::package_from_ast(tree, content);
288
289 let mut contexts = Vec::new();
290 let mut class_stack = Vec::new();
291
292 let recursion_limits = sqry_core::config::RecursionLimits::load_or_default()
294 .expect("Failed to load recursion limits");
295 let file_ops_depth = recursion_limits
296 .effective_file_ops_depth()
297 .expect("Invalid file_ops_depth configuration");
298 let mut guard = sqry_core::query::security::RecursionGuard::new(file_ops_depth)
299 .expect("Failed to create recursion guard");
300
301 if let Err(e) = extract_java_contexts(
302 tree.root_node(),
303 content,
304 &mut contexts,
305 &mut class_stack,
306 package_name.as_deref(),
307 0,
308 max_depth,
309 &mut guard,
310 ) {
311 eprintln!("Warning: Java AST traversal hit recursion limit: {e}");
312 }
313
314 let (field_types, import_map) = extract_field_and_import_types(tree.root_node(), content);
316
317 let (has_jna_import, has_panama_import) = detect_ffi_imports(tree.root_node(), content);
319
320 let jna_library_interfaces = find_jna_library_interfaces(tree.root_node(), content);
322
323 Self {
324 contexts,
325 field_types,
326 import_map,
327 has_jna_import,
328 has_panama_import,
329 jna_library_interfaces,
330 }
331 }
332
333 fn contexts(&self) -> &[MethodContext] {
334 &self.contexts
335 }
336
337 fn find_enclosing(&self, byte_pos: usize) -> Option<&MethodContext> {
339 self.contexts
340 .iter()
341 .filter(|ctx| byte_pos >= ctx.span.0 && byte_pos < ctx.span.1)
342 .max_by_key(|ctx| ctx.depth)
343 }
344}
345
346#[derive(Debug, Clone)]
347#[allow(clippy::struct_excessive_bools)] struct MethodContext {
349 qualified_name: String,
351 span: (usize, usize),
353 depth: usize,
355 is_static: bool,
357 #[allow(dead_code)] is_synchronized: bool,
360 is_constructor: bool,
362 #[allow(dead_code)] is_native: bool,
365 package_name: Option<String>,
367 class_stack: Vec<String>,
369 return_type: Option<String>,
371 visibility: Option<String>,
373}
374
375impl MethodContext {
376 fn qualified_name(&self) -> &str {
377 &self.qualified_name
378 }
379}
380
381fn extract_java_contexts(
390 node: Node,
391 content: &[u8],
392 contexts: &mut Vec<MethodContext>,
393 class_stack: &mut Vec<String>,
394 package_name: Option<&str>,
395 depth: usize,
396 max_depth: usize,
397 guard: &mut sqry_core::query::security::RecursionGuard,
398) -> Result<(), sqry_core::query::security::RecursionError> {
399 guard.enter()?;
400
401 if depth > max_depth {
402 guard.exit();
403 return Ok(());
404 }
405
406 match node.kind() {
407 "class_declaration"
408 | "interface_declaration"
409 | "enum_declaration"
410 | "record_declaration" => {
411 if let Some(name_node) = node.child_by_field_name("name") {
413 let class_name = extract_identifier(name_node, content);
414
415 class_stack.push(class_name.clone());
417
418 if let Some(body_node) = node.child_by_field_name("body") {
420 extract_methods_from_body(
421 body_node,
422 content,
423 class_stack,
424 package_name,
425 contexts,
426 depth + 1,
427 max_depth,
428 guard,
429 )?;
430
431 for i in 0..body_node.child_count() {
433 #[allow(clippy::cast_possible_truncation)]
434 if let Some(child) = body_node.child(i as u32) {
436 extract_java_contexts(
437 child,
438 content,
439 contexts,
440 class_stack,
441 package_name,
442 depth + 1,
443 max_depth,
444 guard,
445 )?;
446 }
447 }
448 }
449
450 class_stack.pop();
452
453 guard.exit();
454 return Ok(());
455 }
456 }
457 _ => {}
458 }
459
460 for i in 0..node.child_count() {
462 #[allow(clippy::cast_possible_truncation)]
463 if let Some(child) = node.child(i as u32) {
465 extract_java_contexts(
466 child,
467 content,
468 contexts,
469 class_stack,
470 package_name,
471 depth,
472 max_depth,
473 guard,
474 )?;
475 }
476 }
477
478 guard.exit();
479 Ok(())
480}
481
482#[allow(clippy::unnecessary_wraps)]
486fn extract_methods_from_body(
487 body_node: Node,
488 content: &[u8],
489 class_stack: &[String],
490 package_name: Option<&str>,
491 contexts: &mut Vec<MethodContext>,
492 depth: usize,
493 _max_depth: usize,
494 _guard: &mut sqry_core::query::security::RecursionGuard,
495) -> Result<(), sqry_core::query::security::RecursionError> {
496 for i in 0..body_node.child_count() {
497 #[allow(clippy::cast_possible_truncation)]
498 if let Some(child) = body_node.child(i as u32) {
500 match child.kind() {
501 "method_declaration" => {
502 if let Some(method_context) =
503 extract_method_context(child, content, class_stack, package_name, depth)
504 {
505 contexts.push(method_context);
506 }
507 }
508 "constructor_declaration" | "compact_constructor_declaration" => {
509 let constructor_context = extract_constructor_context(
510 child,
511 content,
512 class_stack,
513 package_name,
514 depth,
515 );
516 contexts.push(constructor_context);
517 }
518 _ => {}
519 }
520 }
521 }
522 Ok(())
523}
524
525fn extract_method_context(
526 method_node: Node,
527 content: &[u8],
528 class_stack: &[String],
529 package_name: Option<&str>,
530 depth: usize,
531) -> Option<MethodContext> {
532 let name_node = method_node.child_by_field_name("name")?;
533 let method_name = extract_identifier(name_node, content);
534
535 let is_static = has_modifier(method_node, "static", content);
536 let is_synchronized = has_modifier(method_node, "synchronized", content);
537 let is_native = has_modifier(method_node, "native", content);
538 let visibility = extract_visibility(method_node, content);
539
540 let return_type = method_node
543 .child_by_field_name("type")
544 .map(|type_node| extract_full_return_type(type_node, content));
545
546 let qualified_name = build_member_symbol(package_name, class_stack, &method_name);
548
549 Some(MethodContext {
550 qualified_name,
551 span: (method_node.start_byte(), method_node.end_byte()),
552 depth,
553 is_static,
554 is_synchronized,
555 is_constructor: false,
556 is_native,
557 package_name: package_name.map(std::string::ToString::to_string),
558 class_stack: class_stack.to_vec(),
559 return_type,
560 visibility,
561 })
562}
563
564fn extract_constructor_context(
565 constructor_node: Node,
566 content: &[u8],
567 class_stack: &[String],
568 package_name: Option<&str>,
569 depth: usize,
570) -> MethodContext {
571 let qualified_name = build_member_symbol(package_name, class_stack, "<init>");
573 let visibility = extract_visibility(constructor_node, content);
574
575 MethodContext {
576 qualified_name,
577 span: (constructor_node.start_byte(), constructor_node.end_byte()),
578 depth,
579 is_static: false,
580 is_synchronized: false,
581 is_constructor: true,
582 is_native: false,
583 package_name: package_name.map(std::string::ToString::to_string),
584 class_stack: class_stack.to_vec(),
585 return_type: None, visibility,
587 }
588}
589
590fn walk_tree_for_edges(
596 node: Node,
597 content: &[u8],
598 ast_graph: &ASTGraph,
599 scope_tree: &mut JavaScopeTree,
600 helper: &mut GraphBuildHelper,
601 tree: &Tree,
602) -> GraphResult<()> {
603 match node.kind() {
604 "class_declaration"
605 | "interface_declaration"
606 | "enum_declaration"
607 | "record_declaration" => {
608 return handle_type_declaration(node, content, ast_graph, scope_tree, helper, tree);
610 }
611 "method_declaration" | "constructor_declaration" => {
612 handle_method_declaration_parameters(node, content, ast_graph, scope_tree, helper);
614
615 if node.kind() == "method_declaration"
617 && let Some((http_method, path)) = extract_spring_route_info(node, content)
618 {
619 let full_path =
621 if let Some(class_prefix) = extract_class_request_mapping_path(node, content) {
622 let prefix = class_prefix.trim_end_matches('/');
623 let suffix = path.trim_start_matches('/');
624 if suffix.is_empty() {
625 class_prefix
626 } else {
627 format!("{prefix}/{suffix}")
628 }
629 } else {
630 path
631 };
632 let qualified_name = format!("route::{http_method}::{full_path}");
633 let span = Span::from_bytes(node.start_byte(), node.end_byte());
634 let endpoint_id = helper.add_endpoint(&qualified_name, Some(span));
635
636 let byte_pos = node.start_byte();
638 if let Some(context) = ast_graph.find_enclosing(byte_pos) {
639 let method_id = helper.ensure_method(
640 context.qualified_name(),
641 Some(Span::from_bytes(context.span.0, context.span.1)),
642 false,
643 context.is_static,
644 );
645 helper.add_contains_edge(endpoint_id, method_id);
646 }
647 }
648 }
649 "compact_constructor_declaration" => {
650 handle_compact_constructor_parameters(node, content, ast_graph, scope_tree, helper);
651 }
652 "method_invocation" => {
653 handle_method_invocation(node, content, ast_graph, helper);
654 }
655 "object_creation_expression" => {
656 handle_constructor_call(node, content, ast_graph, helper);
657 }
658 "import_declaration" => {
659 handle_import_declaration(node, content, helper);
660 }
661 "local_variable_declaration" => {
662 handle_local_variable_declaration(node, content, ast_graph, scope_tree, helper);
663 }
664 "enhanced_for_statement" => {
665 handle_enhanced_for_declaration(node, content, ast_graph, scope_tree, helper);
666 }
667 "catch_clause" => {
668 handle_catch_parameter_declaration(node, content, ast_graph, scope_tree, helper);
669 }
670 "lambda_expression" => {
671 handle_lambda_parameter_declaration(node, content, ast_graph, scope_tree, helper);
672 }
673 "try_with_resources_statement" => {
674 handle_try_with_resources_declaration(node, content, ast_graph, scope_tree, helper);
675 }
676 "instanceof_expression" => {
677 handle_instanceof_pattern_declaration(node, content, ast_graph, scope_tree, helper);
678 }
679 "switch_label" => {
680 handle_switch_pattern_declaration(node, content, ast_graph, scope_tree, helper);
681 }
682 "identifier" => {
683 handle_identifier_for_reference(node, content, ast_graph, scope_tree, helper);
684 }
685 _ => {}
686 }
687
688 for i in 0..node.child_count() {
690 #[allow(clippy::cast_possible_truncation)]
691 if let Some(child) = node.child(i as u32) {
693 walk_tree_for_edges(child, content, ast_graph, scope_tree, helper, tree)?;
694 }
695 }
696
697 Ok(())
698}
699
700fn handle_type_declaration(
701 node: Node,
702 content: &[u8],
703 ast_graph: &ASTGraph,
704 scope_tree: &mut JavaScopeTree,
705 helper: &mut GraphBuildHelper,
706 tree: &Tree,
707) -> GraphResult<()> {
708 let Some(name_node) = node.child_by_field_name("name") else {
709 return Ok(());
710 };
711 let class_name = extract_identifier(name_node, content);
712 let span = Span::from_bytes(node.start_byte(), node.end_byte());
713
714 let package = PackageResolver::package_from_ast(tree, content);
715 let class_stack = extract_declaration_class_stack(node, content);
716 let qualified_name = qualify_class_name(&class_name, &class_stack, package.as_deref());
717 let class_node_id = add_type_node(helper, node.kind(), &qualified_name, span);
718
719 if is_public(node, content) {
720 export_from_file_module(helper, class_node_id);
721 }
722
723 process_inheritance(node, content, package.as_deref(), class_node_id, helper);
724 if node.kind() == "class_declaration" {
725 process_implements(node, content, package.as_deref(), class_node_id, helper);
726 }
727 if node.kind() == "interface_declaration" {
728 process_interface_extends(node, content, package.as_deref(), class_node_id, helper);
729 }
730
731 process_type_parameter_declarations(node, content, &qualified_name, helper);
735
736 if let Some(body_node) = node.child_by_field_name("body") {
737 let is_interface = node.kind() == "interface_declaration";
738 process_class_member_exports(body_node, content, &qualified_name, helper, is_interface);
739
740 for i in 0..body_node.child_count() {
741 #[allow(clippy::cast_possible_truncation)]
742 if let Some(child) = body_node.child(i as u32) {
744 walk_tree_for_edges(child, content, ast_graph, scope_tree, helper, tree)?;
745 }
746 }
747 }
748
749 Ok(())
750}
751
752fn extract_declaration_class_stack(node: Node, content: &[u8]) -> Vec<String> {
753 let mut class_stack = Vec::new();
754 let mut current_node = Some(node);
755
756 while let Some(current) = current_node {
757 if matches!(
758 current.kind(),
759 "class_declaration"
760 | "interface_declaration"
761 | "enum_declaration"
762 | "record_declaration"
763 ) && let Some(name_node) = current.child_by_field_name("name")
764 {
765 class_stack.push(extract_identifier(name_node, content));
766 }
767
768 current_node = current.parent();
769 }
770
771 class_stack.reverse();
772 class_stack
773}
774
775fn qualify_class_name(class_name: &str, class_stack: &[String], package: Option<&str>) -> String {
776 let scope = class_stack
777 .split_last()
778 .map_or(&[][..], |(_, parent_stack)| parent_stack);
779 build_symbol(package, scope, class_name)
780}
781
782fn add_type_node(
783 helper: &mut GraphBuildHelper,
784 kind: &str,
785 qualified_name: &str,
786 span: Span,
787) -> sqry_core::graph::unified::node::NodeId {
788 let node_id = match kind {
793 "interface_declaration" => helper.add_interface(qualified_name, Some(span)),
794 _ => helper.add_class(qualified_name, Some(span)),
795 };
796 helper.mark_definition(node_id);
797 node_id
798}
799
800fn handle_method_invocation(
801 node: Node,
802 content: &[u8],
803 ast_graph: &ASTGraph,
804 helper: &mut GraphBuildHelper,
805) {
806 if let Some(caller_context) = ast_graph.find_enclosing(node.start_byte()) {
807 let is_ffi = build_ffi_call_edge(node, content, caller_context, ast_graph, helper);
808 if is_ffi {
809 return;
810 }
811 }
812
813 process_method_call_unified(node, content, ast_graph, helper);
814}
815
816fn handle_constructor_call(
817 node: Node,
818 content: &[u8],
819 ast_graph: &ASTGraph,
820 helper: &mut GraphBuildHelper,
821) {
822 process_constructor_call_unified(node, content, ast_graph, helper);
823}
824
825fn handle_import_declaration(node: Node, content: &[u8], helper: &mut GraphBuildHelper) {
826 process_import_unified(node, content, helper);
827}
828
829fn add_field_typeof_edges(ast_graph: &ASTGraph, helper: &mut GraphBuildHelper) {
832 for (field_name, (type_fqn, is_final, visibility, is_static)) in &ast_graph.field_types {
833 let field_id = if *is_final {
835 if let Some(vis) = visibility {
837 helper.add_constant_with_static_and_visibility(
838 field_name,
839 None,
840 *is_static,
841 Some(vis.as_str()),
842 )
843 } else {
844 helper.add_constant_with_static_and_visibility(field_name, None, *is_static, None)
845 }
846 } else {
847 if let Some(vis) = visibility {
849 helper.add_property_with_static_and_visibility(
850 field_name,
851 None,
852 *is_static,
853 Some(vis.as_str()),
854 )
855 } else {
856 helper.add_property_with_static_and_visibility(field_name, None, *is_static, None)
857 }
858 };
859
860 let type_id = helper.add_class(type_fqn, None);
862
863 let bare_name = field_name
870 .rsplit_once("::")
871 .map_or(field_name.as_str(), |(_, simple)| simple);
872 helper.add_typeof_edge_with_context(
873 field_id,
874 type_id,
875 Some(TypeOfContext::Field),
876 None,
877 Some(bare_name),
878 );
879 }
880}
881
882fn extract_method_parameters(
885 method_node: Node,
886 content: &[u8],
887 qualified_method_name: &str,
888 helper: &mut GraphBuildHelper,
889 import_map: &HashMap<String, String>,
890 scope_tree: &mut JavaScopeTree,
891) {
892 let mut cursor = method_node.walk();
894 for child in method_node.children(&mut cursor) {
895 if child.kind() == "formal_parameters" {
896 let mut param_cursor = child.walk();
898 for param_child in child.children(&mut param_cursor) {
899 match param_child.kind() {
900 "formal_parameter" => {
901 handle_formal_parameter(
902 param_child,
903 content,
904 qualified_method_name,
905 helper,
906 import_map,
907 scope_tree,
908 );
909 }
910 "spread_parameter" => {
911 handle_spread_parameter(
912 param_child,
913 content,
914 qualified_method_name,
915 helper,
916 import_map,
917 scope_tree,
918 );
919 }
920 "receiver_parameter" => {
921 handle_receiver_parameter(
922 param_child,
923 content,
924 qualified_method_name,
925 helper,
926 import_map,
927 scope_tree,
928 );
929 }
930 _ => {}
931 }
932 }
933 }
934 }
935}
936
937fn handle_formal_parameter(
939 param_node: Node,
940 content: &[u8],
941 method_name: &str,
942 helper: &mut GraphBuildHelper,
943 import_map: &HashMap<String, String>,
944 scope_tree: &mut JavaScopeTree,
945) {
946 use sqry_core::graph::unified::node::NodeKind;
947
948 let Some(type_node) = param_node.child_by_field_name("type") else {
950 return;
951 };
952
953 let Some(name_node) = param_node.child_by_field_name("name") else {
955 return;
956 };
957
958 let type_text = extract_type_name(type_node, content);
960 let param_name = extract_identifier(name_node, content);
961
962 if type_text.is_empty() || param_name.is_empty() {
963 return;
964 }
965
966 let resolved_type = import_map.get(&type_text).cloned().unwrap_or(type_text);
968
969 let qualified_param = format!("{method_name}::{param_name}");
971 let span = Span::from_bytes(param_node.start_byte(), param_node.end_byte());
972
973 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
975
976 scope_tree.attach_node_id(¶m_name, name_node.start_byte(), param_id);
977
978 let type_id = helper.add_class(&resolved_type, None);
980
981 helper.add_typeof_edge(param_id, type_id);
983}
984
985fn handle_spread_parameter(
987 param_node: Node,
988 content: &[u8],
989 method_name: &str,
990 helper: &mut GraphBuildHelper,
991 import_map: &HashMap<String, String>,
992 scope_tree: &mut JavaScopeTree,
993) {
994 use sqry_core::graph::unified::node::NodeKind;
995
996 let mut type_text = String::new();
1005 let mut param_name = String::new();
1006 let mut param_name_node = None;
1007
1008 let mut cursor = param_node.walk();
1009 for child in param_node.children(&mut cursor) {
1010 match child.kind() {
1011 "type_identifier" | "generic_type" | "scoped_type_identifier" => {
1012 type_text = extract_type_name(child, content);
1013 }
1014 "variable_declarator" => {
1015 if let Some(name_node) = child.child_by_field_name("name") {
1017 param_name = extract_identifier(name_node, content);
1018 param_name_node = Some(name_node);
1019 }
1020 }
1021 _ => {}
1022 }
1023 }
1024
1025 if type_text.is_empty() || param_name.is_empty() {
1026 return;
1027 }
1028
1029 let resolved_type = import_map.get(&type_text).cloned().unwrap_or(type_text);
1031
1032 let qualified_param = format!("{method_name}::{param_name}");
1034 let span = Span::from_bytes(param_node.start_byte(), param_node.end_byte());
1035
1036 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
1038
1039 if let Some(name_node) = param_name_node {
1040 scope_tree.attach_node_id(¶m_name, name_node.start_byte(), param_id);
1041 }
1042
1043 let type_id = helper.add_class(&resolved_type, None);
1046
1047 helper.add_typeof_edge(param_id, type_id);
1049}
1050
1051fn handle_receiver_parameter(
1053 param_node: Node,
1054 content: &[u8],
1055 method_name: &str,
1056 helper: &mut GraphBuildHelper,
1057 import_map: &HashMap<String, String>,
1058 _scope_tree: &mut JavaScopeTree,
1059) {
1060 use sqry_core::graph::unified::node::NodeKind;
1061
1062 let mut type_text = String::new();
1070 let mut cursor = param_node.walk();
1071
1072 for child in param_node.children(&mut cursor) {
1074 if matches!(
1075 child.kind(),
1076 "type_identifier" | "generic_type" | "scoped_type_identifier"
1077 ) {
1078 type_text = extract_type_name(child, content);
1079 break;
1080 }
1081 }
1082
1083 if type_text.is_empty() {
1084 return;
1085 }
1086
1087 let param_name = "this";
1089
1090 let resolved_type = import_map.get(&type_text).cloned().unwrap_or(type_text);
1092
1093 let qualified_param = format!("{method_name}::{param_name}");
1095 let span = Span::from_bytes(param_node.start_byte(), param_node.end_byte());
1096
1097 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
1099
1100 let type_id = helper.add_class(&resolved_type, None);
1102
1103 helper.add_typeof_edge(param_id, type_id);
1105}
1106
1107#[derive(Debug, Clone, Copy, Eq, PartialEq)]
1108enum FieldAccessRole {
1109 Default,
1110 ExplicitThisOrSuper,
1111 Skip,
1112}
1113
1114#[derive(Debug, Clone, Copy, Eq, PartialEq)]
1115enum FieldResolutionMode {
1116 Default,
1117 CurrentOnly,
1118}
1119
1120fn field_access_role(
1121 node: Node,
1122 content: &[u8],
1123 ast_graph: &ASTGraph,
1124 scope_tree: &JavaScopeTree,
1125 identifier_text: &str,
1126) -> FieldAccessRole {
1127 let Some(parent) = node.parent() else {
1128 return FieldAccessRole::Default;
1129 };
1130
1131 if parent.kind() == "field_access" {
1132 if let Some(field_node) = parent.child_by_field_name("field")
1133 && field_node.id() == node.id()
1134 && let Some(object_node) = parent.child_by_field_name("object")
1135 {
1136 if is_explicit_this_or_super(object_node, content) {
1137 return FieldAccessRole::ExplicitThisOrSuper;
1138 }
1139 return FieldAccessRole::Skip;
1140 }
1141
1142 if let Some(object_node) = parent.child_by_field_name("object")
1143 && object_node.id() == node.id()
1144 && !scope_tree.has_local_binding(identifier_text, node.start_byte())
1145 && is_static_type_identifier(identifier_text, ast_graph, scope_tree)
1146 {
1147 return FieldAccessRole::Skip;
1148 }
1149 }
1150
1151 if parent.kind() == "method_invocation"
1152 && let Some(object_node) = parent.child_by_field_name("object")
1153 && object_node.id() == node.id()
1154 && !scope_tree.has_local_binding(identifier_text, node.start_byte())
1155 && is_static_type_identifier(identifier_text, ast_graph, scope_tree)
1156 {
1157 return FieldAccessRole::Skip;
1158 }
1159
1160 if parent.kind() == "method_reference"
1161 && let Some(object_node) = parent.child_by_field_name("object")
1162 && object_node.id() == node.id()
1163 && !scope_tree.has_local_binding(identifier_text, node.start_byte())
1164 && is_static_type_identifier(identifier_text, ast_graph, scope_tree)
1165 {
1166 return FieldAccessRole::Skip;
1167 }
1168
1169 FieldAccessRole::Default
1170}
1171
1172fn is_static_type_identifier(
1173 identifier_text: &str,
1174 ast_graph: &ASTGraph,
1175 scope_tree: &JavaScopeTree,
1176) -> bool {
1177 ast_graph.import_map.contains_key(identifier_text)
1178 || scope_tree.is_known_type_name(identifier_text)
1179}
1180
1181fn is_explicit_this_or_super(node: Node, content: &[u8]) -> bool {
1182 if matches!(node.kind(), "this" | "super") {
1183 return true;
1184 }
1185 if node.kind() == "identifier" {
1186 let text = extract_identifier(node, content);
1187 return matches!(text.as_str(), "this" | "super");
1188 }
1189 if node.kind() == "field_access"
1190 && let Some(field) = node.child_by_field_name("field")
1191 {
1192 let text = extract_identifier(field, content);
1193 if matches!(text.as_str(), "this" | "super") {
1194 return true;
1195 }
1196 }
1197 false
1198}
1199
1200#[allow(clippy::too_many_lines)]
1203fn is_declaration_context(node: Node) -> bool {
1204 let Some(parent) = node.parent() else {
1206 return false;
1207 };
1208
1209 if parent.kind() == "variable_declarator" {
1214 let mut cursor = parent.walk();
1216 for (idx, child) in parent.children(&mut cursor).enumerate() {
1217 if child.id() == node.id() {
1218 #[allow(clippy::cast_possible_truncation)]
1219 if let Some(field_name) = parent.field_name_for_child(idx as u32) {
1220 return field_name == "name";
1222 }
1223 break;
1224 }
1225 }
1226
1227 if let Some(grandparent) = parent.parent()
1229 && grandparent.kind() == "spread_parameter"
1230 {
1231 return true;
1232 }
1233
1234 return false;
1235 }
1236
1237 if parent.kind() == "formal_parameter" {
1239 let mut cursor = parent.walk();
1240 for (idx, child) in parent.children(&mut cursor).enumerate() {
1241 if child.id() == node.id() {
1242 #[allow(clippy::cast_possible_truncation)]
1243 if let Some(field_name) = parent.field_name_for_child(idx as u32) {
1244 return field_name == "name";
1245 }
1246 break;
1247 }
1248 }
1249 return false;
1250 }
1251
1252 if parent.kind() == "enhanced_for_statement" {
1255 let mut cursor = parent.walk();
1257 for (idx, child) in parent.children(&mut cursor).enumerate() {
1258 if child.id() == node.id() {
1259 #[allow(clippy::cast_possible_truncation)]
1260 if let Some(field_name) = parent.field_name_for_child(idx as u32) {
1261 return field_name == "name";
1263 }
1264 break;
1265 }
1266 }
1267 return false;
1268 }
1269
1270 if parent.kind() == "lambda_expression" {
1271 if let Some(params) = parent.child_by_field_name("parameters") {
1272 return params.id() == node.id();
1273 }
1274 return false;
1275 }
1276
1277 if parent.kind() == "inferred_parameters" {
1278 return true;
1279 }
1280
1281 if parent.kind() == "resource" {
1282 if let Some(name_node) = parent.child_by_field_name("name")
1283 && name_node.id() == node.id()
1284 {
1285 let has_type = parent.child_by_field_name("type").is_some();
1286 let has_value = parent.child_by_field_name("value").is_some();
1287 return has_type || has_value;
1288 }
1289 return false;
1290 }
1291
1292 if parent.kind() == "type_pattern" {
1296 if let Some((name_node, _type_node)) = typed_pattern_parts(parent) {
1297 return name_node.id() == node.id();
1298 }
1299 return false;
1300 }
1301
1302 if parent.kind() == "instanceof_expression" {
1304 let mut cursor = parent.walk();
1305 for (idx, child) in parent.children(&mut cursor).enumerate() {
1306 if child.id() == node.id() {
1307 #[allow(clippy::cast_possible_truncation)]
1308 if let Some(field_name) = parent.field_name_for_child(idx as u32) {
1309 return field_name == "name";
1311 }
1312 break;
1313 }
1314 }
1315 return false;
1316 }
1317
1318 if parent.kind() == "record_pattern_component" {
1321 let mut cursor = parent.walk();
1323 for child in parent.children(&mut cursor) {
1324 if child.id() == node.id() && child.kind() == "identifier" {
1325 return true;
1327 }
1328 }
1329 return false;
1330 }
1331
1332 if parent.kind() == "record_component" {
1333 if let Some(name_node) = parent.child_by_field_name("name") {
1334 return name_node.id() == node.id();
1335 }
1336 return false;
1337 }
1338
1339 matches!(
1341 parent.kind(),
1342 "method_declaration"
1343 | "constructor_declaration"
1344 | "compact_constructor_declaration"
1345 | "class_declaration"
1346 | "interface_declaration"
1347 | "enum_declaration"
1348 | "field_declaration"
1349 | "catch_formal_parameter"
1350 )
1351}
1352
1353fn is_method_invocation_name(node: Node) -> bool {
1354 let Some(parent) = node.parent() else {
1355 return false;
1356 };
1357 if parent.kind() != "method_invocation" {
1358 return false;
1359 }
1360 parent
1361 .child_by_field_name("name")
1362 .is_some_and(|name_node| name_node.id() == node.id())
1363}
1364
1365fn is_method_reference_name(node: Node) -> bool {
1366 let Some(parent) = node.parent() else {
1367 return false;
1368 };
1369 if parent.kind() != "method_reference" {
1370 return false;
1371 }
1372 parent
1373 .child_by_field_name("name")
1374 .is_some_and(|name_node| name_node.id() == node.id())
1375}
1376
1377fn is_label_identifier(node: Node) -> bool {
1378 let Some(parent) = node.parent() else {
1379 return false;
1380 };
1381 if parent.kind() == "labeled_statement" {
1382 return true;
1383 }
1384 if matches!(parent.kind(), "break_statement" | "continue_statement")
1385 && let Some(label) = parent.child_by_field_name("label")
1386 {
1387 return label.id() == node.id();
1388 }
1389 false
1390}
1391
1392fn is_class_literal(node: Node) -> bool {
1393 let Some(parent) = node.parent() else {
1394 return false;
1395 };
1396 parent.kind() == "class_literal"
1397}
1398
1399fn is_type_identifier_context(node: Node) -> bool {
1400 let Some(parent) = node.parent() else {
1401 return false;
1402 };
1403 matches!(
1404 parent.kind(),
1405 "type_identifier"
1406 | "scoped_type_identifier"
1407 | "scoped_identifier"
1408 | "generic_type"
1409 | "type_argument"
1410 | "type_bound"
1411 )
1412}
1413
1414fn add_reference_edge_for_target(
1415 usage_node: Node,
1416 identifier_text: &str,
1417 target_id: sqry_core::graph::unified::node::NodeId,
1418 helper: &mut GraphBuildHelper,
1419) {
1420 let usage_span = Span::from_bytes(usage_node.start_byte(), usage_node.end_byte());
1421 let usage_id = helper.add_node(
1422 &format!("{}@{}", identifier_text, usage_node.start_byte()),
1423 Some(usage_span),
1424 sqry_core::graph::unified::node::NodeKind::Variable,
1425 );
1426 helper.add_reference_edge(usage_id, target_id);
1427}
1428
1429fn resolve_field_reference(
1430 node: Node,
1431 identifier_text: &str,
1432 ast_graph: &ASTGraph,
1433 helper: &mut GraphBuildHelper,
1434 mode: FieldResolutionMode,
1435) {
1436 let context = ast_graph.find_enclosing(node.start_byte());
1437 let mut candidates = Vec::new();
1438 if let Some(ctx) = context
1439 && !ctx.class_stack.is_empty()
1440 {
1441 if mode == FieldResolutionMode::CurrentOnly {
1442 let class_path = ctx.class_stack.join("::");
1443 candidates.push(format!("{class_path}::{identifier_text}"));
1444 } else {
1445 let stack_len = ctx.class_stack.len();
1446 for idx in (1..=stack_len).rev() {
1447 let class_path = ctx.class_stack[..idx].join("::");
1448 candidates.push(format!("{class_path}::{identifier_text}"));
1449 }
1450 }
1451 }
1452
1453 if mode != FieldResolutionMode::CurrentOnly {
1454 candidates.push(identifier_text.to_string());
1455 }
1456
1457 for candidate in candidates {
1458 if ast_graph.field_types.contains_key(&candidate) {
1459 add_field_reference(node, identifier_text, &candidate, ast_graph, helper);
1460 return;
1461 }
1462 }
1463}
1464
1465fn add_field_reference(
1466 node: Node,
1467 identifier_text: &str,
1468 field_name: &str,
1469 ast_graph: &ASTGraph,
1470 helper: &mut GraphBuildHelper,
1471) {
1472 let usage_span = Span::from_bytes(node.start_byte(), node.end_byte());
1473 let usage_id = helper.add_node(
1474 &format!("{}@{}", identifier_text, node.start_byte()),
1475 Some(usage_span),
1476 sqry_core::graph::unified::node::NodeKind::Variable,
1477 );
1478
1479 let field_metadata = ast_graph.field_types.get(field_name);
1480 let field_id = if let Some((_, is_final, visibility, is_static)) = field_metadata {
1481 if *is_final {
1482 if let Some(vis) = visibility {
1483 helper.add_constant_with_static_and_visibility(
1484 field_name,
1485 None,
1486 *is_static,
1487 Some(vis.as_str()),
1488 )
1489 } else {
1490 helper.add_constant_with_static_and_visibility(field_name, None, *is_static, None)
1491 }
1492 } else if let Some(vis) = visibility {
1493 helper.add_property_with_static_and_visibility(
1494 field_name,
1495 None,
1496 *is_static,
1497 Some(vis.as_str()),
1498 )
1499 } else {
1500 helper.add_property_with_static_and_visibility(field_name, None, *is_static, None)
1501 }
1502 } else {
1503 helper.add_property_with_static_and_visibility(field_name, None, false, None)
1504 };
1505
1506 helper.add_reference_edge(usage_id, field_id);
1507}
1508
1509#[allow(clippy::similar_names)]
1511fn handle_identifier_for_reference(
1512 node: Node,
1513 content: &[u8],
1514 ast_graph: &ASTGraph,
1515 scope_tree: &mut JavaScopeTree,
1516 helper: &mut GraphBuildHelper,
1517) {
1518 let identifier_text = extract_identifier(node, content);
1519
1520 if identifier_text.is_empty() {
1521 return;
1522 }
1523
1524 if is_declaration_context(node) {
1526 return;
1527 }
1528
1529 if is_method_invocation_name(node)
1530 || is_method_reference_name(node)
1531 || is_label_identifier(node)
1532 || is_class_literal(node)
1533 {
1534 return;
1535 }
1536
1537 if is_type_identifier_context(node) {
1538 return;
1539 }
1540
1541 let field_access_role =
1542 field_access_role(node, content, ast_graph, scope_tree, &identifier_text);
1543 if matches!(field_access_role, FieldAccessRole::Skip) {
1544 return;
1545 }
1546
1547 let allow_local = matches!(field_access_role, FieldAccessRole::Default);
1548 let allow_field = !matches!(field_access_role, FieldAccessRole::Skip);
1549 let field_mode = if matches!(field_access_role, FieldAccessRole::ExplicitThisOrSuper) {
1550 FieldResolutionMode::CurrentOnly
1551 } else {
1552 FieldResolutionMode::Default
1553 };
1554
1555 if allow_local {
1556 match scope_tree.resolve_identifier(node.start_byte(), &identifier_text) {
1557 ResolutionOutcome::Local(binding) => {
1558 let target_id = if let Some(node_id) = binding.node_id {
1559 node_id
1560 } else {
1561 let span = Span::from_bytes(binding.decl_start_byte, binding.decl_end_byte);
1562 let qualified_var = format!("{}@{}", identifier_text, binding.decl_start_byte);
1563 let var_id = helper.add_variable(&qualified_var, Some(span));
1564 scope_tree.attach_node_id(&identifier_text, binding.decl_start_byte, var_id);
1565 var_id
1566 };
1567 add_reference_edge_for_target(node, &identifier_text, target_id, helper);
1568 return;
1569 }
1570 ResolutionOutcome::Member { qualified_name } => {
1571 if let Some(field_name) = qualified_name {
1572 add_field_reference(node, &identifier_text, &field_name, ast_graph, helper);
1573 }
1574 return;
1575 }
1576 ResolutionOutcome::Ambiguous => {
1577 return;
1578 }
1579 ResolutionOutcome::NoMatch => {}
1580 }
1581 }
1582
1583 if !allow_field {
1584 return;
1585 }
1586
1587 resolve_field_reference(node, &identifier_text, ast_graph, helper, field_mode);
1588}
1589
1590fn handle_method_declaration_parameters(
1592 node: Node,
1593 content: &[u8],
1594 ast_graph: &ASTGraph,
1595 scope_tree: &mut JavaScopeTree,
1596 helper: &mut GraphBuildHelper,
1597) {
1598 let byte_pos = node.start_byte();
1600 if let Some(context) = ast_graph.find_enclosing(byte_pos) {
1601 let qualified_method_name = &context.qualified_name;
1602
1603 extract_method_parameters(
1605 node,
1606 content,
1607 qualified_method_name,
1608 helper,
1609 &ast_graph.import_map,
1610 scope_tree,
1611 );
1612
1613 process_type_parameter_declarations(node, content, qualified_method_name, helper);
1622 }
1623}
1624
1625fn handle_local_variable_declaration(
1627 node: Node,
1628 content: &[u8],
1629 ast_graph: &ASTGraph,
1630 scope_tree: &mut JavaScopeTree,
1631 helper: &mut GraphBuildHelper,
1632) {
1633 let Some(type_node) = node.child_by_field_name("type") else {
1635 return;
1636 };
1637
1638 let type_text = extract_type_name(type_node, content);
1639 if type_text.is_empty() {
1640 return;
1641 }
1642
1643 let resolved_type = ast_graph
1645 .import_map
1646 .get(&type_text)
1647 .cloned()
1648 .unwrap_or_else(|| type_text.clone());
1649
1650 let mut cursor = node.walk();
1652 for child in node.children(&mut cursor) {
1653 if child.kind() == "variable_declarator"
1654 && let Some(name_node) = child.child_by_field_name("name")
1655 {
1656 let var_name = extract_identifier(name_node, content);
1657
1658 let qualified_var = format!("{}@{}", var_name, name_node.start_byte());
1660
1661 let span = Span::from_bytes(child.start_byte(), child.end_byte());
1663 let var_id = helper.add_variable(&qualified_var, Some(span));
1664 scope_tree.attach_node_id(&var_name, name_node.start_byte(), var_id);
1665
1666 let type_id = helper.add_class(&resolved_type, None);
1668
1669 helper.add_typeof_edge(var_id, type_id);
1671 }
1672 }
1673}
1674
1675fn handle_enhanced_for_declaration(
1676 node: Node,
1677 content: &[u8],
1678 ast_graph: &ASTGraph,
1679 scope_tree: &mut JavaScopeTree,
1680 helper: &mut GraphBuildHelper,
1681) {
1682 let Some(type_node) = node.child_by_field_name("type") else {
1683 return;
1684 };
1685 let Some(name_node) = node.child_by_field_name("name") else {
1686 return;
1687 };
1688 let Some(body_node) = node.child_by_field_name("body") else {
1689 return;
1690 };
1691
1692 let type_text = extract_type_name(type_node, content);
1693 let var_name = extract_identifier(name_node, content);
1694 if type_text.is_empty() || var_name.is_empty() {
1695 return;
1696 }
1697
1698 let resolved_type = ast_graph
1699 .import_map
1700 .get(&type_text)
1701 .cloned()
1702 .unwrap_or(type_text);
1703
1704 let qualified_var = format!("{}@{}", var_name, name_node.start_byte());
1705 let span = Span::from_bytes(name_node.start_byte(), name_node.end_byte());
1706 let var_id = helper.add_variable(&qualified_var, Some(span));
1707 scope_tree.attach_node_id(&var_name, body_node.start_byte(), var_id);
1708
1709 let type_id = helper.add_class(&resolved_type, None);
1710 helper.add_typeof_edge(var_id, type_id);
1711}
1712
1713fn handle_catch_parameter_declaration(
1714 node: Node,
1715 content: &[u8],
1716 ast_graph: &ASTGraph,
1717 scope_tree: &mut JavaScopeTree,
1718 helper: &mut GraphBuildHelper,
1719) {
1720 let Some(param_node) = node
1721 .child_by_field_name("parameter")
1722 .or_else(|| first_child_of_kind(node, "catch_formal_parameter"))
1723 .or_else(|| first_child_of_kind(node, "formal_parameter"))
1724 else {
1725 return;
1726 };
1727 let Some(name_node) = param_node
1728 .child_by_field_name("name")
1729 .or_else(|| first_child_of_kind(param_node, "identifier"))
1730 else {
1731 return;
1732 };
1733
1734 let var_name = extract_identifier(name_node, content);
1735 if var_name.is_empty() {
1736 return;
1737 }
1738
1739 let qualified_var = format!("{}@{}", var_name, name_node.start_byte());
1740 let span = Span::from_bytes(param_node.start_byte(), param_node.end_byte());
1741 let var_id = helper.add_variable(&qualified_var, Some(span));
1742 scope_tree.attach_node_id(&var_name, name_node.start_byte(), var_id);
1743
1744 if let Some(type_node) = param_node
1745 .child_by_field_name("type")
1746 .or_else(|| first_child_of_kind(param_node, "type_identifier"))
1747 .or_else(|| first_child_of_kind(param_node, "scoped_type_identifier"))
1748 .or_else(|| first_child_of_kind(param_node, "generic_type"))
1749 {
1750 add_typeof_for_catch_type(type_node, content, ast_graph, helper, var_id);
1751 }
1752}
1753
1754fn add_typeof_for_catch_type(
1755 type_node: Node,
1756 content: &[u8],
1757 ast_graph: &ASTGraph,
1758 helper: &mut GraphBuildHelper,
1759 var_id: sqry_core::graph::unified::node::NodeId,
1760) {
1761 if type_node.kind() == "union_type" {
1762 let mut cursor = type_node.walk();
1763 for child in type_node.children(&mut cursor) {
1764 if matches!(
1765 child.kind(),
1766 "type_identifier" | "scoped_type_identifier" | "generic_type"
1767 ) {
1768 let type_text = extract_type_name(child, content);
1769 if !type_text.is_empty() {
1770 let resolved_type = ast_graph
1771 .import_map
1772 .get(&type_text)
1773 .cloned()
1774 .unwrap_or(type_text);
1775 let type_id = helper.add_class(&resolved_type, None);
1776 helper.add_typeof_edge(var_id, type_id);
1777 }
1778 }
1779 }
1780 return;
1781 }
1782
1783 let type_text = extract_type_name(type_node, content);
1784 if type_text.is_empty() {
1785 return;
1786 }
1787 let resolved_type = ast_graph
1788 .import_map
1789 .get(&type_text)
1790 .cloned()
1791 .unwrap_or(type_text);
1792 let type_id = helper.add_class(&resolved_type, None);
1793 helper.add_typeof_edge(var_id, type_id);
1794}
1795
1796fn handle_lambda_parameter_declaration(
1797 node: Node,
1798 content: &[u8],
1799 ast_graph: &ASTGraph,
1800 scope_tree: &mut JavaScopeTree,
1801 helper: &mut GraphBuildHelper,
1802) {
1803 use sqry_core::graph::unified::node::NodeKind;
1804
1805 let Some(params_node) = node.child_by_field_name("parameters") else {
1806 return;
1807 };
1808 let lambda_prefix = format!("lambda@{}", node.start_byte());
1809
1810 if params_node.kind() == "identifier" {
1811 let name = extract_identifier(params_node, content);
1812 if name.is_empty() {
1813 return;
1814 }
1815 let qualified_param = format!("{lambda_prefix}::{name}");
1816 let span = Span::from_bytes(params_node.start_byte(), params_node.end_byte());
1817 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
1818 scope_tree.attach_node_id(&name, params_node.start_byte(), param_id);
1819 return;
1820 }
1821
1822 let mut cursor = params_node.walk();
1823 for child in params_node.children(&mut cursor) {
1824 match child.kind() {
1825 "identifier" => {
1826 let name = extract_identifier(child, content);
1827 if name.is_empty() {
1828 continue;
1829 }
1830 let qualified_param = format!("{lambda_prefix}::{name}");
1831 let span = Span::from_bytes(child.start_byte(), child.end_byte());
1832 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
1833 scope_tree.attach_node_id(&name, child.start_byte(), param_id);
1834 }
1835 "formal_parameter" => {
1836 let Some(name_node) = child.child_by_field_name("name") else {
1837 continue;
1838 };
1839 let Some(type_node) = child.child_by_field_name("type") else {
1840 continue;
1841 };
1842 let name = extract_identifier(name_node, content);
1843 if name.is_empty() {
1844 continue;
1845 }
1846 let type_text = extract_type_name(type_node, content);
1847 let resolved_type = ast_graph
1848 .import_map
1849 .get(&type_text)
1850 .cloned()
1851 .unwrap_or(type_text);
1852 let qualified_param = format!("{lambda_prefix}::{name}");
1853 let span = Span::from_bytes(child.start_byte(), child.end_byte());
1854 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
1855 scope_tree.attach_node_id(&name, name_node.start_byte(), param_id);
1856 let type_id = helper.add_class(&resolved_type, None);
1857 helper.add_typeof_edge(param_id, type_id);
1858 }
1859 _ => {}
1860 }
1861 }
1862}
1863
1864fn handle_try_with_resources_declaration(
1865 node: Node,
1866 content: &[u8],
1867 ast_graph: &ASTGraph,
1868 scope_tree: &mut JavaScopeTree,
1869 helper: &mut GraphBuildHelper,
1870) {
1871 let Some(resources) = node.child_by_field_name("resources") else {
1872 return;
1873 };
1874
1875 let mut cursor = resources.walk();
1876 for resource in resources.children(&mut cursor) {
1877 if resource.kind() != "resource" {
1878 continue;
1879 }
1880 let name_node = resource.child_by_field_name("name");
1881 let type_node = resource.child_by_field_name("type");
1882 let value_node = resource.child_by_field_name("value");
1883 if let Some(name_node) = name_node {
1884 if type_node.is_none() && value_node.is_none() {
1885 continue;
1886 }
1887 let name = extract_identifier(name_node, content);
1888 if name.is_empty() {
1889 continue;
1890 }
1891
1892 let qualified_var = format!("{}@{}", name, name_node.start_byte());
1893 let span = Span::from_bytes(resource.start_byte(), resource.end_byte());
1894 let var_id = helper.add_variable(&qualified_var, Some(span));
1895 scope_tree.attach_node_id(&name, name_node.start_byte(), var_id);
1896
1897 if let Some(type_node) = type_node {
1898 let type_text = extract_type_name(type_node, content);
1899 if !type_text.is_empty() {
1900 let resolved_type = ast_graph
1901 .import_map
1902 .get(&type_text)
1903 .cloned()
1904 .unwrap_or(type_text);
1905 let type_id = helper.add_class(&resolved_type, None);
1906 helper.add_typeof_edge(var_id, type_id);
1907 }
1908 }
1909 }
1910 }
1911}
1912
1913fn handle_instanceof_pattern_declaration(
1914 node: Node,
1915 content: &[u8],
1916 ast_graph: &ASTGraph,
1917 scope_tree: &mut JavaScopeTree,
1918 helper: &mut GraphBuildHelper,
1919) {
1920 let mut patterns = Vec::new();
1921 collect_pattern_declarations(node, &mut patterns);
1922 for (name_node, type_node) in patterns {
1923 let name = extract_identifier(name_node, content);
1924 if name.is_empty() {
1925 continue;
1926 }
1927 let qualified_var = format!("{}@{}", name, name_node.start_byte());
1928 let span = Span::from_bytes(name_node.start_byte(), name_node.end_byte());
1929 let var_id = helper.add_variable(&qualified_var, Some(span));
1930 scope_tree.attach_node_id(&name, name_node.start_byte(), var_id);
1931
1932 if let Some(type_node) = type_node {
1933 let type_text = extract_type_name(type_node, content);
1934 if !type_text.is_empty() {
1935 let resolved_type = ast_graph
1936 .import_map
1937 .get(&type_text)
1938 .cloned()
1939 .unwrap_or(type_text);
1940 let type_id = helper.add_class(&resolved_type, None);
1941 helper.add_typeof_edge(var_id, type_id);
1942 }
1943 }
1944 }
1945}
1946
1947fn handle_switch_pattern_declaration(
1948 node: Node,
1949 content: &[u8],
1950 ast_graph: &ASTGraph,
1951 scope_tree: &mut JavaScopeTree,
1952 helper: &mut GraphBuildHelper,
1953) {
1954 let mut patterns = Vec::new();
1955 collect_pattern_declarations(node, &mut patterns);
1956 for (name_node, type_node) in patterns {
1957 let name = extract_identifier(name_node, content);
1958 if name.is_empty() {
1959 continue;
1960 }
1961 let qualified_var = format!("{}@{}", name, name_node.start_byte());
1962 let span = Span::from_bytes(name_node.start_byte(), name_node.end_byte());
1963 let var_id = helper.add_variable(&qualified_var, Some(span));
1964 scope_tree.attach_node_id(&name, name_node.start_byte(), var_id);
1965
1966 if let Some(type_node) = type_node {
1967 let type_text = extract_type_name(type_node, content);
1968 if !type_text.is_empty() {
1969 let resolved_type = ast_graph
1970 .import_map
1971 .get(&type_text)
1972 .cloned()
1973 .unwrap_or(type_text);
1974 let type_id = helper.add_class(&resolved_type, None);
1975 helper.add_typeof_edge(var_id, type_id);
1976 }
1977 }
1978 }
1979}
1980
1981fn handle_compact_constructor_parameters(
1982 node: Node,
1983 content: &[u8],
1984 ast_graph: &ASTGraph,
1985 scope_tree: &mut JavaScopeTree,
1986 helper: &mut GraphBuildHelper,
1987) {
1988 use sqry_core::graph::unified::node::NodeKind;
1989
1990 let Some(record_node) = node
1991 .parent()
1992 .and_then(|parent| find_record_declaration(parent))
1993 else {
1994 return;
1995 };
1996
1997 let Some(record_name_node) = record_node.child_by_field_name("name") else {
1998 return;
1999 };
2000 let record_name = extract_identifier(record_name_node, content);
2001 if record_name.is_empty() {
2002 return;
2003 }
2004
2005 let mut components = Vec::new();
2006 collect_record_components_nodes(record_node, &mut components);
2007 for component in components {
2008 let Some(name_node) = component.child_by_field_name("name") else {
2009 continue;
2010 };
2011 let Some(type_node) = component.child_by_field_name("type") else {
2012 continue;
2013 };
2014 let name = extract_identifier(name_node, content);
2015 if name.is_empty() {
2016 continue;
2017 }
2018
2019 let type_text = extract_type_name(type_node, content);
2020 if type_text.is_empty() {
2021 continue;
2022 }
2023 let resolved_type = ast_graph
2024 .import_map
2025 .get(&type_text)
2026 .cloned()
2027 .unwrap_or(type_text);
2028
2029 let qualified_param = format!("{record_name}.<init>::{name}");
2030 let span = Span::from_bytes(component.start_byte(), component.end_byte());
2031 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
2032 scope_tree.attach_node_id(&name, name_node.start_byte(), param_id);
2033
2034 let type_id = helper.add_class(&resolved_type, None);
2035 helper.add_typeof_edge(param_id, type_id);
2036 }
2037}
2038
2039fn collect_pattern_declarations<'a>(
2040 node: Node<'a>,
2041 output: &mut Vec<(Node<'a>, Option<Node<'a>>)>,
2042) {
2043 if node.kind() == "instanceof_expression"
2044 && !node_has_direct_child_kind(node, "type_pattern")
2045 && let Some(name_node) = node.child_by_field_name("name")
2046 {
2047 let type_node = first_type_like_child(node);
2048 output.push((name_node, type_node));
2049 }
2050
2051 if node.kind() == "type_pattern"
2052 && let Some((name_node, type_node)) = typed_pattern_parts(node)
2053 {
2054 output.push((name_node, type_node));
2055 }
2056
2057 if node.kind() == "record_pattern_component"
2058 && let Some((name_node, type_node)) = typed_pattern_parts(node)
2059 {
2060 output.push((name_node, type_node));
2061 }
2062
2063 let mut cursor = node.walk();
2064 for child in node.children(&mut cursor) {
2065 collect_pattern_declarations(child, output);
2066 }
2067}
2068
2069fn node_has_direct_child_kind(node: Node, kind: &str) -> bool {
2070 let mut cursor = node.walk();
2071 node.children(&mut cursor).any(|child| child.kind() == kind)
2072}
2073
2074fn typed_pattern_parts(node: Node) -> Option<(Node, Option<Node>)> {
2075 let mut name_node = None;
2076 let mut type_node = None;
2077 let mut cursor = node.walk();
2078 for child in node.children(&mut cursor) {
2079 if matches!(child.kind(), "identifier" | "_reserved_identifier") {
2080 name_node = Some(child);
2081 } else if matches!(
2082 child.kind(),
2083 "type_identifier" | "scoped_type_identifier" | "generic_type"
2084 ) {
2085 type_node = Some(child);
2086 }
2087 }
2088 name_node.map(|name| (name, type_node))
2089}
2090
2091fn first_type_like_child(node: Node) -> Option<Node> {
2092 let mut cursor = node.walk();
2093 for child in node.children(&mut cursor) {
2094 if matches!(
2095 child.kind(),
2096 "type_identifier" | "scoped_type_identifier" | "generic_type"
2097 ) {
2098 return Some(child);
2099 }
2100 }
2101 None
2102}
2103
2104fn find_record_declaration(node: Node) -> Option<Node> {
2105 if node.kind() == "record_declaration" {
2106 return Some(node);
2107 }
2108 node.parent().and_then(find_record_declaration)
2109}
2110
2111fn collect_record_components_nodes<'a>(node: Node<'a>, output: &mut Vec<Node<'a>>) {
2112 if let Some(parameters) = node.child_by_field_name("parameters") {
2113 let mut cursor = parameters.walk();
2114 for child in parameters.children(&mut cursor) {
2115 if matches!(child.kind(), "formal_parameter" | "record_component") {
2116 output.push(child);
2117 }
2118 }
2119 return;
2120 }
2121
2122 let mut cursor = node.walk();
2123 for child in node.children(&mut cursor) {
2124 if child.kind() == "record_component" {
2125 output.push(child);
2126 }
2127 }
2128}
2129
2130fn process_method_call_unified(
2132 call_node: Node,
2133 content: &[u8],
2134 ast_graph: &ASTGraph,
2135 helper: &mut GraphBuildHelper,
2136) {
2137 let Some(caller_context) = ast_graph.find_enclosing(call_node.start_byte()) else {
2138 return;
2139 };
2140 let Ok(callee_name) = extract_method_invocation_name(call_node, content) else {
2141 return;
2142 };
2143
2144 let callee_qualified =
2145 resolve_callee_qualified(&call_node, content, ast_graph, caller_context, &callee_name);
2146 let caller_method_id = ensure_caller_method(helper, caller_context);
2147 let target_method_id = helper.ensure_method(&callee_qualified, None, false, false);
2148
2149 add_call_edge(helper, caller_method_id, target_method_id, call_node);
2150}
2151
2152fn process_constructor_call_unified(
2154 new_node: Node,
2155 content: &[u8],
2156 ast_graph: &ASTGraph,
2157 helper: &mut GraphBuildHelper,
2158) {
2159 let Some(caller_context) = ast_graph.find_enclosing(new_node.start_byte()) else {
2160 return;
2161 };
2162
2163 let Some(type_node) = new_node.child_by_field_name("type") else {
2164 return;
2165 };
2166
2167 let class_name = extract_type_name(type_node, content);
2168 if class_name.is_empty() {
2169 return;
2170 }
2171
2172 let qualified_class = qualify_constructor_class(&class_name, caller_context);
2173 let constructor_name = format!("{qualified_class}.<init>");
2174
2175 let caller_method_id = ensure_caller_method(helper, caller_context);
2176 let target_method_id = helper.ensure_method(&constructor_name, None, false, false);
2177 add_call_edge(helper, caller_method_id, target_method_id, new_node);
2178}
2179
2180fn count_call_arguments(call_node: Node<'_>) -> u8 {
2181 let Some(args_node) = call_node.child_by_field_name("arguments") else {
2182 return 255;
2183 };
2184 let count = args_node.named_child_count();
2185 if count <= 254 {
2186 u8::try_from(count).unwrap_or(u8::MAX)
2187 } else {
2188 u8::MAX
2189 }
2190}
2191
2192fn process_import_unified(import_node: Node, content: &[u8], helper: &mut GraphBuildHelper) {
2194 let has_asterisk = import_has_wildcard(import_node);
2195 let Some(mut imported_name) = extract_import_name(import_node, content) else {
2196 return;
2197 };
2198 if has_asterisk {
2199 imported_name = format!("{imported_name}.*");
2200 }
2201
2202 let module_id = helper.add_module("<module>", None);
2203 let external_id = helper.add_import(
2204 &imported_name,
2205 Some(Span::from_bytes(
2206 import_node.start_byte(),
2207 import_node.end_byte(),
2208 )),
2209 );
2210
2211 helper.add_import_edge(module_id, external_id);
2212}
2213
2214fn ensure_caller_method(
2215 helper: &mut GraphBuildHelper,
2216 caller_context: &MethodContext,
2217) -> sqry_core::graph::unified::node::NodeId {
2218 helper.ensure_method(
2219 caller_context.qualified_name(),
2220 Some(Span::from_bytes(
2221 caller_context.span.0,
2222 caller_context.span.1,
2223 )),
2224 false,
2225 caller_context.is_static,
2226 )
2227}
2228
2229fn resolve_callee_qualified(
2230 call_node: &Node,
2231 content: &[u8],
2232 ast_graph: &ASTGraph,
2233 caller_context: &MethodContext,
2234 callee_name: &str,
2235) -> String {
2236 if let Some(object_node) = call_node.child_by_field_name("object") {
2237 let object_text = extract_node_text(object_node, content);
2238 return resolve_member_call_target(&object_text, ast_graph, caller_context, callee_name);
2239 }
2240
2241 build_member_symbol(
2242 caller_context.package_name.as_deref(),
2243 &caller_context.class_stack,
2244 callee_name,
2245 )
2246}
2247
2248fn resolve_member_call_target(
2249 object_text: &str,
2250 ast_graph: &ASTGraph,
2251 caller_context: &MethodContext,
2252 callee_name: &str,
2253) -> String {
2254 if object_text.contains('.') {
2255 return format!("{object_text}.{callee_name}");
2256 }
2257 if object_text == "this" {
2258 return build_member_symbol(
2259 caller_context.package_name.as_deref(),
2260 &caller_context.class_stack,
2261 callee_name,
2262 );
2263 }
2264
2265 if let Some(class_name) = caller_context.class_stack.last() {
2267 let qualified_field = format!("{class_name}::{object_text}");
2268 if let Some((field_type, _is_final, _visibility, _is_static)) =
2269 ast_graph.field_types.get(&qualified_field)
2270 {
2271 return format!("{field_type}.{callee_name}");
2272 }
2273 }
2274
2275 if let Some((field_type, _is_final, _visibility, _is_static)) =
2277 ast_graph.field_types.get(object_text)
2278 {
2279 return format!("{field_type}.{callee_name}");
2280 }
2281
2282 if let Some(type_fqn) = ast_graph.import_map.get(object_text) {
2283 return format!("{type_fqn}.{callee_name}");
2284 }
2285
2286 format!("{object_text}.{callee_name}")
2287}
2288
2289fn qualify_constructor_class(class_name: &str, caller_context: &MethodContext) -> String {
2290 if class_name.contains('.') {
2291 class_name.to_string()
2292 } else if let Some(pkg) = caller_context.package_name.as_deref() {
2293 format!("{pkg}.{class_name}")
2294 } else {
2295 class_name.to_string()
2296 }
2297}
2298
2299fn add_call_edge(
2300 helper: &mut GraphBuildHelper,
2301 caller_method_id: sqry_core::graph::unified::node::NodeId,
2302 target_method_id: sqry_core::graph::unified::node::NodeId,
2303 call_node: Node,
2304) {
2305 let argument_count = count_call_arguments(call_node);
2306 let call_span = Span::from_bytes(call_node.start_byte(), call_node.end_byte());
2307 helper.add_call_edge_full_with_span(
2308 caller_method_id,
2309 target_method_id,
2310 argument_count,
2311 false,
2312 vec![call_span],
2313 );
2314}
2315
2316fn import_has_wildcard(import_node: Node) -> bool {
2317 let mut cursor = import_node.walk();
2318 import_node
2319 .children(&mut cursor)
2320 .any(|child| child.kind() == "asterisk")
2321}
2322
2323fn extract_import_name(import_node: Node, content: &[u8]) -> Option<String> {
2324 let mut cursor = import_node.walk();
2325 for child in import_node.children(&mut cursor) {
2326 if child.kind() == "scoped_identifier" || child.kind() == "identifier" {
2327 return Some(extract_full_identifier(child, content));
2328 }
2329 }
2330 None
2331}
2332
2333fn process_inheritance(
2343 class_node: Node,
2344 content: &[u8],
2345 package_name: Option<&str>,
2346 child_class_id: sqry_core::graph::unified::node::NodeId,
2347 helper: &mut GraphBuildHelper,
2348) {
2349 if let Some(superclass_node) = class_node.child_by_field_name("superclass") {
2351 let parent_type_name = extract_type_from_superclass(superclass_node, content);
2353 if !parent_type_name.is_empty() {
2354 let parent_qualified = qualify_type_name(&parent_type_name, package_name);
2356 let parent_id = helper.add_class(&parent_qualified, None);
2357 helper.add_inherits_edge(child_class_id, parent_id);
2358 }
2359 }
2360}
2361
2362fn process_implements(
2368 class_node: Node,
2369 content: &[u8],
2370 package_name: Option<&str>,
2371 class_id: sqry_core::graph::unified::node::NodeId,
2372 helper: &mut GraphBuildHelper,
2373) {
2374 let interfaces_node = class_node
2380 .child_by_field_name("interfaces")
2381 .or_else(|| class_node.child_by_field_name("super_interfaces"));
2382
2383 if let Some(node) = interfaces_node {
2384 extract_interface_types(node, content, package_name, class_id, helper);
2385 return;
2386 }
2387
2388 let mut cursor = class_node.walk();
2390 for child in class_node.children(&mut cursor) {
2391 if child.kind() == "super_interfaces" {
2393 extract_interface_types(child, content, package_name, class_id, helper);
2394 return;
2395 }
2396 }
2397}
2398
2399fn process_interface_extends(
2417 interface_node: Node,
2418 content: &[u8],
2419 package_name: Option<&str>,
2420 interface_id: sqry_core::graph::unified::node::NodeId,
2421 helper: &mut GraphBuildHelper,
2422) {
2423 let mut cursor = interface_node.walk();
2425 for child in interface_node.children(&mut cursor) {
2426 if child.kind() == "extends_interfaces" {
2427 extract_parent_interfaces_for_inherits(
2429 child,
2430 content,
2431 package_name,
2432 interface_id,
2433 helper,
2434 );
2435 return;
2436 }
2437 }
2438}
2439
2440fn extract_parent_interfaces_for_inherits(
2443 extends_node: Node,
2444 content: &[u8],
2445 package_name: Option<&str>,
2446 child_interface_id: sqry_core::graph::unified::node::NodeId,
2447 helper: &mut GraphBuildHelper,
2448) {
2449 let mut cursor = extends_node.walk();
2450 for child in extends_node.children(&mut cursor) {
2451 match child.kind() {
2452 "type_identifier" => {
2453 let type_name = extract_identifier(child, content);
2454 if !type_name.is_empty() {
2455 let parent_qualified = qualify_type_name(&type_name, package_name);
2456 let parent_id = helper.add_interface(&parent_qualified, None);
2457 helper.add_inherits_edge(child_interface_id, parent_id);
2458 }
2459 }
2460 "type_list" => {
2461 let mut type_cursor = child.walk();
2462 for type_child in child.children(&mut type_cursor) {
2463 if let Some(type_name) = extract_type_identifier(type_child, content)
2464 && !type_name.is_empty()
2465 {
2466 let parent_qualified = qualify_type_name(&type_name, package_name);
2467 let parent_id = helper.add_interface(&parent_qualified, None);
2468 helper.add_inherits_edge(child_interface_id, parent_id);
2469 }
2470 }
2471 }
2472 "generic_type" | "scoped_type_identifier" => {
2473 if let Some(type_name) = extract_type_identifier(child, content)
2474 && !type_name.is_empty()
2475 {
2476 let parent_qualified = qualify_type_name(&type_name, package_name);
2477 let parent_id = helper.add_interface(&parent_qualified, None);
2478 helper.add_inherits_edge(child_interface_id, parent_id);
2479 }
2480 }
2481 _ => {}
2482 }
2483 }
2484}
2485
2486fn extract_type_from_superclass(superclass_node: Node, content: &[u8]) -> String {
2488 if superclass_node.kind() == "type_identifier" {
2490 return extract_identifier(superclass_node, content);
2491 }
2492
2493 let mut cursor = superclass_node.walk();
2495 for child in superclass_node.children(&mut cursor) {
2496 if let Some(name) = extract_type_identifier(child, content) {
2497 return name;
2498 }
2499 }
2500
2501 extract_identifier(superclass_node, content)
2503}
2504
2505fn extract_interface_types(
2516 interfaces_node: Node,
2517 content: &[u8],
2518 package_name: Option<&str>,
2519 implementor_id: sqry_core::graph::unified::node::NodeId,
2520 helper: &mut GraphBuildHelper,
2521) {
2522 let mut cursor = interfaces_node.walk();
2524 for child in interfaces_node.children(&mut cursor) {
2525 match child.kind() {
2526 "type_identifier" => {
2528 let type_name = extract_identifier(child, content);
2529 if !type_name.is_empty() {
2530 let interface_qualified = qualify_type_name(&type_name, package_name);
2531 let interface_id = helper.add_interface(&interface_qualified, None);
2532 helper.add_implements_edge(implementor_id, interface_id);
2533 }
2534 }
2535 "type_list" => {
2537 let mut type_cursor = child.walk();
2538 for type_child in child.children(&mut type_cursor) {
2539 if let Some(type_name) = extract_type_identifier(type_child, content)
2540 && !type_name.is_empty()
2541 {
2542 let interface_qualified = qualify_type_name(&type_name, package_name);
2543 let interface_id = helper.add_interface(&interface_qualified, None);
2544 helper.add_implements_edge(implementor_id, interface_id);
2545 }
2546 }
2547 }
2548 "generic_type" | "scoped_type_identifier" => {
2550 if let Some(type_name) = extract_type_identifier(child, content)
2551 && !type_name.is_empty()
2552 {
2553 let interface_qualified = qualify_type_name(&type_name, package_name);
2554 let interface_id = helper.add_interface(&interface_qualified, None);
2555 helper.add_implements_edge(implementor_id, interface_id);
2556 }
2557 }
2558 _ => {}
2559 }
2560 }
2561}
2562
2563fn extract_type_identifier(node: Node, content: &[u8]) -> Option<String> {
2565 match node.kind() {
2566 "type_identifier" => Some(extract_identifier(node, content)),
2567 "generic_type" => {
2568 if let Some(name_node) = node.child_by_field_name("name") {
2570 Some(extract_identifier(name_node, content))
2571 } else {
2572 let mut cursor = node.walk();
2574 for child in node.children(&mut cursor) {
2575 if child.kind() == "type_identifier" {
2576 return Some(extract_identifier(child, content));
2577 }
2578 }
2579 None
2580 }
2581 }
2582 "scoped_type_identifier" => {
2583 Some(extract_full_identifier(node, content))
2585 }
2586 _ => None,
2587 }
2588}
2589
2590fn qualify_type_name(type_name: &str, package_name: Option<&str>) -> String {
2592 if type_name.contains('.') {
2594 return type_name.to_string();
2595 }
2596
2597 if let Some(pkg) = package_name {
2599 format!("{pkg}.{type_name}")
2600 } else {
2601 type_name.to_string()
2602 }
2603}
2604
2605#[allow(clippy::type_complexity)]
2614fn extract_field_and_import_types(
2615 node: Node,
2616 content: &[u8],
2617) -> (
2618 HashMap<String, (String, bool, Option<sqry_core::schema::Visibility>, bool)>,
2619 HashMap<String, String>,
2620) {
2621 let import_map = extract_import_map(node, content);
2623
2624 let mut field_types = HashMap::new();
2625 let mut class_stack = Vec::new();
2626 extract_field_types_recursive(
2627 node,
2628 content,
2629 &import_map,
2630 &mut field_types,
2631 &mut class_stack,
2632 );
2633
2634 (field_types, import_map)
2635}
2636
2637fn extract_import_map(node: Node, content: &[u8]) -> HashMap<String, String> {
2639 let mut import_map = HashMap::new();
2640 collect_import_map_recursive(node, content, &mut import_map);
2641 import_map
2642}
2643
2644fn collect_import_map_recursive(
2645 node: Node,
2646 content: &[u8],
2647 import_map: &mut HashMap<String, String>,
2648) {
2649 if node.kind() == "import_declaration" {
2650 let full_path = node.utf8_text(content).unwrap_or("");
2654
2655 if let Some(path_start) = full_path.find("import ") {
2658 let after_import = &full_path[path_start + 7..].trim();
2659 if let Some(path_end) = after_import.find(';') {
2660 let import_path = &after_import[..path_end].trim();
2661
2662 if let Some(simple_name) = import_path.rsplit('.').next() {
2664 import_map.insert(simple_name.to_string(), (*import_path).to_string());
2665 }
2666 }
2667 }
2668 }
2669
2670 let mut cursor = node.walk();
2672 for child in node.children(&mut cursor) {
2673 collect_import_map_recursive(child, content, import_map);
2674 }
2675}
2676
2677fn extract_field_types_recursive(
2678 node: Node,
2679 content: &[u8],
2680 import_map: &HashMap<String, String>,
2681 field_types: &mut HashMap<String, (String, bool, Option<sqry_core::schema::Visibility>, bool)>,
2682 class_stack: &mut Vec<String>,
2683) {
2684 if matches!(
2686 node.kind(),
2687 "class_declaration" | "interface_declaration" | "enum_declaration" | "record_declaration"
2688 ) && let Some(name_node) = node.child_by_field_name("name")
2689 {
2690 let class_name = extract_identifier(name_node, content);
2691 class_stack.push(class_name);
2692
2693 if let Some(body_node) = node.child_by_field_name("body") {
2695 let mut cursor = body_node.walk();
2696 for child in body_node.children(&mut cursor) {
2697 extract_field_types_recursive(child, content, import_map, field_types, class_stack);
2698 }
2699 }
2700
2701 class_stack.pop();
2703 return; }
2705
2706 if node.kind() == "field_declaration" {
2713 let is_final = has_modifier(node, "final", content);
2715 let is_static = has_modifier(node, "static", content);
2716
2717 let visibility = if has_modifier(node, "public", content) {
2720 Some(sqry_core::schema::Visibility::Public)
2721 } else {
2722 Some(sqry_core::schema::Visibility::Private)
2724 };
2725
2726 if let Some(type_node) = node.child_by_field_name("type") {
2728 let type_text = extract_type_name_internal(type_node, content);
2729 if !type_text.is_empty() {
2730 let resolved_type = import_map
2732 .get(&type_text)
2733 .cloned()
2734 .unwrap_or(type_text.clone());
2735
2736 let mut cursor = node.walk();
2738 for child in node.children(&mut cursor) {
2739 if child.kind() == "variable_declarator"
2740 && let Some(name_node) = child.child_by_field_name("name")
2741 {
2742 let field_name = extract_identifier(name_node, content);
2743
2744 let qualified_field = if class_stack.is_empty() {
2747 field_name
2748 } else {
2749 let class_path = class_stack.join("::");
2750 format!("{class_path}::{field_name}")
2751 };
2752
2753 field_types.insert(
2754 qualified_field,
2755 (resolved_type.clone(), is_final, visibility, is_static),
2756 );
2757 }
2758 }
2759 }
2760 }
2761 }
2762
2763 let mut cursor = node.walk();
2765 for child in node.children(&mut cursor) {
2766 extract_field_types_recursive(child, content, import_map, field_types, class_stack);
2767 }
2768}
2769
2770fn extract_type_name_internal(type_node: Node, content: &[u8]) -> String {
2772 match type_node.kind() {
2773 "generic_type" => {
2774 if let Some(name_node) = type_node.child_by_field_name("name") {
2776 extract_identifier(name_node, content)
2777 } else {
2778 extract_identifier(type_node, content)
2779 }
2780 }
2781 "scoped_type_identifier" => {
2782 extract_full_identifier(type_node, content)
2784 }
2785 _ => extract_identifier(type_node, content),
2786 }
2787}
2788
2789fn extract_identifier(node: Node, content: &[u8]) -> String {
2794 node.utf8_text(content).unwrap_or("").to_string()
2795}
2796
2797fn extract_node_text(node: Node, content: &[u8]) -> String {
2798 node.utf8_text(content).unwrap_or("").to_string()
2799}
2800
2801fn extract_full_identifier(node: Node, content: &[u8]) -> String {
2802 node.utf8_text(content).unwrap_or("").to_string()
2803}
2804
2805fn first_child_of_kind<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
2806 let mut cursor = node.walk();
2807 node.children(&mut cursor)
2808 .find(|&child| child.kind() == kind)
2809}
2810
2811fn extract_method_invocation_name(call_node: Node, content: &[u8]) -> GraphResult<String> {
2812 if let Some(name_node) = call_node.child_by_field_name("name") {
2814 Ok(extract_identifier(name_node, content))
2815 } else {
2816 let mut cursor = call_node.walk();
2818 for child in call_node.children(&mut cursor) {
2819 if child.kind() == "identifier" {
2820 return Ok(extract_identifier(child, content));
2821 }
2822 }
2823
2824 Err(GraphBuilderError::ParseError {
2825 span: Span::from_bytes(call_node.start_byte(), call_node.end_byte()),
2826 reason: "Method invocation missing name".into(),
2827 })
2828 }
2829}
2830
2831fn extract_type_name(type_node: Node, content: &[u8]) -> String {
2832 match type_node.kind() {
2834 "generic_type" => {
2835 if let Some(name_node) = type_node.child_by_field_name("name") {
2837 extract_identifier(name_node, content)
2838 } else {
2839 extract_identifier(type_node, content)
2840 }
2841 }
2842 "scoped_type_identifier" => {
2843 extract_full_identifier(type_node, content)
2845 }
2846 _ => extract_identifier(type_node, content),
2847 }
2848}
2849
2850fn extract_full_return_type(type_node: Node, content: &[u8]) -> String {
2853 type_node.utf8_text(content).unwrap_or("").to_string()
2856}
2857
2858fn has_modifier(node: Node, modifier: &str, content: &[u8]) -> bool {
2859 let mut cursor = node.walk();
2860 for child in node.children(&mut cursor) {
2861 if child.kind() == "modifiers" {
2862 let mut mod_cursor = child.walk();
2863 for modifier_child in child.children(&mut mod_cursor) {
2864 if extract_identifier(modifier_child, content) == modifier {
2865 return true;
2866 }
2867 }
2868 }
2869 }
2870 false
2871}
2872
2873#[allow(clippy::unnecessary_wraps)]
2876fn extract_visibility(node: Node, content: &[u8]) -> Option<String> {
2877 if has_modifier(node, "public", content) {
2878 Some("public".to_string())
2879 } else if has_modifier(node, "private", content) {
2880 Some("private".to_string())
2881 } else if has_modifier(node, "protected", content) {
2882 Some("protected".to_string())
2883 } else {
2884 Some("package-private".to_string())
2886 }
2887}
2888
2889fn is_public(node: Node, content: &[u8]) -> bool {
2895 has_modifier(node, "public", content)
2896}
2897
2898fn is_private(node: Node, content: &[u8]) -> bool {
2900 has_modifier(node, "private", content)
2901}
2902
2903fn export_from_file_module(
2905 helper: &mut GraphBuildHelper,
2906 exported: sqry_core::graph::unified::node::NodeId,
2907) {
2908 let module_id = helper.add_module(FILE_MODULE_NAME, None);
2909 helper.add_export_edge(module_id, exported);
2910}
2911
2912fn process_class_member_exports(
2917 body_node: Node,
2918 content: &[u8],
2919 class_qualified_name: &str,
2920 helper: &mut GraphBuildHelper,
2921 is_interface: bool,
2922) {
2923 for i in 0..body_node.child_count() {
2924 #[allow(clippy::cast_possible_truncation)]
2925 if let Some(child) = body_node.child(i as u32) {
2927 match child.kind() {
2928 "method_declaration" => {
2929 let should_export = if is_interface {
2932 !is_private(child, content)
2934 } else {
2935 is_public(child, content)
2937 };
2938
2939 if should_export && let Some(name_node) = child.child_by_field_name("name") {
2940 let method_name = extract_identifier(name_node, content);
2941 let qualified_name = format!("{class_qualified_name}.{method_name}");
2942 let span = Span::from_bytes(child.start_byte(), child.end_byte());
2943 let is_static = has_modifier(child, "static", content);
2944 let method_id =
2945 helper.add_method(&qualified_name, Some(span), false, is_static);
2946 export_from_file_module(helper, method_id);
2947 }
2948 }
2949 "constructor_declaration" => {
2950 if is_public(child, content) {
2951 let qualified_name = format!("{class_qualified_name}.<init>");
2952 let span = Span::from_bytes(child.start_byte(), child.end_byte());
2953 let method_id =
2954 helper.add_method(&qualified_name, Some(span), false, false);
2955 export_from_file_module(helper, method_id);
2956 }
2957 }
2958 "field_declaration" => {
2959 if is_public(child, content) {
2960 let mut cursor = child.walk();
2962 for field_child in child.children(&mut cursor) {
2963 if field_child.kind() == "variable_declarator"
2964 && let Some(name_node) = field_child.child_by_field_name("name")
2965 {
2966 let field_name = extract_identifier(name_node, content);
2967 let qualified_name = format!("{class_qualified_name}.{field_name}");
2968 let span = Span::from_bytes(
2969 field_child.start_byte(),
2970 field_child.end_byte(),
2971 );
2972
2973 let is_final = has_modifier(child, "final", content);
2975 let field_id = if is_final {
2976 helper.add_constant(&qualified_name, Some(span))
2977 } else {
2978 helper.add_variable(&qualified_name, Some(span))
2979 };
2980 export_from_file_module(helper, field_id);
2981 }
2982 }
2983 }
2984 }
2985 "constant_declaration" => {
2986 let mut cursor = child.walk();
2988 for const_child in child.children(&mut cursor) {
2989 if const_child.kind() == "variable_declarator"
2990 && let Some(name_node) = const_child.child_by_field_name("name")
2991 {
2992 let const_name = extract_identifier(name_node, content);
2993 let qualified_name = format!("{class_qualified_name}.{const_name}");
2994 let span =
2995 Span::from_bytes(const_child.start_byte(), const_child.end_byte());
2996 let const_id = helper.add_constant(&qualified_name, Some(span));
2997 export_from_file_module(helper, const_id);
2998 }
2999 }
3000 }
3001 "enum_constant" => {
3002 if let Some(name_node) = child.child_by_field_name("name") {
3004 let const_name = extract_identifier(name_node, content);
3005 let qualified_name = format!("{class_qualified_name}.{const_name}");
3006 let span = Span::from_bytes(child.start_byte(), child.end_byte());
3007 let const_id = helper.add_constant(&qualified_name, Some(span));
3008 export_from_file_module(helper, const_id);
3009 }
3010 }
3011 _ => {}
3012 }
3013 }
3014 }
3015}
3016
3017fn detect_ffi_imports(node: Node, content: &[u8]) -> (bool, bool) {
3024 let mut has_jna = false;
3025 let mut has_panama = false;
3026
3027 detect_ffi_imports_recursive(node, content, &mut has_jna, &mut has_panama);
3028
3029 (has_jna, has_panama)
3030}
3031
3032fn detect_ffi_imports_recursive(
3033 node: Node,
3034 content: &[u8],
3035 has_jna: &mut bool,
3036 has_panama: &mut bool,
3037) {
3038 if node.kind() == "import_declaration" {
3039 let import_text = node.utf8_text(content).unwrap_or("");
3040
3041 if import_text.contains("com.sun.jna") || import_text.contains("net.java.dev.jna") {
3043 *has_jna = true;
3044 }
3045
3046 if import_text.contains("java.lang.foreign") {
3048 *has_panama = true;
3049 }
3050 }
3051
3052 let mut cursor = node.walk();
3053 for child in node.children(&mut cursor) {
3054 detect_ffi_imports_recursive(child, content, has_jna, has_panama);
3055 }
3056}
3057
3058fn find_jna_library_interfaces(node: Node, content: &[u8]) -> Vec<String> {
3061 let mut jna_interfaces = Vec::new();
3062 find_jna_library_interfaces_recursive(node, content, &mut jna_interfaces);
3063 jna_interfaces
3064}
3065
3066fn find_jna_library_interfaces_recursive(
3067 node: Node,
3068 content: &[u8],
3069 jna_interfaces: &mut Vec<String>,
3070) {
3071 if node.kind() == "interface_declaration" {
3072 if let Some(name_node) = node.child_by_field_name("name") {
3074 let interface_name = extract_identifier(name_node, content);
3075
3076 let mut cursor = node.walk();
3078 for child in node.children(&mut cursor) {
3079 if child.kind() == "extends_interfaces" {
3080 let extends_text = child.utf8_text(content).unwrap_or("");
3081 if extends_text.contains("Library") {
3083 jna_interfaces.push(interface_name.clone());
3084 }
3085 }
3086 }
3087 }
3088 }
3089
3090 let mut cursor = node.walk();
3091 for child in node.children(&mut cursor) {
3092 find_jna_library_interfaces_recursive(child, content, jna_interfaces);
3093 }
3094}
3095
3096fn build_ffi_call_edge(
3099 call_node: Node,
3100 content: &[u8],
3101 caller_context: &MethodContext,
3102 ast_graph: &ASTGraph,
3103 helper: &mut GraphBuildHelper,
3104) -> bool {
3105 let Ok(method_name) = extract_method_invocation_name(call_node, content) else {
3107 return false;
3108 };
3109
3110 if ast_graph.has_jna_import && is_jna_native_load(call_node, content, &method_name) {
3112 let library_name = extract_jna_library_name(call_node, content);
3113 build_jna_native_load_edge(caller_context, &library_name, call_node, helper);
3114 return true;
3115 }
3116
3117 if ast_graph.has_jna_import
3119 && let Some(object_node) = call_node.child_by_field_name("object")
3120 {
3121 let object_text = extract_node_text(object_node, content);
3122
3123 let field_type = if let Some(class_name) = caller_context.class_stack.last() {
3125 let qualified_field = format!("{class_name}::{object_text}");
3126 ast_graph
3127 .field_types
3128 .get(&qualified_field)
3129 .or_else(|| ast_graph.field_types.get(&object_text))
3130 } else {
3131 ast_graph.field_types.get(&object_text)
3132 };
3133
3134 if let Some((type_name, _is_final, _visibility, _is_static)) = field_type {
3136 let simple_type = simple_type_name(type_name);
3137 if ast_graph.jna_library_interfaces.contains(&simple_type) {
3138 build_jna_method_call_edge(
3139 caller_context,
3140 &simple_type,
3141 &method_name,
3142 call_node,
3143 helper,
3144 );
3145 return true;
3146 }
3147 }
3148 }
3149
3150 if ast_graph.has_panama_import {
3152 if let Some(object_node) = call_node.child_by_field_name("object") {
3153 let object_text = extract_node_text(object_node, content);
3154
3155 if object_text == "Linker" && method_name == "nativeLinker" {
3157 build_panama_linker_edge(caller_context, call_node, helper);
3158 return true;
3159 }
3160
3161 if object_text == "SymbolLookup" && method_name == "libraryLookup" {
3163 let library_name = extract_first_string_arg(call_node, content);
3164 build_panama_library_lookup_edge(caller_context, &library_name, call_node, helper);
3165 return true;
3166 }
3167
3168 if method_name == "invokeExact" || method_name == "invoke" {
3170 if is_potential_panama_invoke(call_node, content) {
3173 build_panama_invoke_edge(caller_context, &method_name, call_node, helper);
3174 return true;
3175 }
3176 }
3177 }
3178
3179 if method_name == "nativeLinker" {
3181 let full_text = call_node.utf8_text(content).unwrap_or("");
3182 if full_text.contains("Linker") {
3183 build_panama_linker_edge(caller_context, call_node, helper);
3184 return true;
3185 }
3186 }
3187 }
3188
3189 false
3190}
3191
3192fn is_jna_native_load(call_node: Node, content: &[u8], method_name: &str) -> bool {
3194 if method_name != "load" && method_name != "loadLibrary" {
3195 return false;
3196 }
3197
3198 if let Some(object_node) = call_node.child_by_field_name("object") {
3199 let object_text = extract_node_text(object_node, content);
3200 return object_text == "Native" || object_text == "com.sun.jna.Native";
3201 }
3202
3203 false
3204}
3205
3206fn extract_jna_library_name(call_node: Node, content: &[u8]) -> String {
3209 if let Some(args_node) = call_node.child_by_field_name("arguments") {
3210 let mut cursor = args_node.walk();
3211 for child in args_node.children(&mut cursor) {
3212 if child.kind() == "string_literal" {
3213 let text = child.utf8_text(content).unwrap_or("\"unknown\"");
3214 return text.trim_matches('"').to_string();
3216 }
3217 }
3218 }
3219 "unknown".to_string()
3220}
3221
3222fn extract_first_string_arg(call_node: Node, content: &[u8]) -> String {
3224 if let Some(args_node) = call_node.child_by_field_name("arguments") {
3225 let mut cursor = args_node.walk();
3226 for child in args_node.children(&mut cursor) {
3227 if child.kind() == "string_literal" {
3228 let text = child.utf8_text(content).unwrap_or("\"unknown\"");
3229 return text.trim_matches('"').to_string();
3230 }
3231 }
3232 }
3233 "unknown".to_string()
3234}
3235
3236fn is_potential_panama_invoke(call_node: Node, content: &[u8]) -> bool {
3238 if let Some(object_node) = call_node.child_by_field_name("object") {
3240 let object_text = extract_node_text(object_node, content);
3241 let lower = object_text.to_lowercase();
3243 return lower.contains("handle")
3244 || lower.contains("downcall")
3245 || lower.contains("mh")
3246 || lower.contains("foreign");
3247 }
3248 false
3249}
3250
3251fn simple_type_name(type_name: &str) -> String {
3253 type_name
3254 .rsplit('.')
3255 .next()
3256 .unwrap_or(type_name)
3257 .to_string()
3258}
3259
3260fn build_jna_native_load_edge(
3262 caller_context: &MethodContext,
3263 library_name: &str,
3264 call_node: Node,
3265 helper: &mut GraphBuildHelper,
3266) {
3267 let caller_id = helper.ensure_method(
3268 caller_context.qualified_name(),
3269 Some(Span::from_bytes(
3270 caller_context.span.0,
3271 caller_context.span.1,
3272 )),
3273 false,
3274 caller_context.is_static,
3275 );
3276
3277 let target_name = format!("native::{library_name}");
3278 let target_id = helper.add_function(
3279 &target_name,
3280 Some(Span::from_bytes(
3281 call_node.start_byte(),
3282 call_node.end_byte(),
3283 )),
3284 false,
3285 false,
3286 );
3287
3288 helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3289}
3290
3291fn build_jna_method_call_edge(
3293 caller_context: &MethodContext,
3294 interface_name: &str,
3295 method_name: &str,
3296 call_node: Node,
3297 helper: &mut GraphBuildHelper,
3298) {
3299 let caller_id = helper.ensure_method(
3300 caller_context.qualified_name(),
3301 Some(Span::from_bytes(
3302 caller_context.span.0,
3303 caller_context.span.1,
3304 )),
3305 false,
3306 caller_context.is_static,
3307 );
3308
3309 let target_name = format!("native::{interface_name}::{method_name}");
3310 let target_id = helper.add_function(
3311 &target_name,
3312 Some(Span::from_bytes(
3313 call_node.start_byte(),
3314 call_node.end_byte(),
3315 )),
3316 false,
3317 false,
3318 );
3319
3320 helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3321}
3322
3323fn build_panama_linker_edge(
3325 caller_context: &MethodContext,
3326 call_node: Node,
3327 helper: &mut GraphBuildHelper,
3328) {
3329 let caller_id = helper.ensure_method(
3330 caller_context.qualified_name(),
3331 Some(Span::from_bytes(
3332 caller_context.span.0,
3333 caller_context.span.1,
3334 )),
3335 false,
3336 caller_context.is_static,
3337 );
3338
3339 let target_name = "native::panama::nativeLinker";
3340 let target_id = helper.add_function(
3341 target_name,
3342 Some(Span::from_bytes(
3343 call_node.start_byte(),
3344 call_node.end_byte(),
3345 )),
3346 false,
3347 false,
3348 );
3349
3350 helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3351}
3352
3353fn build_panama_library_lookup_edge(
3355 caller_context: &MethodContext,
3356 library_name: &str,
3357 call_node: Node,
3358 helper: &mut GraphBuildHelper,
3359) {
3360 let caller_id = helper.ensure_method(
3361 caller_context.qualified_name(),
3362 Some(Span::from_bytes(
3363 caller_context.span.0,
3364 caller_context.span.1,
3365 )),
3366 false,
3367 caller_context.is_static,
3368 );
3369
3370 let target_name = format!("native::panama::{library_name}");
3371 let target_id = helper.add_function(
3372 &target_name,
3373 Some(Span::from_bytes(
3374 call_node.start_byte(),
3375 call_node.end_byte(),
3376 )),
3377 false,
3378 false,
3379 );
3380
3381 helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3382}
3383
3384fn build_panama_invoke_edge(
3386 caller_context: &MethodContext,
3387 method_name: &str,
3388 call_node: Node,
3389 helper: &mut GraphBuildHelper,
3390) {
3391 let caller_id = helper.ensure_method(
3392 caller_context.qualified_name(),
3393 Some(Span::from_bytes(
3394 caller_context.span.0,
3395 caller_context.span.1,
3396 )),
3397 false,
3398 caller_context.is_static,
3399 );
3400
3401 let target_name = format!("native::panama::{method_name}");
3402 let target_id = helper.add_function(
3403 &target_name,
3404 Some(Span::from_bytes(
3405 call_node.start_byte(),
3406 call_node.end_byte(),
3407 )),
3408 false,
3409 false,
3410 );
3411
3412 helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3413}
3414
3415fn build_jni_native_method_edge(method_context: &MethodContext, helper: &mut GraphBuildHelper) {
3418 let method_id = helper.ensure_method(
3420 method_context.qualified_name(),
3421 Some(Span::from_bytes(
3422 method_context.span.0,
3423 method_context.span.1,
3424 )),
3425 false,
3426 method_context.is_static,
3427 );
3428
3429 let native_target = format!("native::jni::{}", method_context.qualified_name());
3432 let target_id = helper.add_function(&native_target, None, false, false);
3433
3434 helper.add_ffi_edge(method_id, target_id, FfiConvention::C);
3435}
3436
3437fn extract_spring_route_info(method_node: Node, content: &[u8]) -> Option<(String, String)> {
3451 let mut cursor = method_node.walk();
3453 let modifiers_node = method_node
3454 .children(&mut cursor)
3455 .find(|child| child.kind() == "modifiers")?;
3456
3457 let mut mod_cursor = modifiers_node.walk();
3459 for annotation_node in modifiers_node.children(&mut mod_cursor) {
3460 if annotation_node.kind() != "annotation" {
3461 continue;
3462 }
3463
3464 let Some(annotation_name) = extract_annotation_name(annotation_node, content) else {
3466 continue;
3467 };
3468
3469 let http_method: String = match annotation_name.as_str() {
3471 "GetMapping" => "GET".to_string(),
3472 "PostMapping" => "POST".to_string(),
3473 "PutMapping" => "PUT".to_string(),
3474 "DeleteMapping" => "DELETE".to_string(),
3475 "PatchMapping" => "PATCH".to_string(),
3476 "RequestMapping" => {
3477 extract_request_mapping_method(annotation_node, content)
3479 .unwrap_or_else(|| "GET".to_string())
3480 }
3481 _ => continue,
3482 };
3483
3484 let Some(path) = extract_annotation_path(annotation_node, content) else {
3486 continue;
3487 };
3488
3489 return Some((http_method, path));
3490 }
3491
3492 None
3493}
3494
3495fn extract_annotation_name(annotation_node: Node, content: &[u8]) -> Option<String> {
3500 let mut cursor = annotation_node.walk();
3501 for child in annotation_node.children(&mut cursor) {
3502 match child.kind() {
3503 "identifier" => {
3504 return Some(extract_identifier(child, content));
3505 }
3506 "scoped_identifier" => {
3507 let full_text = extract_identifier(child, content);
3510 return full_text.rsplit('.').next().map(String::from);
3511 }
3512 _ => {}
3513 }
3514 }
3515 None
3516}
3517
3518fn extract_annotation_path(annotation_node: Node, content: &[u8]) -> Option<String> {
3525 let mut cursor = annotation_node.walk();
3527 let args_node = annotation_node
3528 .children(&mut cursor)
3529 .find(|child| child.kind() == "annotation_argument_list")?;
3530
3531 let mut args_cursor = args_node.walk();
3533 for arg_child in args_node.children(&mut args_cursor) {
3534 match arg_child.kind() {
3535 "string_literal" => {
3537 return extract_string_content(arg_child, content);
3538 }
3539 "element_value_pair" => {
3541 if let Some(path) = extract_path_from_element_value_pair(arg_child, content) {
3542 return Some(path);
3543 }
3544 }
3545 _ => {}
3546 }
3547 }
3548
3549 None
3550}
3551
3552fn extract_request_mapping_method(annotation_node: Node, content: &[u8]) -> Option<String> {
3560 let mut cursor = annotation_node.walk();
3562 let args_node = annotation_node
3563 .children(&mut cursor)
3564 .find(|child| child.kind() == "annotation_argument_list")?;
3565
3566 let mut args_cursor = args_node.walk();
3568 for arg_child in args_node.children(&mut args_cursor) {
3569 if arg_child.kind() != "element_value_pair" {
3570 continue;
3571 }
3572
3573 let Some(key_node) = arg_child.child_by_field_name("key") else {
3575 continue;
3576 };
3577 let key_text = extract_identifier(key_node, content);
3578 if key_text != "method" {
3579 continue;
3580 }
3581
3582 let Some(value_node) = arg_child.child_by_field_name("value") else {
3584 continue;
3585 };
3586 let value_text = extract_identifier(value_node, content);
3587
3588 if let Some(method) = value_text.rsplit('.').next() {
3590 let method_upper = method.to_uppercase();
3591 if matches!(
3592 method_upper.as_str(),
3593 "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS"
3594 ) {
3595 return Some(method_upper);
3596 }
3597 }
3598 }
3599
3600 None
3601}
3602
3603fn extract_path_from_element_value_pair(pair_node: Node, content: &[u8]) -> Option<String> {
3607 let key_node = pair_node.child_by_field_name("key")?;
3608 let key_text = extract_identifier(key_node, content);
3609
3610 if key_text != "path" && key_text != "value" {
3612 return None;
3613 }
3614
3615 let value_node = pair_node.child_by_field_name("value")?;
3616 if value_node.kind() == "string_literal" {
3617 return extract_string_content(value_node, content);
3618 }
3619
3620 None
3621}
3622
3623fn extract_class_request_mapping_path(method_node: Node, content: &[u8]) -> Option<String> {
3640 let mut current = method_node.parent()?;
3642 loop {
3643 if current.kind() == "class_declaration" {
3644 break;
3645 }
3646 current = current.parent()?;
3647 }
3648
3649 let mut cursor = current.walk();
3651 let modifiers = current
3652 .children(&mut cursor)
3653 .find(|child| child.kind() == "modifiers")?;
3654
3655 let mut mod_cursor = modifiers.walk();
3656 for annotation in modifiers.children(&mut mod_cursor) {
3657 if annotation.kind() != "annotation" {
3658 continue;
3659 }
3660 let Some(name) = extract_annotation_name(annotation, content) else {
3661 continue;
3662 };
3663 if name == "RequestMapping" {
3664 return extract_annotation_path(annotation, content);
3665 }
3666 }
3667
3668 None
3669}
3670
3671fn extract_string_content(string_node: Node, content: &[u8]) -> Option<String> {
3675 let text = string_node.utf8_text(content).ok()?;
3676 let trimmed = text.trim();
3677
3678 if trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() >= 2 {
3680 Some(trimmed[1..trimmed.len() - 1].to_string())
3681 } else {
3682 None
3683 }
3684}
3685
3686fn process_type_parameter_declarations(
3711 decl_node: Node,
3712 content: &[u8],
3713 parent_qualified_name: &str,
3714 helper: &mut GraphBuildHelper,
3715) {
3716 let Some(params_node) = decl_node.child_by_field_name("type_parameters") else {
3717 return;
3718 };
3719
3720 let mut cursor = params_node.walk();
3721 for param_node in params_node.children(&mut cursor) {
3722 if param_node.kind() != "type_parameter" {
3723 continue;
3724 }
3725
3726 let Some(name_node) = first_type_parameter_name_node(param_node) else {
3731 continue;
3732 };
3733 let Ok(param_name) = name_node.utf8_text(content) else {
3734 continue;
3735 };
3736
3737 let qualified_param = format!("{parent_qualified_name}.{param_name}");
3738 let span = Span::from_bytes(name_node.start_byte(), name_node.end_byte());
3739 let param_id = helper.add_type(&qualified_param, Some(span));
3744
3745 if let Some(bound_node) = param_node
3748 .children(&mut param_node.walk())
3749 .find(|c| c.kind() == "type_bound")
3750 {
3751 emit_type_bound_constraints(bound_node, content, param_id, helper);
3752 }
3753 }
3754}
3755
3756fn first_type_parameter_name_node(param_node: Node<'_>) -> Option<Node<'_>> {
3761 let mut cursor = param_node.walk();
3762 for child in param_node.children(&mut cursor) {
3763 if matches!(child.kind(), "type_identifier" | "identifier") {
3764 return Some(child);
3765 }
3766 }
3767 None
3768}
3769
3770fn emit_type_bound_constraints(
3781 bound_node: Node,
3782 content: &[u8],
3783 param_id: sqry_core::graph::unified::node::NodeId,
3784 helper: &mut GraphBuildHelper,
3785) {
3786 let mut cursor = bound_node.walk();
3787 for child in bound_node.children(&mut cursor) {
3788 if !child.is_named() {
3792 continue;
3793 }
3794 let bound_name = extract_bound_type_base_name(child, content);
3795 if bound_name.is_empty() {
3796 continue;
3797 }
3798 let constraint_id = helper.add_type(&bound_name, None);
3799 helper.add_typeof_edge_with_context(
3800 param_id,
3801 constraint_id,
3802 Some(TypeOfContext::Constraint),
3803 None,
3804 None,
3805 );
3806 }
3807}
3808
3809fn extract_bound_type_base_name(type_node: Node, content: &[u8]) -> String {
3825 match type_node.kind() {
3826 "generic_type" => {
3827 let mut cursor = type_node.walk();
3828 for child in type_node.children(&mut cursor) {
3829 if matches!(child.kind(), "type_identifier" | "scoped_type_identifier") {
3830 return extract_bound_type_base_name(child, content);
3831 }
3832 }
3833 extract_identifier(type_node, content)
3834 }
3835 "scoped_type_identifier" => extract_full_identifier(type_node, content),
3836 _ => extract_identifier(type_node, content),
3837 }
3838}
3839
3840#[cfg(test)]
3841mod shape_tests {
3842 use super::{cf_bucket_for_java_kind, java_shape_mapping};
3843 use sqry_core::graph::unified::build::shape::{
3844 CfBucket, ShapeBudget, ShapeMapping, compute_shape_descriptor,
3845 };
3846
3847 const SAMPLE: &str = include_str!(concat!(
3848 env!("CARGO_MANIFEST_DIR"),
3849 "/../test-fixtures/shape/reference/Sample.java"
3850 ));
3851
3852 fn parse(src: &str) -> tree_sitter::Tree {
3853 let lang: tree_sitter::Language = tree_sitter_java::LANGUAGE.into();
3854 let mut p = tree_sitter::Parser::new();
3855 p.set_language(&lang).expect("load java grammar");
3856 p.parse(src, None).expect("parse")
3857 }
3858
3859 fn method_named<'t>(tree: &'t tree_sitter::Tree, name: &str) -> tree_sitter::Node<'t> {
3860 let root = tree.root_node();
3861 let mut stack = vec![root];
3862 while let Some(node) = stack.pop() {
3863 if node.kind() == "method_declaration"
3864 && node
3865 .child_by_field_name("name")
3866 .and_then(|n| n.utf8_text(SAMPLE.as_bytes()).ok())
3867 == Some(name)
3868 {
3869 return node;
3870 }
3871 let mut c = node.walk();
3872 for ch in node.children(&mut c) {
3873 stack.push(ch);
3874 }
3875 }
3876 panic!("no method_declaration named {name}");
3877 }
3878
3879 #[test]
3880 fn cf_table_is_non_empty() {
3881 let mapping = java_shape_mapping();
3882 let lang: tree_sitter::Language = tree_sitter_java::LANGUAGE.into();
3883 let mut covered = 0;
3884 for id in 0..lang.node_kind_count() {
3885 if mapping.cf_bucket(id as u16).is_some() {
3886 covered += 1;
3887 }
3888 }
3889 assert!(
3890 covered >= 10,
3891 "expected many Java CF kinds mapped, got {covered}"
3892 );
3893 }
3894
3895 #[test]
3896 fn histogram_covers_real_control_flow() {
3897 let tree = parse(SAMPLE);
3898 let func = method_named(&tree, "classify");
3899 let d = compute_shape_descriptor(
3900 func,
3901 SAMPLE.as_bytes(),
3902 java_shape_mapping(),
3903 &ShapeBudget::default(),
3904 );
3905 assert!(!d.is_unhashable());
3906 for bucket in [
3907 CfBucket::Branch,
3908 CfBucket::Loop,
3909 CfBucket::Match,
3910 CfBucket::Try,
3911 CfBucket::Catch,
3912 CfBucket::Throw,
3913 CfBucket::Return,
3914 CfBucket::BreakContinue,
3915 CfBucket::Call,
3916 CfBucket::Assign,
3917 ] {
3918 assert!(
3919 d.cf_histogram[bucket.index()] >= 1,
3920 "classify must exercise {bucket:?}"
3921 );
3922 }
3923 }
3924
3925 #[test]
3926 fn lambda_body_covers_closure() {
3927 let tree = parse(SAMPLE);
3928 let func = method_named(&tree, "adder");
3929 let d = compute_shape_descriptor(
3930 func,
3931 SAMPLE.as_bytes(),
3932 java_shape_mapping(),
3933 &ShapeBudget::default(),
3934 );
3935 assert!(
3936 d.cf_histogram[CfBucket::Closure.index()] >= 1,
3937 "lambda closure"
3938 );
3939 }
3940
3941 #[test]
3942 fn signature_shape_reads_arity_and_return() {
3943 let tree = parse(SAMPLE);
3944 let func = method_named(&tree, "classify");
3945 let mapping = java_shape_mapping();
3946 let shape = mapping.signature_shape(func, SAMPLE.as_bytes());
3947 assert_eq!(shape.arity_positional, 2);
3949 assert!(shape.has_return_annotation, "int return type");
3950 }
3951
3952 #[test]
3953 fn unknown_kind_maps_to_none() {
3954 assert!(cf_bucket_for_java_kind("program").is_none());
3955 assert!(cf_bucket_for_java_kind("identifier").is_none());
3956 }
3957}