1use std::{collections::HashMap, path::Path, sync::OnceLock};
2
3use crate::relations::java_common::{PackageResolver, build_member_symbol, build_symbol};
4use crate::relations::local_scopes::{self, JavaScopeTree, ResolutionOutcome};
5use sqry_core::graph::unified::StagingGraph;
6use sqry_core::graph::unified::build::helper::GraphBuildHelper;
7use sqry_core::graph::unified::build::shape::{CfBucket, ShapeMapping};
8use sqry_core::graph::unified::edge::FfiConvention;
9use sqry_core::graph::unified::edge::kind::TypeOfContext;
10use sqry_core::graph::unified::storage::shape::SignatureShape;
11use sqry_core::graph::{GraphBuilder, GraphBuilderError, GraphResult, Language, Span};
12use tree_sitter::{Node, Tree};
13
14const DEFAULT_SCOPE_DEPTH: usize = 4;
15
16const FILE_MODULE_NAME: &str = "<file_module>";
19
20#[derive(Debug, Clone, Copy)]
41pub struct JavaGraphBuilder {
42 max_scope_depth: usize,
43}
44
45impl Default for JavaGraphBuilder {
46 fn default() -> Self {
47 Self {
48 max_scope_depth: DEFAULT_SCOPE_DEPTH,
49 }
50 }
51}
52
53impl JavaGraphBuilder {
54 #[must_use]
55 pub fn new(max_scope_depth: usize) -> Self {
56 Self { max_scope_depth }
57 }
58}
59
60impl GraphBuilder for JavaGraphBuilder {
61 fn build_graph(
62 &self,
63 tree: &Tree,
64 content: &[u8],
65 file: &Path,
66 staging: &mut StagingGraph,
67 ) -> GraphResult<()> {
68 let mut helper = GraphBuildHelper::new(staging, file, Language::Java);
69
70 let ast_graph = ASTGraph::from_tree(tree, content, self.max_scope_depth);
72 let mut scope_tree = local_scopes::build(tree.root_node(), content, Some(file))?;
73
74 for context in ast_graph.contexts() {
76 let qualified_name = context.qualified_name();
77 let span = Span::from_bytes(context.span.0, context.span.1);
78
79 if context.is_constructor {
80 helper.add_method_with_visibility(
81 qualified_name,
82 Some(span),
83 false,
84 false,
85 context.visibility.as_deref(),
86 );
87 } else {
88 let method_id = helper.add_method_with_signature(
90 qualified_name,
91 Some(span),
92 false,
93 context.is_static,
94 context.visibility.as_deref(),
95 context.return_type.as_deref(),
96 );
97
98 if let Some(return_type_text) = context.return_type.as_deref()
107 && return_type_text.trim() != "void"
108 {
109 let type_id = helper.add_type(return_type_text, None);
110 let method_simple_name = qualified_name
111 .rsplit_once('.')
112 .map_or(qualified_name, |(_, simple)| simple);
113 helper.add_typeof_edge_with_context(
114 method_id,
115 type_id,
116 Some(TypeOfContext::Return),
117 Some(0),
118 Some(method_simple_name),
119 );
120 }
121
122 if context.is_native {
124 build_jni_native_method_edge(context, &mut helper);
125 }
126 }
127 }
128
129 add_field_typeof_edges(&ast_graph, &mut helper);
131
132 let root = tree.root_node();
134 walk_tree_for_edges(
135 root,
136 content,
137 &ast_graph,
138 &mut scope_tree,
139 &mut helper,
140 tree,
141 )?;
142
143 Ok(())
144 }
145
146 fn language(&self) -> Language {
147 Language::Java
148 }
149
150 fn shape_mapping(&self) -> Option<&dyn ShapeMapping> {
151 Some(java_shape_mapping())
152 }
153}
154
155pub struct JavaShapeMapping {
162 cf_by_kind_id: Vec<Option<CfBucket>>,
163}
164
165impl JavaShapeMapping {
166 fn build() -> Self {
168 let lang: tree_sitter::Language = tree_sitter_java::LANGUAGE.into();
169 let count = lang.node_kind_count();
170 let mut cf_by_kind_id = vec![None; count];
171 for (id, slot) in cf_by_kind_id.iter_mut().enumerate() {
172 let Ok(kind_id) = u16::try_from(id) else {
173 break;
174 };
175 if !lang.node_kind_is_named(kind_id) {
176 continue;
177 }
178 if let Some(name) = lang.node_kind_for_id(kind_id) {
179 *slot = cf_bucket_for_java_kind(name);
180 }
181 }
182 Self { cf_by_kind_id }
183 }
184}
185
186impl ShapeMapping for JavaShapeMapping {
187 fn cf_bucket(&self, ts_node_kind_id: u16) -> Option<CfBucket> {
188 self.cf_by_kind_id
189 .get(ts_node_kind_id as usize)
190 .copied()
191 .flatten()
192 }
193
194 fn signature_shape(&self, fn_node: Node, _src: &[u8]) -> SignatureShape {
195 let mut shape = SignatureShape::default();
196 if let Some(params) = fn_node.child_by_field_name("parameters") {
197 let mut cursor = params.walk();
198 for child in params.named_children(&mut cursor) {
199 match child.kind() {
200 "formal_parameter" => {
201 shape.arity_positional = shape.arity_positional.saturating_add(1);
202 }
203 "spread_parameter" => {
205 shape.has_varargs = true;
206 shape.arity_positional = shape.arity_positional.saturating_add(1);
207 }
208 _ => {}
209 }
210 }
211 }
212 shape.has_return_annotation = fn_node.child_by_field_name("type").is_some();
215 shape
216 }
217}
218
219fn cf_bucket_for_java_kind(name: &str) -> Option<CfBucket> {
222 let bucket = match name {
223 "if_statement" | "ternary_expression" => CfBucket::Branch,
224 "for_statement" | "enhanced_for_statement" | "while_statement" | "do_statement" => {
225 CfBucket::Loop
226 }
227 "switch_expression" | "switch_block_statement_group" | "switch_label" | "switch_rule" => {
228 CfBucket::Match
229 }
230 "try_statement" => CfBucket::Try,
231 "catch_clause" => CfBucket::Catch,
232 "throw_statement" => CfBucket::Throw,
233 "try_with_resources_statement" | "synchronized_statement" => CfBucket::Resource,
235 "return_statement" => CfBucket::Return,
236 "yield_statement" => CfBucket::Yield,
237 "break_statement" | "continue_statement" => CfBucket::BreakContinue,
238 "method_invocation" | "object_creation_expression" | "explicit_constructor_invocation" => {
239 CfBucket::Call
240 }
241 "local_variable_declaration" | "assignment_expression" => CfBucket::Assign,
242 "lambda_expression" => CfBucket::Closure,
243 _ => return None,
244 };
245 Some(bucket)
246}
247
248#[must_use]
250pub fn java_shape_mapping() -> &'static JavaShapeMapping {
251 static MAPPING: OnceLock<JavaShapeMapping> = OnceLock::new();
252 MAPPING.get_or_init(JavaShapeMapping::build)
253}
254
255#[derive(Debug)]
260struct ASTGraph {
261 contexts: Vec<MethodContext>,
262 field_types: HashMap<String, (String, bool, Option<sqry_core::schema::Visibility>, bool)>,
272 import_map: HashMap<String, String>,
276 has_jna_import: bool,
278 has_panama_import: bool,
280 jna_library_interfaces: Vec<String>,
282}
283
284impl ASTGraph {
285 fn from_tree(tree: &Tree, content: &[u8], max_depth: usize) -> Self {
286 let package_name = PackageResolver::package_from_ast(tree, content);
288
289 let mut contexts = Vec::new();
290 let mut class_stack = Vec::new();
291
292 let recursion_limits = sqry_core::config::RecursionLimits::load_or_default()
294 .expect("Failed to load recursion limits");
295 let file_ops_depth = recursion_limits
296 .effective_file_ops_depth()
297 .expect("Invalid file_ops_depth configuration");
298 let mut guard = sqry_core::query::security::RecursionGuard::new(file_ops_depth)
299 .expect("Failed to create recursion guard");
300
301 if let Err(e) = extract_java_contexts(
302 tree.root_node(),
303 content,
304 &mut contexts,
305 &mut class_stack,
306 package_name.as_deref(),
307 0,
308 max_depth,
309 &mut guard,
310 ) {
311 eprintln!("Warning: Java AST traversal hit recursion limit: {e}");
312 }
313
314 let (field_types, import_map) = extract_field_and_import_types(tree.root_node(), content);
316
317 let (has_jna_import, has_panama_import) = detect_ffi_imports(tree.root_node(), content);
319
320 let jna_library_interfaces = find_jna_library_interfaces(tree.root_node(), content);
322
323 Self {
324 contexts,
325 field_types,
326 import_map,
327 has_jna_import,
328 has_panama_import,
329 jna_library_interfaces,
330 }
331 }
332
333 fn contexts(&self) -> &[MethodContext] {
334 &self.contexts
335 }
336
337 fn find_enclosing(&self, byte_pos: usize) -> Option<&MethodContext> {
339 self.contexts
340 .iter()
341 .filter(|ctx| byte_pos >= ctx.span.0 && byte_pos < ctx.span.1)
342 .max_by_key(|ctx| ctx.depth)
343 }
344}
345
346#[derive(Debug, Clone)]
347struct MethodContext {
348 qualified_name: String,
350 span: (usize, usize),
352 depth: usize,
354 is_static: bool,
356 is_constructor: bool,
358 is_native: bool,
360 package_name: Option<String>,
362 class_stack: Vec<String>,
364 return_type: Option<String>,
366 visibility: Option<String>,
368}
369
370impl MethodContext {
371 fn qualified_name(&self) -> &str {
372 &self.qualified_name
373 }
374}
375
376fn extract_java_contexts(
385 node: Node,
386 content: &[u8],
387 contexts: &mut Vec<MethodContext>,
388 class_stack: &mut Vec<String>,
389 package_name: Option<&str>,
390 depth: usize,
391 max_depth: usize,
392 guard: &mut sqry_core::query::security::RecursionGuard,
393) -> Result<(), sqry_core::query::security::RecursionError> {
394 guard.enter()?;
395
396 if depth > max_depth {
397 guard.exit();
398 return Ok(());
399 }
400
401 match node.kind() {
402 "class_declaration"
403 | "interface_declaration"
404 | "enum_declaration"
405 | "record_declaration" => {
406 if let Some(name_node) = node.child_by_field_name("name") {
408 let class_name = extract_identifier(name_node, content);
409
410 class_stack.push(class_name.clone());
412
413 if let Some(body_node) = node.child_by_field_name("body") {
415 extract_methods_from_body(
416 body_node,
417 content,
418 class_stack,
419 package_name,
420 contexts,
421 depth + 1,
422 max_depth,
423 guard,
424 )?;
425
426 for i in 0..body_node.child_count() {
428 #[allow(clippy::cast_possible_truncation)]
429 if let Some(child) = body_node.child(i as u32) {
431 extract_java_contexts(
432 child,
433 content,
434 contexts,
435 class_stack,
436 package_name,
437 depth + 1,
438 max_depth,
439 guard,
440 )?;
441 }
442 }
443 }
444
445 class_stack.pop();
447
448 guard.exit();
449 return Ok(());
450 }
451 }
452 _ => {}
453 }
454
455 for i in 0..node.child_count() {
457 #[allow(clippy::cast_possible_truncation)]
458 if let Some(child) = node.child(i as u32) {
460 extract_java_contexts(
461 child,
462 content,
463 contexts,
464 class_stack,
465 package_name,
466 depth,
467 max_depth,
468 guard,
469 )?;
470 }
471 }
472
473 guard.exit();
474 Ok(())
475}
476
477#[allow(clippy::unnecessary_wraps)]
481fn extract_methods_from_body(
482 body_node: Node,
483 content: &[u8],
484 class_stack: &[String],
485 package_name: Option<&str>,
486 contexts: &mut Vec<MethodContext>,
487 depth: usize,
488 _max_depth: usize,
489 _guard: &mut sqry_core::query::security::RecursionGuard,
490) -> Result<(), sqry_core::query::security::RecursionError> {
491 for i in 0..body_node.child_count() {
492 #[allow(clippy::cast_possible_truncation)]
493 if let Some(child) = body_node.child(i as u32) {
495 match child.kind() {
496 "method_declaration" => {
497 if let Some(method_context) =
498 extract_method_context(child, content, class_stack, package_name, depth)
499 {
500 contexts.push(method_context);
501 }
502 }
503 "constructor_declaration" | "compact_constructor_declaration" => {
504 let constructor_context = extract_constructor_context(
505 child,
506 content,
507 class_stack,
508 package_name,
509 depth,
510 );
511 contexts.push(constructor_context);
512 }
513 _ => {}
514 }
515 }
516 }
517 Ok(())
518}
519
520fn extract_method_context(
521 method_node: Node,
522 content: &[u8],
523 class_stack: &[String],
524 package_name: Option<&str>,
525 depth: usize,
526) -> Option<MethodContext> {
527 let name_node = method_node.child_by_field_name("name")?;
528 let method_name = extract_identifier(name_node, content);
529
530 let is_static = has_modifier(method_node, "static", content);
531 let is_native = has_modifier(method_node, "native", content);
532 let visibility = extract_visibility(method_node, content);
533
534 let return_type = method_node
537 .child_by_field_name("type")
538 .map(|type_node| extract_full_return_type(type_node, content));
539
540 let qualified_name = build_member_symbol(package_name, class_stack, &method_name);
542
543 Some(MethodContext {
544 qualified_name,
545 span: (method_node.start_byte(), method_node.end_byte()),
546 depth,
547 is_static,
548 is_constructor: false,
549 is_native,
550 package_name: package_name.map(std::string::ToString::to_string),
551 class_stack: class_stack.to_vec(),
552 return_type,
553 visibility,
554 })
555}
556
557fn extract_constructor_context(
558 constructor_node: Node,
559 content: &[u8],
560 class_stack: &[String],
561 package_name: Option<&str>,
562 depth: usize,
563) -> MethodContext {
564 let qualified_name = build_member_symbol(package_name, class_stack, "<init>");
566 let visibility = extract_visibility(constructor_node, content);
567
568 MethodContext {
569 qualified_name,
570 span: (constructor_node.start_byte(), constructor_node.end_byte()),
571 depth,
572 is_static: false,
573 is_constructor: true,
574 is_native: false,
575 package_name: package_name.map(std::string::ToString::to_string),
576 class_stack: class_stack.to_vec(),
577 return_type: None, visibility,
579 }
580}
581
582fn walk_tree_for_edges(
588 node: Node,
589 content: &[u8],
590 ast_graph: &ASTGraph,
591 scope_tree: &mut JavaScopeTree,
592 helper: &mut GraphBuildHelper,
593 tree: &Tree,
594) -> GraphResult<()> {
595 match node.kind() {
596 "class_declaration"
597 | "interface_declaration"
598 | "enum_declaration"
599 | "record_declaration" => {
600 return handle_type_declaration(node, content, ast_graph, scope_tree, helper, tree);
602 }
603 "method_declaration" | "constructor_declaration" => {
604 handle_method_declaration_parameters(node, content, ast_graph, scope_tree, helper);
606
607 if node.kind() == "method_declaration"
609 && let Some((http_method, path)) = extract_spring_route_info(node, content)
610 {
611 let full_path =
613 if let Some(class_prefix) = extract_class_request_mapping_path(node, content) {
614 let prefix = class_prefix.trim_end_matches('/');
615 let suffix = path.trim_start_matches('/');
616 if suffix.is_empty() {
617 class_prefix
618 } else {
619 format!("{prefix}/{suffix}")
620 }
621 } else {
622 path
623 };
624 let qualified_name = format!("route::{http_method}::{full_path}");
625 let span = Span::from_bytes(node.start_byte(), node.end_byte());
626 let endpoint_id = helper.add_endpoint(&qualified_name, Some(span));
627
628 let byte_pos = node.start_byte();
630 if let Some(context) = ast_graph.find_enclosing(byte_pos) {
631 let method_id = helper.ensure_method(
632 context.qualified_name(),
633 Some(Span::from_bytes(context.span.0, context.span.1)),
634 false,
635 context.is_static,
636 );
637 helper.add_contains_edge(endpoint_id, method_id);
638 }
639 }
640 }
641 "compact_constructor_declaration" => {
642 handle_compact_constructor_parameters(node, content, ast_graph, scope_tree, helper);
643 }
644 "method_invocation" => {
645 handle_method_invocation(node, content, ast_graph, helper);
646 }
647 "object_creation_expression" => {
648 handle_constructor_call(node, content, ast_graph, helper);
649 }
650 "import_declaration" => {
651 handle_import_declaration(node, content, helper);
652 }
653 "local_variable_declaration" => {
654 handle_local_variable_declaration(node, content, ast_graph, scope_tree, helper);
655 }
656 "enhanced_for_statement" => {
657 handle_enhanced_for_declaration(node, content, ast_graph, scope_tree, helper);
658 }
659 "catch_clause" => {
660 handle_catch_parameter_declaration(node, content, ast_graph, scope_tree, helper);
661 }
662 "lambda_expression" => {
663 handle_lambda_parameter_declaration(node, content, ast_graph, scope_tree, helper);
664 }
665 "try_with_resources_statement" => {
666 handle_try_with_resources_declaration(node, content, ast_graph, scope_tree, helper);
667 }
668 "instanceof_expression" => {
669 handle_instanceof_pattern_declaration(node, content, ast_graph, scope_tree, helper);
670 }
671 "switch_label" => {
672 handle_switch_pattern_declaration(node, content, ast_graph, scope_tree, helper);
673 }
674 "identifier" => {
675 handle_identifier_for_reference(node, content, ast_graph, scope_tree, helper);
676 }
677 _ => {}
678 }
679
680 for i in 0..node.child_count() {
682 #[allow(clippy::cast_possible_truncation)]
683 if let Some(child) = node.child(i as u32) {
685 walk_tree_for_edges(child, content, ast_graph, scope_tree, helper, tree)?;
686 }
687 }
688
689 Ok(())
690}
691
692fn handle_type_declaration(
693 node: Node,
694 content: &[u8],
695 ast_graph: &ASTGraph,
696 scope_tree: &mut JavaScopeTree,
697 helper: &mut GraphBuildHelper,
698 tree: &Tree,
699) -> GraphResult<()> {
700 let Some(name_node) = node.child_by_field_name("name") else {
701 return Ok(());
702 };
703 let class_name = extract_identifier(name_node, content);
704 let span = Span::from_bytes(node.start_byte(), node.end_byte());
705
706 let package = PackageResolver::package_from_ast(tree, content);
707 let class_stack = extract_declaration_class_stack(node, content);
708 let qualified_name = qualify_class_name(&class_name, &class_stack, package.as_deref());
709 let class_node_id = add_type_node(helper, node.kind(), &qualified_name, span);
710
711 if is_public(node, content) {
712 export_from_file_module(helper, class_node_id);
713 }
714
715 process_inheritance(node, content, package.as_deref(), class_node_id, helper);
716 if node.kind() == "class_declaration" {
717 process_implements(node, content, package.as_deref(), class_node_id, helper);
718 }
719 if node.kind() == "interface_declaration" {
720 process_interface_extends(node, content, package.as_deref(), class_node_id, helper);
721 }
722
723 process_type_parameter_declarations(node, content, &qualified_name, helper);
727
728 if let Some(body_node) = node.child_by_field_name("body") {
729 let is_interface = node.kind() == "interface_declaration";
730 process_class_member_exports(body_node, content, &qualified_name, helper, is_interface);
731
732 for i in 0..body_node.child_count() {
733 #[allow(clippy::cast_possible_truncation)]
734 if let Some(child) = body_node.child(i as u32) {
736 walk_tree_for_edges(child, content, ast_graph, scope_tree, helper, tree)?;
737 }
738 }
739 }
740
741 Ok(())
742}
743
744fn extract_declaration_class_stack(node: Node, content: &[u8]) -> Vec<String> {
745 let mut class_stack = Vec::new();
746 let mut current_node = Some(node);
747
748 while let Some(current) = current_node {
749 if matches!(
750 current.kind(),
751 "class_declaration"
752 | "interface_declaration"
753 | "enum_declaration"
754 | "record_declaration"
755 ) && let Some(name_node) = current.child_by_field_name("name")
756 {
757 class_stack.push(extract_identifier(name_node, content));
758 }
759
760 current_node = current.parent();
761 }
762
763 class_stack.reverse();
764 class_stack
765}
766
767fn qualify_class_name(class_name: &str, class_stack: &[String], package: Option<&str>) -> String {
768 let scope = class_stack
769 .split_last()
770 .map_or(&[][..], |(_, parent_stack)| parent_stack);
771 build_symbol(package, scope, class_name)
772}
773
774fn add_type_node(
775 helper: &mut GraphBuildHelper,
776 kind: &str,
777 qualified_name: &str,
778 span: Span,
779) -> sqry_core::graph::unified::node::NodeId {
780 let node_id = match kind {
785 "interface_declaration" => helper.add_interface(qualified_name, Some(span)),
786 _ => helper.add_class(qualified_name, Some(span)),
787 };
788 helper.mark_definition(node_id);
789 node_id
790}
791
792fn handle_method_invocation(
793 node: Node,
794 content: &[u8],
795 ast_graph: &ASTGraph,
796 helper: &mut GraphBuildHelper,
797) {
798 if let Some(caller_context) = ast_graph.find_enclosing(node.start_byte()) {
799 let is_ffi = build_ffi_call_edge(node, content, caller_context, ast_graph, helper);
800 if is_ffi {
801 return;
802 }
803 }
804
805 process_method_call_unified(node, content, ast_graph, helper);
806}
807
808fn handle_constructor_call(
809 node: Node,
810 content: &[u8],
811 ast_graph: &ASTGraph,
812 helper: &mut GraphBuildHelper,
813) {
814 process_constructor_call_unified(node, content, ast_graph, helper);
815}
816
817fn handle_import_declaration(node: Node, content: &[u8], helper: &mut GraphBuildHelper) {
818 process_import_unified(node, content, helper);
819}
820
821fn add_field_typeof_edges(ast_graph: &ASTGraph, helper: &mut GraphBuildHelper) {
824 for (field_name, (type_fqn, is_final, visibility, is_static)) in &ast_graph.field_types {
825 let field_id = if *is_final {
827 if let Some(vis) = visibility {
829 helper.add_constant_with_static_and_visibility(
830 field_name,
831 None,
832 *is_static,
833 Some(vis.as_str()),
834 )
835 } else {
836 helper.add_constant_with_static_and_visibility(field_name, None, *is_static, None)
837 }
838 } else {
839 if let Some(vis) = visibility {
841 helper.add_property_with_static_and_visibility(
842 field_name,
843 None,
844 *is_static,
845 Some(vis.as_str()),
846 )
847 } else {
848 helper.add_property_with_static_and_visibility(field_name, None, *is_static, None)
849 }
850 };
851
852 let type_id = helper.add_class(type_fqn, None);
854
855 let bare_name = field_name
862 .rsplit_once("::")
863 .map_or(field_name.as_str(), |(_, simple)| simple);
864 helper.add_typeof_edge_with_context(
865 field_id,
866 type_id,
867 Some(TypeOfContext::Field),
868 None,
869 Some(bare_name),
870 );
871 }
872}
873
874fn extract_method_parameters(
877 method_node: Node,
878 content: &[u8],
879 qualified_method_name: &str,
880 helper: &mut GraphBuildHelper,
881 import_map: &HashMap<String, String>,
882 scope_tree: &mut JavaScopeTree,
883) {
884 let mut cursor = method_node.walk();
886 for child in method_node.children(&mut cursor) {
887 if child.kind() == "formal_parameters" {
888 let mut param_cursor = child.walk();
890 for param_child in child.children(&mut param_cursor) {
891 match param_child.kind() {
892 "formal_parameter" => {
893 handle_formal_parameter(
894 param_child,
895 content,
896 qualified_method_name,
897 helper,
898 import_map,
899 scope_tree,
900 );
901 }
902 "spread_parameter" => {
903 handle_spread_parameter(
904 param_child,
905 content,
906 qualified_method_name,
907 helper,
908 import_map,
909 scope_tree,
910 );
911 }
912 "receiver_parameter" => {
913 handle_receiver_parameter(
914 param_child,
915 content,
916 qualified_method_name,
917 helper,
918 import_map,
919 scope_tree,
920 );
921 }
922 _ => {}
923 }
924 }
925 }
926 }
927}
928
929fn handle_formal_parameter(
931 param_node: Node,
932 content: &[u8],
933 method_name: &str,
934 helper: &mut GraphBuildHelper,
935 import_map: &HashMap<String, String>,
936 scope_tree: &mut JavaScopeTree,
937) {
938 use sqry_core::graph::unified::node::NodeKind;
939
940 let Some(type_node) = param_node.child_by_field_name("type") else {
942 return;
943 };
944
945 let Some(name_node) = param_node.child_by_field_name("name") else {
947 return;
948 };
949
950 let type_text = extract_type_name(type_node, content);
952 let param_name = extract_identifier(name_node, content);
953
954 if type_text.is_empty() || param_name.is_empty() {
955 return;
956 }
957
958 let resolved_type = import_map.get(&type_text).cloned().unwrap_or(type_text);
960
961 let qualified_param = format!("{method_name}::{param_name}");
963 let span = Span::from_bytes(param_node.start_byte(), param_node.end_byte());
964
965 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
967
968 scope_tree.attach_node_id(¶m_name, name_node.start_byte(), param_id);
969
970 let type_id = helper.add_class(&resolved_type, None);
972
973 helper.add_typeof_edge(param_id, type_id);
975}
976
977fn handle_spread_parameter(
979 param_node: Node,
980 content: &[u8],
981 method_name: &str,
982 helper: &mut GraphBuildHelper,
983 import_map: &HashMap<String, String>,
984 scope_tree: &mut JavaScopeTree,
985) {
986 use sqry_core::graph::unified::node::NodeKind;
987
988 let mut type_text = String::new();
997 let mut param_name = String::new();
998 let mut param_name_node = None;
999
1000 let mut cursor = param_node.walk();
1001 for child in param_node.children(&mut cursor) {
1002 match child.kind() {
1003 "type_identifier" | "generic_type" | "scoped_type_identifier" => {
1004 type_text = extract_type_name(child, content);
1005 }
1006 "variable_declarator" => {
1007 if let Some(name_node) = child.child_by_field_name("name") {
1009 param_name = extract_identifier(name_node, content);
1010 param_name_node = Some(name_node);
1011 }
1012 }
1013 _ => {}
1014 }
1015 }
1016
1017 if type_text.is_empty() || param_name.is_empty() {
1018 return;
1019 }
1020
1021 let resolved_type = import_map.get(&type_text).cloned().unwrap_or(type_text);
1023
1024 let qualified_param = format!("{method_name}::{param_name}");
1026 let span = Span::from_bytes(param_node.start_byte(), param_node.end_byte());
1027
1028 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
1030
1031 if let Some(name_node) = param_name_node {
1032 scope_tree.attach_node_id(¶m_name, name_node.start_byte(), param_id);
1033 }
1034
1035 let type_id = helper.add_class(&resolved_type, None);
1038
1039 helper.add_typeof_edge(param_id, type_id);
1041}
1042
1043fn handle_receiver_parameter(
1045 param_node: Node,
1046 content: &[u8],
1047 method_name: &str,
1048 helper: &mut GraphBuildHelper,
1049 import_map: &HashMap<String, String>,
1050 _scope_tree: &mut JavaScopeTree,
1051) {
1052 use sqry_core::graph::unified::node::NodeKind;
1053
1054 let mut type_text = String::new();
1062 let mut cursor = param_node.walk();
1063
1064 for child in param_node.children(&mut cursor) {
1066 if matches!(
1067 child.kind(),
1068 "type_identifier" | "generic_type" | "scoped_type_identifier"
1069 ) {
1070 type_text = extract_type_name(child, content);
1071 break;
1072 }
1073 }
1074
1075 if type_text.is_empty() {
1076 return;
1077 }
1078
1079 let param_name = "this";
1081
1082 let resolved_type = import_map.get(&type_text).cloned().unwrap_or(type_text);
1084
1085 let qualified_param = format!("{method_name}::{param_name}");
1087 let span = Span::from_bytes(param_node.start_byte(), param_node.end_byte());
1088
1089 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
1091
1092 let type_id = helper.add_class(&resolved_type, None);
1094
1095 helper.add_typeof_edge(param_id, type_id);
1097}
1098
1099#[derive(Debug, Clone, Copy, Eq, PartialEq)]
1100enum FieldAccessRole {
1101 Default,
1102 ExplicitThisOrSuper,
1103 Skip,
1104}
1105
1106#[derive(Debug, Clone, Copy, Eq, PartialEq)]
1107enum FieldResolutionMode {
1108 Default,
1109 CurrentOnly,
1110}
1111
1112fn field_access_role(
1113 node: Node,
1114 content: &[u8],
1115 ast_graph: &ASTGraph,
1116 scope_tree: &JavaScopeTree,
1117 identifier_text: &str,
1118) -> FieldAccessRole {
1119 let Some(parent) = node.parent() else {
1120 return FieldAccessRole::Default;
1121 };
1122
1123 if parent.kind() == "field_access" {
1124 if let Some(field_node) = parent.child_by_field_name("field")
1125 && field_node.id() == node.id()
1126 && let Some(object_node) = parent.child_by_field_name("object")
1127 {
1128 if is_explicit_this_or_super(object_node, content) {
1129 return FieldAccessRole::ExplicitThisOrSuper;
1130 }
1131 return FieldAccessRole::Skip;
1132 }
1133
1134 if let Some(object_node) = parent.child_by_field_name("object")
1135 && object_node.id() == node.id()
1136 && !scope_tree.has_local_binding(identifier_text, node.start_byte())
1137 && is_static_type_identifier(identifier_text, ast_graph, scope_tree)
1138 {
1139 return FieldAccessRole::Skip;
1140 }
1141 }
1142
1143 if parent.kind() == "method_invocation"
1144 && let Some(object_node) = parent.child_by_field_name("object")
1145 && object_node.id() == node.id()
1146 && !scope_tree.has_local_binding(identifier_text, node.start_byte())
1147 && is_static_type_identifier(identifier_text, ast_graph, scope_tree)
1148 {
1149 return FieldAccessRole::Skip;
1150 }
1151
1152 if parent.kind() == "method_reference"
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 FieldAccessRole::Default
1162}
1163
1164fn is_static_type_identifier(
1165 identifier_text: &str,
1166 ast_graph: &ASTGraph,
1167 scope_tree: &JavaScopeTree,
1168) -> bool {
1169 ast_graph.import_map.contains_key(identifier_text)
1170 || scope_tree.is_known_type_name(identifier_text)
1171}
1172
1173fn is_explicit_this_or_super(node: Node, content: &[u8]) -> bool {
1174 if matches!(node.kind(), "this" | "super") {
1175 return true;
1176 }
1177 if node.kind() == "identifier" {
1178 let text = extract_identifier(node, content);
1179 return matches!(text.as_str(), "this" | "super");
1180 }
1181 if node.kind() == "field_access"
1182 && let Some(field) = node.child_by_field_name("field")
1183 {
1184 let text = extract_identifier(field, content);
1185 if matches!(text.as_str(), "this" | "super") {
1186 return true;
1187 }
1188 }
1189 false
1190}
1191
1192#[allow(clippy::too_many_lines)]
1195fn is_declaration_context(node: Node) -> bool {
1196 let Some(parent) = node.parent() else {
1198 return false;
1199 };
1200
1201 if parent.kind() == "variable_declarator" {
1206 let mut cursor = parent.walk();
1208 for (idx, child) in parent.children(&mut cursor).enumerate() {
1209 if child.id() == node.id() {
1210 #[allow(clippy::cast_possible_truncation)]
1211 if let Some(field_name) = parent.field_name_for_child(idx as u32) {
1212 return field_name == "name";
1214 }
1215 break;
1216 }
1217 }
1218
1219 if let Some(grandparent) = parent.parent()
1221 && grandparent.kind() == "spread_parameter"
1222 {
1223 return true;
1224 }
1225
1226 return false;
1227 }
1228
1229 if parent.kind() == "formal_parameter" {
1231 let mut cursor = parent.walk();
1232 for (idx, child) in parent.children(&mut cursor).enumerate() {
1233 if child.id() == node.id() {
1234 #[allow(clippy::cast_possible_truncation)]
1235 if let Some(field_name) = parent.field_name_for_child(idx as u32) {
1236 return field_name == "name";
1237 }
1238 break;
1239 }
1240 }
1241 return false;
1242 }
1243
1244 if parent.kind() == "enhanced_for_statement" {
1247 let mut cursor = parent.walk();
1249 for (idx, child) in parent.children(&mut cursor).enumerate() {
1250 if child.id() == node.id() {
1251 #[allow(clippy::cast_possible_truncation)]
1252 if let Some(field_name) = parent.field_name_for_child(idx as u32) {
1253 return field_name == "name";
1255 }
1256 break;
1257 }
1258 }
1259 return false;
1260 }
1261
1262 if parent.kind() == "lambda_expression" {
1263 if let Some(params) = parent.child_by_field_name("parameters") {
1264 return params.id() == node.id();
1265 }
1266 return false;
1267 }
1268
1269 if parent.kind() == "inferred_parameters" {
1270 return true;
1271 }
1272
1273 if parent.kind() == "resource" {
1274 if let Some(name_node) = parent.child_by_field_name("name")
1275 && name_node.id() == node.id()
1276 {
1277 let has_type = parent.child_by_field_name("type").is_some();
1278 let has_value = parent.child_by_field_name("value").is_some();
1279 return has_type || has_value;
1280 }
1281 return false;
1282 }
1283
1284 if parent.kind() == "type_pattern" {
1288 if let Some((name_node, _type_node)) = typed_pattern_parts(parent) {
1289 return name_node.id() == node.id();
1290 }
1291 return false;
1292 }
1293
1294 if parent.kind() == "instanceof_expression" {
1296 let mut cursor = parent.walk();
1297 for (idx, child) in parent.children(&mut cursor).enumerate() {
1298 if child.id() == node.id() {
1299 #[allow(clippy::cast_possible_truncation)]
1300 if let Some(field_name) = parent.field_name_for_child(idx as u32) {
1301 return field_name == "name";
1303 }
1304 break;
1305 }
1306 }
1307 return false;
1308 }
1309
1310 if parent.kind() == "record_pattern_component" {
1313 let mut cursor = parent.walk();
1315 for child in parent.children(&mut cursor) {
1316 if child.id() == node.id() && child.kind() == "identifier" {
1317 return true;
1319 }
1320 }
1321 return false;
1322 }
1323
1324 if parent.kind() == "record_component" {
1325 if let Some(name_node) = parent.child_by_field_name("name") {
1326 return name_node.id() == node.id();
1327 }
1328 return false;
1329 }
1330
1331 matches!(
1333 parent.kind(),
1334 "method_declaration"
1335 | "constructor_declaration"
1336 | "compact_constructor_declaration"
1337 | "class_declaration"
1338 | "interface_declaration"
1339 | "enum_declaration"
1340 | "field_declaration"
1341 | "catch_formal_parameter"
1342 )
1343}
1344
1345fn is_method_invocation_name(node: Node) -> bool {
1346 let Some(parent) = node.parent() else {
1347 return false;
1348 };
1349 if parent.kind() != "method_invocation" {
1350 return false;
1351 }
1352 parent
1353 .child_by_field_name("name")
1354 .is_some_and(|name_node| name_node.id() == node.id())
1355}
1356
1357fn is_method_reference_name(node: Node) -> bool {
1358 let Some(parent) = node.parent() else {
1359 return false;
1360 };
1361 if parent.kind() != "method_reference" {
1362 return false;
1363 }
1364 parent
1365 .child_by_field_name("name")
1366 .is_some_and(|name_node| name_node.id() == node.id())
1367}
1368
1369fn is_label_identifier(node: Node) -> bool {
1370 let Some(parent) = node.parent() else {
1371 return false;
1372 };
1373 if parent.kind() == "labeled_statement" {
1374 return true;
1375 }
1376 if matches!(parent.kind(), "break_statement" | "continue_statement")
1377 && let Some(label) = parent.child_by_field_name("label")
1378 {
1379 return label.id() == node.id();
1380 }
1381 false
1382}
1383
1384fn is_class_literal(node: Node) -> bool {
1385 let Some(parent) = node.parent() else {
1386 return false;
1387 };
1388 parent.kind() == "class_literal"
1389}
1390
1391fn is_type_identifier_context(node: Node) -> bool {
1392 let Some(parent) = node.parent() else {
1393 return false;
1394 };
1395 matches!(
1396 parent.kind(),
1397 "type_identifier"
1398 | "scoped_type_identifier"
1399 | "scoped_identifier"
1400 | "generic_type"
1401 | "type_argument"
1402 | "type_bound"
1403 )
1404}
1405
1406fn add_reference_edge_for_target(
1407 usage_node: Node,
1408 identifier_text: &str,
1409 target_id: sqry_core::graph::unified::node::NodeId,
1410 helper: &mut GraphBuildHelper,
1411) {
1412 let usage_span = Span::from_bytes(usage_node.start_byte(), usage_node.end_byte());
1413 let usage_id = helper.add_node(
1414 &format!("{}@{}", identifier_text, usage_node.start_byte()),
1415 Some(usage_span),
1416 sqry_core::graph::unified::node::NodeKind::Variable,
1417 );
1418 helper.add_reference_edge(usage_id, target_id);
1419}
1420
1421fn resolve_field_reference(
1422 node: Node,
1423 identifier_text: &str,
1424 ast_graph: &ASTGraph,
1425 helper: &mut GraphBuildHelper,
1426 mode: FieldResolutionMode,
1427) {
1428 let context = ast_graph.find_enclosing(node.start_byte());
1429 let mut candidates = Vec::new();
1430 if let Some(ctx) = context
1431 && !ctx.class_stack.is_empty()
1432 {
1433 if mode == FieldResolutionMode::CurrentOnly {
1434 let class_path = ctx.class_stack.join("::");
1435 candidates.push(format!("{class_path}::{identifier_text}"));
1436 } else {
1437 let stack_len = ctx.class_stack.len();
1438 for idx in (1..=stack_len).rev() {
1439 let class_path = ctx.class_stack[..idx].join("::");
1440 candidates.push(format!("{class_path}::{identifier_text}"));
1441 }
1442 }
1443 }
1444
1445 if mode != FieldResolutionMode::CurrentOnly {
1446 candidates.push(identifier_text.to_string());
1447 }
1448
1449 for candidate in candidates {
1450 if ast_graph.field_types.contains_key(&candidate) {
1451 add_field_reference(node, identifier_text, &candidate, ast_graph, helper);
1452 return;
1453 }
1454 }
1455}
1456
1457fn add_field_reference(
1458 node: Node,
1459 identifier_text: &str,
1460 field_name: &str,
1461 ast_graph: &ASTGraph,
1462 helper: &mut GraphBuildHelper,
1463) {
1464 let usage_span = Span::from_bytes(node.start_byte(), node.end_byte());
1465 let usage_id = helper.add_node(
1466 &format!("{}@{}", identifier_text, node.start_byte()),
1467 Some(usage_span),
1468 sqry_core::graph::unified::node::NodeKind::Variable,
1469 );
1470
1471 let field_metadata = ast_graph.field_types.get(field_name);
1472 let field_id = if let Some((_, is_final, visibility, is_static)) = field_metadata {
1473 if *is_final {
1474 if let Some(vis) = visibility {
1475 helper.add_constant_with_static_and_visibility(
1476 field_name,
1477 None,
1478 *is_static,
1479 Some(vis.as_str()),
1480 )
1481 } else {
1482 helper.add_constant_with_static_and_visibility(field_name, None, *is_static, None)
1483 }
1484 } else if let Some(vis) = visibility {
1485 helper.add_property_with_static_and_visibility(
1486 field_name,
1487 None,
1488 *is_static,
1489 Some(vis.as_str()),
1490 )
1491 } else {
1492 helper.add_property_with_static_and_visibility(field_name, None, *is_static, None)
1493 }
1494 } else {
1495 helper.add_property_with_static_and_visibility(field_name, None, false, None)
1496 };
1497
1498 helper.add_reference_edge(usage_id, field_id);
1499}
1500
1501#[allow(clippy::similar_names)]
1503fn handle_identifier_for_reference(
1504 node: Node,
1505 content: &[u8],
1506 ast_graph: &ASTGraph,
1507 scope_tree: &mut JavaScopeTree,
1508 helper: &mut GraphBuildHelper,
1509) {
1510 let identifier_text = extract_identifier(node, content);
1511
1512 if identifier_text.is_empty() {
1513 return;
1514 }
1515
1516 if is_declaration_context(node) {
1518 return;
1519 }
1520
1521 if is_method_invocation_name(node)
1522 || is_method_reference_name(node)
1523 || is_label_identifier(node)
1524 || is_class_literal(node)
1525 {
1526 return;
1527 }
1528
1529 if is_type_identifier_context(node) {
1530 return;
1531 }
1532
1533 let field_access_role =
1534 field_access_role(node, content, ast_graph, scope_tree, &identifier_text);
1535 if matches!(field_access_role, FieldAccessRole::Skip) {
1536 return;
1537 }
1538
1539 let allow_local = matches!(field_access_role, FieldAccessRole::Default);
1540 let allow_field = !matches!(field_access_role, FieldAccessRole::Skip);
1541 let field_mode = if matches!(field_access_role, FieldAccessRole::ExplicitThisOrSuper) {
1542 FieldResolutionMode::CurrentOnly
1543 } else {
1544 FieldResolutionMode::Default
1545 };
1546
1547 if allow_local {
1548 match scope_tree.resolve_identifier(node.start_byte(), &identifier_text) {
1549 ResolutionOutcome::Local(binding) => {
1550 let target_id = if let Some(node_id) = binding.node_id {
1551 node_id
1552 } else {
1553 let span = Span::from_bytes(binding.decl_start_byte, binding.decl_end_byte);
1554 let qualified_var = format!("{}@{}", identifier_text, binding.decl_start_byte);
1555 let var_id = helper.add_variable(&qualified_var, Some(span));
1556 scope_tree.attach_node_id(&identifier_text, binding.decl_start_byte, var_id);
1557 var_id
1558 };
1559 add_reference_edge_for_target(node, &identifier_text, target_id, helper);
1560 return;
1561 }
1562 ResolutionOutcome::Member { qualified_name } => {
1563 if let Some(field_name) = qualified_name {
1564 add_field_reference(node, &identifier_text, &field_name, ast_graph, helper);
1565 }
1566 return;
1567 }
1568 ResolutionOutcome::Ambiguous => {
1569 return;
1570 }
1571 ResolutionOutcome::NoMatch => {}
1572 }
1573 }
1574
1575 if !allow_field {
1576 return;
1577 }
1578
1579 resolve_field_reference(node, &identifier_text, ast_graph, helper, field_mode);
1580}
1581
1582fn handle_method_declaration_parameters(
1584 node: Node,
1585 content: &[u8],
1586 ast_graph: &ASTGraph,
1587 scope_tree: &mut JavaScopeTree,
1588 helper: &mut GraphBuildHelper,
1589) {
1590 let byte_pos = node.start_byte();
1592 if let Some(context) = ast_graph.find_enclosing(byte_pos) {
1593 let qualified_method_name = &context.qualified_name;
1594
1595 extract_method_parameters(
1597 node,
1598 content,
1599 qualified_method_name,
1600 helper,
1601 &ast_graph.import_map,
1602 scope_tree,
1603 );
1604
1605 process_type_parameter_declarations(node, content, qualified_method_name, helper);
1614 }
1615}
1616
1617fn handle_local_variable_declaration(
1619 node: Node,
1620 content: &[u8],
1621 ast_graph: &ASTGraph,
1622 scope_tree: &mut JavaScopeTree,
1623 helper: &mut GraphBuildHelper,
1624) {
1625 let Some(type_node) = node.child_by_field_name("type") else {
1627 return;
1628 };
1629
1630 let type_text = extract_type_name(type_node, content);
1631 if type_text.is_empty() {
1632 return;
1633 }
1634
1635 let resolved_type = ast_graph
1637 .import_map
1638 .get(&type_text)
1639 .cloned()
1640 .unwrap_or_else(|| type_text.clone());
1641
1642 let mut cursor = node.walk();
1644 for child in node.children(&mut cursor) {
1645 if child.kind() == "variable_declarator"
1646 && let Some(name_node) = child.child_by_field_name("name")
1647 {
1648 let var_name = extract_identifier(name_node, content);
1649
1650 let qualified_var = format!("{}@{}", var_name, name_node.start_byte());
1652
1653 let span = Span::from_bytes(child.start_byte(), child.end_byte());
1655 let var_id = helper.add_variable(&qualified_var, Some(span));
1656 scope_tree.attach_node_id(&var_name, name_node.start_byte(), var_id);
1657
1658 let type_id = helper.add_class(&resolved_type, None);
1660
1661 helper.add_typeof_edge(var_id, type_id);
1663 }
1664 }
1665}
1666
1667fn handle_enhanced_for_declaration(
1668 node: Node,
1669 content: &[u8],
1670 ast_graph: &ASTGraph,
1671 scope_tree: &mut JavaScopeTree,
1672 helper: &mut GraphBuildHelper,
1673) {
1674 let Some(type_node) = node.child_by_field_name("type") else {
1675 return;
1676 };
1677 let Some(name_node) = node.child_by_field_name("name") else {
1678 return;
1679 };
1680 let Some(body_node) = node.child_by_field_name("body") else {
1681 return;
1682 };
1683
1684 let type_text = extract_type_name(type_node, content);
1685 let var_name = extract_identifier(name_node, content);
1686 if type_text.is_empty() || var_name.is_empty() {
1687 return;
1688 }
1689
1690 let resolved_type = ast_graph
1691 .import_map
1692 .get(&type_text)
1693 .cloned()
1694 .unwrap_or(type_text);
1695
1696 let qualified_var = format!("{}@{}", var_name, name_node.start_byte());
1697 let span = Span::from_bytes(name_node.start_byte(), name_node.end_byte());
1698 let var_id = helper.add_variable(&qualified_var, Some(span));
1699 scope_tree.attach_node_id(&var_name, body_node.start_byte(), var_id);
1700
1701 let type_id = helper.add_class(&resolved_type, None);
1702 helper.add_typeof_edge(var_id, type_id);
1703}
1704
1705fn handle_catch_parameter_declaration(
1706 node: Node,
1707 content: &[u8],
1708 ast_graph: &ASTGraph,
1709 scope_tree: &mut JavaScopeTree,
1710 helper: &mut GraphBuildHelper,
1711) {
1712 let Some(param_node) = node
1713 .child_by_field_name("parameter")
1714 .or_else(|| first_child_of_kind(node, "catch_formal_parameter"))
1715 .or_else(|| first_child_of_kind(node, "formal_parameter"))
1716 else {
1717 return;
1718 };
1719 let Some(name_node) = param_node
1720 .child_by_field_name("name")
1721 .or_else(|| first_child_of_kind(param_node, "identifier"))
1722 else {
1723 return;
1724 };
1725
1726 let var_name = extract_identifier(name_node, content);
1727 if var_name.is_empty() {
1728 return;
1729 }
1730
1731 let qualified_var = format!("{}@{}", var_name, name_node.start_byte());
1732 let span = Span::from_bytes(param_node.start_byte(), param_node.end_byte());
1733 let var_id = helper.add_variable(&qualified_var, Some(span));
1734 scope_tree.attach_node_id(&var_name, name_node.start_byte(), var_id);
1735
1736 if let Some(type_node) = param_node
1737 .child_by_field_name("type")
1738 .or_else(|| first_child_of_kind(param_node, "type_identifier"))
1739 .or_else(|| first_child_of_kind(param_node, "scoped_type_identifier"))
1740 .or_else(|| first_child_of_kind(param_node, "generic_type"))
1741 {
1742 add_typeof_for_catch_type(type_node, content, ast_graph, helper, var_id);
1743 }
1744}
1745
1746fn add_typeof_for_catch_type(
1747 type_node: Node,
1748 content: &[u8],
1749 ast_graph: &ASTGraph,
1750 helper: &mut GraphBuildHelper,
1751 var_id: sqry_core::graph::unified::node::NodeId,
1752) {
1753 if type_node.kind() == "union_type" {
1754 let mut cursor = type_node.walk();
1755 for child in type_node.children(&mut cursor) {
1756 if matches!(
1757 child.kind(),
1758 "type_identifier" | "scoped_type_identifier" | "generic_type"
1759 ) {
1760 let type_text = extract_type_name(child, content);
1761 if !type_text.is_empty() {
1762 let resolved_type = ast_graph
1763 .import_map
1764 .get(&type_text)
1765 .cloned()
1766 .unwrap_or(type_text);
1767 let type_id = helper.add_class(&resolved_type, None);
1768 helper.add_typeof_edge(var_id, type_id);
1769 }
1770 }
1771 }
1772 return;
1773 }
1774
1775 let type_text = extract_type_name(type_node, content);
1776 if type_text.is_empty() {
1777 return;
1778 }
1779 let resolved_type = ast_graph
1780 .import_map
1781 .get(&type_text)
1782 .cloned()
1783 .unwrap_or(type_text);
1784 let type_id = helper.add_class(&resolved_type, None);
1785 helper.add_typeof_edge(var_id, type_id);
1786}
1787
1788fn handle_lambda_parameter_declaration(
1789 node: Node,
1790 content: &[u8],
1791 ast_graph: &ASTGraph,
1792 scope_tree: &mut JavaScopeTree,
1793 helper: &mut GraphBuildHelper,
1794) {
1795 use sqry_core::graph::unified::node::NodeKind;
1796
1797 let Some(params_node) = node.child_by_field_name("parameters") else {
1798 return;
1799 };
1800 let lambda_prefix = format!("lambda@{}", node.start_byte());
1801
1802 if params_node.kind() == "identifier" {
1803 let name = extract_identifier(params_node, content);
1804 if name.is_empty() {
1805 return;
1806 }
1807 let qualified_param = format!("{lambda_prefix}::{name}");
1808 let span = Span::from_bytes(params_node.start_byte(), params_node.end_byte());
1809 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
1810 scope_tree.attach_node_id(&name, params_node.start_byte(), param_id);
1811 return;
1812 }
1813
1814 let mut cursor = params_node.walk();
1815 for child in params_node.children(&mut cursor) {
1816 match child.kind() {
1817 "identifier" => {
1818 let name = extract_identifier(child, content);
1819 if name.is_empty() {
1820 continue;
1821 }
1822 let qualified_param = format!("{lambda_prefix}::{name}");
1823 let span = Span::from_bytes(child.start_byte(), child.end_byte());
1824 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
1825 scope_tree.attach_node_id(&name, child.start_byte(), param_id);
1826 }
1827 "formal_parameter" => {
1828 let Some(name_node) = child.child_by_field_name("name") else {
1829 continue;
1830 };
1831 let Some(type_node) = child.child_by_field_name("type") else {
1832 continue;
1833 };
1834 let name = extract_identifier(name_node, content);
1835 if name.is_empty() {
1836 continue;
1837 }
1838 let type_text = extract_type_name(type_node, content);
1839 let resolved_type = ast_graph
1840 .import_map
1841 .get(&type_text)
1842 .cloned()
1843 .unwrap_or(type_text);
1844 let qualified_param = format!("{lambda_prefix}::{name}");
1845 let span = Span::from_bytes(child.start_byte(), child.end_byte());
1846 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
1847 scope_tree.attach_node_id(&name, name_node.start_byte(), param_id);
1848 let type_id = helper.add_class(&resolved_type, None);
1849 helper.add_typeof_edge(param_id, type_id);
1850 }
1851 _ => {}
1852 }
1853 }
1854}
1855
1856fn handle_try_with_resources_declaration(
1857 node: Node,
1858 content: &[u8],
1859 ast_graph: &ASTGraph,
1860 scope_tree: &mut JavaScopeTree,
1861 helper: &mut GraphBuildHelper,
1862) {
1863 let Some(resources) = node.child_by_field_name("resources") else {
1864 return;
1865 };
1866
1867 let mut cursor = resources.walk();
1868 for resource in resources.children(&mut cursor) {
1869 if resource.kind() != "resource" {
1870 continue;
1871 }
1872 let name_node = resource.child_by_field_name("name");
1873 let type_node = resource.child_by_field_name("type");
1874 let value_node = resource.child_by_field_name("value");
1875 if let Some(name_node) = name_node {
1876 if type_node.is_none() && value_node.is_none() {
1877 continue;
1878 }
1879 let name = extract_identifier(name_node, content);
1880 if name.is_empty() {
1881 continue;
1882 }
1883
1884 let qualified_var = format!("{}@{}", name, name_node.start_byte());
1885 let span = Span::from_bytes(resource.start_byte(), resource.end_byte());
1886 let var_id = helper.add_variable(&qualified_var, Some(span));
1887 scope_tree.attach_node_id(&name, name_node.start_byte(), var_id);
1888
1889 if let Some(type_node) = type_node {
1890 let type_text = extract_type_name(type_node, content);
1891 if !type_text.is_empty() {
1892 let resolved_type = ast_graph
1893 .import_map
1894 .get(&type_text)
1895 .cloned()
1896 .unwrap_or(type_text);
1897 let type_id = helper.add_class(&resolved_type, None);
1898 helper.add_typeof_edge(var_id, type_id);
1899 }
1900 }
1901 }
1902 }
1903}
1904
1905fn handle_instanceof_pattern_declaration(
1906 node: Node,
1907 content: &[u8],
1908 ast_graph: &ASTGraph,
1909 scope_tree: &mut JavaScopeTree,
1910 helper: &mut GraphBuildHelper,
1911) {
1912 let mut patterns = Vec::new();
1913 collect_pattern_declarations(node, &mut patterns);
1914 for (name_node, type_node) in patterns {
1915 let name = extract_identifier(name_node, content);
1916 if name.is_empty() {
1917 continue;
1918 }
1919 let qualified_var = format!("{}@{}", name, name_node.start_byte());
1920 let span = Span::from_bytes(name_node.start_byte(), name_node.end_byte());
1921 let var_id = helper.add_variable(&qualified_var, Some(span));
1922 scope_tree.attach_node_id(&name, name_node.start_byte(), var_id);
1923
1924 if let Some(type_node) = type_node {
1925 let type_text = extract_type_name(type_node, content);
1926 if !type_text.is_empty() {
1927 let resolved_type = ast_graph
1928 .import_map
1929 .get(&type_text)
1930 .cloned()
1931 .unwrap_or(type_text);
1932 let type_id = helper.add_class(&resolved_type, None);
1933 helper.add_typeof_edge(var_id, type_id);
1934 }
1935 }
1936 }
1937}
1938
1939fn handle_switch_pattern_declaration(
1940 node: Node,
1941 content: &[u8],
1942 ast_graph: &ASTGraph,
1943 scope_tree: &mut JavaScopeTree,
1944 helper: &mut GraphBuildHelper,
1945) {
1946 let mut patterns = Vec::new();
1947 collect_pattern_declarations(node, &mut patterns);
1948 for (name_node, type_node) in patterns {
1949 let name = extract_identifier(name_node, content);
1950 if name.is_empty() {
1951 continue;
1952 }
1953 let qualified_var = format!("{}@{}", name, name_node.start_byte());
1954 let span = Span::from_bytes(name_node.start_byte(), name_node.end_byte());
1955 let var_id = helper.add_variable(&qualified_var, Some(span));
1956 scope_tree.attach_node_id(&name, name_node.start_byte(), var_id);
1957
1958 if let Some(type_node) = type_node {
1959 let type_text = extract_type_name(type_node, content);
1960 if !type_text.is_empty() {
1961 let resolved_type = ast_graph
1962 .import_map
1963 .get(&type_text)
1964 .cloned()
1965 .unwrap_or(type_text);
1966 let type_id = helper.add_class(&resolved_type, None);
1967 helper.add_typeof_edge(var_id, type_id);
1968 }
1969 }
1970 }
1971}
1972
1973fn handle_compact_constructor_parameters(
1974 node: Node,
1975 content: &[u8],
1976 ast_graph: &ASTGraph,
1977 scope_tree: &mut JavaScopeTree,
1978 helper: &mut GraphBuildHelper,
1979) {
1980 use sqry_core::graph::unified::node::NodeKind;
1981
1982 let Some(record_node) = node
1983 .parent()
1984 .and_then(|parent| find_record_declaration(parent))
1985 else {
1986 return;
1987 };
1988
1989 let Some(record_name_node) = record_node.child_by_field_name("name") else {
1990 return;
1991 };
1992 let record_name = extract_identifier(record_name_node, content);
1993 if record_name.is_empty() {
1994 return;
1995 }
1996
1997 let mut components = Vec::new();
1998 collect_record_components_nodes(record_node, &mut components);
1999 for component in components {
2000 let Some(name_node) = component.child_by_field_name("name") else {
2001 continue;
2002 };
2003 let Some(type_node) = component.child_by_field_name("type") else {
2004 continue;
2005 };
2006 let name = extract_identifier(name_node, content);
2007 if name.is_empty() {
2008 continue;
2009 }
2010
2011 let type_text = extract_type_name(type_node, content);
2012 if type_text.is_empty() {
2013 continue;
2014 }
2015 let resolved_type = ast_graph
2016 .import_map
2017 .get(&type_text)
2018 .cloned()
2019 .unwrap_or(type_text);
2020
2021 let qualified_param = format!("{record_name}.<init>::{name}");
2022 let span = Span::from_bytes(component.start_byte(), component.end_byte());
2023 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
2024 scope_tree.attach_node_id(&name, name_node.start_byte(), param_id);
2025
2026 let type_id = helper.add_class(&resolved_type, None);
2027 helper.add_typeof_edge(param_id, type_id);
2028 }
2029}
2030
2031fn collect_pattern_declarations<'a>(
2032 node: Node<'a>,
2033 output: &mut Vec<(Node<'a>, Option<Node<'a>>)>,
2034) {
2035 if node.kind() == "instanceof_expression"
2036 && !node_has_direct_child_kind(node, "type_pattern")
2037 && let Some(name_node) = node.child_by_field_name("name")
2038 {
2039 let type_node = first_type_like_child(node);
2040 output.push((name_node, type_node));
2041 }
2042
2043 if node.kind() == "type_pattern"
2044 && let Some((name_node, type_node)) = typed_pattern_parts(node)
2045 {
2046 output.push((name_node, type_node));
2047 }
2048
2049 if node.kind() == "record_pattern_component"
2050 && let Some((name_node, type_node)) = typed_pattern_parts(node)
2051 {
2052 output.push((name_node, type_node));
2053 }
2054
2055 let mut cursor = node.walk();
2056 for child in node.children(&mut cursor) {
2057 collect_pattern_declarations(child, output);
2058 }
2059}
2060
2061fn node_has_direct_child_kind(node: Node, kind: &str) -> bool {
2062 let mut cursor = node.walk();
2063 node.children(&mut cursor).any(|child| child.kind() == kind)
2064}
2065
2066fn typed_pattern_parts(node: Node) -> Option<(Node, Option<Node>)> {
2067 let mut name_node = None;
2068 let mut type_node = None;
2069 let mut cursor = node.walk();
2070 for child in node.children(&mut cursor) {
2071 if matches!(child.kind(), "identifier" | "_reserved_identifier") {
2072 name_node = Some(child);
2073 } else if matches!(
2074 child.kind(),
2075 "type_identifier" | "scoped_type_identifier" | "generic_type"
2076 ) {
2077 type_node = Some(child);
2078 }
2079 }
2080 name_node.map(|name| (name, type_node))
2081}
2082
2083fn first_type_like_child(node: Node) -> Option<Node> {
2084 let mut cursor = node.walk();
2085 for child in node.children(&mut cursor) {
2086 if matches!(
2087 child.kind(),
2088 "type_identifier" | "scoped_type_identifier" | "generic_type"
2089 ) {
2090 return Some(child);
2091 }
2092 }
2093 None
2094}
2095
2096fn find_record_declaration(node: Node) -> Option<Node> {
2097 if node.kind() == "record_declaration" {
2098 return Some(node);
2099 }
2100 node.parent().and_then(find_record_declaration)
2101}
2102
2103fn collect_record_components_nodes<'a>(node: Node<'a>, output: &mut Vec<Node<'a>>) {
2104 if let Some(parameters) = node.child_by_field_name("parameters") {
2105 let mut cursor = parameters.walk();
2106 for child in parameters.children(&mut cursor) {
2107 if matches!(child.kind(), "formal_parameter" | "record_component") {
2108 output.push(child);
2109 }
2110 }
2111 return;
2112 }
2113
2114 let mut cursor = node.walk();
2115 for child in node.children(&mut cursor) {
2116 if child.kind() == "record_component" {
2117 output.push(child);
2118 }
2119 }
2120}
2121
2122fn process_method_call_unified(
2124 call_node: Node,
2125 content: &[u8],
2126 ast_graph: &ASTGraph,
2127 helper: &mut GraphBuildHelper,
2128) {
2129 let Some(caller_context) = ast_graph.find_enclosing(call_node.start_byte()) else {
2130 return;
2131 };
2132 let Ok(callee_name) = extract_method_invocation_name(call_node, content) else {
2133 return;
2134 };
2135
2136 let callee_qualified =
2137 resolve_callee_qualified(&call_node, content, ast_graph, caller_context, &callee_name);
2138 let caller_method_id = ensure_caller_method(helper, caller_context);
2139 let target_method_id = helper.ensure_method(&callee_qualified, None, false, false);
2140
2141 add_call_edge(helper, caller_method_id, target_method_id, call_node);
2142}
2143
2144fn process_constructor_call_unified(
2146 new_node: Node,
2147 content: &[u8],
2148 ast_graph: &ASTGraph,
2149 helper: &mut GraphBuildHelper,
2150) {
2151 let Some(caller_context) = ast_graph.find_enclosing(new_node.start_byte()) else {
2152 return;
2153 };
2154
2155 let Some(type_node) = new_node.child_by_field_name("type") else {
2156 return;
2157 };
2158
2159 let class_name = extract_type_name(type_node, content);
2160 if class_name.is_empty() {
2161 return;
2162 }
2163
2164 let qualified_class = qualify_constructor_class(&class_name, caller_context);
2165 let constructor_name = format!("{qualified_class}.<init>");
2166
2167 let caller_method_id = ensure_caller_method(helper, caller_context);
2168 let target_method_id = helper.ensure_method(&constructor_name, None, false, false);
2169 add_call_edge(helper, caller_method_id, target_method_id, new_node);
2170}
2171
2172fn count_call_arguments(call_node: Node<'_>) -> u8 {
2173 let Some(args_node) = call_node.child_by_field_name("arguments") else {
2174 return 255;
2175 };
2176 let count = args_node.named_child_count();
2177 if count <= 254 {
2178 u8::try_from(count).unwrap_or(u8::MAX)
2179 } else {
2180 u8::MAX
2181 }
2182}
2183
2184fn process_import_unified(import_node: Node, content: &[u8], helper: &mut GraphBuildHelper) {
2186 let has_asterisk = import_has_wildcard(import_node);
2187 let Some(mut imported_name) = extract_import_name(import_node, content) else {
2188 return;
2189 };
2190 if has_asterisk {
2191 imported_name = format!("{imported_name}.*");
2192 }
2193
2194 let module_id = helper.add_module("<module>", None);
2195 let external_id = helper.add_import(
2196 &imported_name,
2197 Some(Span::from_bytes(
2198 import_node.start_byte(),
2199 import_node.end_byte(),
2200 )),
2201 );
2202
2203 helper.add_import_edge(module_id, external_id);
2204}
2205
2206fn ensure_caller_method(
2207 helper: &mut GraphBuildHelper,
2208 caller_context: &MethodContext,
2209) -> sqry_core::graph::unified::node::NodeId {
2210 helper.ensure_method(
2211 caller_context.qualified_name(),
2212 Some(Span::from_bytes(
2213 caller_context.span.0,
2214 caller_context.span.1,
2215 )),
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_bytes(call_node.start_byte(), call_node.end_byte());
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_bytes(call_node.start_byte(), call_node.end_byte()),
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_bytes(child.start_byte(), child.end_byte());
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_bytes(child.start_byte(), child.end_byte());
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_bytes(
2961 field_child.start_byte(),
2962 field_child.end_byte(),
2963 );
2964
2965 let is_final = has_modifier(child, "final", content);
2967 let field_id = if is_final {
2968 helper.add_constant(&qualified_name, Some(span))
2969 } else {
2970 helper.add_variable(&qualified_name, Some(span))
2971 };
2972 export_from_file_module(helper, field_id);
2973 }
2974 }
2975 }
2976 }
2977 "constant_declaration" => {
2978 let mut cursor = child.walk();
2980 for const_child in child.children(&mut cursor) {
2981 if const_child.kind() == "variable_declarator"
2982 && let Some(name_node) = const_child.child_by_field_name("name")
2983 {
2984 let const_name = extract_identifier(name_node, content);
2985 let qualified_name = format!("{class_qualified_name}.{const_name}");
2986 let span =
2987 Span::from_bytes(const_child.start_byte(), const_child.end_byte());
2988 let const_id = helper.add_constant(&qualified_name, Some(span));
2989 export_from_file_module(helper, const_id);
2990 }
2991 }
2992 }
2993 "enum_constant" => {
2994 if let Some(name_node) = child.child_by_field_name("name") {
2996 let const_name = extract_identifier(name_node, content);
2997 let qualified_name = format!("{class_qualified_name}.{const_name}");
2998 let span = Span::from_bytes(child.start_byte(), child.end_byte());
2999 let const_id = helper.add_constant(&qualified_name, Some(span));
3000 export_from_file_module(helper, const_id);
3001 }
3002 }
3003 _ => {}
3004 }
3005 }
3006 }
3007}
3008
3009fn detect_ffi_imports(node: Node, content: &[u8]) -> (bool, bool) {
3016 let mut has_jna = false;
3017 let mut has_panama = false;
3018
3019 detect_ffi_imports_recursive(node, content, &mut has_jna, &mut has_panama);
3020
3021 (has_jna, has_panama)
3022}
3023
3024fn detect_ffi_imports_recursive(
3025 node: Node,
3026 content: &[u8],
3027 has_jna: &mut bool,
3028 has_panama: &mut bool,
3029) {
3030 if node.kind() == "import_declaration" {
3031 let import_text = node.utf8_text(content).unwrap_or("");
3032
3033 if import_text.contains("com.sun.jna") || import_text.contains("net.java.dev.jna") {
3035 *has_jna = true;
3036 }
3037
3038 if import_text.contains("java.lang.foreign") {
3040 *has_panama = true;
3041 }
3042 }
3043
3044 let mut cursor = node.walk();
3045 for child in node.children(&mut cursor) {
3046 detect_ffi_imports_recursive(child, content, has_jna, has_panama);
3047 }
3048}
3049
3050fn find_jna_library_interfaces(node: Node, content: &[u8]) -> Vec<String> {
3053 let mut jna_interfaces = Vec::new();
3054 find_jna_library_interfaces_recursive(node, content, &mut jna_interfaces);
3055 jna_interfaces
3056}
3057
3058fn find_jna_library_interfaces_recursive(
3059 node: Node,
3060 content: &[u8],
3061 jna_interfaces: &mut Vec<String>,
3062) {
3063 if node.kind() == "interface_declaration" {
3064 if let Some(name_node) = node.child_by_field_name("name") {
3066 let interface_name = extract_identifier(name_node, content);
3067
3068 let mut cursor = node.walk();
3070 for child in node.children(&mut cursor) {
3071 if child.kind() == "extends_interfaces" {
3072 let extends_text = child.utf8_text(content).unwrap_or("");
3073 if extends_text.contains("Library") {
3075 jna_interfaces.push(interface_name.clone());
3076 }
3077 }
3078 }
3079 }
3080 }
3081
3082 let mut cursor = node.walk();
3083 for child in node.children(&mut cursor) {
3084 find_jna_library_interfaces_recursive(child, content, jna_interfaces);
3085 }
3086}
3087
3088fn build_ffi_call_edge(
3091 call_node: Node,
3092 content: &[u8],
3093 caller_context: &MethodContext,
3094 ast_graph: &ASTGraph,
3095 helper: &mut GraphBuildHelper,
3096) -> bool {
3097 let Ok(method_name) = extract_method_invocation_name(call_node, content) else {
3099 return false;
3100 };
3101
3102 if ast_graph.has_jna_import && is_jna_native_load(call_node, content, &method_name) {
3104 let library_name = extract_jna_library_name(call_node, content);
3105 build_jna_native_load_edge(caller_context, &library_name, call_node, helper);
3106 return true;
3107 }
3108
3109 if ast_graph.has_jna_import
3111 && let Some(object_node) = call_node.child_by_field_name("object")
3112 {
3113 let object_text = extract_node_text(object_node, content);
3114
3115 let field_type = if let Some(class_name) = caller_context.class_stack.last() {
3117 let qualified_field = format!("{class_name}::{object_text}");
3118 ast_graph
3119 .field_types
3120 .get(&qualified_field)
3121 .or_else(|| ast_graph.field_types.get(&object_text))
3122 } else {
3123 ast_graph.field_types.get(&object_text)
3124 };
3125
3126 if let Some((type_name, _is_final, _visibility, _is_static)) = field_type {
3128 let simple_type = simple_type_name(type_name);
3129 if ast_graph.jna_library_interfaces.contains(&simple_type) {
3130 build_jna_method_call_edge(
3131 caller_context,
3132 &simple_type,
3133 &method_name,
3134 call_node,
3135 helper,
3136 );
3137 return true;
3138 }
3139 }
3140 }
3141
3142 if ast_graph.has_panama_import {
3144 if let Some(object_node) = call_node.child_by_field_name("object") {
3145 let object_text = extract_node_text(object_node, content);
3146
3147 if object_text == "Linker" && method_name == "nativeLinker" {
3149 build_panama_linker_edge(caller_context, call_node, helper);
3150 return true;
3151 }
3152
3153 if object_text == "SymbolLookup" && method_name == "libraryLookup" {
3155 let library_name = extract_first_string_arg(call_node, content);
3156 build_panama_library_lookup_edge(caller_context, &library_name, call_node, helper);
3157 return true;
3158 }
3159
3160 if method_name == "invokeExact" || method_name == "invoke" {
3162 if is_potential_panama_invoke(call_node, content) {
3165 build_panama_invoke_edge(caller_context, &method_name, call_node, helper);
3166 return true;
3167 }
3168 }
3169 }
3170
3171 if method_name == "nativeLinker" {
3173 let full_text = call_node.utf8_text(content).unwrap_or("");
3174 if full_text.contains("Linker") {
3175 build_panama_linker_edge(caller_context, call_node, helper);
3176 return true;
3177 }
3178 }
3179 }
3180
3181 false
3182}
3183
3184fn is_jna_native_load(call_node: Node, content: &[u8], method_name: &str) -> bool {
3186 if method_name != "load" && method_name != "loadLibrary" {
3187 return false;
3188 }
3189
3190 if let Some(object_node) = call_node.child_by_field_name("object") {
3191 let object_text = extract_node_text(object_node, content);
3192 return object_text == "Native" || object_text == "com.sun.jna.Native";
3193 }
3194
3195 false
3196}
3197
3198fn extract_jna_library_name(call_node: Node, content: &[u8]) -> String {
3201 if let Some(args_node) = call_node.child_by_field_name("arguments") {
3202 let mut cursor = args_node.walk();
3203 for child in args_node.children(&mut cursor) {
3204 if child.kind() == "string_literal" {
3205 let text = child.utf8_text(content).unwrap_or("\"unknown\"");
3206 return text.trim_matches('"').to_string();
3208 }
3209 }
3210 }
3211 "unknown".to_string()
3212}
3213
3214fn extract_first_string_arg(call_node: Node, content: &[u8]) -> String {
3216 if let Some(args_node) = call_node.child_by_field_name("arguments") {
3217 let mut cursor = args_node.walk();
3218 for child in args_node.children(&mut cursor) {
3219 if child.kind() == "string_literal" {
3220 let text = child.utf8_text(content).unwrap_or("\"unknown\"");
3221 return text.trim_matches('"').to_string();
3222 }
3223 }
3224 }
3225 "unknown".to_string()
3226}
3227
3228fn is_potential_panama_invoke(call_node: Node, content: &[u8]) -> bool {
3230 if let Some(object_node) = call_node.child_by_field_name("object") {
3232 let object_text = extract_node_text(object_node, content);
3233 let lower = object_text.to_lowercase();
3235 return lower.contains("handle")
3236 || lower.contains("downcall")
3237 || lower.contains("mh")
3238 || lower.contains("foreign");
3239 }
3240 false
3241}
3242
3243fn simple_type_name(type_name: &str) -> String {
3245 type_name
3246 .rsplit('.')
3247 .next()
3248 .unwrap_or(type_name)
3249 .to_string()
3250}
3251
3252fn build_jna_native_load_edge(
3254 caller_context: &MethodContext,
3255 library_name: &str,
3256 call_node: Node,
3257 helper: &mut GraphBuildHelper,
3258) {
3259 let caller_id = helper.ensure_method(
3260 caller_context.qualified_name(),
3261 Some(Span::from_bytes(
3262 caller_context.span.0,
3263 caller_context.span.1,
3264 )),
3265 false,
3266 caller_context.is_static,
3267 );
3268
3269 let target_name = format!("native::{library_name}");
3270 let target_id = helper.add_function(
3271 &target_name,
3272 Some(Span::from_bytes(
3273 call_node.start_byte(),
3274 call_node.end_byte(),
3275 )),
3276 false,
3277 false,
3278 );
3279
3280 helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3281}
3282
3283fn build_jna_method_call_edge(
3285 caller_context: &MethodContext,
3286 interface_name: &str,
3287 method_name: &str,
3288 call_node: Node,
3289 helper: &mut GraphBuildHelper,
3290) {
3291 let caller_id = helper.ensure_method(
3292 caller_context.qualified_name(),
3293 Some(Span::from_bytes(
3294 caller_context.span.0,
3295 caller_context.span.1,
3296 )),
3297 false,
3298 caller_context.is_static,
3299 );
3300
3301 let target_name = format!("native::{interface_name}::{method_name}");
3302 let target_id = helper.add_function(
3303 &target_name,
3304 Some(Span::from_bytes(
3305 call_node.start_byte(),
3306 call_node.end_byte(),
3307 )),
3308 false,
3309 false,
3310 );
3311
3312 helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3313}
3314
3315fn build_panama_linker_edge(
3317 caller_context: &MethodContext,
3318 call_node: Node,
3319 helper: &mut GraphBuildHelper,
3320) {
3321 let caller_id = helper.ensure_method(
3322 caller_context.qualified_name(),
3323 Some(Span::from_bytes(
3324 caller_context.span.0,
3325 caller_context.span.1,
3326 )),
3327 false,
3328 caller_context.is_static,
3329 );
3330
3331 let target_name = "native::panama::nativeLinker";
3332 let target_id = helper.add_function(
3333 target_name,
3334 Some(Span::from_bytes(
3335 call_node.start_byte(),
3336 call_node.end_byte(),
3337 )),
3338 false,
3339 false,
3340 );
3341
3342 helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3343}
3344
3345fn build_panama_library_lookup_edge(
3347 caller_context: &MethodContext,
3348 library_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(Span::from_bytes(
3355 caller_context.span.0,
3356 caller_context.span.1,
3357 )),
3358 false,
3359 caller_context.is_static,
3360 );
3361
3362 let target_name = format!("native::panama::{library_name}");
3363 let target_id = helper.add_function(
3364 &target_name,
3365 Some(Span::from_bytes(
3366 call_node.start_byte(),
3367 call_node.end_byte(),
3368 )),
3369 false,
3370 false,
3371 );
3372
3373 helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3374}
3375
3376fn build_panama_invoke_edge(
3378 caller_context: &MethodContext,
3379 method_name: &str,
3380 call_node: Node,
3381 helper: &mut GraphBuildHelper,
3382) {
3383 let caller_id = helper.ensure_method(
3384 caller_context.qualified_name(),
3385 Some(Span::from_bytes(
3386 caller_context.span.0,
3387 caller_context.span.1,
3388 )),
3389 false,
3390 caller_context.is_static,
3391 );
3392
3393 let target_name = format!("native::panama::{method_name}");
3394 let target_id = helper.add_function(
3395 &target_name,
3396 Some(Span::from_bytes(
3397 call_node.start_byte(),
3398 call_node.end_byte(),
3399 )),
3400 false,
3401 false,
3402 );
3403
3404 helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3405}
3406
3407fn build_jni_native_method_edge(method_context: &MethodContext, helper: &mut GraphBuildHelper) {
3410 let method_id = helper.ensure_method(
3412 method_context.qualified_name(),
3413 Some(Span::from_bytes(
3414 method_context.span.0,
3415 method_context.span.1,
3416 )),
3417 false,
3418 method_context.is_static,
3419 );
3420
3421 let native_target = format!("native::jni::{}", method_context.qualified_name());
3424 let target_id = helper.add_function(&native_target, None, false, false);
3425
3426 helper.add_ffi_edge(method_id, target_id, FfiConvention::C);
3427}
3428
3429fn extract_spring_route_info(method_node: Node, content: &[u8]) -> Option<(String, String)> {
3443 let mut cursor = method_node.walk();
3445 let modifiers_node = method_node
3446 .children(&mut cursor)
3447 .find(|child| child.kind() == "modifiers")?;
3448
3449 let mut mod_cursor = modifiers_node.walk();
3451 for annotation_node in modifiers_node.children(&mut mod_cursor) {
3452 if annotation_node.kind() != "annotation" {
3453 continue;
3454 }
3455
3456 let Some(annotation_name) = extract_annotation_name(annotation_node, content) else {
3458 continue;
3459 };
3460
3461 let http_method: String = match annotation_name.as_str() {
3463 "GetMapping" => "GET".to_string(),
3464 "PostMapping" => "POST".to_string(),
3465 "PutMapping" => "PUT".to_string(),
3466 "DeleteMapping" => "DELETE".to_string(),
3467 "PatchMapping" => "PATCH".to_string(),
3468 "RequestMapping" => {
3469 extract_request_mapping_method(annotation_node, content)
3471 .unwrap_or_else(|| "GET".to_string())
3472 }
3473 _ => continue,
3474 };
3475
3476 let Some(path) = extract_annotation_path(annotation_node, content) else {
3478 continue;
3479 };
3480
3481 return Some((http_method, path));
3482 }
3483
3484 None
3485}
3486
3487fn extract_annotation_name(annotation_node: Node, content: &[u8]) -> Option<String> {
3492 let mut cursor = annotation_node.walk();
3493 for child in annotation_node.children(&mut cursor) {
3494 match child.kind() {
3495 "identifier" => {
3496 return Some(extract_identifier(child, content));
3497 }
3498 "scoped_identifier" => {
3499 let full_text = extract_identifier(child, content);
3502 return full_text.rsplit('.').next().map(String::from);
3503 }
3504 _ => {}
3505 }
3506 }
3507 None
3508}
3509
3510fn extract_annotation_path(annotation_node: Node, content: &[u8]) -> Option<String> {
3517 let mut cursor = annotation_node.walk();
3519 let args_node = annotation_node
3520 .children(&mut cursor)
3521 .find(|child| child.kind() == "annotation_argument_list")?;
3522
3523 let mut args_cursor = args_node.walk();
3525 for arg_child in args_node.children(&mut args_cursor) {
3526 match arg_child.kind() {
3527 "string_literal" => {
3529 return extract_string_content(arg_child, content);
3530 }
3531 "element_value_pair" => {
3533 if let Some(path) = extract_path_from_element_value_pair(arg_child, content) {
3534 return Some(path);
3535 }
3536 }
3537 _ => {}
3538 }
3539 }
3540
3541 None
3542}
3543
3544fn extract_request_mapping_method(annotation_node: Node, content: &[u8]) -> Option<String> {
3552 let mut cursor = annotation_node.walk();
3554 let args_node = annotation_node
3555 .children(&mut cursor)
3556 .find(|child| child.kind() == "annotation_argument_list")?;
3557
3558 let mut args_cursor = args_node.walk();
3560 for arg_child in args_node.children(&mut args_cursor) {
3561 if arg_child.kind() != "element_value_pair" {
3562 continue;
3563 }
3564
3565 let Some(key_node) = arg_child.child_by_field_name("key") else {
3567 continue;
3568 };
3569 let key_text = extract_identifier(key_node, content);
3570 if key_text != "method" {
3571 continue;
3572 }
3573
3574 let Some(value_node) = arg_child.child_by_field_name("value") else {
3576 continue;
3577 };
3578 let value_text = extract_identifier(value_node, content);
3579
3580 if let Some(method) = value_text.rsplit('.').next() {
3582 let method_upper = method.to_uppercase();
3583 if matches!(
3584 method_upper.as_str(),
3585 "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS"
3586 ) {
3587 return Some(method_upper);
3588 }
3589 }
3590 }
3591
3592 None
3593}
3594
3595fn extract_path_from_element_value_pair(pair_node: Node, content: &[u8]) -> Option<String> {
3599 let key_node = pair_node.child_by_field_name("key")?;
3600 let key_text = extract_identifier(key_node, content);
3601
3602 if key_text != "path" && key_text != "value" {
3604 return None;
3605 }
3606
3607 let value_node = pair_node.child_by_field_name("value")?;
3608 if value_node.kind() == "string_literal" {
3609 return extract_string_content(value_node, content);
3610 }
3611
3612 None
3613}
3614
3615fn extract_class_request_mapping_path(method_node: Node, content: &[u8]) -> Option<String> {
3632 let mut current = method_node.parent()?;
3634 loop {
3635 if current.kind() == "class_declaration" {
3636 break;
3637 }
3638 current = current.parent()?;
3639 }
3640
3641 let mut cursor = current.walk();
3643 let modifiers = current
3644 .children(&mut cursor)
3645 .find(|child| child.kind() == "modifiers")?;
3646
3647 let mut mod_cursor = modifiers.walk();
3648 for annotation in modifiers.children(&mut mod_cursor) {
3649 if annotation.kind() != "annotation" {
3650 continue;
3651 }
3652 let Some(name) = extract_annotation_name(annotation, content) else {
3653 continue;
3654 };
3655 if name == "RequestMapping" {
3656 return extract_annotation_path(annotation, content);
3657 }
3658 }
3659
3660 None
3661}
3662
3663fn extract_string_content(string_node: Node, content: &[u8]) -> Option<String> {
3667 let text = string_node.utf8_text(content).ok()?;
3668 let trimmed = text.trim();
3669
3670 if trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() >= 2 {
3672 Some(trimmed[1..trimmed.len() - 1].to_string())
3673 } else {
3674 None
3675 }
3676}
3677
3678fn process_type_parameter_declarations(
3703 decl_node: Node,
3704 content: &[u8],
3705 parent_qualified_name: &str,
3706 helper: &mut GraphBuildHelper,
3707) {
3708 let Some(params_node) = decl_node.child_by_field_name("type_parameters") else {
3709 return;
3710 };
3711
3712 let mut cursor = params_node.walk();
3713 for param_node in params_node.children(&mut cursor) {
3714 if param_node.kind() != "type_parameter" {
3715 continue;
3716 }
3717
3718 let Some(name_node) = first_type_parameter_name_node(param_node) else {
3723 continue;
3724 };
3725 let Ok(param_name) = name_node.utf8_text(content) else {
3726 continue;
3727 };
3728
3729 let qualified_param = format!("{parent_qualified_name}.{param_name}");
3730 let span = Span::from_bytes(name_node.start_byte(), name_node.end_byte());
3731 let param_id = helper.add_type(&qualified_param, Some(span));
3736
3737 if let Some(bound_node) = param_node
3740 .children(&mut param_node.walk())
3741 .find(|c| c.kind() == "type_bound")
3742 {
3743 emit_type_bound_constraints(bound_node, content, param_id, helper);
3744 }
3745 }
3746}
3747
3748fn first_type_parameter_name_node(param_node: Node<'_>) -> Option<Node<'_>> {
3753 let mut cursor = param_node.walk();
3754 for child in param_node.children(&mut cursor) {
3755 if matches!(child.kind(), "type_identifier" | "identifier") {
3756 return Some(child);
3757 }
3758 }
3759 None
3760}
3761
3762fn emit_type_bound_constraints(
3773 bound_node: Node,
3774 content: &[u8],
3775 param_id: sqry_core::graph::unified::node::NodeId,
3776 helper: &mut GraphBuildHelper,
3777) {
3778 let mut cursor = bound_node.walk();
3779 for child in bound_node.children(&mut cursor) {
3780 if !child.is_named() {
3784 continue;
3785 }
3786 let bound_name = extract_bound_type_base_name(child, content);
3787 if bound_name.is_empty() {
3788 continue;
3789 }
3790 let constraint_id = helper.add_type(&bound_name, None);
3791 helper.add_typeof_edge_with_context(
3792 param_id,
3793 constraint_id,
3794 Some(TypeOfContext::Constraint),
3795 None,
3796 None,
3797 );
3798 }
3799}
3800
3801fn extract_bound_type_base_name(type_node: Node, content: &[u8]) -> String {
3817 match type_node.kind() {
3818 "generic_type" => {
3819 let mut cursor = type_node.walk();
3820 for child in type_node.children(&mut cursor) {
3821 if matches!(child.kind(), "type_identifier" | "scoped_type_identifier") {
3822 return extract_bound_type_base_name(child, content);
3823 }
3824 }
3825 extract_identifier(type_node, content)
3826 }
3827 "scoped_type_identifier" => extract_full_identifier(type_node, content),
3828 _ => extract_identifier(type_node, content),
3829 }
3830}
3831
3832#[cfg(test)]
3833mod shape_tests {
3834 use super::{cf_bucket_for_java_kind, java_shape_mapping};
3835 use sqry_core::graph::unified::build::shape::{
3836 CfBucket, ShapeBudget, ShapeMapping, compute_shape_descriptor,
3837 };
3838
3839 const SAMPLE: &str = include_str!(concat!(
3840 env!("CARGO_MANIFEST_DIR"),
3841 "/../test-fixtures/shape/reference/Sample.java"
3842 ));
3843
3844 fn parse(src: &str) -> tree_sitter::Tree {
3845 let lang: tree_sitter::Language = tree_sitter_java::LANGUAGE.into();
3846 let mut p = tree_sitter::Parser::new();
3847 p.set_language(&lang).expect("load java grammar");
3848 p.parse(src, None).expect("parse")
3849 }
3850
3851 fn method_named<'t>(tree: &'t tree_sitter::Tree, name: &str) -> tree_sitter::Node<'t> {
3852 let root = tree.root_node();
3853 let mut stack = vec![root];
3854 while let Some(node) = stack.pop() {
3855 if node.kind() == "method_declaration"
3856 && node
3857 .child_by_field_name("name")
3858 .and_then(|n| n.utf8_text(SAMPLE.as_bytes()).ok())
3859 == Some(name)
3860 {
3861 return node;
3862 }
3863 let mut c = node.walk();
3864 for ch in node.children(&mut c) {
3865 stack.push(ch);
3866 }
3867 }
3868 panic!("no method_declaration named {name}");
3869 }
3870
3871 #[test]
3872 fn cf_table_is_non_empty() {
3873 let mapping = java_shape_mapping();
3874 let lang: tree_sitter::Language = tree_sitter_java::LANGUAGE.into();
3875 let mut covered = 0;
3876 for id in 0..lang.node_kind_count() {
3877 if mapping.cf_bucket(id as u16).is_some() {
3878 covered += 1;
3879 }
3880 }
3881 assert!(
3882 covered >= 10,
3883 "expected many Java CF kinds mapped, got {covered}"
3884 );
3885 }
3886
3887 #[test]
3888 fn histogram_covers_real_control_flow() {
3889 let tree = parse(SAMPLE);
3890 let func = method_named(&tree, "classify");
3891 let d = compute_shape_descriptor(
3892 func,
3893 SAMPLE.as_bytes(),
3894 java_shape_mapping(),
3895 &ShapeBudget::default(),
3896 );
3897 assert!(!d.is_unhashable());
3898 for bucket in [
3899 CfBucket::Branch,
3900 CfBucket::Loop,
3901 CfBucket::Match,
3902 CfBucket::Try,
3903 CfBucket::Catch,
3904 CfBucket::Throw,
3905 CfBucket::Return,
3906 CfBucket::BreakContinue,
3907 CfBucket::Call,
3908 CfBucket::Assign,
3909 ] {
3910 assert!(
3911 d.cf_histogram[bucket.index()] >= 1,
3912 "classify must exercise {bucket:?}"
3913 );
3914 }
3915 }
3916
3917 #[test]
3918 fn lambda_body_covers_closure() {
3919 let tree = parse(SAMPLE);
3920 let func = method_named(&tree, "adder");
3921 let d = compute_shape_descriptor(
3922 func,
3923 SAMPLE.as_bytes(),
3924 java_shape_mapping(),
3925 &ShapeBudget::default(),
3926 );
3927 assert!(
3928 d.cf_histogram[CfBucket::Closure.index()] >= 1,
3929 "lambda closure"
3930 );
3931 }
3932
3933 #[test]
3934 fn signature_shape_reads_arity_and_return() {
3935 let tree = parse(SAMPLE);
3936 let func = method_named(&tree, "classify");
3937 let mapping = java_shape_mapping();
3938 let shape = mapping.signature_shape(func, SAMPLE.as_bytes());
3939 assert_eq!(shape.arity_positional, 2);
3941 assert!(shape.has_return_annotation, "int return type");
3942 }
3943
3944 #[test]
3945 fn unknown_kind_maps_to_none() {
3946 assert!(cf_bucket_for_java_kind("program").is_none());
3947 assert!(cf_bucket_for_java_kind("identifier").is_none());
3948 }
3949}