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 = context.decl_span;
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)]
347struct MethodContext {
348 qualified_name: String,
350 span: (usize, usize),
353 decl_span: Span,
358 depth: usize,
360 is_static: bool,
362 is_constructor: bool,
364 is_native: bool,
366 package_name: Option<String>,
368 class_stack: Vec<String>,
370 return_type: Option<String>,
372 visibility: Option<String>,
374}
375
376impl MethodContext {
377 fn qualified_name(&self) -> &str {
378 &self.qualified_name
379 }
380}
381
382fn extract_java_contexts(
391 node: Node,
392 content: &[u8],
393 contexts: &mut Vec<MethodContext>,
394 class_stack: &mut Vec<String>,
395 package_name: Option<&str>,
396 depth: usize,
397 max_depth: usize,
398 guard: &mut sqry_core::query::security::RecursionGuard,
399) -> Result<(), sqry_core::query::security::RecursionError> {
400 guard.enter()?;
401
402 if depth > max_depth {
403 guard.exit();
404 return Ok(());
405 }
406
407 match node.kind() {
408 "class_declaration"
409 | "interface_declaration"
410 | "enum_declaration"
411 | "record_declaration" => {
412 if let Some(name_node) = node.child_by_field_name("name") {
414 let class_name = extract_identifier(name_node, content);
415
416 class_stack.push(class_name.clone());
418
419 if let Some(body_node) = node.child_by_field_name("body") {
421 extract_methods_from_body(
422 body_node,
423 content,
424 class_stack,
425 package_name,
426 contexts,
427 depth + 1,
428 max_depth,
429 guard,
430 )?;
431
432 for i in 0..body_node.child_count() {
434 #[allow(clippy::cast_possible_truncation)]
435 if let Some(child) = body_node.child(i as u32) {
437 extract_java_contexts(
438 child,
439 content,
440 contexts,
441 class_stack,
442 package_name,
443 depth + 1,
444 max_depth,
445 guard,
446 )?;
447 }
448 }
449 }
450
451 class_stack.pop();
453
454 guard.exit();
455 return Ok(());
456 }
457 }
458 _ => {}
459 }
460
461 for i in 0..node.child_count() {
463 #[allow(clippy::cast_possible_truncation)]
464 if let Some(child) = node.child(i as u32) {
466 extract_java_contexts(
467 child,
468 content,
469 contexts,
470 class_stack,
471 package_name,
472 depth,
473 max_depth,
474 guard,
475 )?;
476 }
477 }
478
479 guard.exit();
480 Ok(())
481}
482
483#[allow(clippy::unnecessary_wraps)]
487fn extract_methods_from_body(
488 body_node: Node,
489 content: &[u8],
490 class_stack: &[String],
491 package_name: Option<&str>,
492 contexts: &mut Vec<MethodContext>,
493 depth: usize,
494 _max_depth: usize,
495 _guard: &mut sqry_core::query::security::RecursionGuard,
496) -> Result<(), sqry_core::query::security::RecursionError> {
497 for i in 0..body_node.child_count() {
498 #[allow(clippy::cast_possible_truncation)]
499 if let Some(child) = body_node.child(i as u32) {
501 match child.kind() {
502 "method_declaration" => {
503 if let Some(method_context) =
504 extract_method_context(child, content, class_stack, package_name, depth)
505 {
506 contexts.push(method_context);
507 }
508 }
509 "constructor_declaration" | "compact_constructor_declaration" => {
510 let constructor_context = extract_constructor_context(
511 child,
512 content,
513 class_stack,
514 package_name,
515 depth,
516 );
517 contexts.push(constructor_context);
518 }
519 _ => {}
520 }
521 }
522 }
523 Ok(())
524}
525
526fn extract_method_context(
527 method_node: Node,
528 content: &[u8],
529 class_stack: &[String],
530 package_name: Option<&str>,
531 depth: usize,
532) -> Option<MethodContext> {
533 let name_node = method_node.child_by_field_name("name")?;
534 let method_name = extract_identifier(name_node, content);
535
536 let is_static = has_modifier(method_node, "static", 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 decl_span: Span::from_node(&method_node),
553 depth,
554 is_static,
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 decl_span: Span::from_node(&constructor_node),
579 depth,
580 is_static: 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_node(&node);
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_node(&node);
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_node(¶m_node);
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_node(¶m_node);
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_node(¶m_node);
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_node(&usage_node);
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_node(&node);
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_node(&child);
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_node(&name_node);
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_node(¶m_node);
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_node(¶ms_node);
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_node(&child);
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_node(&child);
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_node(&resource);
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_node(&name_node);
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_node(&name_node);
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_node(&component);
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_node(&call_node);
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_node(&call_node),
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_node(&child);
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_node(&child);
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 = Span::from_node(&const_child);
2995 let const_id = helper.add_constant(&qualified_name, Some(span));
2996 export_from_file_module(helper, const_id);
2997 }
2998 }
2999 }
3000 "enum_constant" => {
3001 if let Some(name_node) = child.child_by_field_name("name") {
3003 let const_name = extract_identifier(name_node, content);
3004 let qualified_name = format!("{class_qualified_name}.{const_name}");
3005 let span = Span::from_node(&child);
3006 let const_id = helper.add_constant(&qualified_name, Some(span));
3007 export_from_file_module(helper, const_id);
3008 }
3009 }
3010 _ => {}
3011 }
3012 }
3013 }
3014}
3015
3016fn detect_ffi_imports(node: Node, content: &[u8]) -> (bool, bool) {
3023 let mut has_jna = false;
3024 let mut has_panama = false;
3025
3026 detect_ffi_imports_recursive(node, content, &mut has_jna, &mut has_panama);
3027
3028 (has_jna, has_panama)
3029}
3030
3031fn detect_ffi_imports_recursive(
3032 node: Node,
3033 content: &[u8],
3034 has_jna: &mut bool,
3035 has_panama: &mut bool,
3036) {
3037 if node.kind() == "import_declaration" {
3038 let import_text = node.utf8_text(content).unwrap_or("");
3039
3040 if import_text.contains("com.sun.jna") || import_text.contains("net.java.dev.jna") {
3042 *has_jna = true;
3043 }
3044
3045 if import_text.contains("java.lang.foreign") {
3047 *has_panama = true;
3048 }
3049 }
3050
3051 let mut cursor = node.walk();
3052 for child in node.children(&mut cursor) {
3053 detect_ffi_imports_recursive(child, content, has_jna, has_panama);
3054 }
3055}
3056
3057fn find_jna_library_interfaces(node: Node, content: &[u8]) -> Vec<String> {
3060 let mut jna_interfaces = Vec::new();
3061 find_jna_library_interfaces_recursive(node, content, &mut jna_interfaces);
3062 jna_interfaces
3063}
3064
3065fn find_jna_library_interfaces_recursive(
3066 node: Node,
3067 content: &[u8],
3068 jna_interfaces: &mut Vec<String>,
3069) {
3070 if node.kind() == "interface_declaration" {
3071 if let Some(name_node) = node.child_by_field_name("name") {
3073 let interface_name = extract_identifier(name_node, content);
3074
3075 let mut cursor = node.walk();
3077 for child in node.children(&mut cursor) {
3078 if child.kind() == "extends_interfaces" {
3079 let extends_text = child.utf8_text(content).unwrap_or("");
3080 if extends_text.contains("Library") {
3082 jna_interfaces.push(interface_name.clone());
3083 }
3084 }
3085 }
3086 }
3087 }
3088
3089 let mut cursor = node.walk();
3090 for child in node.children(&mut cursor) {
3091 find_jna_library_interfaces_recursive(child, content, jna_interfaces);
3092 }
3093}
3094
3095fn build_ffi_call_edge(
3098 call_node: Node,
3099 content: &[u8],
3100 caller_context: &MethodContext,
3101 ast_graph: &ASTGraph,
3102 helper: &mut GraphBuildHelper,
3103) -> bool {
3104 let Ok(method_name) = extract_method_invocation_name(call_node, content) else {
3106 return false;
3107 };
3108
3109 if ast_graph.has_jna_import && is_jna_native_load(call_node, content, &method_name) {
3111 let library_name = extract_jna_library_name(call_node, content);
3112 build_jna_native_load_edge(caller_context, &library_name, call_node, helper);
3113 return true;
3114 }
3115
3116 if ast_graph.has_jna_import
3118 && let Some(object_node) = call_node.child_by_field_name("object")
3119 {
3120 let object_text = extract_node_text(object_node, content);
3121
3122 let field_type = if let Some(class_name) = caller_context.class_stack.last() {
3124 let qualified_field = format!("{class_name}::{object_text}");
3125 ast_graph
3126 .field_types
3127 .get(&qualified_field)
3128 .or_else(|| ast_graph.field_types.get(&object_text))
3129 } else {
3130 ast_graph.field_types.get(&object_text)
3131 };
3132
3133 if let Some((type_name, _is_final, _visibility, _is_static)) = field_type {
3135 let simple_type = simple_type_name(type_name);
3136 if ast_graph.jna_library_interfaces.contains(&simple_type) {
3137 build_jna_method_call_edge(
3138 caller_context,
3139 &simple_type,
3140 &method_name,
3141 call_node,
3142 helper,
3143 );
3144 return true;
3145 }
3146 }
3147 }
3148
3149 if ast_graph.has_panama_import {
3151 if let Some(object_node) = call_node.child_by_field_name("object") {
3152 let object_text = extract_node_text(object_node, content);
3153
3154 if object_text == "Linker" && method_name == "nativeLinker" {
3156 build_panama_linker_edge(caller_context, call_node, helper);
3157 return true;
3158 }
3159
3160 if object_text == "SymbolLookup" && method_name == "libraryLookup" {
3162 let library_name = extract_first_string_arg(call_node, content);
3163 build_panama_library_lookup_edge(caller_context, &library_name, call_node, helper);
3164 return true;
3165 }
3166
3167 if method_name == "invokeExact" || method_name == "invoke" {
3169 if is_potential_panama_invoke(call_node, content) {
3172 build_panama_invoke_edge(caller_context, &method_name, call_node, helper);
3173 return true;
3174 }
3175 }
3176 }
3177
3178 if method_name == "nativeLinker" {
3180 let full_text = call_node.utf8_text(content).unwrap_or("");
3181 if full_text.contains("Linker") {
3182 build_panama_linker_edge(caller_context, call_node, helper);
3183 return true;
3184 }
3185 }
3186 }
3187
3188 false
3189}
3190
3191fn is_jna_native_load(call_node: Node, content: &[u8], method_name: &str) -> bool {
3193 if method_name != "load" && method_name != "loadLibrary" {
3194 return false;
3195 }
3196
3197 if let Some(object_node) = call_node.child_by_field_name("object") {
3198 let object_text = extract_node_text(object_node, content);
3199 return object_text == "Native" || object_text == "com.sun.jna.Native";
3200 }
3201
3202 false
3203}
3204
3205fn extract_jna_library_name(call_node: Node, content: &[u8]) -> String {
3208 if let Some(args_node) = call_node.child_by_field_name("arguments") {
3209 let mut cursor = args_node.walk();
3210 for child in args_node.children(&mut cursor) {
3211 if child.kind() == "string_literal" {
3212 let text = child.utf8_text(content).unwrap_or("\"unknown\"");
3213 return text.trim_matches('"').to_string();
3215 }
3216 }
3217 }
3218 "unknown".to_string()
3219}
3220
3221fn extract_first_string_arg(call_node: Node, content: &[u8]) -> String {
3223 if let Some(args_node) = call_node.child_by_field_name("arguments") {
3224 let mut cursor = args_node.walk();
3225 for child in args_node.children(&mut cursor) {
3226 if child.kind() == "string_literal" {
3227 let text = child.utf8_text(content).unwrap_or("\"unknown\"");
3228 return text.trim_matches('"').to_string();
3229 }
3230 }
3231 }
3232 "unknown".to_string()
3233}
3234
3235fn is_potential_panama_invoke(call_node: Node, content: &[u8]) -> bool {
3237 if let Some(object_node) = call_node.child_by_field_name("object") {
3239 let object_text = extract_node_text(object_node, content);
3240 let lower = object_text.to_lowercase();
3242 return lower.contains("handle")
3243 || lower.contains("downcall")
3244 || lower.contains("mh")
3245 || lower.contains("foreign");
3246 }
3247 false
3248}
3249
3250fn simple_type_name(type_name: &str) -> String {
3252 type_name
3253 .rsplit('.')
3254 .next()
3255 .unwrap_or(type_name)
3256 .to_string()
3257}
3258
3259fn build_jna_native_load_edge(
3261 caller_context: &MethodContext,
3262 library_name: &str,
3263 call_node: Node,
3264 helper: &mut GraphBuildHelper,
3265) {
3266 let caller_id = helper.ensure_method(
3267 caller_context.qualified_name(),
3268 Some(Span::from_bytes(
3269 caller_context.span.0,
3270 caller_context.span.1,
3271 )),
3272 false,
3273 caller_context.is_static,
3274 );
3275
3276 let target_name = format!("native::{library_name}");
3277 let target_id = helper.add_function(
3278 &target_name,
3279 Some(Span::from_bytes(
3280 call_node.start_byte(),
3281 call_node.end_byte(),
3282 )),
3283 false,
3284 false,
3285 );
3286
3287 helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3288}
3289
3290fn build_jna_method_call_edge(
3292 caller_context: &MethodContext,
3293 interface_name: &str,
3294 method_name: &str,
3295 call_node: Node,
3296 helper: &mut GraphBuildHelper,
3297) {
3298 let caller_id = helper.ensure_method(
3299 caller_context.qualified_name(),
3300 Some(Span::from_bytes(
3301 caller_context.span.0,
3302 caller_context.span.1,
3303 )),
3304 false,
3305 caller_context.is_static,
3306 );
3307
3308 let target_name = format!("native::{interface_name}::{method_name}");
3309 let target_id = helper.add_function(
3310 &target_name,
3311 Some(Span::from_bytes(
3312 call_node.start_byte(),
3313 call_node.end_byte(),
3314 )),
3315 false,
3316 false,
3317 );
3318
3319 helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3320}
3321
3322fn build_panama_linker_edge(
3324 caller_context: &MethodContext,
3325 call_node: Node,
3326 helper: &mut GraphBuildHelper,
3327) {
3328 let caller_id = helper.ensure_method(
3329 caller_context.qualified_name(),
3330 Some(Span::from_bytes(
3331 caller_context.span.0,
3332 caller_context.span.1,
3333 )),
3334 false,
3335 caller_context.is_static,
3336 );
3337
3338 let target_name = "native::panama::nativeLinker";
3339 let target_id = helper.add_function(
3340 target_name,
3341 Some(Span::from_bytes(
3342 call_node.start_byte(),
3343 call_node.end_byte(),
3344 )),
3345 false,
3346 false,
3347 );
3348
3349 helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3350}
3351
3352fn build_panama_library_lookup_edge(
3354 caller_context: &MethodContext,
3355 library_name: &str,
3356 call_node: Node,
3357 helper: &mut GraphBuildHelper,
3358) {
3359 let caller_id = helper.ensure_method(
3360 caller_context.qualified_name(),
3361 Some(Span::from_bytes(
3362 caller_context.span.0,
3363 caller_context.span.1,
3364 )),
3365 false,
3366 caller_context.is_static,
3367 );
3368
3369 let target_name = format!("native::panama::{library_name}");
3370 let target_id = helper.add_function(
3371 &target_name,
3372 Some(Span::from_bytes(
3373 call_node.start_byte(),
3374 call_node.end_byte(),
3375 )),
3376 false,
3377 false,
3378 );
3379
3380 helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3381}
3382
3383fn build_panama_invoke_edge(
3385 caller_context: &MethodContext,
3386 method_name: &str,
3387 call_node: Node,
3388 helper: &mut GraphBuildHelper,
3389) {
3390 let caller_id = helper.ensure_method(
3391 caller_context.qualified_name(),
3392 Some(Span::from_bytes(
3393 caller_context.span.0,
3394 caller_context.span.1,
3395 )),
3396 false,
3397 caller_context.is_static,
3398 );
3399
3400 let target_name = format!("native::panama::{method_name}");
3401 let target_id = helper.add_function(
3402 &target_name,
3403 Some(Span::from_bytes(
3404 call_node.start_byte(),
3405 call_node.end_byte(),
3406 )),
3407 false,
3408 false,
3409 );
3410
3411 helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3412}
3413
3414fn build_jni_native_method_edge(method_context: &MethodContext, helper: &mut GraphBuildHelper) {
3417 let method_id = helper.ensure_method(
3419 method_context.qualified_name(),
3420 Some(Span::from_bytes(
3421 method_context.span.0,
3422 method_context.span.1,
3423 )),
3424 false,
3425 method_context.is_static,
3426 );
3427
3428 let native_target = format!("native::jni::{}", method_context.qualified_name());
3431 let target_id = helper.add_function(&native_target, None, false, false);
3432
3433 helper.add_ffi_edge(method_id, target_id, FfiConvention::C);
3434}
3435
3436fn extract_spring_route_info(method_node: Node, content: &[u8]) -> Option<(String, String)> {
3450 let mut cursor = method_node.walk();
3452 let modifiers_node = method_node
3453 .children(&mut cursor)
3454 .find(|child| child.kind() == "modifiers")?;
3455
3456 let mut mod_cursor = modifiers_node.walk();
3458 for annotation_node in modifiers_node.children(&mut mod_cursor) {
3459 if annotation_node.kind() != "annotation" {
3460 continue;
3461 }
3462
3463 let Some(annotation_name) = extract_annotation_name(annotation_node, content) else {
3465 continue;
3466 };
3467
3468 let http_method: String = match annotation_name.as_str() {
3470 "GetMapping" => "GET".to_string(),
3471 "PostMapping" => "POST".to_string(),
3472 "PutMapping" => "PUT".to_string(),
3473 "DeleteMapping" => "DELETE".to_string(),
3474 "PatchMapping" => "PATCH".to_string(),
3475 "RequestMapping" => {
3476 extract_request_mapping_method(annotation_node, content)
3478 .unwrap_or_else(|| "GET".to_string())
3479 }
3480 _ => continue,
3481 };
3482
3483 let Some(path) = extract_annotation_path(annotation_node, content) else {
3485 continue;
3486 };
3487
3488 return Some((http_method, path));
3489 }
3490
3491 None
3492}
3493
3494fn extract_annotation_name(annotation_node: Node, content: &[u8]) -> Option<String> {
3499 let mut cursor = annotation_node.walk();
3500 for child in annotation_node.children(&mut cursor) {
3501 match child.kind() {
3502 "identifier" => {
3503 return Some(extract_identifier(child, content));
3504 }
3505 "scoped_identifier" => {
3506 let full_text = extract_identifier(child, content);
3509 return full_text.rsplit('.').next().map(String::from);
3510 }
3511 _ => {}
3512 }
3513 }
3514 None
3515}
3516
3517fn extract_annotation_path(annotation_node: Node, content: &[u8]) -> Option<String> {
3524 let mut cursor = annotation_node.walk();
3526 let args_node = annotation_node
3527 .children(&mut cursor)
3528 .find(|child| child.kind() == "annotation_argument_list")?;
3529
3530 let mut args_cursor = args_node.walk();
3532 for arg_child in args_node.children(&mut args_cursor) {
3533 match arg_child.kind() {
3534 "string_literal" => {
3536 return extract_string_content(arg_child, content);
3537 }
3538 "element_value_pair" => {
3540 if let Some(path) = extract_path_from_element_value_pair(arg_child, content) {
3541 return Some(path);
3542 }
3543 }
3544 _ => {}
3545 }
3546 }
3547
3548 None
3549}
3550
3551fn extract_request_mapping_method(annotation_node: Node, content: &[u8]) -> Option<String> {
3559 let mut cursor = annotation_node.walk();
3561 let args_node = annotation_node
3562 .children(&mut cursor)
3563 .find(|child| child.kind() == "annotation_argument_list")?;
3564
3565 let mut args_cursor = args_node.walk();
3567 for arg_child in args_node.children(&mut args_cursor) {
3568 if arg_child.kind() != "element_value_pair" {
3569 continue;
3570 }
3571
3572 let Some(key_node) = arg_child.child_by_field_name("key") else {
3574 continue;
3575 };
3576 let key_text = extract_identifier(key_node, content);
3577 if key_text != "method" {
3578 continue;
3579 }
3580
3581 let Some(value_node) = arg_child.child_by_field_name("value") else {
3583 continue;
3584 };
3585 let value_text = extract_identifier(value_node, content);
3586
3587 if let Some(method) = value_text.rsplit('.').next() {
3589 let method_upper = method.to_uppercase();
3590 if matches!(
3591 method_upper.as_str(),
3592 "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS"
3593 ) {
3594 return Some(method_upper);
3595 }
3596 }
3597 }
3598
3599 None
3600}
3601
3602fn extract_path_from_element_value_pair(pair_node: Node, content: &[u8]) -> Option<String> {
3606 let key_node = pair_node.child_by_field_name("key")?;
3607 let key_text = extract_identifier(key_node, content);
3608
3609 if key_text != "path" && key_text != "value" {
3611 return None;
3612 }
3613
3614 let value_node = pair_node.child_by_field_name("value")?;
3615 if value_node.kind() == "string_literal" {
3616 return extract_string_content(value_node, content);
3617 }
3618
3619 None
3620}
3621
3622fn extract_class_request_mapping_path(method_node: Node, content: &[u8]) -> Option<String> {
3639 let mut current = method_node.parent()?;
3641 loop {
3642 if current.kind() == "class_declaration" {
3643 break;
3644 }
3645 current = current.parent()?;
3646 }
3647
3648 let mut cursor = current.walk();
3650 let modifiers = current
3651 .children(&mut cursor)
3652 .find(|child| child.kind() == "modifiers")?;
3653
3654 let mut mod_cursor = modifiers.walk();
3655 for annotation in modifiers.children(&mut mod_cursor) {
3656 if annotation.kind() != "annotation" {
3657 continue;
3658 }
3659 let Some(name) = extract_annotation_name(annotation, content) else {
3660 continue;
3661 };
3662 if name == "RequestMapping" {
3663 return extract_annotation_path(annotation, content);
3664 }
3665 }
3666
3667 None
3668}
3669
3670fn extract_string_content(string_node: Node, content: &[u8]) -> Option<String> {
3674 let text = string_node.utf8_text(content).ok()?;
3675 let trimmed = text.trim();
3676
3677 if trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() >= 2 {
3679 Some(trimmed[1..trimmed.len() - 1].to_string())
3680 } else {
3681 None
3682 }
3683}
3684
3685fn process_type_parameter_declarations(
3710 decl_node: Node,
3711 content: &[u8],
3712 parent_qualified_name: &str,
3713 helper: &mut GraphBuildHelper,
3714) {
3715 let Some(params_node) = decl_node.child_by_field_name("type_parameters") else {
3716 return;
3717 };
3718
3719 let mut cursor = params_node.walk();
3720 for param_node in params_node.children(&mut cursor) {
3721 if param_node.kind() != "type_parameter" {
3722 continue;
3723 }
3724
3725 let Some(name_node) = first_type_parameter_name_node(param_node) else {
3730 continue;
3731 };
3732 let Ok(param_name) = name_node.utf8_text(content) else {
3733 continue;
3734 };
3735
3736 let qualified_param = format!("{parent_qualified_name}.{param_name}");
3737 let span = Span::from_node(&name_node);
3738 let param_id = helper.add_type(&qualified_param, Some(span));
3743
3744 if let Some(bound_node) = param_node
3747 .children(&mut param_node.walk())
3748 .find(|c| c.kind() == "type_bound")
3749 {
3750 emit_type_bound_constraints(bound_node, content, param_id, helper);
3751 }
3752 }
3753}
3754
3755fn first_type_parameter_name_node(param_node: Node<'_>) -> Option<Node<'_>> {
3760 let mut cursor = param_node.walk();
3761 for child in param_node.children(&mut cursor) {
3762 if matches!(child.kind(), "type_identifier" | "identifier") {
3763 return Some(child);
3764 }
3765 }
3766 None
3767}
3768
3769fn emit_type_bound_constraints(
3780 bound_node: Node,
3781 content: &[u8],
3782 param_id: sqry_core::graph::unified::node::NodeId,
3783 helper: &mut GraphBuildHelper,
3784) {
3785 let mut cursor = bound_node.walk();
3786 for child in bound_node.children(&mut cursor) {
3787 if !child.is_named() {
3791 continue;
3792 }
3793 let bound_name = extract_bound_type_base_name(child, content);
3794 if bound_name.is_empty() {
3795 continue;
3796 }
3797 let constraint_id = helper.add_type(&bound_name, None);
3798 helper.add_typeof_edge_with_context(
3799 param_id,
3800 constraint_id,
3801 Some(TypeOfContext::Constraint),
3802 None,
3803 None,
3804 );
3805 }
3806}
3807
3808fn extract_bound_type_base_name(type_node: Node, content: &[u8]) -> String {
3824 match type_node.kind() {
3825 "generic_type" => {
3826 let mut cursor = type_node.walk();
3827 for child in type_node.children(&mut cursor) {
3828 if matches!(child.kind(), "type_identifier" | "scoped_type_identifier") {
3829 return extract_bound_type_base_name(child, content);
3830 }
3831 }
3832 extract_identifier(type_node, content)
3833 }
3834 "scoped_type_identifier" => extract_full_identifier(type_node, content),
3835 _ => extract_identifier(type_node, content),
3836 }
3837}
3838
3839#[cfg(test)]
3840mod shape_tests {
3841 use super::{cf_bucket_for_java_kind, java_shape_mapping};
3842 use sqry_core::graph::unified::build::shape::{
3843 CfBucket, ShapeBudget, ShapeMapping, compute_shape_descriptor,
3844 };
3845
3846 const SAMPLE: &str = include_str!(concat!(
3847 env!("CARGO_MANIFEST_DIR"),
3848 "/../test-fixtures/shape/reference/Sample.java"
3849 ));
3850
3851 fn parse(src: &str) -> tree_sitter::Tree {
3852 let lang: tree_sitter::Language = tree_sitter_java::LANGUAGE.into();
3853 let mut p = tree_sitter::Parser::new();
3854 p.set_language(&lang).expect("load java grammar");
3855 p.parse(src, None).expect("parse")
3856 }
3857
3858 fn method_named<'t>(tree: &'t tree_sitter::Tree, name: &str) -> tree_sitter::Node<'t> {
3859 let root = tree.root_node();
3860 let mut stack = vec![root];
3861 while let Some(node) = stack.pop() {
3862 if node.kind() == "method_declaration"
3863 && node
3864 .child_by_field_name("name")
3865 .and_then(|n| n.utf8_text(SAMPLE.as_bytes()).ok())
3866 == Some(name)
3867 {
3868 return node;
3869 }
3870 let mut c = node.walk();
3871 for ch in node.children(&mut c) {
3872 stack.push(ch);
3873 }
3874 }
3875 panic!("no method_declaration named {name}");
3876 }
3877
3878 #[test]
3879 fn cf_table_is_non_empty() {
3880 let mapping = java_shape_mapping();
3881 let lang: tree_sitter::Language = tree_sitter_java::LANGUAGE.into();
3882 let mut covered = 0;
3883 for id in 0..lang.node_kind_count() {
3884 if mapping.cf_bucket(id as u16).is_some() {
3885 covered += 1;
3886 }
3887 }
3888 assert!(
3889 covered >= 10,
3890 "expected many Java CF kinds mapped, got {covered}"
3891 );
3892 }
3893
3894 #[test]
3895 fn histogram_covers_real_control_flow() {
3896 let tree = parse(SAMPLE);
3897 let func = method_named(&tree, "classify");
3898 let d = compute_shape_descriptor(
3899 func,
3900 SAMPLE.as_bytes(),
3901 java_shape_mapping(),
3902 &ShapeBudget::default(),
3903 );
3904 assert!(!d.is_unhashable());
3905 for bucket in [
3906 CfBucket::Branch,
3907 CfBucket::Loop,
3908 CfBucket::Match,
3909 CfBucket::Try,
3910 CfBucket::Catch,
3911 CfBucket::Throw,
3912 CfBucket::Return,
3913 CfBucket::BreakContinue,
3914 CfBucket::Call,
3915 CfBucket::Assign,
3916 ] {
3917 assert!(
3918 d.cf_histogram[bucket.index()] >= 1,
3919 "classify must exercise {bucket:?}"
3920 );
3921 }
3922 }
3923
3924 #[test]
3925 fn lambda_body_covers_closure() {
3926 let tree = parse(SAMPLE);
3927 let func = method_named(&tree, "adder");
3928 let d = compute_shape_descriptor(
3929 func,
3930 SAMPLE.as_bytes(),
3931 java_shape_mapping(),
3932 &ShapeBudget::default(),
3933 );
3934 assert!(
3935 d.cf_histogram[CfBucket::Closure.index()] >= 1,
3936 "lambda closure"
3937 );
3938 }
3939
3940 #[test]
3941 fn signature_shape_reads_arity_and_return() {
3942 let tree = parse(SAMPLE);
3943 let func = method_named(&tree, "classify");
3944 let mapping = java_shape_mapping();
3945 let shape = mapping.signature_shape(func, SAMPLE.as_bytes());
3946 assert_eq!(shape.arity_positional, 2);
3948 assert!(shape.has_return_annotation, "int return type");
3949 }
3950
3951 #[test]
3952 fn unknown_kind_maps_to_none() {
3953 assert!(cf_bucket_for_java_kind("program").is_none());
3954 assert!(cf_bucket_for_java_kind("identifier").is_none());
3955 }
3956}