1use std::{collections::HashMap, path::Path};
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::edge::FfiConvention;
8use sqry_core::graph::unified::edge::kind::TypeOfContext;
9use sqry_core::graph::{GraphBuilder, GraphBuilderError, GraphResult, Language, Span};
10use tree_sitter::{Node, Tree};
11
12const DEFAULT_SCOPE_DEPTH: usize = 4;
13
14const FILE_MODULE_NAME: &str = "<file_module>";
17
18#[derive(Debug, Clone, Copy)]
39pub struct JavaGraphBuilder {
40 max_scope_depth: usize,
41}
42
43impl Default for JavaGraphBuilder {
44 fn default() -> Self {
45 Self {
46 max_scope_depth: DEFAULT_SCOPE_DEPTH,
47 }
48 }
49}
50
51impl JavaGraphBuilder {
52 #[must_use]
53 pub fn new(max_scope_depth: usize) -> Self {
54 Self { max_scope_depth }
55 }
56}
57
58impl GraphBuilder for JavaGraphBuilder {
59 fn build_graph(
60 &self,
61 tree: &Tree,
62 content: &[u8],
63 file: &Path,
64 staging: &mut StagingGraph,
65 ) -> GraphResult<()> {
66 let mut helper = GraphBuildHelper::new(staging, file, Language::Java);
67
68 let ast_graph = ASTGraph::from_tree(tree, content, self.max_scope_depth);
70 let mut scope_tree = local_scopes::build(tree.root_node(), content, Some(file))?;
71
72 for context in ast_graph.contexts() {
74 let qualified_name = context.qualified_name();
75 let span = Span::from_bytes(context.span.0, context.span.1);
76
77 if context.is_constructor {
78 helper.add_method_with_visibility(
79 qualified_name,
80 Some(span),
81 false,
82 false,
83 context.visibility.as_deref(),
84 );
85 } else {
86 let method_id = helper.add_method_with_signature(
88 qualified_name,
89 Some(span),
90 false,
91 context.is_static,
92 context.visibility.as_deref(),
93 context.return_type.as_deref(),
94 );
95
96 if let Some(return_type_text) = context.return_type.as_deref()
105 && return_type_text.trim() != "void"
106 {
107 let type_id = helper.add_type(return_type_text, None);
108 let method_simple_name = qualified_name
109 .rsplit_once('.')
110 .map_or(qualified_name, |(_, simple)| simple);
111 helper.add_typeof_edge_with_context(
112 method_id,
113 type_id,
114 Some(TypeOfContext::Return),
115 Some(0),
116 Some(method_simple_name),
117 );
118 }
119
120 if context.is_native {
122 build_jni_native_method_edge(context, &mut helper);
123 }
124 }
125 }
126
127 add_field_typeof_edges(&ast_graph, &mut helper);
129
130 let root = tree.root_node();
132 walk_tree_for_edges(
133 root,
134 content,
135 &ast_graph,
136 &mut scope_tree,
137 &mut helper,
138 tree,
139 )?;
140
141 Ok(())
142 }
143
144 fn language(&self) -> Language {
145 Language::Java
146 }
147}
148
149#[derive(Debug)]
154struct ASTGraph {
155 contexts: Vec<MethodContext>,
156 field_types: HashMap<String, (String, bool, Option<sqry_core::schema::Visibility>, bool)>,
166 import_map: HashMap<String, String>,
170 has_jna_import: bool,
172 has_panama_import: bool,
174 jna_library_interfaces: Vec<String>,
176}
177
178impl ASTGraph {
179 fn from_tree(tree: &Tree, content: &[u8], max_depth: usize) -> Self {
180 let package_name = PackageResolver::package_from_ast(tree, content);
182
183 let mut contexts = Vec::new();
184 let mut class_stack = Vec::new();
185
186 let recursion_limits = sqry_core::config::RecursionLimits::load_or_default()
188 .expect("Failed to load recursion limits");
189 let file_ops_depth = recursion_limits
190 .effective_file_ops_depth()
191 .expect("Invalid file_ops_depth configuration");
192 let mut guard = sqry_core::query::security::RecursionGuard::new(file_ops_depth)
193 .expect("Failed to create recursion guard");
194
195 if let Err(e) = extract_java_contexts(
196 tree.root_node(),
197 content,
198 &mut contexts,
199 &mut class_stack,
200 package_name.as_deref(),
201 0,
202 max_depth,
203 &mut guard,
204 ) {
205 eprintln!("Warning: Java AST traversal hit recursion limit: {e}");
206 }
207
208 let (field_types, import_map) = extract_field_and_import_types(tree.root_node(), content);
210
211 let (has_jna_import, has_panama_import) = detect_ffi_imports(tree.root_node(), content);
213
214 let jna_library_interfaces = find_jna_library_interfaces(tree.root_node(), content);
216
217 Self {
218 contexts,
219 field_types,
220 import_map,
221 has_jna_import,
222 has_panama_import,
223 jna_library_interfaces,
224 }
225 }
226
227 fn contexts(&self) -> &[MethodContext] {
228 &self.contexts
229 }
230
231 fn find_enclosing(&self, byte_pos: usize) -> Option<&MethodContext> {
233 self.contexts
234 .iter()
235 .filter(|ctx| byte_pos >= ctx.span.0 && byte_pos < ctx.span.1)
236 .max_by_key(|ctx| ctx.depth)
237 }
238}
239
240#[derive(Debug, Clone)]
241#[allow(clippy::struct_excessive_bools)] struct MethodContext {
243 qualified_name: String,
245 span: (usize, usize),
247 depth: usize,
249 is_static: bool,
251 #[allow(dead_code)] is_synchronized: bool,
254 is_constructor: bool,
256 #[allow(dead_code)] is_native: bool,
259 package_name: Option<String>,
261 class_stack: Vec<String>,
263 return_type: Option<String>,
265 visibility: Option<String>,
267}
268
269impl MethodContext {
270 fn qualified_name(&self) -> &str {
271 &self.qualified_name
272 }
273}
274
275fn extract_java_contexts(
284 node: Node,
285 content: &[u8],
286 contexts: &mut Vec<MethodContext>,
287 class_stack: &mut Vec<String>,
288 package_name: Option<&str>,
289 depth: usize,
290 max_depth: usize,
291 guard: &mut sqry_core::query::security::RecursionGuard,
292) -> Result<(), sqry_core::query::security::RecursionError> {
293 guard.enter()?;
294
295 if depth > max_depth {
296 guard.exit();
297 return Ok(());
298 }
299
300 match node.kind() {
301 "class_declaration"
302 | "interface_declaration"
303 | "enum_declaration"
304 | "record_declaration" => {
305 if let Some(name_node) = node.child_by_field_name("name") {
307 let class_name = extract_identifier(name_node, content);
308
309 class_stack.push(class_name.clone());
311
312 if let Some(body_node) = node.child_by_field_name("body") {
314 extract_methods_from_body(
315 body_node,
316 content,
317 class_stack,
318 package_name,
319 contexts,
320 depth + 1,
321 max_depth,
322 guard,
323 )?;
324
325 for i in 0..body_node.child_count() {
327 #[allow(clippy::cast_possible_truncation)]
328 if let Some(child) = body_node.child(i as u32) {
330 extract_java_contexts(
331 child,
332 content,
333 contexts,
334 class_stack,
335 package_name,
336 depth + 1,
337 max_depth,
338 guard,
339 )?;
340 }
341 }
342 }
343
344 class_stack.pop();
346
347 guard.exit();
348 return Ok(());
349 }
350 }
351 _ => {}
352 }
353
354 for i in 0..node.child_count() {
356 #[allow(clippy::cast_possible_truncation)]
357 if let Some(child) = node.child(i as u32) {
359 extract_java_contexts(
360 child,
361 content,
362 contexts,
363 class_stack,
364 package_name,
365 depth,
366 max_depth,
367 guard,
368 )?;
369 }
370 }
371
372 guard.exit();
373 Ok(())
374}
375
376#[allow(clippy::unnecessary_wraps)]
380fn extract_methods_from_body(
381 body_node: Node,
382 content: &[u8],
383 class_stack: &[String],
384 package_name: Option<&str>,
385 contexts: &mut Vec<MethodContext>,
386 depth: usize,
387 _max_depth: usize,
388 _guard: &mut sqry_core::query::security::RecursionGuard,
389) -> Result<(), sqry_core::query::security::RecursionError> {
390 for i in 0..body_node.child_count() {
391 #[allow(clippy::cast_possible_truncation)]
392 if let Some(child) = body_node.child(i as u32) {
394 match child.kind() {
395 "method_declaration" => {
396 if let Some(method_context) =
397 extract_method_context(child, content, class_stack, package_name, depth)
398 {
399 contexts.push(method_context);
400 }
401 }
402 "constructor_declaration" | "compact_constructor_declaration" => {
403 let constructor_context = extract_constructor_context(
404 child,
405 content,
406 class_stack,
407 package_name,
408 depth,
409 );
410 contexts.push(constructor_context);
411 }
412 _ => {}
413 }
414 }
415 }
416 Ok(())
417}
418
419fn extract_method_context(
420 method_node: Node,
421 content: &[u8],
422 class_stack: &[String],
423 package_name: Option<&str>,
424 depth: usize,
425) -> Option<MethodContext> {
426 let name_node = method_node.child_by_field_name("name")?;
427 let method_name = extract_identifier(name_node, content);
428
429 let is_static = has_modifier(method_node, "static", content);
430 let is_synchronized = has_modifier(method_node, "synchronized", content);
431 let is_native = has_modifier(method_node, "native", content);
432 let visibility = extract_visibility(method_node, content);
433
434 let return_type = method_node
437 .child_by_field_name("type")
438 .map(|type_node| extract_full_return_type(type_node, content));
439
440 let qualified_name = build_member_symbol(package_name, class_stack, &method_name);
442
443 Some(MethodContext {
444 qualified_name,
445 span: (method_node.start_byte(), method_node.end_byte()),
446 depth,
447 is_static,
448 is_synchronized,
449 is_constructor: false,
450 is_native,
451 package_name: package_name.map(std::string::ToString::to_string),
452 class_stack: class_stack.to_vec(),
453 return_type,
454 visibility,
455 })
456}
457
458fn extract_constructor_context(
459 constructor_node: Node,
460 content: &[u8],
461 class_stack: &[String],
462 package_name: Option<&str>,
463 depth: usize,
464) -> MethodContext {
465 let qualified_name = build_member_symbol(package_name, class_stack, "<init>");
467 let visibility = extract_visibility(constructor_node, content);
468
469 MethodContext {
470 qualified_name,
471 span: (constructor_node.start_byte(), constructor_node.end_byte()),
472 depth,
473 is_static: false,
474 is_synchronized: false,
475 is_constructor: true,
476 is_native: false,
477 package_name: package_name.map(std::string::ToString::to_string),
478 class_stack: class_stack.to_vec(),
479 return_type: None, visibility,
481 }
482}
483
484fn walk_tree_for_edges(
490 node: Node,
491 content: &[u8],
492 ast_graph: &ASTGraph,
493 scope_tree: &mut JavaScopeTree,
494 helper: &mut GraphBuildHelper,
495 tree: &Tree,
496) -> GraphResult<()> {
497 match node.kind() {
498 "class_declaration"
499 | "interface_declaration"
500 | "enum_declaration"
501 | "record_declaration" => {
502 return handle_type_declaration(node, content, ast_graph, scope_tree, helper, tree);
504 }
505 "method_declaration" | "constructor_declaration" => {
506 handle_method_declaration_parameters(node, content, ast_graph, scope_tree, helper);
508
509 if node.kind() == "method_declaration"
511 && let Some((http_method, path)) = extract_spring_route_info(node, content)
512 {
513 let full_path =
515 if let Some(class_prefix) = extract_class_request_mapping_path(node, content) {
516 let prefix = class_prefix.trim_end_matches('/');
517 let suffix = path.trim_start_matches('/');
518 if suffix.is_empty() {
519 class_prefix
520 } else {
521 format!("{prefix}/{suffix}")
522 }
523 } else {
524 path
525 };
526 let qualified_name = format!("route::{http_method}::{full_path}");
527 let span = Span::from_bytes(node.start_byte(), node.end_byte());
528 let endpoint_id = helper.add_endpoint(&qualified_name, Some(span));
529
530 let byte_pos = node.start_byte();
532 if let Some(context) = ast_graph.find_enclosing(byte_pos) {
533 let method_id = helper.ensure_method(
534 context.qualified_name(),
535 Some(Span::from_bytes(context.span.0, context.span.1)),
536 false,
537 context.is_static,
538 );
539 helper.add_contains_edge(endpoint_id, method_id);
540 }
541 }
542 }
543 "compact_constructor_declaration" => {
544 handle_compact_constructor_parameters(node, content, ast_graph, scope_tree, helper);
545 }
546 "method_invocation" => {
547 handle_method_invocation(node, content, ast_graph, helper);
548 }
549 "object_creation_expression" => {
550 handle_constructor_call(node, content, ast_graph, helper);
551 }
552 "import_declaration" => {
553 handle_import_declaration(node, content, helper);
554 }
555 "local_variable_declaration" => {
556 handle_local_variable_declaration(node, content, ast_graph, scope_tree, helper);
557 }
558 "enhanced_for_statement" => {
559 handle_enhanced_for_declaration(node, content, ast_graph, scope_tree, helper);
560 }
561 "catch_clause" => {
562 handle_catch_parameter_declaration(node, content, ast_graph, scope_tree, helper);
563 }
564 "lambda_expression" => {
565 handle_lambda_parameter_declaration(node, content, ast_graph, scope_tree, helper);
566 }
567 "try_with_resources_statement" => {
568 handle_try_with_resources_declaration(node, content, ast_graph, scope_tree, helper);
569 }
570 "instanceof_expression" => {
571 handle_instanceof_pattern_declaration(node, content, ast_graph, scope_tree, helper);
572 }
573 "switch_label" => {
574 handle_switch_pattern_declaration(node, content, ast_graph, scope_tree, helper);
575 }
576 "identifier" => {
577 handle_identifier_for_reference(node, content, ast_graph, scope_tree, helper);
578 }
579 _ => {}
580 }
581
582 for i in 0..node.child_count() {
584 #[allow(clippy::cast_possible_truncation)]
585 if let Some(child) = node.child(i as u32) {
587 walk_tree_for_edges(child, content, ast_graph, scope_tree, helper, tree)?;
588 }
589 }
590
591 Ok(())
592}
593
594fn handle_type_declaration(
595 node: Node,
596 content: &[u8],
597 ast_graph: &ASTGraph,
598 scope_tree: &mut JavaScopeTree,
599 helper: &mut GraphBuildHelper,
600 tree: &Tree,
601) -> GraphResult<()> {
602 let Some(name_node) = node.child_by_field_name("name") else {
603 return Ok(());
604 };
605 let class_name = extract_identifier(name_node, content);
606 let span = Span::from_bytes(node.start_byte(), node.end_byte());
607
608 let package = PackageResolver::package_from_ast(tree, content);
609 let class_stack = extract_declaration_class_stack(node, content);
610 let qualified_name = qualify_class_name(&class_name, &class_stack, package.as_deref());
611 let class_node_id = add_type_node(helper, node.kind(), &qualified_name, span);
612
613 if is_public(node, content) {
614 export_from_file_module(helper, class_node_id);
615 }
616
617 process_inheritance(node, content, package.as_deref(), class_node_id, helper);
618 if node.kind() == "class_declaration" {
619 process_implements(node, content, package.as_deref(), class_node_id, helper);
620 }
621 if node.kind() == "interface_declaration" {
622 process_interface_extends(node, content, package.as_deref(), class_node_id, helper);
623 }
624
625 process_type_parameter_declarations(node, content, &qualified_name, helper);
629
630 if let Some(body_node) = node.child_by_field_name("body") {
631 let is_interface = node.kind() == "interface_declaration";
632 process_class_member_exports(body_node, content, &qualified_name, helper, is_interface);
633
634 for i in 0..body_node.child_count() {
635 #[allow(clippy::cast_possible_truncation)]
636 if let Some(child) = body_node.child(i as u32) {
638 walk_tree_for_edges(child, content, ast_graph, scope_tree, helper, tree)?;
639 }
640 }
641 }
642
643 Ok(())
644}
645
646fn extract_declaration_class_stack(node: Node, content: &[u8]) -> Vec<String> {
647 let mut class_stack = Vec::new();
648 let mut current_node = Some(node);
649
650 while let Some(current) = current_node {
651 if matches!(
652 current.kind(),
653 "class_declaration"
654 | "interface_declaration"
655 | "enum_declaration"
656 | "record_declaration"
657 ) && let Some(name_node) = current.child_by_field_name("name")
658 {
659 class_stack.push(extract_identifier(name_node, content));
660 }
661
662 current_node = current.parent();
663 }
664
665 class_stack.reverse();
666 class_stack
667}
668
669fn qualify_class_name(class_name: &str, class_stack: &[String], package: Option<&str>) -> String {
670 let scope = class_stack
671 .split_last()
672 .map_or(&[][..], |(_, parent_stack)| parent_stack);
673 build_symbol(package, scope, class_name)
674}
675
676fn add_type_node(
677 helper: &mut GraphBuildHelper,
678 kind: &str,
679 qualified_name: &str,
680 span: Span,
681) -> sqry_core::graph::unified::node::NodeId {
682 match kind {
683 "interface_declaration" => helper.add_interface(qualified_name, Some(span)),
684 _ => helper.add_class(qualified_name, Some(span)),
685 }
686}
687
688fn handle_method_invocation(
689 node: Node,
690 content: &[u8],
691 ast_graph: &ASTGraph,
692 helper: &mut GraphBuildHelper,
693) {
694 if let Some(caller_context) = ast_graph.find_enclosing(node.start_byte()) {
695 let is_ffi = build_ffi_call_edge(node, content, caller_context, ast_graph, helper);
696 if is_ffi {
697 return;
698 }
699 }
700
701 process_method_call_unified(node, content, ast_graph, helper);
702}
703
704fn handle_constructor_call(
705 node: Node,
706 content: &[u8],
707 ast_graph: &ASTGraph,
708 helper: &mut GraphBuildHelper,
709) {
710 process_constructor_call_unified(node, content, ast_graph, helper);
711}
712
713fn handle_import_declaration(node: Node, content: &[u8], helper: &mut GraphBuildHelper) {
714 process_import_unified(node, content, helper);
715}
716
717fn add_field_typeof_edges(ast_graph: &ASTGraph, helper: &mut GraphBuildHelper) {
720 for (field_name, (type_fqn, is_final, visibility, is_static)) in &ast_graph.field_types {
721 let field_id = if *is_final {
723 if let Some(vis) = visibility {
725 helper.add_constant_with_static_and_visibility(
726 field_name,
727 None,
728 *is_static,
729 Some(vis.as_str()),
730 )
731 } else {
732 helper.add_constant_with_static_and_visibility(field_name, None, *is_static, None)
733 }
734 } else {
735 if let Some(vis) = visibility {
737 helper.add_property_with_static_and_visibility(
738 field_name,
739 None,
740 *is_static,
741 Some(vis.as_str()),
742 )
743 } else {
744 helper.add_property_with_static_and_visibility(field_name, None, *is_static, None)
745 }
746 };
747
748 let type_id = helper.add_class(type_fqn, None);
750
751 let bare_name = field_name
758 .rsplit_once("::")
759 .map_or(field_name.as_str(), |(_, simple)| simple);
760 helper.add_typeof_edge_with_context(
761 field_id,
762 type_id,
763 Some(TypeOfContext::Field),
764 None,
765 Some(bare_name),
766 );
767 }
768}
769
770fn extract_method_parameters(
773 method_node: Node,
774 content: &[u8],
775 qualified_method_name: &str,
776 helper: &mut GraphBuildHelper,
777 import_map: &HashMap<String, String>,
778 scope_tree: &mut JavaScopeTree,
779) {
780 let mut cursor = method_node.walk();
782 for child in method_node.children(&mut cursor) {
783 if child.kind() == "formal_parameters" {
784 let mut param_cursor = child.walk();
786 for param_child in child.children(&mut param_cursor) {
787 match param_child.kind() {
788 "formal_parameter" => {
789 handle_formal_parameter(
790 param_child,
791 content,
792 qualified_method_name,
793 helper,
794 import_map,
795 scope_tree,
796 );
797 }
798 "spread_parameter" => {
799 handle_spread_parameter(
800 param_child,
801 content,
802 qualified_method_name,
803 helper,
804 import_map,
805 scope_tree,
806 );
807 }
808 "receiver_parameter" => {
809 handle_receiver_parameter(
810 param_child,
811 content,
812 qualified_method_name,
813 helper,
814 import_map,
815 scope_tree,
816 );
817 }
818 _ => {}
819 }
820 }
821 }
822 }
823}
824
825fn handle_formal_parameter(
827 param_node: Node,
828 content: &[u8],
829 method_name: &str,
830 helper: &mut GraphBuildHelper,
831 import_map: &HashMap<String, String>,
832 scope_tree: &mut JavaScopeTree,
833) {
834 use sqry_core::graph::unified::node::NodeKind;
835
836 let Some(type_node) = param_node.child_by_field_name("type") else {
838 return;
839 };
840
841 let Some(name_node) = param_node.child_by_field_name("name") else {
843 return;
844 };
845
846 let type_text = extract_type_name(type_node, content);
848 let param_name = extract_identifier(name_node, content);
849
850 if type_text.is_empty() || param_name.is_empty() {
851 return;
852 }
853
854 let resolved_type = import_map.get(&type_text).cloned().unwrap_or(type_text);
856
857 let qualified_param = format!("{method_name}::{param_name}");
859 let span = Span::from_bytes(param_node.start_byte(), param_node.end_byte());
860
861 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
863
864 scope_tree.attach_node_id(¶m_name, name_node.start_byte(), param_id);
865
866 let type_id = helper.add_class(&resolved_type, None);
868
869 helper.add_typeof_edge(param_id, type_id);
871}
872
873fn handle_spread_parameter(
875 param_node: Node,
876 content: &[u8],
877 method_name: &str,
878 helper: &mut GraphBuildHelper,
879 import_map: &HashMap<String, String>,
880 scope_tree: &mut JavaScopeTree,
881) {
882 use sqry_core::graph::unified::node::NodeKind;
883
884 let mut type_text = String::new();
893 let mut param_name = String::new();
894 let mut param_name_node = None;
895
896 let mut cursor = param_node.walk();
897 for child in param_node.children(&mut cursor) {
898 match child.kind() {
899 "type_identifier" | "generic_type" | "scoped_type_identifier" => {
900 type_text = extract_type_name(child, content);
901 }
902 "variable_declarator" => {
903 if let Some(name_node) = child.child_by_field_name("name") {
905 param_name = extract_identifier(name_node, content);
906 param_name_node = Some(name_node);
907 }
908 }
909 _ => {}
910 }
911 }
912
913 if type_text.is_empty() || param_name.is_empty() {
914 return;
915 }
916
917 let resolved_type = import_map.get(&type_text).cloned().unwrap_or(type_text);
919
920 let qualified_param = format!("{method_name}::{param_name}");
922 let span = Span::from_bytes(param_node.start_byte(), param_node.end_byte());
923
924 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
926
927 if let Some(name_node) = param_name_node {
928 scope_tree.attach_node_id(¶m_name, name_node.start_byte(), param_id);
929 }
930
931 let type_id = helper.add_class(&resolved_type, None);
934
935 helper.add_typeof_edge(param_id, type_id);
937}
938
939fn handle_receiver_parameter(
941 param_node: Node,
942 content: &[u8],
943 method_name: &str,
944 helper: &mut GraphBuildHelper,
945 import_map: &HashMap<String, String>,
946 _scope_tree: &mut JavaScopeTree,
947) {
948 use sqry_core::graph::unified::node::NodeKind;
949
950 let mut type_text = String::new();
958 let mut cursor = param_node.walk();
959
960 for child in param_node.children(&mut cursor) {
962 if matches!(
963 child.kind(),
964 "type_identifier" | "generic_type" | "scoped_type_identifier"
965 ) {
966 type_text = extract_type_name(child, content);
967 break;
968 }
969 }
970
971 if type_text.is_empty() {
972 return;
973 }
974
975 let param_name = "this";
977
978 let resolved_type = import_map.get(&type_text).cloned().unwrap_or(type_text);
980
981 let qualified_param = format!("{method_name}::{param_name}");
983 let span = Span::from_bytes(param_node.start_byte(), param_node.end_byte());
984
985 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
987
988 let type_id = helper.add_class(&resolved_type, None);
990
991 helper.add_typeof_edge(param_id, type_id);
993}
994
995#[derive(Debug, Clone, Copy, Eq, PartialEq)]
996enum FieldAccessRole {
997 Default,
998 ExplicitThisOrSuper,
999 Skip,
1000}
1001
1002#[derive(Debug, Clone, Copy, Eq, PartialEq)]
1003enum FieldResolutionMode {
1004 Default,
1005 CurrentOnly,
1006}
1007
1008fn field_access_role(
1009 node: Node,
1010 content: &[u8],
1011 ast_graph: &ASTGraph,
1012 scope_tree: &JavaScopeTree,
1013 identifier_text: &str,
1014) -> FieldAccessRole {
1015 let Some(parent) = node.parent() else {
1016 return FieldAccessRole::Default;
1017 };
1018
1019 if parent.kind() == "field_access" {
1020 if let Some(field_node) = parent.child_by_field_name("field")
1021 && field_node.id() == node.id()
1022 && let Some(object_node) = parent.child_by_field_name("object")
1023 {
1024 if is_explicit_this_or_super(object_node, content) {
1025 return FieldAccessRole::ExplicitThisOrSuper;
1026 }
1027 return FieldAccessRole::Skip;
1028 }
1029
1030 if let Some(object_node) = parent.child_by_field_name("object")
1031 && object_node.id() == node.id()
1032 && !scope_tree.has_local_binding(identifier_text, node.start_byte())
1033 && is_static_type_identifier(identifier_text, ast_graph, scope_tree)
1034 {
1035 return FieldAccessRole::Skip;
1036 }
1037 }
1038
1039 if parent.kind() == "method_invocation"
1040 && let Some(object_node) = parent.child_by_field_name("object")
1041 && object_node.id() == node.id()
1042 && !scope_tree.has_local_binding(identifier_text, node.start_byte())
1043 && is_static_type_identifier(identifier_text, ast_graph, scope_tree)
1044 {
1045 return FieldAccessRole::Skip;
1046 }
1047
1048 if parent.kind() == "method_reference"
1049 && let Some(object_node) = parent.child_by_field_name("object")
1050 && object_node.id() == node.id()
1051 && !scope_tree.has_local_binding(identifier_text, node.start_byte())
1052 && is_static_type_identifier(identifier_text, ast_graph, scope_tree)
1053 {
1054 return FieldAccessRole::Skip;
1055 }
1056
1057 FieldAccessRole::Default
1058}
1059
1060fn is_static_type_identifier(
1061 identifier_text: &str,
1062 ast_graph: &ASTGraph,
1063 scope_tree: &JavaScopeTree,
1064) -> bool {
1065 ast_graph.import_map.contains_key(identifier_text)
1066 || scope_tree.is_known_type_name(identifier_text)
1067}
1068
1069fn is_explicit_this_or_super(node: Node, content: &[u8]) -> bool {
1070 if matches!(node.kind(), "this" | "super") {
1071 return true;
1072 }
1073 if node.kind() == "identifier" {
1074 let text = extract_identifier(node, content);
1075 return matches!(text.as_str(), "this" | "super");
1076 }
1077 if node.kind() == "field_access"
1078 && let Some(field) = node.child_by_field_name("field")
1079 {
1080 let text = extract_identifier(field, content);
1081 if matches!(text.as_str(), "this" | "super") {
1082 return true;
1083 }
1084 }
1085 false
1086}
1087
1088#[allow(clippy::too_many_lines)]
1091fn is_declaration_context(node: Node) -> bool {
1092 let Some(parent) = node.parent() else {
1094 return false;
1095 };
1096
1097 if parent.kind() == "variable_declarator" {
1102 let mut cursor = parent.walk();
1104 for (idx, child) in parent.children(&mut cursor).enumerate() {
1105 if child.id() == node.id() {
1106 #[allow(clippy::cast_possible_truncation)]
1107 if let Some(field_name) = parent.field_name_for_child(idx as u32) {
1108 return field_name == "name";
1110 }
1111 break;
1112 }
1113 }
1114
1115 if let Some(grandparent) = parent.parent()
1117 && grandparent.kind() == "spread_parameter"
1118 {
1119 return true;
1120 }
1121
1122 return false;
1123 }
1124
1125 if parent.kind() == "formal_parameter" {
1127 let mut cursor = parent.walk();
1128 for (idx, child) in parent.children(&mut cursor).enumerate() {
1129 if child.id() == node.id() {
1130 #[allow(clippy::cast_possible_truncation)]
1131 if let Some(field_name) = parent.field_name_for_child(idx as u32) {
1132 return field_name == "name";
1133 }
1134 break;
1135 }
1136 }
1137 return false;
1138 }
1139
1140 if parent.kind() == "enhanced_for_statement" {
1143 let mut cursor = parent.walk();
1145 for (idx, child) in parent.children(&mut cursor).enumerate() {
1146 if child.id() == node.id() {
1147 #[allow(clippy::cast_possible_truncation)]
1148 if let Some(field_name) = parent.field_name_for_child(idx as u32) {
1149 return field_name == "name";
1151 }
1152 break;
1153 }
1154 }
1155 return false;
1156 }
1157
1158 if parent.kind() == "lambda_expression" {
1159 if let Some(params) = parent.child_by_field_name("parameters") {
1160 return params.id() == node.id();
1161 }
1162 return false;
1163 }
1164
1165 if parent.kind() == "inferred_parameters" {
1166 return true;
1167 }
1168
1169 if parent.kind() == "resource" {
1170 if let Some(name_node) = parent.child_by_field_name("name")
1171 && name_node.id() == node.id()
1172 {
1173 let has_type = parent.child_by_field_name("type").is_some();
1174 let has_value = parent.child_by_field_name("value").is_some();
1175 return has_type || has_value;
1176 }
1177 return false;
1178 }
1179
1180 if parent.kind() == "type_pattern" {
1184 if let Some((name_node, _type_node)) = typed_pattern_parts(parent) {
1185 return name_node.id() == node.id();
1186 }
1187 return false;
1188 }
1189
1190 if parent.kind() == "instanceof_expression" {
1192 let mut cursor = parent.walk();
1193 for (idx, child) in parent.children(&mut cursor).enumerate() {
1194 if child.id() == node.id() {
1195 #[allow(clippy::cast_possible_truncation)]
1196 if let Some(field_name) = parent.field_name_for_child(idx as u32) {
1197 return field_name == "name";
1199 }
1200 break;
1201 }
1202 }
1203 return false;
1204 }
1205
1206 if parent.kind() == "record_pattern_component" {
1209 let mut cursor = parent.walk();
1211 for child in parent.children(&mut cursor) {
1212 if child.id() == node.id() && child.kind() == "identifier" {
1213 return true;
1215 }
1216 }
1217 return false;
1218 }
1219
1220 if parent.kind() == "record_component" {
1221 if let Some(name_node) = parent.child_by_field_name("name") {
1222 return name_node.id() == node.id();
1223 }
1224 return false;
1225 }
1226
1227 matches!(
1229 parent.kind(),
1230 "method_declaration"
1231 | "constructor_declaration"
1232 | "compact_constructor_declaration"
1233 | "class_declaration"
1234 | "interface_declaration"
1235 | "enum_declaration"
1236 | "field_declaration"
1237 | "catch_formal_parameter"
1238 )
1239}
1240
1241fn is_method_invocation_name(node: Node) -> bool {
1242 let Some(parent) = node.parent() else {
1243 return false;
1244 };
1245 if parent.kind() != "method_invocation" {
1246 return false;
1247 }
1248 parent
1249 .child_by_field_name("name")
1250 .is_some_and(|name_node| name_node.id() == node.id())
1251}
1252
1253fn is_method_reference_name(node: Node) -> bool {
1254 let Some(parent) = node.parent() else {
1255 return false;
1256 };
1257 if parent.kind() != "method_reference" {
1258 return false;
1259 }
1260 parent
1261 .child_by_field_name("name")
1262 .is_some_and(|name_node| name_node.id() == node.id())
1263}
1264
1265fn is_label_identifier(node: Node) -> bool {
1266 let Some(parent) = node.parent() else {
1267 return false;
1268 };
1269 if parent.kind() == "labeled_statement" {
1270 return true;
1271 }
1272 if matches!(parent.kind(), "break_statement" | "continue_statement")
1273 && let Some(label) = parent.child_by_field_name("label")
1274 {
1275 return label.id() == node.id();
1276 }
1277 false
1278}
1279
1280fn is_class_literal(node: Node) -> bool {
1281 let Some(parent) = node.parent() else {
1282 return false;
1283 };
1284 parent.kind() == "class_literal"
1285}
1286
1287fn is_type_identifier_context(node: Node) -> bool {
1288 let Some(parent) = node.parent() else {
1289 return false;
1290 };
1291 matches!(
1292 parent.kind(),
1293 "type_identifier"
1294 | "scoped_type_identifier"
1295 | "scoped_identifier"
1296 | "generic_type"
1297 | "type_argument"
1298 | "type_bound"
1299 )
1300}
1301
1302fn add_reference_edge_for_target(
1303 usage_node: Node,
1304 identifier_text: &str,
1305 target_id: sqry_core::graph::unified::node::NodeId,
1306 helper: &mut GraphBuildHelper,
1307) {
1308 let usage_span = Span::from_bytes(usage_node.start_byte(), usage_node.end_byte());
1309 let usage_id = helper.add_node(
1310 &format!("{}@{}", identifier_text, usage_node.start_byte()),
1311 Some(usage_span),
1312 sqry_core::graph::unified::node::NodeKind::Variable,
1313 );
1314 helper.add_reference_edge(usage_id, target_id);
1315}
1316
1317fn resolve_field_reference(
1318 node: Node,
1319 identifier_text: &str,
1320 ast_graph: &ASTGraph,
1321 helper: &mut GraphBuildHelper,
1322 mode: FieldResolutionMode,
1323) {
1324 let context = ast_graph.find_enclosing(node.start_byte());
1325 let mut candidates = Vec::new();
1326 if let Some(ctx) = context
1327 && !ctx.class_stack.is_empty()
1328 {
1329 if mode == FieldResolutionMode::CurrentOnly {
1330 let class_path = ctx.class_stack.join("::");
1331 candidates.push(format!("{class_path}::{identifier_text}"));
1332 } else {
1333 let stack_len = ctx.class_stack.len();
1334 for idx in (1..=stack_len).rev() {
1335 let class_path = ctx.class_stack[..idx].join("::");
1336 candidates.push(format!("{class_path}::{identifier_text}"));
1337 }
1338 }
1339 }
1340
1341 if mode != FieldResolutionMode::CurrentOnly {
1342 candidates.push(identifier_text.to_string());
1343 }
1344
1345 for candidate in candidates {
1346 if ast_graph.field_types.contains_key(&candidate) {
1347 add_field_reference(node, identifier_text, &candidate, ast_graph, helper);
1348 return;
1349 }
1350 }
1351}
1352
1353fn add_field_reference(
1354 node: Node,
1355 identifier_text: &str,
1356 field_name: &str,
1357 ast_graph: &ASTGraph,
1358 helper: &mut GraphBuildHelper,
1359) {
1360 let usage_span = Span::from_bytes(node.start_byte(), node.end_byte());
1361 let usage_id = helper.add_node(
1362 &format!("{}@{}", identifier_text, node.start_byte()),
1363 Some(usage_span),
1364 sqry_core::graph::unified::node::NodeKind::Variable,
1365 );
1366
1367 let field_metadata = ast_graph.field_types.get(field_name);
1368 let field_id = if let Some((_, is_final, visibility, is_static)) = field_metadata {
1369 if *is_final {
1370 if let Some(vis) = visibility {
1371 helper.add_constant_with_static_and_visibility(
1372 field_name,
1373 None,
1374 *is_static,
1375 Some(vis.as_str()),
1376 )
1377 } else {
1378 helper.add_constant_with_static_and_visibility(field_name, None, *is_static, None)
1379 }
1380 } else if let Some(vis) = visibility {
1381 helper.add_property_with_static_and_visibility(
1382 field_name,
1383 None,
1384 *is_static,
1385 Some(vis.as_str()),
1386 )
1387 } else {
1388 helper.add_property_with_static_and_visibility(field_name, None, *is_static, None)
1389 }
1390 } else {
1391 helper.add_property_with_static_and_visibility(field_name, None, false, None)
1392 };
1393
1394 helper.add_reference_edge(usage_id, field_id);
1395}
1396
1397#[allow(clippy::similar_names)]
1399fn handle_identifier_for_reference(
1400 node: Node,
1401 content: &[u8],
1402 ast_graph: &ASTGraph,
1403 scope_tree: &mut JavaScopeTree,
1404 helper: &mut GraphBuildHelper,
1405) {
1406 let identifier_text = extract_identifier(node, content);
1407
1408 if identifier_text.is_empty() {
1409 return;
1410 }
1411
1412 if is_declaration_context(node) {
1414 return;
1415 }
1416
1417 if is_method_invocation_name(node)
1418 || is_method_reference_name(node)
1419 || is_label_identifier(node)
1420 || is_class_literal(node)
1421 {
1422 return;
1423 }
1424
1425 if is_type_identifier_context(node) {
1426 return;
1427 }
1428
1429 let field_access_role =
1430 field_access_role(node, content, ast_graph, scope_tree, &identifier_text);
1431 if matches!(field_access_role, FieldAccessRole::Skip) {
1432 return;
1433 }
1434
1435 let allow_local = matches!(field_access_role, FieldAccessRole::Default);
1436 let allow_field = !matches!(field_access_role, FieldAccessRole::Skip);
1437 let field_mode = if matches!(field_access_role, FieldAccessRole::ExplicitThisOrSuper) {
1438 FieldResolutionMode::CurrentOnly
1439 } else {
1440 FieldResolutionMode::Default
1441 };
1442
1443 if allow_local {
1444 match scope_tree.resolve_identifier(node.start_byte(), &identifier_text) {
1445 ResolutionOutcome::Local(binding) => {
1446 let target_id = if let Some(node_id) = binding.node_id {
1447 node_id
1448 } else {
1449 let span = Span::from_bytes(binding.decl_start_byte, binding.decl_end_byte);
1450 let qualified_var = format!("{}@{}", identifier_text, binding.decl_start_byte);
1451 let var_id = helper.add_variable(&qualified_var, Some(span));
1452 scope_tree.attach_node_id(&identifier_text, binding.decl_start_byte, var_id);
1453 var_id
1454 };
1455 add_reference_edge_for_target(node, &identifier_text, target_id, helper);
1456 return;
1457 }
1458 ResolutionOutcome::Member { qualified_name } => {
1459 if let Some(field_name) = qualified_name {
1460 add_field_reference(node, &identifier_text, &field_name, ast_graph, helper);
1461 }
1462 return;
1463 }
1464 ResolutionOutcome::Ambiguous => {
1465 return;
1466 }
1467 ResolutionOutcome::NoMatch => {}
1468 }
1469 }
1470
1471 if !allow_field {
1472 return;
1473 }
1474
1475 resolve_field_reference(node, &identifier_text, ast_graph, helper, field_mode);
1476}
1477
1478fn handle_method_declaration_parameters(
1480 node: Node,
1481 content: &[u8],
1482 ast_graph: &ASTGraph,
1483 scope_tree: &mut JavaScopeTree,
1484 helper: &mut GraphBuildHelper,
1485) {
1486 let byte_pos = node.start_byte();
1488 if let Some(context) = ast_graph.find_enclosing(byte_pos) {
1489 let qualified_method_name = &context.qualified_name;
1490
1491 extract_method_parameters(
1493 node,
1494 content,
1495 qualified_method_name,
1496 helper,
1497 &ast_graph.import_map,
1498 scope_tree,
1499 );
1500
1501 process_type_parameter_declarations(node, content, qualified_method_name, helper);
1510 }
1511}
1512
1513fn handle_local_variable_declaration(
1515 node: Node,
1516 content: &[u8],
1517 ast_graph: &ASTGraph,
1518 scope_tree: &mut JavaScopeTree,
1519 helper: &mut GraphBuildHelper,
1520) {
1521 let Some(type_node) = node.child_by_field_name("type") else {
1523 return;
1524 };
1525
1526 let type_text = extract_type_name(type_node, content);
1527 if type_text.is_empty() {
1528 return;
1529 }
1530
1531 let resolved_type = ast_graph
1533 .import_map
1534 .get(&type_text)
1535 .cloned()
1536 .unwrap_or_else(|| type_text.clone());
1537
1538 let mut cursor = node.walk();
1540 for child in node.children(&mut cursor) {
1541 if child.kind() == "variable_declarator"
1542 && let Some(name_node) = child.child_by_field_name("name")
1543 {
1544 let var_name = extract_identifier(name_node, content);
1545
1546 let qualified_var = format!("{}@{}", var_name, name_node.start_byte());
1548
1549 let span = Span::from_bytes(child.start_byte(), child.end_byte());
1551 let var_id = helper.add_variable(&qualified_var, Some(span));
1552 scope_tree.attach_node_id(&var_name, name_node.start_byte(), var_id);
1553
1554 let type_id = helper.add_class(&resolved_type, None);
1556
1557 helper.add_typeof_edge(var_id, type_id);
1559 }
1560 }
1561}
1562
1563fn handle_enhanced_for_declaration(
1564 node: Node,
1565 content: &[u8],
1566 ast_graph: &ASTGraph,
1567 scope_tree: &mut JavaScopeTree,
1568 helper: &mut GraphBuildHelper,
1569) {
1570 let Some(type_node) = node.child_by_field_name("type") else {
1571 return;
1572 };
1573 let Some(name_node) = node.child_by_field_name("name") else {
1574 return;
1575 };
1576 let Some(body_node) = node.child_by_field_name("body") else {
1577 return;
1578 };
1579
1580 let type_text = extract_type_name(type_node, content);
1581 let var_name = extract_identifier(name_node, content);
1582 if type_text.is_empty() || var_name.is_empty() {
1583 return;
1584 }
1585
1586 let resolved_type = ast_graph
1587 .import_map
1588 .get(&type_text)
1589 .cloned()
1590 .unwrap_or(type_text);
1591
1592 let qualified_var = format!("{}@{}", var_name, name_node.start_byte());
1593 let span = Span::from_bytes(name_node.start_byte(), name_node.end_byte());
1594 let var_id = helper.add_variable(&qualified_var, Some(span));
1595 scope_tree.attach_node_id(&var_name, body_node.start_byte(), var_id);
1596
1597 let type_id = helper.add_class(&resolved_type, None);
1598 helper.add_typeof_edge(var_id, type_id);
1599}
1600
1601fn handle_catch_parameter_declaration(
1602 node: Node,
1603 content: &[u8],
1604 ast_graph: &ASTGraph,
1605 scope_tree: &mut JavaScopeTree,
1606 helper: &mut GraphBuildHelper,
1607) {
1608 let Some(param_node) = node
1609 .child_by_field_name("parameter")
1610 .or_else(|| first_child_of_kind(node, "catch_formal_parameter"))
1611 .or_else(|| first_child_of_kind(node, "formal_parameter"))
1612 else {
1613 return;
1614 };
1615 let Some(name_node) = param_node
1616 .child_by_field_name("name")
1617 .or_else(|| first_child_of_kind(param_node, "identifier"))
1618 else {
1619 return;
1620 };
1621
1622 let var_name = extract_identifier(name_node, content);
1623 if var_name.is_empty() {
1624 return;
1625 }
1626
1627 let qualified_var = format!("{}@{}", var_name, name_node.start_byte());
1628 let span = Span::from_bytes(param_node.start_byte(), param_node.end_byte());
1629 let var_id = helper.add_variable(&qualified_var, Some(span));
1630 scope_tree.attach_node_id(&var_name, name_node.start_byte(), var_id);
1631
1632 if let Some(type_node) = param_node
1633 .child_by_field_name("type")
1634 .or_else(|| first_child_of_kind(param_node, "type_identifier"))
1635 .or_else(|| first_child_of_kind(param_node, "scoped_type_identifier"))
1636 .or_else(|| first_child_of_kind(param_node, "generic_type"))
1637 {
1638 add_typeof_for_catch_type(type_node, content, ast_graph, helper, var_id);
1639 }
1640}
1641
1642fn add_typeof_for_catch_type(
1643 type_node: Node,
1644 content: &[u8],
1645 ast_graph: &ASTGraph,
1646 helper: &mut GraphBuildHelper,
1647 var_id: sqry_core::graph::unified::node::NodeId,
1648) {
1649 if type_node.kind() == "union_type" {
1650 let mut cursor = type_node.walk();
1651 for child in type_node.children(&mut cursor) {
1652 if matches!(
1653 child.kind(),
1654 "type_identifier" | "scoped_type_identifier" | "generic_type"
1655 ) {
1656 let type_text = extract_type_name(child, content);
1657 if !type_text.is_empty() {
1658 let resolved_type = ast_graph
1659 .import_map
1660 .get(&type_text)
1661 .cloned()
1662 .unwrap_or(type_text);
1663 let type_id = helper.add_class(&resolved_type, None);
1664 helper.add_typeof_edge(var_id, type_id);
1665 }
1666 }
1667 }
1668 return;
1669 }
1670
1671 let type_text = extract_type_name(type_node, content);
1672 if type_text.is_empty() {
1673 return;
1674 }
1675 let resolved_type = ast_graph
1676 .import_map
1677 .get(&type_text)
1678 .cloned()
1679 .unwrap_or(type_text);
1680 let type_id = helper.add_class(&resolved_type, None);
1681 helper.add_typeof_edge(var_id, type_id);
1682}
1683
1684fn handle_lambda_parameter_declaration(
1685 node: Node,
1686 content: &[u8],
1687 ast_graph: &ASTGraph,
1688 scope_tree: &mut JavaScopeTree,
1689 helper: &mut GraphBuildHelper,
1690) {
1691 use sqry_core::graph::unified::node::NodeKind;
1692
1693 let Some(params_node) = node.child_by_field_name("parameters") else {
1694 return;
1695 };
1696 let lambda_prefix = format!("lambda@{}", node.start_byte());
1697
1698 if params_node.kind() == "identifier" {
1699 let name = extract_identifier(params_node, content);
1700 if name.is_empty() {
1701 return;
1702 }
1703 let qualified_param = format!("{lambda_prefix}::{name}");
1704 let span = Span::from_bytes(params_node.start_byte(), params_node.end_byte());
1705 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
1706 scope_tree.attach_node_id(&name, params_node.start_byte(), param_id);
1707 return;
1708 }
1709
1710 let mut cursor = params_node.walk();
1711 for child in params_node.children(&mut cursor) {
1712 match child.kind() {
1713 "identifier" => {
1714 let name = extract_identifier(child, content);
1715 if name.is_empty() {
1716 continue;
1717 }
1718 let qualified_param = format!("{lambda_prefix}::{name}");
1719 let span = Span::from_bytes(child.start_byte(), child.end_byte());
1720 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
1721 scope_tree.attach_node_id(&name, child.start_byte(), param_id);
1722 }
1723 "formal_parameter" => {
1724 let Some(name_node) = child.child_by_field_name("name") else {
1725 continue;
1726 };
1727 let Some(type_node) = child.child_by_field_name("type") else {
1728 continue;
1729 };
1730 let name = extract_identifier(name_node, content);
1731 if name.is_empty() {
1732 continue;
1733 }
1734 let type_text = extract_type_name(type_node, content);
1735 let resolved_type = ast_graph
1736 .import_map
1737 .get(&type_text)
1738 .cloned()
1739 .unwrap_or(type_text);
1740 let qualified_param = format!("{lambda_prefix}::{name}");
1741 let span = Span::from_bytes(child.start_byte(), child.end_byte());
1742 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
1743 scope_tree.attach_node_id(&name, name_node.start_byte(), param_id);
1744 let type_id = helper.add_class(&resolved_type, None);
1745 helper.add_typeof_edge(param_id, type_id);
1746 }
1747 _ => {}
1748 }
1749 }
1750}
1751
1752fn handle_try_with_resources_declaration(
1753 node: Node,
1754 content: &[u8],
1755 ast_graph: &ASTGraph,
1756 scope_tree: &mut JavaScopeTree,
1757 helper: &mut GraphBuildHelper,
1758) {
1759 let Some(resources) = node.child_by_field_name("resources") else {
1760 return;
1761 };
1762
1763 let mut cursor = resources.walk();
1764 for resource in resources.children(&mut cursor) {
1765 if resource.kind() != "resource" {
1766 continue;
1767 }
1768 let name_node = resource.child_by_field_name("name");
1769 let type_node = resource.child_by_field_name("type");
1770 let value_node = resource.child_by_field_name("value");
1771 if let Some(name_node) = name_node {
1772 if type_node.is_none() && value_node.is_none() {
1773 continue;
1774 }
1775 let name = extract_identifier(name_node, content);
1776 if name.is_empty() {
1777 continue;
1778 }
1779
1780 let qualified_var = format!("{}@{}", name, name_node.start_byte());
1781 let span = Span::from_bytes(resource.start_byte(), resource.end_byte());
1782 let var_id = helper.add_variable(&qualified_var, Some(span));
1783 scope_tree.attach_node_id(&name, name_node.start_byte(), var_id);
1784
1785 if let Some(type_node) = type_node {
1786 let type_text = extract_type_name(type_node, content);
1787 if !type_text.is_empty() {
1788 let resolved_type = ast_graph
1789 .import_map
1790 .get(&type_text)
1791 .cloned()
1792 .unwrap_or(type_text);
1793 let type_id = helper.add_class(&resolved_type, None);
1794 helper.add_typeof_edge(var_id, type_id);
1795 }
1796 }
1797 }
1798 }
1799}
1800
1801fn handle_instanceof_pattern_declaration(
1802 node: Node,
1803 content: &[u8],
1804 ast_graph: &ASTGraph,
1805 scope_tree: &mut JavaScopeTree,
1806 helper: &mut GraphBuildHelper,
1807) {
1808 let mut patterns = Vec::new();
1809 collect_pattern_declarations(node, &mut patterns);
1810 for (name_node, type_node) in patterns {
1811 let name = extract_identifier(name_node, content);
1812 if name.is_empty() {
1813 continue;
1814 }
1815 let qualified_var = format!("{}@{}", name, name_node.start_byte());
1816 let span = Span::from_bytes(name_node.start_byte(), name_node.end_byte());
1817 let var_id = helper.add_variable(&qualified_var, Some(span));
1818 scope_tree.attach_node_id(&name, name_node.start_byte(), var_id);
1819
1820 if let Some(type_node) = type_node {
1821 let type_text = extract_type_name(type_node, content);
1822 if !type_text.is_empty() {
1823 let resolved_type = ast_graph
1824 .import_map
1825 .get(&type_text)
1826 .cloned()
1827 .unwrap_or(type_text);
1828 let type_id = helper.add_class(&resolved_type, None);
1829 helper.add_typeof_edge(var_id, type_id);
1830 }
1831 }
1832 }
1833}
1834
1835fn handle_switch_pattern_declaration(
1836 node: Node,
1837 content: &[u8],
1838 ast_graph: &ASTGraph,
1839 scope_tree: &mut JavaScopeTree,
1840 helper: &mut GraphBuildHelper,
1841) {
1842 let mut patterns = Vec::new();
1843 collect_pattern_declarations(node, &mut patterns);
1844 for (name_node, type_node) in patterns {
1845 let name = extract_identifier(name_node, content);
1846 if name.is_empty() {
1847 continue;
1848 }
1849 let qualified_var = format!("{}@{}", name, name_node.start_byte());
1850 let span = Span::from_bytes(name_node.start_byte(), name_node.end_byte());
1851 let var_id = helper.add_variable(&qualified_var, Some(span));
1852 scope_tree.attach_node_id(&name, name_node.start_byte(), var_id);
1853
1854 if let Some(type_node) = type_node {
1855 let type_text = extract_type_name(type_node, content);
1856 if !type_text.is_empty() {
1857 let resolved_type = ast_graph
1858 .import_map
1859 .get(&type_text)
1860 .cloned()
1861 .unwrap_or(type_text);
1862 let type_id = helper.add_class(&resolved_type, None);
1863 helper.add_typeof_edge(var_id, type_id);
1864 }
1865 }
1866 }
1867}
1868
1869fn handle_compact_constructor_parameters(
1870 node: Node,
1871 content: &[u8],
1872 ast_graph: &ASTGraph,
1873 scope_tree: &mut JavaScopeTree,
1874 helper: &mut GraphBuildHelper,
1875) {
1876 use sqry_core::graph::unified::node::NodeKind;
1877
1878 let Some(record_node) = node
1879 .parent()
1880 .and_then(|parent| find_record_declaration(parent))
1881 else {
1882 return;
1883 };
1884
1885 let Some(record_name_node) = record_node.child_by_field_name("name") else {
1886 return;
1887 };
1888 let record_name = extract_identifier(record_name_node, content);
1889 if record_name.is_empty() {
1890 return;
1891 }
1892
1893 let mut components = Vec::new();
1894 collect_record_components_nodes(record_node, &mut components);
1895 for component in components {
1896 let Some(name_node) = component.child_by_field_name("name") else {
1897 continue;
1898 };
1899 let Some(type_node) = component.child_by_field_name("type") else {
1900 continue;
1901 };
1902 let name = extract_identifier(name_node, content);
1903 if name.is_empty() {
1904 continue;
1905 }
1906
1907 let type_text = extract_type_name(type_node, content);
1908 if type_text.is_empty() {
1909 continue;
1910 }
1911 let resolved_type = ast_graph
1912 .import_map
1913 .get(&type_text)
1914 .cloned()
1915 .unwrap_or(type_text);
1916
1917 let qualified_param = format!("{record_name}.<init>::{name}");
1918 let span = Span::from_bytes(component.start_byte(), component.end_byte());
1919 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
1920 scope_tree.attach_node_id(&name, name_node.start_byte(), param_id);
1921
1922 let type_id = helper.add_class(&resolved_type, None);
1923 helper.add_typeof_edge(param_id, type_id);
1924 }
1925}
1926
1927fn collect_pattern_declarations<'a>(
1928 node: Node<'a>,
1929 output: &mut Vec<(Node<'a>, Option<Node<'a>>)>,
1930) {
1931 if node.kind() == "instanceof_expression"
1932 && !node_has_direct_child_kind(node, "type_pattern")
1933 && let Some(name_node) = node.child_by_field_name("name")
1934 {
1935 let type_node = first_type_like_child(node);
1936 output.push((name_node, type_node));
1937 }
1938
1939 if node.kind() == "type_pattern"
1940 && let Some((name_node, type_node)) = typed_pattern_parts(node)
1941 {
1942 output.push((name_node, type_node));
1943 }
1944
1945 if node.kind() == "record_pattern_component"
1946 && let Some((name_node, type_node)) = typed_pattern_parts(node)
1947 {
1948 output.push((name_node, type_node));
1949 }
1950
1951 let mut cursor = node.walk();
1952 for child in node.children(&mut cursor) {
1953 collect_pattern_declarations(child, output);
1954 }
1955}
1956
1957fn node_has_direct_child_kind(node: Node, kind: &str) -> bool {
1958 let mut cursor = node.walk();
1959 node.children(&mut cursor).any(|child| child.kind() == kind)
1960}
1961
1962fn typed_pattern_parts(node: Node) -> Option<(Node, Option<Node>)> {
1963 let mut name_node = None;
1964 let mut type_node = None;
1965 let mut cursor = node.walk();
1966 for child in node.children(&mut cursor) {
1967 if matches!(child.kind(), "identifier" | "_reserved_identifier") {
1968 name_node = Some(child);
1969 } else if matches!(
1970 child.kind(),
1971 "type_identifier" | "scoped_type_identifier" | "generic_type"
1972 ) {
1973 type_node = Some(child);
1974 }
1975 }
1976 name_node.map(|name| (name, type_node))
1977}
1978
1979fn first_type_like_child(node: Node) -> Option<Node> {
1980 let mut cursor = node.walk();
1981 for child in node.children(&mut cursor) {
1982 if matches!(
1983 child.kind(),
1984 "type_identifier" | "scoped_type_identifier" | "generic_type"
1985 ) {
1986 return Some(child);
1987 }
1988 }
1989 None
1990}
1991
1992fn find_record_declaration(node: Node) -> Option<Node> {
1993 if node.kind() == "record_declaration" {
1994 return Some(node);
1995 }
1996 node.parent().and_then(find_record_declaration)
1997}
1998
1999fn collect_record_components_nodes<'a>(node: Node<'a>, output: &mut Vec<Node<'a>>) {
2000 if let Some(parameters) = node.child_by_field_name("parameters") {
2001 let mut cursor = parameters.walk();
2002 for child in parameters.children(&mut cursor) {
2003 if matches!(child.kind(), "formal_parameter" | "record_component") {
2004 output.push(child);
2005 }
2006 }
2007 return;
2008 }
2009
2010 let mut cursor = node.walk();
2011 for child in node.children(&mut cursor) {
2012 if child.kind() == "record_component" {
2013 output.push(child);
2014 }
2015 }
2016}
2017
2018fn process_method_call_unified(
2020 call_node: Node,
2021 content: &[u8],
2022 ast_graph: &ASTGraph,
2023 helper: &mut GraphBuildHelper,
2024) {
2025 let Some(caller_context) = ast_graph.find_enclosing(call_node.start_byte()) else {
2026 return;
2027 };
2028 let Ok(callee_name) = extract_method_invocation_name(call_node, content) else {
2029 return;
2030 };
2031
2032 let callee_qualified =
2033 resolve_callee_qualified(&call_node, content, ast_graph, caller_context, &callee_name);
2034 let caller_method_id = ensure_caller_method(helper, caller_context);
2035 let target_method_id = helper.ensure_method(&callee_qualified, None, false, false);
2036
2037 add_call_edge(helper, caller_method_id, target_method_id, call_node);
2038}
2039
2040fn process_constructor_call_unified(
2042 new_node: Node,
2043 content: &[u8],
2044 ast_graph: &ASTGraph,
2045 helper: &mut GraphBuildHelper,
2046) {
2047 let Some(caller_context) = ast_graph.find_enclosing(new_node.start_byte()) else {
2048 return;
2049 };
2050
2051 let Some(type_node) = new_node.child_by_field_name("type") else {
2052 return;
2053 };
2054
2055 let class_name = extract_type_name(type_node, content);
2056 if class_name.is_empty() {
2057 return;
2058 }
2059
2060 let qualified_class = qualify_constructor_class(&class_name, caller_context);
2061 let constructor_name = format!("{qualified_class}.<init>");
2062
2063 let caller_method_id = ensure_caller_method(helper, caller_context);
2064 let target_method_id = helper.ensure_method(&constructor_name, None, false, false);
2065 add_call_edge(helper, caller_method_id, target_method_id, new_node);
2066}
2067
2068fn count_call_arguments(call_node: Node<'_>) -> u8 {
2069 let Some(args_node) = call_node.child_by_field_name("arguments") else {
2070 return 255;
2071 };
2072 let count = args_node.named_child_count();
2073 if count <= 254 {
2074 u8::try_from(count).unwrap_or(u8::MAX)
2075 } else {
2076 u8::MAX
2077 }
2078}
2079
2080fn process_import_unified(import_node: Node, content: &[u8], helper: &mut GraphBuildHelper) {
2082 let has_asterisk = import_has_wildcard(import_node);
2083 let Some(mut imported_name) = extract_import_name(import_node, content) else {
2084 return;
2085 };
2086 if has_asterisk {
2087 imported_name = format!("{imported_name}.*");
2088 }
2089
2090 let module_id = helper.add_module("<module>", None);
2091 let external_id = helper.add_import(
2092 &imported_name,
2093 Some(Span::from_bytes(
2094 import_node.start_byte(),
2095 import_node.end_byte(),
2096 )),
2097 );
2098
2099 helper.add_import_edge(module_id, external_id);
2100}
2101
2102fn ensure_caller_method(
2103 helper: &mut GraphBuildHelper,
2104 caller_context: &MethodContext,
2105) -> sqry_core::graph::unified::node::NodeId {
2106 helper.ensure_method(
2107 caller_context.qualified_name(),
2108 Some(Span::from_bytes(
2109 caller_context.span.0,
2110 caller_context.span.1,
2111 )),
2112 false,
2113 caller_context.is_static,
2114 )
2115}
2116
2117fn resolve_callee_qualified(
2118 call_node: &Node,
2119 content: &[u8],
2120 ast_graph: &ASTGraph,
2121 caller_context: &MethodContext,
2122 callee_name: &str,
2123) -> String {
2124 if let Some(object_node) = call_node.child_by_field_name("object") {
2125 let object_text = extract_node_text(object_node, content);
2126 return resolve_member_call_target(&object_text, ast_graph, caller_context, callee_name);
2127 }
2128
2129 build_member_symbol(
2130 caller_context.package_name.as_deref(),
2131 &caller_context.class_stack,
2132 callee_name,
2133 )
2134}
2135
2136fn resolve_member_call_target(
2137 object_text: &str,
2138 ast_graph: &ASTGraph,
2139 caller_context: &MethodContext,
2140 callee_name: &str,
2141) -> String {
2142 if object_text.contains('.') {
2143 return format!("{object_text}.{callee_name}");
2144 }
2145 if object_text == "this" {
2146 return build_member_symbol(
2147 caller_context.package_name.as_deref(),
2148 &caller_context.class_stack,
2149 callee_name,
2150 );
2151 }
2152
2153 if let Some(class_name) = caller_context.class_stack.last() {
2155 let qualified_field = format!("{class_name}::{object_text}");
2156 if let Some((field_type, _is_final, _visibility, _is_static)) =
2157 ast_graph.field_types.get(&qualified_field)
2158 {
2159 return format!("{field_type}.{callee_name}");
2160 }
2161 }
2162
2163 if let Some((field_type, _is_final, _visibility, _is_static)) =
2165 ast_graph.field_types.get(object_text)
2166 {
2167 return format!("{field_type}.{callee_name}");
2168 }
2169
2170 if let Some(type_fqn) = ast_graph.import_map.get(object_text) {
2171 return format!("{type_fqn}.{callee_name}");
2172 }
2173
2174 format!("{object_text}.{callee_name}")
2175}
2176
2177fn qualify_constructor_class(class_name: &str, caller_context: &MethodContext) -> String {
2178 if class_name.contains('.') {
2179 class_name.to_string()
2180 } else if let Some(pkg) = caller_context.package_name.as_deref() {
2181 format!("{pkg}.{class_name}")
2182 } else {
2183 class_name.to_string()
2184 }
2185}
2186
2187fn add_call_edge(
2188 helper: &mut GraphBuildHelper,
2189 caller_method_id: sqry_core::graph::unified::node::NodeId,
2190 target_method_id: sqry_core::graph::unified::node::NodeId,
2191 call_node: Node,
2192) {
2193 let argument_count = count_call_arguments(call_node);
2194 let call_span = Span::from_bytes(call_node.start_byte(), call_node.end_byte());
2195 helper.add_call_edge_full_with_span(
2196 caller_method_id,
2197 target_method_id,
2198 argument_count,
2199 false,
2200 vec![call_span],
2201 );
2202}
2203
2204fn import_has_wildcard(import_node: Node) -> bool {
2205 let mut cursor = import_node.walk();
2206 import_node
2207 .children(&mut cursor)
2208 .any(|child| child.kind() == "asterisk")
2209}
2210
2211fn extract_import_name(import_node: Node, content: &[u8]) -> Option<String> {
2212 let mut cursor = import_node.walk();
2213 for child in import_node.children(&mut cursor) {
2214 if child.kind() == "scoped_identifier" || child.kind() == "identifier" {
2215 return Some(extract_full_identifier(child, content));
2216 }
2217 }
2218 None
2219}
2220
2221fn process_inheritance(
2231 class_node: Node,
2232 content: &[u8],
2233 package_name: Option<&str>,
2234 child_class_id: sqry_core::graph::unified::node::NodeId,
2235 helper: &mut GraphBuildHelper,
2236) {
2237 if let Some(superclass_node) = class_node.child_by_field_name("superclass") {
2239 let parent_type_name = extract_type_from_superclass(superclass_node, content);
2241 if !parent_type_name.is_empty() {
2242 let parent_qualified = qualify_type_name(&parent_type_name, package_name);
2244 let parent_id = helper.add_class(&parent_qualified, None);
2245 helper.add_inherits_edge(child_class_id, parent_id);
2246 }
2247 }
2248}
2249
2250fn process_implements(
2256 class_node: Node,
2257 content: &[u8],
2258 package_name: Option<&str>,
2259 class_id: sqry_core::graph::unified::node::NodeId,
2260 helper: &mut GraphBuildHelper,
2261) {
2262 let interfaces_node = class_node
2268 .child_by_field_name("interfaces")
2269 .or_else(|| class_node.child_by_field_name("super_interfaces"));
2270
2271 if let Some(node) = interfaces_node {
2272 extract_interface_types(node, content, package_name, class_id, helper);
2273 return;
2274 }
2275
2276 let mut cursor = class_node.walk();
2278 for child in class_node.children(&mut cursor) {
2279 if child.kind() == "super_interfaces" {
2281 extract_interface_types(child, content, package_name, class_id, helper);
2282 return;
2283 }
2284 }
2285}
2286
2287fn process_interface_extends(
2305 interface_node: Node,
2306 content: &[u8],
2307 package_name: Option<&str>,
2308 interface_id: sqry_core::graph::unified::node::NodeId,
2309 helper: &mut GraphBuildHelper,
2310) {
2311 let mut cursor = interface_node.walk();
2313 for child in interface_node.children(&mut cursor) {
2314 if child.kind() == "extends_interfaces" {
2315 extract_parent_interfaces_for_inherits(
2317 child,
2318 content,
2319 package_name,
2320 interface_id,
2321 helper,
2322 );
2323 return;
2324 }
2325 }
2326}
2327
2328fn extract_parent_interfaces_for_inherits(
2331 extends_node: Node,
2332 content: &[u8],
2333 package_name: Option<&str>,
2334 child_interface_id: sqry_core::graph::unified::node::NodeId,
2335 helper: &mut GraphBuildHelper,
2336) {
2337 let mut cursor = extends_node.walk();
2338 for child in extends_node.children(&mut cursor) {
2339 match child.kind() {
2340 "type_identifier" => {
2341 let type_name = extract_identifier(child, content);
2342 if !type_name.is_empty() {
2343 let parent_qualified = qualify_type_name(&type_name, package_name);
2344 let parent_id = helper.add_interface(&parent_qualified, None);
2345 helper.add_inherits_edge(child_interface_id, parent_id);
2346 }
2347 }
2348 "type_list" => {
2349 let mut type_cursor = child.walk();
2350 for type_child in child.children(&mut type_cursor) {
2351 if let Some(type_name) = extract_type_identifier(type_child, content)
2352 && !type_name.is_empty()
2353 {
2354 let parent_qualified = qualify_type_name(&type_name, package_name);
2355 let parent_id = helper.add_interface(&parent_qualified, None);
2356 helper.add_inherits_edge(child_interface_id, parent_id);
2357 }
2358 }
2359 }
2360 "generic_type" | "scoped_type_identifier" => {
2361 if let Some(type_name) = extract_type_identifier(child, content)
2362 && !type_name.is_empty()
2363 {
2364 let parent_qualified = qualify_type_name(&type_name, package_name);
2365 let parent_id = helper.add_interface(&parent_qualified, None);
2366 helper.add_inherits_edge(child_interface_id, parent_id);
2367 }
2368 }
2369 _ => {}
2370 }
2371 }
2372}
2373
2374fn extract_type_from_superclass(superclass_node: Node, content: &[u8]) -> String {
2376 if superclass_node.kind() == "type_identifier" {
2378 return extract_identifier(superclass_node, content);
2379 }
2380
2381 let mut cursor = superclass_node.walk();
2383 for child in superclass_node.children(&mut cursor) {
2384 if let Some(name) = extract_type_identifier(child, content) {
2385 return name;
2386 }
2387 }
2388
2389 extract_identifier(superclass_node, content)
2391}
2392
2393fn extract_interface_types(
2404 interfaces_node: Node,
2405 content: &[u8],
2406 package_name: Option<&str>,
2407 implementor_id: sqry_core::graph::unified::node::NodeId,
2408 helper: &mut GraphBuildHelper,
2409) {
2410 let mut cursor = interfaces_node.walk();
2412 for child in interfaces_node.children(&mut cursor) {
2413 match child.kind() {
2414 "type_identifier" => {
2416 let type_name = extract_identifier(child, content);
2417 if !type_name.is_empty() {
2418 let interface_qualified = qualify_type_name(&type_name, package_name);
2419 let interface_id = helper.add_interface(&interface_qualified, None);
2420 helper.add_implements_edge(implementor_id, interface_id);
2421 }
2422 }
2423 "type_list" => {
2425 let mut type_cursor = child.walk();
2426 for type_child in child.children(&mut type_cursor) {
2427 if let Some(type_name) = extract_type_identifier(type_child, content)
2428 && !type_name.is_empty()
2429 {
2430 let interface_qualified = qualify_type_name(&type_name, package_name);
2431 let interface_id = helper.add_interface(&interface_qualified, None);
2432 helper.add_implements_edge(implementor_id, interface_id);
2433 }
2434 }
2435 }
2436 "generic_type" | "scoped_type_identifier" => {
2438 if let Some(type_name) = extract_type_identifier(child, content)
2439 && !type_name.is_empty()
2440 {
2441 let interface_qualified = qualify_type_name(&type_name, package_name);
2442 let interface_id = helper.add_interface(&interface_qualified, None);
2443 helper.add_implements_edge(implementor_id, interface_id);
2444 }
2445 }
2446 _ => {}
2447 }
2448 }
2449}
2450
2451fn extract_type_identifier(node: Node, content: &[u8]) -> Option<String> {
2453 match node.kind() {
2454 "type_identifier" => Some(extract_identifier(node, content)),
2455 "generic_type" => {
2456 if let Some(name_node) = node.child_by_field_name("name") {
2458 Some(extract_identifier(name_node, content))
2459 } else {
2460 let mut cursor = node.walk();
2462 for child in node.children(&mut cursor) {
2463 if child.kind() == "type_identifier" {
2464 return Some(extract_identifier(child, content));
2465 }
2466 }
2467 None
2468 }
2469 }
2470 "scoped_type_identifier" => {
2471 Some(extract_full_identifier(node, content))
2473 }
2474 _ => None,
2475 }
2476}
2477
2478fn qualify_type_name(type_name: &str, package_name: Option<&str>) -> String {
2480 if type_name.contains('.') {
2482 return type_name.to_string();
2483 }
2484
2485 if let Some(pkg) = package_name {
2487 format!("{pkg}.{type_name}")
2488 } else {
2489 type_name.to_string()
2490 }
2491}
2492
2493#[allow(clippy::type_complexity)]
2502fn extract_field_and_import_types(
2503 node: Node,
2504 content: &[u8],
2505) -> (
2506 HashMap<String, (String, bool, Option<sqry_core::schema::Visibility>, bool)>,
2507 HashMap<String, String>,
2508) {
2509 let import_map = extract_import_map(node, content);
2511
2512 let mut field_types = HashMap::new();
2513 let mut class_stack = Vec::new();
2514 extract_field_types_recursive(
2515 node,
2516 content,
2517 &import_map,
2518 &mut field_types,
2519 &mut class_stack,
2520 );
2521
2522 (field_types, import_map)
2523}
2524
2525fn extract_import_map(node: Node, content: &[u8]) -> HashMap<String, String> {
2527 let mut import_map = HashMap::new();
2528 collect_import_map_recursive(node, content, &mut import_map);
2529 import_map
2530}
2531
2532fn collect_import_map_recursive(
2533 node: Node,
2534 content: &[u8],
2535 import_map: &mut HashMap<String, String>,
2536) {
2537 if node.kind() == "import_declaration" {
2538 let full_path = node.utf8_text(content).unwrap_or("");
2542
2543 if let Some(path_start) = full_path.find("import ") {
2546 let after_import = &full_path[path_start + 7..].trim();
2547 if let Some(path_end) = after_import.find(';') {
2548 let import_path = &after_import[..path_end].trim();
2549
2550 if let Some(simple_name) = import_path.rsplit('.').next() {
2552 import_map.insert(simple_name.to_string(), (*import_path).to_string());
2553 }
2554 }
2555 }
2556 }
2557
2558 let mut cursor = node.walk();
2560 for child in node.children(&mut cursor) {
2561 collect_import_map_recursive(child, content, import_map);
2562 }
2563}
2564
2565fn extract_field_types_recursive(
2566 node: Node,
2567 content: &[u8],
2568 import_map: &HashMap<String, String>,
2569 field_types: &mut HashMap<String, (String, bool, Option<sqry_core::schema::Visibility>, bool)>,
2570 class_stack: &mut Vec<String>,
2571) {
2572 if matches!(
2574 node.kind(),
2575 "class_declaration" | "interface_declaration" | "enum_declaration" | "record_declaration"
2576 ) && let Some(name_node) = node.child_by_field_name("name")
2577 {
2578 let class_name = extract_identifier(name_node, content);
2579 class_stack.push(class_name);
2580
2581 if let Some(body_node) = node.child_by_field_name("body") {
2583 let mut cursor = body_node.walk();
2584 for child in body_node.children(&mut cursor) {
2585 extract_field_types_recursive(child, content, import_map, field_types, class_stack);
2586 }
2587 }
2588
2589 class_stack.pop();
2591 return; }
2593
2594 if node.kind() == "field_declaration" {
2601 let is_final = has_modifier(node, "final", content);
2603 let is_static = has_modifier(node, "static", content);
2604
2605 let visibility = if has_modifier(node, "public", content) {
2608 Some(sqry_core::schema::Visibility::Public)
2609 } else {
2610 Some(sqry_core::schema::Visibility::Private)
2612 };
2613
2614 if let Some(type_node) = node.child_by_field_name("type") {
2616 let type_text = extract_type_name_internal(type_node, content);
2617 if !type_text.is_empty() {
2618 let resolved_type = import_map
2620 .get(&type_text)
2621 .cloned()
2622 .unwrap_or(type_text.clone());
2623
2624 let mut cursor = node.walk();
2626 for child in node.children(&mut cursor) {
2627 if child.kind() == "variable_declarator"
2628 && let Some(name_node) = child.child_by_field_name("name")
2629 {
2630 let field_name = extract_identifier(name_node, content);
2631
2632 let qualified_field = if class_stack.is_empty() {
2635 field_name
2636 } else {
2637 let class_path = class_stack.join("::");
2638 format!("{class_path}::{field_name}")
2639 };
2640
2641 field_types.insert(
2642 qualified_field,
2643 (resolved_type.clone(), is_final, visibility, is_static),
2644 );
2645 }
2646 }
2647 }
2648 }
2649 }
2650
2651 let mut cursor = node.walk();
2653 for child in node.children(&mut cursor) {
2654 extract_field_types_recursive(child, content, import_map, field_types, class_stack);
2655 }
2656}
2657
2658fn extract_type_name_internal(type_node: Node, content: &[u8]) -> String {
2660 match type_node.kind() {
2661 "generic_type" => {
2662 if let Some(name_node) = type_node.child_by_field_name("name") {
2664 extract_identifier(name_node, content)
2665 } else {
2666 extract_identifier(type_node, content)
2667 }
2668 }
2669 "scoped_type_identifier" => {
2670 extract_full_identifier(type_node, content)
2672 }
2673 _ => extract_identifier(type_node, content),
2674 }
2675}
2676
2677fn extract_identifier(node: Node, content: &[u8]) -> String {
2682 node.utf8_text(content).unwrap_or("").to_string()
2683}
2684
2685fn extract_node_text(node: Node, content: &[u8]) -> String {
2686 node.utf8_text(content).unwrap_or("").to_string()
2687}
2688
2689fn extract_full_identifier(node: Node, content: &[u8]) -> String {
2690 node.utf8_text(content).unwrap_or("").to_string()
2691}
2692
2693fn first_child_of_kind<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
2694 let mut cursor = node.walk();
2695 node.children(&mut cursor)
2696 .find(|&child| child.kind() == kind)
2697}
2698
2699fn extract_method_invocation_name(call_node: Node, content: &[u8]) -> GraphResult<String> {
2700 if let Some(name_node) = call_node.child_by_field_name("name") {
2702 Ok(extract_identifier(name_node, content))
2703 } else {
2704 let mut cursor = call_node.walk();
2706 for child in call_node.children(&mut cursor) {
2707 if child.kind() == "identifier" {
2708 return Ok(extract_identifier(child, content));
2709 }
2710 }
2711
2712 Err(GraphBuilderError::ParseError {
2713 span: Span::from_bytes(call_node.start_byte(), call_node.end_byte()),
2714 reason: "Method invocation missing name".into(),
2715 })
2716 }
2717}
2718
2719fn extract_type_name(type_node: Node, content: &[u8]) -> String {
2720 match type_node.kind() {
2722 "generic_type" => {
2723 if let Some(name_node) = type_node.child_by_field_name("name") {
2725 extract_identifier(name_node, content)
2726 } else {
2727 extract_identifier(type_node, content)
2728 }
2729 }
2730 "scoped_type_identifier" => {
2731 extract_full_identifier(type_node, content)
2733 }
2734 _ => extract_identifier(type_node, content),
2735 }
2736}
2737
2738fn extract_full_return_type(type_node: Node, content: &[u8]) -> String {
2741 type_node.utf8_text(content).unwrap_or("").to_string()
2744}
2745
2746fn has_modifier(node: Node, modifier: &str, content: &[u8]) -> bool {
2747 let mut cursor = node.walk();
2748 for child in node.children(&mut cursor) {
2749 if child.kind() == "modifiers" {
2750 let mut mod_cursor = child.walk();
2751 for modifier_child in child.children(&mut mod_cursor) {
2752 if extract_identifier(modifier_child, content) == modifier {
2753 return true;
2754 }
2755 }
2756 }
2757 }
2758 false
2759}
2760
2761#[allow(clippy::unnecessary_wraps)]
2764fn extract_visibility(node: Node, content: &[u8]) -> Option<String> {
2765 if has_modifier(node, "public", content) {
2766 Some("public".to_string())
2767 } else if has_modifier(node, "private", content) {
2768 Some("private".to_string())
2769 } else if has_modifier(node, "protected", content) {
2770 Some("protected".to_string())
2771 } else {
2772 Some("package-private".to_string())
2774 }
2775}
2776
2777fn is_public(node: Node, content: &[u8]) -> bool {
2783 has_modifier(node, "public", content)
2784}
2785
2786fn is_private(node: Node, content: &[u8]) -> bool {
2788 has_modifier(node, "private", content)
2789}
2790
2791fn export_from_file_module(
2793 helper: &mut GraphBuildHelper,
2794 exported: sqry_core::graph::unified::node::NodeId,
2795) {
2796 let module_id = helper.add_module(FILE_MODULE_NAME, None);
2797 helper.add_export_edge(module_id, exported);
2798}
2799
2800fn process_class_member_exports(
2805 body_node: Node,
2806 content: &[u8],
2807 class_qualified_name: &str,
2808 helper: &mut GraphBuildHelper,
2809 is_interface: bool,
2810) {
2811 for i in 0..body_node.child_count() {
2812 #[allow(clippy::cast_possible_truncation)]
2813 if let Some(child) = body_node.child(i as u32) {
2815 match child.kind() {
2816 "method_declaration" => {
2817 let should_export = if is_interface {
2820 !is_private(child, content)
2822 } else {
2823 is_public(child, content)
2825 };
2826
2827 if should_export && let Some(name_node) = child.child_by_field_name("name") {
2828 let method_name = extract_identifier(name_node, content);
2829 let qualified_name = format!("{class_qualified_name}.{method_name}");
2830 let span = Span::from_bytes(child.start_byte(), child.end_byte());
2831 let is_static = has_modifier(child, "static", content);
2832 let method_id =
2833 helper.add_method(&qualified_name, Some(span), false, is_static);
2834 export_from_file_module(helper, method_id);
2835 }
2836 }
2837 "constructor_declaration" => {
2838 if is_public(child, content) {
2839 let qualified_name = format!("{class_qualified_name}.<init>");
2840 let span = Span::from_bytes(child.start_byte(), child.end_byte());
2841 let method_id =
2842 helper.add_method(&qualified_name, Some(span), false, false);
2843 export_from_file_module(helper, method_id);
2844 }
2845 }
2846 "field_declaration" => {
2847 if is_public(child, content) {
2848 let mut cursor = child.walk();
2850 for field_child in child.children(&mut cursor) {
2851 if field_child.kind() == "variable_declarator"
2852 && let Some(name_node) = field_child.child_by_field_name("name")
2853 {
2854 let field_name = extract_identifier(name_node, content);
2855 let qualified_name = format!("{class_qualified_name}.{field_name}");
2856 let span = Span::from_bytes(
2857 field_child.start_byte(),
2858 field_child.end_byte(),
2859 );
2860
2861 let is_final = has_modifier(child, "final", content);
2863 let field_id = if is_final {
2864 helper.add_constant(&qualified_name, Some(span))
2865 } else {
2866 helper.add_variable(&qualified_name, Some(span))
2867 };
2868 export_from_file_module(helper, field_id);
2869 }
2870 }
2871 }
2872 }
2873 "constant_declaration" => {
2874 let mut cursor = child.walk();
2876 for const_child in child.children(&mut cursor) {
2877 if const_child.kind() == "variable_declarator"
2878 && let Some(name_node) = const_child.child_by_field_name("name")
2879 {
2880 let const_name = extract_identifier(name_node, content);
2881 let qualified_name = format!("{class_qualified_name}.{const_name}");
2882 let span =
2883 Span::from_bytes(const_child.start_byte(), const_child.end_byte());
2884 let const_id = helper.add_constant(&qualified_name, Some(span));
2885 export_from_file_module(helper, const_id);
2886 }
2887 }
2888 }
2889 "enum_constant" => {
2890 if let Some(name_node) = child.child_by_field_name("name") {
2892 let const_name = extract_identifier(name_node, content);
2893 let qualified_name = format!("{class_qualified_name}.{const_name}");
2894 let span = Span::from_bytes(child.start_byte(), child.end_byte());
2895 let const_id = helper.add_constant(&qualified_name, Some(span));
2896 export_from_file_module(helper, const_id);
2897 }
2898 }
2899 _ => {}
2900 }
2901 }
2902 }
2903}
2904
2905fn detect_ffi_imports(node: Node, content: &[u8]) -> (bool, bool) {
2912 let mut has_jna = false;
2913 let mut has_panama = false;
2914
2915 detect_ffi_imports_recursive(node, content, &mut has_jna, &mut has_panama);
2916
2917 (has_jna, has_panama)
2918}
2919
2920fn detect_ffi_imports_recursive(
2921 node: Node,
2922 content: &[u8],
2923 has_jna: &mut bool,
2924 has_panama: &mut bool,
2925) {
2926 if node.kind() == "import_declaration" {
2927 let import_text = node.utf8_text(content).unwrap_or("");
2928
2929 if import_text.contains("com.sun.jna") || import_text.contains("net.java.dev.jna") {
2931 *has_jna = true;
2932 }
2933
2934 if import_text.contains("java.lang.foreign") {
2936 *has_panama = true;
2937 }
2938 }
2939
2940 let mut cursor = node.walk();
2941 for child in node.children(&mut cursor) {
2942 detect_ffi_imports_recursive(child, content, has_jna, has_panama);
2943 }
2944}
2945
2946fn find_jna_library_interfaces(node: Node, content: &[u8]) -> Vec<String> {
2949 let mut jna_interfaces = Vec::new();
2950 find_jna_library_interfaces_recursive(node, content, &mut jna_interfaces);
2951 jna_interfaces
2952}
2953
2954fn find_jna_library_interfaces_recursive(
2955 node: Node,
2956 content: &[u8],
2957 jna_interfaces: &mut Vec<String>,
2958) {
2959 if node.kind() == "interface_declaration" {
2960 if let Some(name_node) = node.child_by_field_name("name") {
2962 let interface_name = extract_identifier(name_node, content);
2963
2964 let mut cursor = node.walk();
2966 for child in node.children(&mut cursor) {
2967 if child.kind() == "extends_interfaces" {
2968 let extends_text = child.utf8_text(content).unwrap_or("");
2969 if extends_text.contains("Library") {
2971 jna_interfaces.push(interface_name.clone());
2972 }
2973 }
2974 }
2975 }
2976 }
2977
2978 let mut cursor = node.walk();
2979 for child in node.children(&mut cursor) {
2980 find_jna_library_interfaces_recursive(child, content, jna_interfaces);
2981 }
2982}
2983
2984fn build_ffi_call_edge(
2987 call_node: Node,
2988 content: &[u8],
2989 caller_context: &MethodContext,
2990 ast_graph: &ASTGraph,
2991 helper: &mut GraphBuildHelper,
2992) -> bool {
2993 let Ok(method_name) = extract_method_invocation_name(call_node, content) else {
2995 return false;
2996 };
2997
2998 if ast_graph.has_jna_import && is_jna_native_load(call_node, content, &method_name) {
3000 let library_name = extract_jna_library_name(call_node, content);
3001 build_jna_native_load_edge(caller_context, &library_name, call_node, helper);
3002 return true;
3003 }
3004
3005 if ast_graph.has_jna_import
3007 && let Some(object_node) = call_node.child_by_field_name("object")
3008 {
3009 let object_text = extract_node_text(object_node, content);
3010
3011 let field_type = if let Some(class_name) = caller_context.class_stack.last() {
3013 let qualified_field = format!("{class_name}::{object_text}");
3014 ast_graph
3015 .field_types
3016 .get(&qualified_field)
3017 .or_else(|| ast_graph.field_types.get(&object_text))
3018 } else {
3019 ast_graph.field_types.get(&object_text)
3020 };
3021
3022 if let Some((type_name, _is_final, _visibility, _is_static)) = field_type {
3024 let simple_type = simple_type_name(type_name);
3025 if ast_graph.jna_library_interfaces.contains(&simple_type) {
3026 build_jna_method_call_edge(
3027 caller_context,
3028 &simple_type,
3029 &method_name,
3030 call_node,
3031 helper,
3032 );
3033 return true;
3034 }
3035 }
3036 }
3037
3038 if ast_graph.has_panama_import {
3040 if let Some(object_node) = call_node.child_by_field_name("object") {
3041 let object_text = extract_node_text(object_node, content);
3042
3043 if object_text == "Linker" && method_name == "nativeLinker" {
3045 build_panama_linker_edge(caller_context, call_node, helper);
3046 return true;
3047 }
3048
3049 if object_text == "SymbolLookup" && method_name == "libraryLookup" {
3051 let library_name = extract_first_string_arg(call_node, content);
3052 build_panama_library_lookup_edge(caller_context, &library_name, call_node, helper);
3053 return true;
3054 }
3055
3056 if method_name == "invokeExact" || method_name == "invoke" {
3058 if is_potential_panama_invoke(call_node, content) {
3061 build_panama_invoke_edge(caller_context, &method_name, call_node, helper);
3062 return true;
3063 }
3064 }
3065 }
3066
3067 if method_name == "nativeLinker" {
3069 let full_text = call_node.utf8_text(content).unwrap_or("");
3070 if full_text.contains("Linker") {
3071 build_panama_linker_edge(caller_context, call_node, helper);
3072 return true;
3073 }
3074 }
3075 }
3076
3077 false
3078}
3079
3080fn is_jna_native_load(call_node: Node, content: &[u8], method_name: &str) -> bool {
3082 if method_name != "load" && method_name != "loadLibrary" {
3083 return false;
3084 }
3085
3086 if let Some(object_node) = call_node.child_by_field_name("object") {
3087 let object_text = extract_node_text(object_node, content);
3088 return object_text == "Native" || object_text == "com.sun.jna.Native";
3089 }
3090
3091 false
3092}
3093
3094fn extract_jna_library_name(call_node: Node, content: &[u8]) -> String {
3097 if let Some(args_node) = call_node.child_by_field_name("arguments") {
3098 let mut cursor = args_node.walk();
3099 for child in args_node.children(&mut cursor) {
3100 if child.kind() == "string_literal" {
3101 let text = child.utf8_text(content).unwrap_or("\"unknown\"");
3102 return text.trim_matches('"').to_string();
3104 }
3105 }
3106 }
3107 "unknown".to_string()
3108}
3109
3110fn extract_first_string_arg(call_node: Node, content: &[u8]) -> String {
3112 if let Some(args_node) = call_node.child_by_field_name("arguments") {
3113 let mut cursor = args_node.walk();
3114 for child in args_node.children(&mut cursor) {
3115 if child.kind() == "string_literal" {
3116 let text = child.utf8_text(content).unwrap_or("\"unknown\"");
3117 return text.trim_matches('"').to_string();
3118 }
3119 }
3120 }
3121 "unknown".to_string()
3122}
3123
3124fn is_potential_panama_invoke(call_node: Node, content: &[u8]) -> bool {
3126 if let Some(object_node) = call_node.child_by_field_name("object") {
3128 let object_text = extract_node_text(object_node, content);
3129 let lower = object_text.to_lowercase();
3131 return lower.contains("handle")
3132 || lower.contains("downcall")
3133 || lower.contains("mh")
3134 || lower.contains("foreign");
3135 }
3136 false
3137}
3138
3139fn simple_type_name(type_name: &str) -> String {
3141 type_name
3142 .rsplit('.')
3143 .next()
3144 .unwrap_or(type_name)
3145 .to_string()
3146}
3147
3148fn build_jna_native_load_edge(
3150 caller_context: &MethodContext,
3151 library_name: &str,
3152 call_node: Node,
3153 helper: &mut GraphBuildHelper,
3154) {
3155 let caller_id = helper.ensure_method(
3156 caller_context.qualified_name(),
3157 Some(Span::from_bytes(
3158 caller_context.span.0,
3159 caller_context.span.1,
3160 )),
3161 false,
3162 caller_context.is_static,
3163 );
3164
3165 let target_name = format!("native::{library_name}");
3166 let target_id = helper.add_function(
3167 &target_name,
3168 Some(Span::from_bytes(
3169 call_node.start_byte(),
3170 call_node.end_byte(),
3171 )),
3172 false,
3173 false,
3174 );
3175
3176 helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3177}
3178
3179fn build_jna_method_call_edge(
3181 caller_context: &MethodContext,
3182 interface_name: &str,
3183 method_name: &str,
3184 call_node: Node,
3185 helper: &mut GraphBuildHelper,
3186) {
3187 let caller_id = helper.ensure_method(
3188 caller_context.qualified_name(),
3189 Some(Span::from_bytes(
3190 caller_context.span.0,
3191 caller_context.span.1,
3192 )),
3193 false,
3194 caller_context.is_static,
3195 );
3196
3197 let target_name = format!("native::{interface_name}::{method_name}");
3198 let target_id = helper.add_function(
3199 &target_name,
3200 Some(Span::from_bytes(
3201 call_node.start_byte(),
3202 call_node.end_byte(),
3203 )),
3204 false,
3205 false,
3206 );
3207
3208 helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3209}
3210
3211fn build_panama_linker_edge(
3213 caller_context: &MethodContext,
3214 call_node: Node,
3215 helper: &mut GraphBuildHelper,
3216) {
3217 let caller_id = helper.ensure_method(
3218 caller_context.qualified_name(),
3219 Some(Span::from_bytes(
3220 caller_context.span.0,
3221 caller_context.span.1,
3222 )),
3223 false,
3224 caller_context.is_static,
3225 );
3226
3227 let target_name = "native::panama::nativeLinker";
3228 let target_id = helper.add_function(
3229 target_name,
3230 Some(Span::from_bytes(
3231 call_node.start_byte(),
3232 call_node.end_byte(),
3233 )),
3234 false,
3235 false,
3236 );
3237
3238 helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3239}
3240
3241fn build_panama_library_lookup_edge(
3243 caller_context: &MethodContext,
3244 library_name: &str,
3245 call_node: Node,
3246 helper: &mut GraphBuildHelper,
3247) {
3248 let caller_id = helper.ensure_method(
3249 caller_context.qualified_name(),
3250 Some(Span::from_bytes(
3251 caller_context.span.0,
3252 caller_context.span.1,
3253 )),
3254 false,
3255 caller_context.is_static,
3256 );
3257
3258 let target_name = format!("native::panama::{library_name}");
3259 let target_id = helper.add_function(
3260 &target_name,
3261 Some(Span::from_bytes(
3262 call_node.start_byte(),
3263 call_node.end_byte(),
3264 )),
3265 false,
3266 false,
3267 );
3268
3269 helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3270}
3271
3272fn build_panama_invoke_edge(
3274 caller_context: &MethodContext,
3275 method_name: &str,
3276 call_node: Node,
3277 helper: &mut GraphBuildHelper,
3278) {
3279 let caller_id = helper.ensure_method(
3280 caller_context.qualified_name(),
3281 Some(Span::from_bytes(
3282 caller_context.span.0,
3283 caller_context.span.1,
3284 )),
3285 false,
3286 caller_context.is_static,
3287 );
3288
3289 let target_name = format!("native::panama::{method_name}");
3290 let target_id = helper.add_function(
3291 &target_name,
3292 Some(Span::from_bytes(
3293 call_node.start_byte(),
3294 call_node.end_byte(),
3295 )),
3296 false,
3297 false,
3298 );
3299
3300 helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3301}
3302
3303fn build_jni_native_method_edge(method_context: &MethodContext, helper: &mut GraphBuildHelper) {
3306 let method_id = helper.ensure_method(
3308 method_context.qualified_name(),
3309 Some(Span::from_bytes(
3310 method_context.span.0,
3311 method_context.span.1,
3312 )),
3313 false,
3314 method_context.is_static,
3315 );
3316
3317 let native_target = format!("native::jni::{}", method_context.qualified_name());
3320 let target_id = helper.add_function(&native_target, None, false, false);
3321
3322 helper.add_ffi_edge(method_id, target_id, FfiConvention::C);
3323}
3324
3325fn extract_spring_route_info(method_node: Node, content: &[u8]) -> Option<(String, String)> {
3339 let mut cursor = method_node.walk();
3341 let modifiers_node = method_node
3342 .children(&mut cursor)
3343 .find(|child| child.kind() == "modifiers")?;
3344
3345 let mut mod_cursor = modifiers_node.walk();
3347 for annotation_node in modifiers_node.children(&mut mod_cursor) {
3348 if annotation_node.kind() != "annotation" {
3349 continue;
3350 }
3351
3352 let Some(annotation_name) = extract_annotation_name(annotation_node, content) else {
3354 continue;
3355 };
3356
3357 let http_method: String = match annotation_name.as_str() {
3359 "GetMapping" => "GET".to_string(),
3360 "PostMapping" => "POST".to_string(),
3361 "PutMapping" => "PUT".to_string(),
3362 "DeleteMapping" => "DELETE".to_string(),
3363 "PatchMapping" => "PATCH".to_string(),
3364 "RequestMapping" => {
3365 extract_request_mapping_method(annotation_node, content)
3367 .unwrap_or_else(|| "GET".to_string())
3368 }
3369 _ => continue,
3370 };
3371
3372 let Some(path) = extract_annotation_path(annotation_node, content) else {
3374 continue;
3375 };
3376
3377 return Some((http_method, path));
3378 }
3379
3380 None
3381}
3382
3383fn extract_annotation_name(annotation_node: Node, content: &[u8]) -> Option<String> {
3388 let mut cursor = annotation_node.walk();
3389 for child in annotation_node.children(&mut cursor) {
3390 match child.kind() {
3391 "identifier" => {
3392 return Some(extract_identifier(child, content));
3393 }
3394 "scoped_identifier" => {
3395 let full_text = extract_identifier(child, content);
3398 return full_text.rsplit('.').next().map(String::from);
3399 }
3400 _ => {}
3401 }
3402 }
3403 None
3404}
3405
3406fn extract_annotation_path(annotation_node: Node, content: &[u8]) -> Option<String> {
3413 let mut cursor = annotation_node.walk();
3415 let args_node = annotation_node
3416 .children(&mut cursor)
3417 .find(|child| child.kind() == "annotation_argument_list")?;
3418
3419 let mut args_cursor = args_node.walk();
3421 for arg_child in args_node.children(&mut args_cursor) {
3422 match arg_child.kind() {
3423 "string_literal" => {
3425 return extract_string_content(arg_child, content);
3426 }
3427 "element_value_pair" => {
3429 if let Some(path) = extract_path_from_element_value_pair(arg_child, content) {
3430 return Some(path);
3431 }
3432 }
3433 _ => {}
3434 }
3435 }
3436
3437 None
3438}
3439
3440fn extract_request_mapping_method(annotation_node: Node, content: &[u8]) -> Option<String> {
3448 let mut cursor = annotation_node.walk();
3450 let args_node = annotation_node
3451 .children(&mut cursor)
3452 .find(|child| child.kind() == "annotation_argument_list")?;
3453
3454 let mut args_cursor = args_node.walk();
3456 for arg_child in args_node.children(&mut args_cursor) {
3457 if arg_child.kind() != "element_value_pair" {
3458 continue;
3459 }
3460
3461 let Some(key_node) = arg_child.child_by_field_name("key") else {
3463 continue;
3464 };
3465 let key_text = extract_identifier(key_node, content);
3466 if key_text != "method" {
3467 continue;
3468 }
3469
3470 let Some(value_node) = arg_child.child_by_field_name("value") else {
3472 continue;
3473 };
3474 let value_text = extract_identifier(value_node, content);
3475
3476 if let Some(method) = value_text.rsplit('.').next() {
3478 let method_upper = method.to_uppercase();
3479 if matches!(
3480 method_upper.as_str(),
3481 "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS"
3482 ) {
3483 return Some(method_upper);
3484 }
3485 }
3486 }
3487
3488 None
3489}
3490
3491fn extract_path_from_element_value_pair(pair_node: Node, content: &[u8]) -> Option<String> {
3495 let key_node = pair_node.child_by_field_name("key")?;
3496 let key_text = extract_identifier(key_node, content);
3497
3498 if key_text != "path" && key_text != "value" {
3500 return None;
3501 }
3502
3503 let value_node = pair_node.child_by_field_name("value")?;
3504 if value_node.kind() == "string_literal" {
3505 return extract_string_content(value_node, content);
3506 }
3507
3508 None
3509}
3510
3511fn extract_class_request_mapping_path(method_node: Node, content: &[u8]) -> Option<String> {
3528 let mut current = method_node.parent()?;
3530 loop {
3531 if current.kind() == "class_declaration" {
3532 break;
3533 }
3534 current = current.parent()?;
3535 }
3536
3537 let mut cursor = current.walk();
3539 let modifiers = current
3540 .children(&mut cursor)
3541 .find(|child| child.kind() == "modifiers")?;
3542
3543 let mut mod_cursor = modifiers.walk();
3544 for annotation in modifiers.children(&mut mod_cursor) {
3545 if annotation.kind() != "annotation" {
3546 continue;
3547 }
3548 let Some(name) = extract_annotation_name(annotation, content) else {
3549 continue;
3550 };
3551 if name == "RequestMapping" {
3552 return extract_annotation_path(annotation, content);
3553 }
3554 }
3555
3556 None
3557}
3558
3559fn extract_string_content(string_node: Node, content: &[u8]) -> Option<String> {
3563 let text = string_node.utf8_text(content).ok()?;
3564 let trimmed = text.trim();
3565
3566 if trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() >= 2 {
3568 Some(trimmed[1..trimmed.len() - 1].to_string())
3569 } else {
3570 None
3571 }
3572}
3573
3574fn process_type_parameter_declarations(
3599 decl_node: Node,
3600 content: &[u8],
3601 parent_qualified_name: &str,
3602 helper: &mut GraphBuildHelper,
3603) {
3604 let Some(params_node) = decl_node.child_by_field_name("type_parameters") else {
3605 return;
3606 };
3607
3608 let mut cursor = params_node.walk();
3609 for param_node in params_node.children(&mut cursor) {
3610 if param_node.kind() != "type_parameter" {
3611 continue;
3612 }
3613
3614 let Some(name_node) = first_type_parameter_name_node(param_node) else {
3619 continue;
3620 };
3621 let Ok(param_name) = name_node.utf8_text(content) else {
3622 continue;
3623 };
3624
3625 let qualified_param = format!("{parent_qualified_name}.{param_name}");
3626 let span = Span::from_bytes(name_node.start_byte(), name_node.end_byte());
3627 let param_id = helper.add_type(&qualified_param, Some(span));
3632
3633 if let Some(bound_node) = param_node
3636 .children(&mut param_node.walk())
3637 .find(|c| c.kind() == "type_bound")
3638 {
3639 emit_type_bound_constraints(bound_node, content, param_id, helper);
3640 }
3641 }
3642}
3643
3644fn first_type_parameter_name_node(param_node: Node<'_>) -> Option<Node<'_>> {
3649 let mut cursor = param_node.walk();
3650 for child in param_node.children(&mut cursor) {
3651 if matches!(child.kind(), "type_identifier" | "identifier") {
3652 return Some(child);
3653 }
3654 }
3655 None
3656}
3657
3658fn emit_type_bound_constraints(
3669 bound_node: Node,
3670 content: &[u8],
3671 param_id: sqry_core::graph::unified::node::NodeId,
3672 helper: &mut GraphBuildHelper,
3673) {
3674 let mut cursor = bound_node.walk();
3675 for child in bound_node.children(&mut cursor) {
3676 if !child.is_named() {
3680 continue;
3681 }
3682 let bound_name = extract_bound_type_base_name(child, content);
3683 if bound_name.is_empty() {
3684 continue;
3685 }
3686 let constraint_id = helper.add_type(&bound_name, None);
3687 helper.add_typeof_edge_with_context(
3688 param_id,
3689 constraint_id,
3690 Some(TypeOfContext::Constraint),
3691 None,
3692 None,
3693 );
3694 }
3695}
3696
3697fn extract_bound_type_base_name(type_node: Node, content: &[u8]) -> String {
3713 match type_node.kind() {
3714 "generic_type" => {
3715 let mut cursor = type_node.walk();
3716 for child in type_node.children(&mut cursor) {
3717 if matches!(child.kind(), "type_identifier" | "scoped_type_identifier") {
3718 return extract_bound_type_base_name(child, content);
3719 }
3720 }
3721 extract_identifier(type_node, content)
3722 }
3723 "scoped_type_identifier" => extract_full_identifier(type_node, content),
3724 _ => extract_identifier(type_node, content),
3725 }
3726}