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