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