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::{GraphBuilder, GraphBuilderError, GraphResult, Language, Span};
9use tree_sitter::{Node, Tree};
10
11const DEFAULT_SCOPE_DEPTH: usize = 4;
12
13const FILE_MODULE_NAME: &str = "<file_module>";
16
17#[derive(Debug, Clone, Copy)]
38pub struct JavaGraphBuilder {
39 max_scope_depth: usize,
40}
41
42impl Default for JavaGraphBuilder {
43 fn default() -> Self {
44 Self {
45 max_scope_depth: DEFAULT_SCOPE_DEPTH,
46 }
47 }
48}
49
50impl JavaGraphBuilder {
51 #[must_use]
52 pub fn new(max_scope_depth: usize) -> Self {
53 Self { max_scope_depth }
54 }
55}
56
57impl GraphBuilder for JavaGraphBuilder {
58 fn build_graph(
59 &self,
60 tree: &Tree,
61 content: &[u8],
62 file: &Path,
63 staging: &mut StagingGraph,
64 ) -> GraphResult<()> {
65 let mut helper = GraphBuildHelper::new(staging, file, Language::Java);
66
67 let ast_graph = ASTGraph::from_tree(tree, content, self.max_scope_depth);
69 let mut scope_tree = local_scopes::build(tree.root_node(), content)?;
70
71 for context in ast_graph.contexts() {
73 let qualified_name = context.qualified_name();
74 let span = Span::from_bytes(context.span.0, context.span.1);
75
76 if context.is_constructor {
77 helper.add_method_with_visibility(
78 qualified_name,
79 Some(span),
80 false,
81 false,
82 context.visibility.as_deref(),
83 );
84 } else {
85 helper.add_method_with_signature(
87 qualified_name,
88 Some(span),
89 false,
90 context.is_static,
91 context.visibility.as_deref(),
92 context.return_type.as_deref(),
93 );
94
95 if context.is_native {
97 build_jni_native_method_edge(context, &mut helper);
98 }
99 }
100 }
101
102 add_field_typeof_edges(&ast_graph, &mut helper);
104
105 let root = tree.root_node();
107 walk_tree_for_edges(
108 root,
109 content,
110 &ast_graph,
111 &mut scope_tree,
112 &mut helper,
113 tree,
114 )?;
115
116 Ok(())
117 }
118
119 fn language(&self) -> Language {
120 Language::Java
121 }
122}
123
124#[derive(Debug)]
129struct ASTGraph {
130 contexts: Vec<MethodContext>,
131 field_types: HashMap<String, (String, bool, Option<sqry_core::schema::Visibility>, bool)>,
141 import_map: HashMap<String, String>,
145 has_jna_import: bool,
147 has_panama_import: bool,
149 jna_library_interfaces: Vec<String>,
151}
152
153impl ASTGraph {
154 fn from_tree(tree: &Tree, content: &[u8], max_depth: usize) -> Self {
155 let package_name = PackageResolver::package_from_ast(tree, content);
157
158 let mut contexts = Vec::new();
159 let mut class_stack = Vec::new();
160
161 let recursion_limits = sqry_core::config::RecursionLimits::load_or_default()
163 .expect("Failed to load recursion limits");
164 let file_ops_depth = recursion_limits
165 .effective_file_ops_depth()
166 .expect("Invalid file_ops_depth configuration");
167 let mut guard = sqry_core::query::security::RecursionGuard::new(file_ops_depth)
168 .expect("Failed to create recursion guard");
169
170 if let Err(e) = extract_java_contexts(
171 tree.root_node(),
172 content,
173 &mut contexts,
174 &mut class_stack,
175 package_name.as_deref(),
176 0,
177 max_depth,
178 &mut guard,
179 ) {
180 eprintln!("Warning: Java AST traversal hit recursion limit: {e}");
181 }
182
183 let (field_types, import_map) = extract_field_and_import_types(tree.root_node(), content);
185
186 let (has_jna_import, has_panama_import) = detect_ffi_imports(tree.root_node(), content);
188
189 let jna_library_interfaces = find_jna_library_interfaces(tree.root_node(), content);
191
192 Self {
193 contexts,
194 field_types,
195 import_map,
196 has_jna_import,
197 has_panama_import,
198 jna_library_interfaces,
199 }
200 }
201
202 fn contexts(&self) -> &[MethodContext] {
203 &self.contexts
204 }
205
206 fn find_enclosing(&self, byte_pos: usize) -> Option<&MethodContext> {
208 self.contexts
209 .iter()
210 .filter(|ctx| byte_pos >= ctx.span.0 && byte_pos < ctx.span.1)
211 .max_by_key(|ctx| ctx.depth)
212 }
213}
214
215#[derive(Debug, Clone)]
216#[allow(clippy::struct_excessive_bools)] struct MethodContext {
218 qualified_name: String,
220 span: (usize, usize),
222 depth: usize,
224 is_static: bool,
226 #[allow(dead_code)] is_synchronized: bool,
229 is_constructor: bool,
231 #[allow(dead_code)] is_native: bool,
234 package_name: Option<String>,
236 class_stack: Vec<String>,
238 return_type: Option<String>,
240 visibility: Option<String>,
242}
243
244impl MethodContext {
245 fn qualified_name(&self) -> &str {
246 &self.qualified_name
247 }
248}
249
250fn extract_java_contexts(
259 node: Node,
260 content: &[u8],
261 contexts: &mut Vec<MethodContext>,
262 class_stack: &mut Vec<String>,
263 package_name: Option<&str>,
264 depth: usize,
265 max_depth: usize,
266 guard: &mut sqry_core::query::security::RecursionGuard,
267) -> Result<(), sqry_core::query::security::RecursionError> {
268 guard.enter()?;
269
270 if depth > max_depth {
271 guard.exit();
272 return Ok(());
273 }
274
275 match node.kind() {
276 "class_declaration" | "interface_declaration" | "enum_declaration" => {
277 if let Some(name_node) = node.child_by_field_name("name") {
279 let class_name = extract_identifier(name_node, content);
280
281 class_stack.push(class_name.clone());
283
284 if let Some(body_node) = node.child_by_field_name("body") {
286 extract_methods_from_body(
287 body_node,
288 content,
289 class_stack,
290 package_name,
291 contexts,
292 depth + 1,
293 max_depth,
294 guard,
295 )?;
296
297 for i in 0..body_node.child_count() {
299 if let Some(child) = body_node.child(i as u32) {
300 extract_java_contexts(
301 child,
302 content,
303 contexts,
304 class_stack,
305 package_name,
306 depth + 1,
307 max_depth,
308 guard,
309 )?;
310 }
311 }
312 }
313
314 class_stack.pop();
316
317 guard.exit();
318 return Ok(());
319 }
320 }
321 _ => {}
322 }
323
324 for i in 0..node.child_count() {
326 if let Some(child) = node.child(i as u32) {
327 extract_java_contexts(
328 child,
329 content,
330 contexts,
331 class_stack,
332 package_name,
333 depth,
334 max_depth,
335 guard,
336 )?;
337 }
338 }
339
340 guard.exit();
341 Ok(())
342}
343
344#[allow(clippy::unnecessary_wraps)]
348fn extract_methods_from_body(
349 body_node: Node,
350 content: &[u8],
351 class_stack: &[String],
352 package_name: Option<&str>,
353 contexts: &mut Vec<MethodContext>,
354 depth: usize,
355 _max_depth: usize,
356 _guard: &mut sqry_core::query::security::RecursionGuard,
357) -> Result<(), sqry_core::query::security::RecursionError> {
358 for i in 0..body_node.child_count() {
359 if let Some(child) = body_node.child(i as u32) {
360 match child.kind() {
361 "method_declaration" => {
362 if let Some(method_context) =
363 extract_method_context(child, content, class_stack, package_name, depth)
364 {
365 contexts.push(method_context);
366 }
367 }
368 "constructor_declaration" => {
369 let constructor_context = extract_constructor_context(
370 child,
371 content,
372 class_stack,
373 package_name,
374 depth,
375 );
376 contexts.push(constructor_context);
377 }
378 _ => {}
379 }
380 }
381 }
382 Ok(())
383}
384
385fn extract_method_context(
386 method_node: Node,
387 content: &[u8],
388 class_stack: &[String],
389 package_name: Option<&str>,
390 depth: usize,
391) -> Option<MethodContext> {
392 let name_node = method_node.child_by_field_name("name")?;
393 let method_name = extract_identifier(name_node, content);
394
395 let is_static = has_modifier(method_node, "static", content);
396 let is_synchronized = has_modifier(method_node, "synchronized", content);
397 let is_native = has_modifier(method_node, "native", content);
398 let visibility = extract_visibility(method_node, content);
399
400 let return_type = method_node
403 .child_by_field_name("type")
404 .map(|type_node| extract_full_return_type(type_node, content));
405
406 let qualified_name = build_member_symbol(package_name, class_stack, &method_name);
408
409 Some(MethodContext {
410 qualified_name,
411 span: (method_node.start_byte(), method_node.end_byte()),
412 depth,
413 is_static,
414 is_synchronized,
415 is_constructor: false,
416 is_native,
417 package_name: package_name.map(std::string::ToString::to_string),
418 class_stack: class_stack.to_vec(),
419 return_type,
420 visibility,
421 })
422}
423
424fn extract_constructor_context(
425 constructor_node: Node,
426 content: &[u8],
427 class_stack: &[String],
428 package_name: Option<&str>,
429 depth: usize,
430) -> MethodContext {
431 let qualified_name = build_member_symbol(package_name, class_stack, "<init>");
433 let visibility = extract_visibility(constructor_node, content);
434
435 MethodContext {
436 qualified_name,
437 span: (constructor_node.start_byte(), constructor_node.end_byte()),
438 depth,
439 is_static: false,
440 is_synchronized: false,
441 is_constructor: true,
442 is_native: false,
443 package_name: package_name.map(std::string::ToString::to_string),
444 class_stack: class_stack.to_vec(),
445 return_type: None, visibility,
447 }
448}
449
450fn walk_tree_for_edges(
456 node: Node,
457 content: &[u8],
458 ast_graph: &ASTGraph,
459 scope_tree: &mut JavaScopeTree,
460 helper: &mut GraphBuildHelper,
461 tree: &Tree,
462) -> GraphResult<()> {
463 match node.kind() {
464 "class_declaration" | "interface_declaration" | "enum_declaration" => {
465 return handle_type_declaration(node, content, ast_graph, scope_tree, helper, tree);
467 }
468 "method_declaration" | "constructor_declaration" => {
469 handle_method_declaration_parameters(node, content, ast_graph, scope_tree, helper);
471
472 if node.kind() == "method_declaration"
474 && let Some((http_method, path)) = extract_spring_route_info(node, content)
475 {
476 let full_path =
478 if let Some(class_prefix) = extract_class_request_mapping_path(node, content) {
479 let prefix = class_prefix.trim_end_matches('/');
480 let suffix = path.trim_start_matches('/');
481 if suffix.is_empty() {
482 class_prefix
483 } else {
484 format!("{prefix}/{suffix}")
485 }
486 } else {
487 path
488 };
489 let qualified_name = format!("route::{http_method}::{full_path}");
490 let span = Span::from_bytes(node.start_byte(), node.end_byte());
491 let endpoint_id = helper.add_endpoint(&qualified_name, Some(span));
492
493 let byte_pos = node.start_byte();
495 if let Some(context) = ast_graph.find_enclosing(byte_pos) {
496 let method_id = helper.ensure_method(
497 context.qualified_name(),
498 Some(Span::from_bytes(context.span.0, context.span.1)),
499 false,
500 context.is_static,
501 );
502 helper.add_contains_edge(endpoint_id, method_id);
503 }
504 }
505 }
506 "compact_constructor_declaration" => {
507 handle_compact_constructor_parameters(node, content, ast_graph, scope_tree, helper);
508 }
509 "method_invocation" => {
510 handle_method_invocation(node, content, ast_graph, helper);
511 }
512 "object_creation_expression" => {
513 handle_constructor_call(node, content, ast_graph, helper);
514 }
515 "import_declaration" => {
516 handle_import_declaration(node, content, helper);
517 }
518 "local_variable_declaration" => {
519 handle_local_variable_declaration(node, content, ast_graph, scope_tree, helper);
520 }
521 "enhanced_for_statement" => {
522 handle_enhanced_for_declaration(node, content, ast_graph, scope_tree, helper);
523 }
524 "catch_clause" => {
525 handle_catch_parameter_declaration(node, content, ast_graph, scope_tree, helper);
526 }
527 "lambda_expression" => {
528 handle_lambda_parameter_declaration(node, content, ast_graph, scope_tree, helper);
529 }
530 "try_with_resources_statement" => {
531 handle_try_with_resources_declaration(node, content, ast_graph, scope_tree, helper);
532 }
533 "instanceof_expression" => {
534 handle_instanceof_pattern_declaration(node, content, ast_graph, scope_tree, helper);
535 }
536 "switch_label" => {
537 handle_switch_pattern_declaration(node, content, ast_graph, scope_tree, helper);
538 }
539 "identifier" => {
540 handle_identifier_for_reference(node, content, ast_graph, scope_tree, helper);
541 }
542 _ => {}
543 }
544
545 for i in 0..node.child_count() {
547 if let Some(child) = node.child(i as u32) {
548 walk_tree_for_edges(child, content, ast_graph, scope_tree, helper, tree)?;
549 }
550 }
551
552 Ok(())
553}
554
555fn handle_type_declaration(
556 node: Node,
557 content: &[u8],
558 ast_graph: &ASTGraph,
559 scope_tree: &mut JavaScopeTree,
560 helper: &mut GraphBuildHelper,
561 tree: &Tree,
562) -> GraphResult<()> {
563 let Some(name_node) = node.child_by_field_name("name") else {
564 return Ok(());
565 };
566 let class_name = extract_identifier(name_node, content);
567 let span = Span::from_bytes(node.start_byte(), node.end_byte());
568
569 let package = PackageResolver::package_from_ast(tree, content);
570 let class_stack = extract_declaration_class_stack(node, content);
571 let qualified_name = qualify_class_name(&class_name, &class_stack, package.as_deref());
572 let class_node_id = add_type_node(helper, node.kind(), &qualified_name, span);
573
574 if is_public(node, content) {
575 export_from_file_module(helper, class_node_id);
576 }
577
578 process_inheritance(node, content, package.as_deref(), class_node_id, helper);
579 if node.kind() == "class_declaration" {
580 process_implements(node, content, package.as_deref(), class_node_id, helper);
581 }
582 if node.kind() == "interface_declaration" {
583 process_interface_extends(node, content, package.as_deref(), class_node_id, helper);
584 }
585
586 if let Some(body_node) = node.child_by_field_name("body") {
587 let is_interface = node.kind() == "interface_declaration";
588 process_class_member_exports(body_node, content, &qualified_name, helper, is_interface);
589
590 for i in 0..body_node.child_count() {
591 if let Some(child) = body_node.child(i as u32) {
592 walk_tree_for_edges(child, content, ast_graph, scope_tree, helper, tree)?;
593 }
594 }
595 }
596
597 Ok(())
598}
599
600fn extract_declaration_class_stack(node: Node, content: &[u8]) -> Vec<String> {
601 let mut class_stack = Vec::new();
602 let mut current_node = Some(node);
603
604 while let Some(current) = current_node {
605 if matches!(
606 current.kind(),
607 "class_declaration" | "interface_declaration" | "enum_declaration"
608 ) && let Some(name_node) = current.child_by_field_name("name")
609 {
610 class_stack.push(extract_identifier(name_node, content));
611 }
612
613 current_node = current.parent();
614 }
615
616 class_stack.reverse();
617 class_stack
618}
619
620fn qualify_class_name(class_name: &str, class_stack: &[String], package: Option<&str>) -> String {
621 let scope = class_stack
622 .split_last()
623 .map_or(&[][..], |(_, parent_stack)| parent_stack);
624 build_symbol(package, scope, class_name)
625}
626
627fn add_type_node(
628 helper: &mut GraphBuildHelper,
629 kind: &str,
630 qualified_name: &str,
631 span: Span,
632) -> sqry_core::graph::unified::node::NodeId {
633 match kind {
634 "interface_declaration" => helper.add_interface(qualified_name, Some(span)),
635 _ => helper.add_class(qualified_name, Some(span)),
636 }
637}
638
639fn handle_method_invocation(
640 node: Node,
641 content: &[u8],
642 ast_graph: &ASTGraph,
643 helper: &mut GraphBuildHelper,
644) {
645 if let Some(caller_context) = ast_graph.find_enclosing(node.start_byte()) {
646 let is_ffi = build_ffi_call_edge(node, content, caller_context, ast_graph, helper);
647 if is_ffi {
648 return;
649 }
650 }
651
652 process_method_call_unified(node, content, ast_graph, helper);
653}
654
655fn handle_constructor_call(
656 node: Node,
657 content: &[u8],
658 ast_graph: &ASTGraph,
659 helper: &mut GraphBuildHelper,
660) {
661 process_constructor_call_unified(node, content, ast_graph, helper);
662}
663
664fn handle_import_declaration(node: Node, content: &[u8], helper: &mut GraphBuildHelper) {
665 process_import_unified(node, content, helper);
666}
667
668fn add_field_typeof_edges(ast_graph: &ASTGraph, helper: &mut GraphBuildHelper) {
671 for (field_name, (type_fqn, is_final, visibility, is_static)) in &ast_graph.field_types {
672 let field_id = if *is_final {
674 if let Some(vis) = visibility {
676 helper.add_constant_with_static_and_visibility(
677 field_name,
678 None,
679 *is_static,
680 Some(vis.as_str()),
681 )
682 } else {
683 helper.add_constant_with_static_and_visibility(field_name, None, *is_static, None)
684 }
685 } else {
686 if let Some(vis) = visibility {
688 helper.add_property_with_static_and_visibility(
689 field_name,
690 None,
691 *is_static,
692 Some(vis.as_str()),
693 )
694 } else {
695 helper.add_property_with_static_and_visibility(field_name, None, *is_static, None)
696 }
697 };
698
699 let type_id = helper.add_class(type_fqn, None);
701
702 helper.add_typeof_edge(field_id, type_id);
704 }
705}
706
707fn extract_method_parameters(
710 method_node: Node,
711 content: &[u8],
712 qualified_method_name: &str,
713 helper: &mut GraphBuildHelper,
714 import_map: &HashMap<String, String>,
715 scope_tree: &mut JavaScopeTree,
716) {
717 let mut cursor = method_node.walk();
719 for child in method_node.children(&mut cursor) {
720 if child.kind() == "formal_parameters" {
721 let mut param_cursor = child.walk();
723 for param_child in child.children(&mut param_cursor) {
724 match param_child.kind() {
725 "formal_parameter" => {
726 handle_formal_parameter(
727 param_child,
728 content,
729 qualified_method_name,
730 helper,
731 import_map,
732 scope_tree,
733 );
734 }
735 "spread_parameter" => {
736 handle_spread_parameter(
737 param_child,
738 content,
739 qualified_method_name,
740 helper,
741 import_map,
742 scope_tree,
743 );
744 }
745 "receiver_parameter" => {
746 handle_receiver_parameter(
747 param_child,
748 content,
749 qualified_method_name,
750 helper,
751 import_map,
752 scope_tree,
753 );
754 }
755 _ => {}
756 }
757 }
758 }
759 }
760}
761
762fn handle_formal_parameter(
764 param_node: Node,
765 content: &[u8],
766 method_name: &str,
767 helper: &mut GraphBuildHelper,
768 import_map: &HashMap<String, String>,
769 scope_tree: &mut JavaScopeTree,
770) {
771 use sqry_core::graph::unified::node::NodeKind;
772
773 let Some(type_node) = param_node.child_by_field_name("type") else {
775 return;
776 };
777
778 let Some(name_node) = param_node.child_by_field_name("name") else {
780 return;
781 };
782
783 let type_text = extract_type_name(type_node, content);
785 let param_name = extract_identifier(name_node, content);
786
787 if type_text.is_empty() || param_name.is_empty() {
788 return;
789 }
790
791 let resolved_type = import_map.get(&type_text).cloned().unwrap_or(type_text);
793
794 let qualified_param = format!("{method_name}::{param_name}");
796 let span = Span::from_bytes(param_node.start_byte(), param_node.end_byte());
797
798 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
800
801 scope_tree.attach_node_id(¶m_name, name_node.start_byte(), param_id);
802
803 let type_id = helper.add_class(&resolved_type, None);
805
806 helper.add_typeof_edge(param_id, type_id);
808}
809
810fn handle_spread_parameter(
812 param_node: Node,
813 content: &[u8],
814 method_name: &str,
815 helper: &mut GraphBuildHelper,
816 import_map: &HashMap<String, String>,
817 scope_tree: &mut JavaScopeTree,
818) {
819 use sqry_core::graph::unified::node::NodeKind;
820
821 let mut type_text = String::new();
830 let mut param_name = String::new();
831 let mut param_name_node = None;
832
833 let mut cursor = param_node.walk();
834 for child in param_node.children(&mut cursor) {
835 match child.kind() {
836 "type_identifier" | "generic_type" | "scoped_type_identifier" => {
837 type_text = extract_type_name(child, content);
838 }
839 "variable_declarator" => {
840 if let Some(name_node) = child.child_by_field_name("name") {
842 param_name = extract_identifier(name_node, content);
843 param_name_node = Some(name_node);
844 }
845 }
846 _ => {}
847 }
848 }
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 if let Some(name_node) = param_name_node {
865 scope_tree.attach_node_id(¶m_name, name_node.start_byte(), param_id);
866 }
867
868 let type_id = helper.add_class(&resolved_type, None);
871
872 helper.add_typeof_edge(param_id, type_id);
874}
875
876fn handle_receiver_parameter(
878 param_node: Node,
879 content: &[u8],
880 method_name: &str,
881 helper: &mut GraphBuildHelper,
882 import_map: &HashMap<String, String>,
883 _scope_tree: &mut JavaScopeTree,
884) {
885 use sqry_core::graph::unified::node::NodeKind;
886
887 let mut type_text = String::new();
895 let mut cursor = param_node.walk();
896
897 for child in param_node.children(&mut cursor) {
899 if matches!(
900 child.kind(),
901 "type_identifier" | "generic_type" | "scoped_type_identifier"
902 ) {
903 type_text = extract_type_name(child, content);
904 break;
905 }
906 }
907
908 if type_text.is_empty() {
909 return;
910 }
911
912 let param_name = "this";
914
915 let resolved_type = import_map.get(&type_text).cloned().unwrap_or(type_text);
917
918 let qualified_param = format!("{method_name}::{param_name}");
920 let span = Span::from_bytes(param_node.start_byte(), param_node.end_byte());
921
922 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
924
925 let type_id = helper.add_class(&resolved_type, None);
927
928 helper.add_typeof_edge(param_id, type_id);
930}
931
932#[derive(Debug, Clone, Copy, Eq, PartialEq)]
933enum FieldAccessRole {
934 Default,
935 ExplicitThisOrSuper,
936 Skip,
937}
938
939#[derive(Debug, Clone, Copy, Eq, PartialEq)]
940enum FieldResolutionMode {
941 Default,
942 CurrentOnly,
943}
944
945fn field_access_role(
946 node: Node,
947 content: &[u8],
948 ast_graph: &ASTGraph,
949 scope_tree: &JavaScopeTree,
950 identifier_text: &str,
951) -> FieldAccessRole {
952 let Some(parent) = node.parent() else {
953 return FieldAccessRole::Default;
954 };
955
956 if parent.kind() == "field_access" {
957 if let Some(field_node) = parent.child_by_field_name("field")
958 && field_node.id() == node.id()
959 && let Some(object_node) = parent.child_by_field_name("object")
960 {
961 if is_explicit_this_or_super(object_node, content) {
962 return FieldAccessRole::ExplicitThisOrSuper;
963 }
964 return FieldAccessRole::Skip;
965 }
966
967 if let Some(object_node) = parent.child_by_field_name("object")
968 && object_node.id() == node.id()
969 && !scope_tree.has_local_binding(identifier_text, node.start_byte())
970 && is_static_type_identifier(identifier_text, ast_graph, scope_tree)
971 {
972 return FieldAccessRole::Skip;
973 }
974 }
975
976 if parent.kind() == "method_invocation"
977 && let Some(object_node) = parent.child_by_field_name("object")
978 && object_node.id() == node.id()
979 && !scope_tree.has_local_binding(identifier_text, node.start_byte())
980 && is_static_type_identifier(identifier_text, ast_graph, scope_tree)
981 {
982 return FieldAccessRole::Skip;
983 }
984
985 if parent.kind() == "method_reference"
986 && let Some(object_node) = parent.child_by_field_name("object")
987 && object_node.id() == node.id()
988 && !scope_tree.has_local_binding(identifier_text, node.start_byte())
989 && is_static_type_identifier(identifier_text, ast_graph, scope_tree)
990 {
991 return FieldAccessRole::Skip;
992 }
993
994 FieldAccessRole::Default
995}
996
997fn is_static_type_identifier(
998 identifier_text: &str,
999 ast_graph: &ASTGraph,
1000 scope_tree: &JavaScopeTree,
1001) -> bool {
1002 ast_graph.import_map.contains_key(identifier_text)
1003 || scope_tree.is_known_type_name(identifier_text)
1004}
1005
1006fn is_explicit_this_or_super(node: Node, content: &[u8]) -> bool {
1007 if matches!(node.kind(), "this" | "super") {
1008 return true;
1009 }
1010 if node.kind() == "identifier" {
1011 let text = extract_identifier(node, content);
1012 return matches!(text.as_str(), "this" | "super");
1013 }
1014 if node.kind() == "field_access"
1015 && let Some(field) = node.child_by_field_name("field")
1016 {
1017 let text = extract_identifier(field, content);
1018 if matches!(text.as_str(), "this" | "super") {
1019 return true;
1020 }
1021 }
1022 false
1023}
1024
1025#[allow(clippy::too_many_lines)]
1028fn is_declaration_context(node: Node) -> bool {
1029 let Some(parent) = node.parent() else {
1031 return false;
1032 };
1033
1034 if parent.kind() == "variable_declarator" {
1039 let mut cursor = parent.walk();
1041 for (idx, child) in parent.children(&mut cursor).enumerate() {
1042 if child.id() == node.id() {
1043 #[allow(clippy::cast_possible_truncation)]
1044 if let Some(field_name) = parent.field_name_for_child(idx as u32) {
1045 return field_name == "name";
1047 }
1048 break;
1049 }
1050 }
1051
1052 if let Some(grandparent) = parent.parent()
1054 && grandparent.kind() == "spread_parameter"
1055 {
1056 return true;
1057 }
1058
1059 return false;
1060 }
1061
1062 if parent.kind() == "formal_parameter" {
1064 let mut cursor = parent.walk();
1065 for (idx, child) in parent.children(&mut cursor).enumerate() {
1066 if child.id() == node.id() {
1067 #[allow(clippy::cast_possible_truncation)]
1068 if let Some(field_name) = parent.field_name_for_child(idx as u32) {
1069 return field_name == "name";
1070 }
1071 break;
1072 }
1073 }
1074 return false;
1075 }
1076
1077 if parent.kind() == "enhanced_for_statement" {
1080 let mut cursor = parent.walk();
1082 for (idx, child) in parent.children(&mut cursor).enumerate() {
1083 if child.id() == node.id() {
1084 #[allow(clippy::cast_possible_truncation)]
1085 if let Some(field_name) = parent.field_name_for_child(idx as u32) {
1086 return field_name == "name";
1088 }
1089 break;
1090 }
1091 }
1092 return false;
1093 }
1094
1095 if parent.kind() == "lambda_expression" {
1096 if let Some(params) = parent.child_by_field_name("parameters") {
1097 return params.id() == node.id();
1098 }
1099 return false;
1100 }
1101
1102 if parent.kind() == "inferred_parameters" {
1103 return true;
1104 }
1105
1106 if parent.kind() == "resource" {
1107 if let Some(name_node) = parent.child_by_field_name("name")
1108 && name_node.id() == node.id()
1109 {
1110 let has_type = parent.child_by_field_name("type").is_some();
1111 let has_value = parent.child_by_field_name("value").is_some();
1112 return has_type || has_value;
1113 }
1114 return false;
1115 }
1116
1117 if parent.kind() == "type_pattern" {
1121 if let Some(name_node) = parent.child_by_field_name("name")
1122 && name_node.id() == node.id()
1123 {
1124 return true;
1125 }
1126 return false;
1127 }
1128
1129 if parent.kind() == "instanceof_expression" {
1131 let mut cursor = parent.walk();
1132 for (idx, child) in parent.children(&mut cursor).enumerate() {
1133 if child.id() == node.id() {
1134 #[allow(clippy::cast_possible_truncation)]
1135 if let Some(field_name) = parent.field_name_for_child(idx as u32) {
1136 return field_name == "name";
1138 }
1139 break;
1140 }
1141 }
1142 return false;
1143 }
1144
1145 if parent.kind() == "record_pattern_component" {
1148 let mut cursor = parent.walk();
1150 for child in parent.children(&mut cursor) {
1151 if child.id() == node.id() && child.kind() == "identifier" {
1152 return true;
1154 }
1155 }
1156 return false;
1157 }
1158
1159 if parent.kind() == "record_component" {
1160 if let Some(name_node) = parent.child_by_field_name("name") {
1161 return name_node.id() == node.id();
1162 }
1163 return false;
1164 }
1165
1166 matches!(
1168 parent.kind(),
1169 "method_declaration"
1170 | "constructor_declaration"
1171 | "compact_constructor_declaration"
1172 | "class_declaration"
1173 | "interface_declaration"
1174 | "enum_declaration"
1175 | "field_declaration"
1176 | "catch_formal_parameter"
1177 )
1178}
1179
1180fn is_method_invocation_name(node: Node) -> bool {
1181 let Some(parent) = node.parent() else {
1182 return false;
1183 };
1184 if parent.kind() != "method_invocation" {
1185 return false;
1186 }
1187 parent
1188 .child_by_field_name("name")
1189 .is_some_and(|name_node| name_node.id() == node.id())
1190}
1191
1192fn is_method_reference_name(node: Node) -> bool {
1193 let Some(parent) = node.parent() else {
1194 return false;
1195 };
1196 if parent.kind() != "method_reference" {
1197 return false;
1198 }
1199 parent
1200 .child_by_field_name("name")
1201 .is_some_and(|name_node| name_node.id() == node.id())
1202}
1203
1204fn is_label_identifier(node: Node) -> bool {
1205 let Some(parent) = node.parent() else {
1206 return false;
1207 };
1208 if parent.kind() == "labeled_statement" {
1209 return true;
1210 }
1211 if matches!(parent.kind(), "break_statement" | "continue_statement")
1212 && let Some(label) = parent.child_by_field_name("label")
1213 {
1214 return label.id() == node.id();
1215 }
1216 false
1217}
1218
1219fn is_class_literal(node: Node) -> bool {
1220 let Some(parent) = node.parent() else {
1221 return false;
1222 };
1223 parent.kind() == "class_literal"
1224}
1225
1226fn is_type_identifier_context(node: Node) -> bool {
1227 let Some(parent) = node.parent() else {
1228 return false;
1229 };
1230 matches!(
1231 parent.kind(),
1232 "type_identifier"
1233 | "scoped_type_identifier"
1234 | "scoped_identifier"
1235 | "generic_type"
1236 | "type_argument"
1237 | "type_bound"
1238 )
1239}
1240
1241fn add_reference_edge_for_target(
1242 usage_node: Node,
1243 identifier_text: &str,
1244 target_id: sqry_core::graph::unified::node::NodeId,
1245 helper: &mut GraphBuildHelper,
1246) {
1247 let usage_span = Span::from_bytes(usage_node.start_byte(), usage_node.end_byte());
1248 let usage_id = helper.add_node(
1249 &format!("{}@{}", identifier_text, usage_node.start_byte()),
1250 Some(usage_span),
1251 sqry_core::graph::unified::node::NodeKind::Variable,
1252 );
1253 helper.add_reference_edge(usage_id, target_id);
1254}
1255
1256fn resolve_field_reference(
1257 node: Node,
1258 identifier_text: &str,
1259 ast_graph: &ASTGraph,
1260 helper: &mut GraphBuildHelper,
1261 mode: FieldResolutionMode,
1262) {
1263 let context = ast_graph.find_enclosing(node.start_byte());
1264 let mut candidates = Vec::new();
1265 if let Some(ctx) = context
1266 && !ctx.class_stack.is_empty()
1267 {
1268 if mode == FieldResolutionMode::CurrentOnly {
1269 let class_path = ctx.class_stack.join("::");
1270 candidates.push(format!("{class_path}::{identifier_text}"));
1271 } else {
1272 let stack_len = ctx.class_stack.len();
1273 for idx in (1..=stack_len).rev() {
1274 let class_path = ctx.class_stack[..idx].join("::");
1275 candidates.push(format!("{class_path}::{identifier_text}"));
1276 }
1277 }
1278 }
1279
1280 if mode != FieldResolutionMode::CurrentOnly {
1281 candidates.push(identifier_text.to_string());
1282 }
1283
1284 for candidate in candidates {
1285 if ast_graph.field_types.contains_key(&candidate) {
1286 add_field_reference(node, identifier_text, &candidate, ast_graph, helper);
1287 return;
1288 }
1289 }
1290}
1291
1292fn add_field_reference(
1293 node: Node,
1294 identifier_text: &str,
1295 field_name: &str,
1296 ast_graph: &ASTGraph,
1297 helper: &mut GraphBuildHelper,
1298) {
1299 let usage_span = Span::from_bytes(node.start_byte(), node.end_byte());
1300 let usage_id = helper.add_node(
1301 &format!("{}@{}", identifier_text, node.start_byte()),
1302 Some(usage_span),
1303 sqry_core::graph::unified::node::NodeKind::Variable,
1304 );
1305
1306 let field_metadata = ast_graph.field_types.get(field_name);
1307 let field_id = if let Some((_, is_final, visibility, is_static)) = field_metadata {
1308 if *is_final {
1309 if let Some(vis) = visibility {
1310 helper.add_constant_with_static_and_visibility(
1311 field_name,
1312 None,
1313 *is_static,
1314 Some(vis.as_str()),
1315 )
1316 } else {
1317 helper.add_constant_with_static_and_visibility(field_name, None, *is_static, None)
1318 }
1319 } else if let Some(vis) = visibility {
1320 helper.add_property_with_static_and_visibility(
1321 field_name,
1322 None,
1323 *is_static,
1324 Some(vis.as_str()),
1325 )
1326 } else {
1327 helper.add_property_with_static_and_visibility(field_name, None, *is_static, None)
1328 }
1329 } else {
1330 helper.add_property_with_static_and_visibility(field_name, None, false, None)
1331 };
1332
1333 helper.add_reference_edge(usage_id, field_id);
1334}
1335
1336#[allow(clippy::similar_names)]
1338fn handle_identifier_for_reference(
1339 node: Node,
1340 content: &[u8],
1341 ast_graph: &ASTGraph,
1342 scope_tree: &mut JavaScopeTree,
1343 helper: &mut GraphBuildHelper,
1344) {
1345 let identifier_text = extract_identifier(node, content);
1346
1347 if identifier_text.is_empty() {
1348 return;
1349 }
1350
1351 if is_declaration_context(node) {
1353 return;
1354 }
1355
1356 if is_method_invocation_name(node)
1357 || is_method_reference_name(node)
1358 || is_label_identifier(node)
1359 || is_class_literal(node)
1360 {
1361 return;
1362 }
1363
1364 if is_type_identifier_context(node) {
1365 return;
1366 }
1367
1368 let field_access_role =
1369 field_access_role(node, content, ast_graph, scope_tree, &identifier_text);
1370 if matches!(field_access_role, FieldAccessRole::Skip) {
1371 return;
1372 }
1373
1374 let allow_local = matches!(field_access_role, FieldAccessRole::Default);
1375 let allow_field = !matches!(field_access_role, FieldAccessRole::Skip);
1376 let field_mode = if matches!(field_access_role, FieldAccessRole::ExplicitThisOrSuper) {
1377 FieldResolutionMode::CurrentOnly
1378 } else {
1379 FieldResolutionMode::Default
1380 };
1381
1382 if allow_local {
1383 match scope_tree.resolve_identifier(node.start_byte(), &identifier_text) {
1384 ResolutionOutcome::Local(binding) => {
1385 let target_id = if let Some(node_id) = binding.node_id {
1386 node_id
1387 } else {
1388 let span = Span::from_bytes(binding.decl_start_byte, binding.decl_end_byte);
1389 let qualified_var = format!("{}@{}", identifier_text, binding.decl_start_byte);
1390 let var_id = helper.add_variable(&qualified_var, Some(span));
1391 scope_tree.attach_node_id(&identifier_text, binding.decl_start_byte, var_id);
1392 var_id
1393 };
1394 add_reference_edge_for_target(node, &identifier_text, target_id, helper);
1395 return;
1396 }
1397 ResolutionOutcome::Member { qualified_name } => {
1398 if let Some(field_name) = qualified_name {
1399 add_field_reference(node, &identifier_text, &field_name, ast_graph, helper);
1400 }
1401 return;
1402 }
1403 ResolutionOutcome::Ambiguous => {
1404 return;
1405 }
1406 ResolutionOutcome::NoMatch => {}
1407 }
1408 }
1409
1410 if !allow_field {
1411 return;
1412 }
1413
1414 resolve_field_reference(node, &identifier_text, ast_graph, helper, field_mode);
1415}
1416
1417fn handle_method_declaration_parameters(
1419 node: Node,
1420 content: &[u8],
1421 ast_graph: &ASTGraph,
1422 scope_tree: &mut JavaScopeTree,
1423 helper: &mut GraphBuildHelper,
1424) {
1425 let byte_pos = node.start_byte();
1427 if let Some(context) = ast_graph.find_enclosing(byte_pos) {
1428 let qualified_method_name = &context.qualified_name;
1429
1430 extract_method_parameters(
1432 node,
1433 content,
1434 qualified_method_name,
1435 helper,
1436 &ast_graph.import_map,
1437 scope_tree,
1438 );
1439 }
1440}
1441
1442fn handle_local_variable_declaration(
1444 node: Node,
1445 content: &[u8],
1446 ast_graph: &ASTGraph,
1447 scope_tree: &mut JavaScopeTree,
1448 helper: &mut GraphBuildHelper,
1449) {
1450 let Some(type_node) = node.child_by_field_name("type") else {
1452 return;
1453 };
1454
1455 let type_text = extract_type_name(type_node, content);
1456 if type_text.is_empty() {
1457 return;
1458 }
1459
1460 let resolved_type = ast_graph
1462 .import_map
1463 .get(&type_text)
1464 .cloned()
1465 .unwrap_or_else(|| type_text.clone());
1466
1467 let mut cursor = node.walk();
1469 for child in node.children(&mut cursor) {
1470 if child.kind() == "variable_declarator"
1471 && let Some(name_node) = child.child_by_field_name("name")
1472 {
1473 let var_name = extract_identifier(name_node, content);
1474
1475 let qualified_var = format!("{}@{}", var_name, name_node.start_byte());
1477
1478 let span = Span::from_bytes(child.start_byte(), child.end_byte());
1480 let var_id = helper.add_variable(&qualified_var, Some(span));
1481 scope_tree.attach_node_id(&var_name, name_node.start_byte(), var_id);
1482
1483 let type_id = helper.add_class(&resolved_type, None);
1485
1486 helper.add_typeof_edge(var_id, type_id);
1488 }
1489 }
1490}
1491
1492fn handle_enhanced_for_declaration(
1493 node: Node,
1494 content: &[u8],
1495 ast_graph: &ASTGraph,
1496 scope_tree: &mut JavaScopeTree,
1497 helper: &mut GraphBuildHelper,
1498) {
1499 let Some(type_node) = node.child_by_field_name("type") else {
1500 return;
1501 };
1502 let Some(name_node) = node.child_by_field_name("name") else {
1503 return;
1504 };
1505 let Some(body_node) = node.child_by_field_name("body") else {
1506 return;
1507 };
1508
1509 let type_text = extract_type_name(type_node, content);
1510 let var_name = extract_identifier(name_node, content);
1511 if type_text.is_empty() || var_name.is_empty() {
1512 return;
1513 }
1514
1515 let resolved_type = ast_graph
1516 .import_map
1517 .get(&type_text)
1518 .cloned()
1519 .unwrap_or(type_text);
1520
1521 let qualified_var = format!("{}@{}", var_name, name_node.start_byte());
1522 let span = Span::from_bytes(name_node.start_byte(), name_node.end_byte());
1523 let var_id = helper.add_variable(&qualified_var, Some(span));
1524 scope_tree.attach_node_id(&var_name, body_node.start_byte(), var_id);
1525
1526 let type_id = helper.add_class(&resolved_type, None);
1527 helper.add_typeof_edge(var_id, type_id);
1528}
1529
1530fn handle_catch_parameter_declaration(
1531 node: Node,
1532 content: &[u8],
1533 ast_graph: &ASTGraph,
1534 scope_tree: &mut JavaScopeTree,
1535 helper: &mut GraphBuildHelper,
1536) {
1537 let Some(param_node) = node
1538 .child_by_field_name("parameter")
1539 .or_else(|| first_child_of_kind(node, "catch_formal_parameter"))
1540 .or_else(|| first_child_of_kind(node, "formal_parameter"))
1541 else {
1542 return;
1543 };
1544 let Some(name_node) = param_node
1545 .child_by_field_name("name")
1546 .or_else(|| first_child_of_kind(param_node, "identifier"))
1547 else {
1548 return;
1549 };
1550
1551 let var_name = extract_identifier(name_node, content);
1552 if var_name.is_empty() {
1553 return;
1554 }
1555
1556 let qualified_var = format!("{}@{}", var_name, name_node.start_byte());
1557 let span = Span::from_bytes(param_node.start_byte(), param_node.end_byte());
1558 let var_id = helper.add_variable(&qualified_var, Some(span));
1559 scope_tree.attach_node_id(&var_name, name_node.start_byte(), var_id);
1560
1561 if let Some(type_node) = param_node
1562 .child_by_field_name("type")
1563 .or_else(|| first_child_of_kind(param_node, "type_identifier"))
1564 .or_else(|| first_child_of_kind(param_node, "scoped_type_identifier"))
1565 .or_else(|| first_child_of_kind(param_node, "generic_type"))
1566 {
1567 add_typeof_for_catch_type(type_node, content, ast_graph, helper, var_id);
1568 }
1569}
1570
1571fn add_typeof_for_catch_type(
1572 type_node: Node,
1573 content: &[u8],
1574 ast_graph: &ASTGraph,
1575 helper: &mut GraphBuildHelper,
1576 var_id: sqry_core::graph::unified::node::NodeId,
1577) {
1578 if type_node.kind() == "union_type" {
1579 let mut cursor = type_node.walk();
1580 for child in type_node.children(&mut cursor) {
1581 if matches!(
1582 child.kind(),
1583 "type_identifier" | "scoped_type_identifier" | "generic_type"
1584 ) {
1585 let type_text = extract_type_name(child, content);
1586 if !type_text.is_empty() {
1587 let resolved_type = ast_graph
1588 .import_map
1589 .get(&type_text)
1590 .cloned()
1591 .unwrap_or(type_text);
1592 let type_id = helper.add_class(&resolved_type, None);
1593 helper.add_typeof_edge(var_id, type_id);
1594 }
1595 }
1596 }
1597 return;
1598 }
1599
1600 let type_text = extract_type_name(type_node, content);
1601 if type_text.is_empty() {
1602 return;
1603 }
1604 let resolved_type = ast_graph
1605 .import_map
1606 .get(&type_text)
1607 .cloned()
1608 .unwrap_or(type_text);
1609 let type_id = helper.add_class(&resolved_type, None);
1610 helper.add_typeof_edge(var_id, type_id);
1611}
1612
1613fn handle_lambda_parameter_declaration(
1614 node: Node,
1615 content: &[u8],
1616 ast_graph: &ASTGraph,
1617 scope_tree: &mut JavaScopeTree,
1618 helper: &mut GraphBuildHelper,
1619) {
1620 use sqry_core::graph::unified::node::NodeKind;
1621
1622 let Some(params_node) = node.child_by_field_name("parameters") else {
1623 return;
1624 };
1625 let lambda_prefix = format!("lambda@{}", node.start_byte());
1626
1627 if params_node.kind() == "identifier" {
1628 let name = extract_identifier(params_node, content);
1629 if name.is_empty() {
1630 return;
1631 }
1632 let qualified_param = format!("{lambda_prefix}::{name}");
1633 let span = Span::from_bytes(params_node.start_byte(), params_node.end_byte());
1634 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
1635 scope_tree.attach_node_id(&name, params_node.start_byte(), param_id);
1636 return;
1637 }
1638
1639 let mut cursor = params_node.walk();
1640 for child in params_node.children(&mut cursor) {
1641 match child.kind() {
1642 "identifier" => {
1643 let name = extract_identifier(child, content);
1644 if name.is_empty() {
1645 continue;
1646 }
1647 let qualified_param = format!("{lambda_prefix}::{name}");
1648 let span = Span::from_bytes(child.start_byte(), child.end_byte());
1649 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
1650 scope_tree.attach_node_id(&name, child.start_byte(), param_id);
1651 }
1652 "formal_parameter" => {
1653 let Some(name_node) = child.child_by_field_name("name") else {
1654 continue;
1655 };
1656 let Some(type_node) = child.child_by_field_name("type") else {
1657 continue;
1658 };
1659 let name = extract_identifier(name_node, content);
1660 if name.is_empty() {
1661 continue;
1662 }
1663 let type_text = extract_type_name(type_node, content);
1664 let resolved_type = ast_graph
1665 .import_map
1666 .get(&type_text)
1667 .cloned()
1668 .unwrap_or(type_text);
1669 let qualified_param = format!("{lambda_prefix}::{name}");
1670 let span = Span::from_bytes(child.start_byte(), child.end_byte());
1671 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
1672 scope_tree.attach_node_id(&name, name_node.start_byte(), param_id);
1673 let type_id = helper.add_class(&resolved_type, None);
1674 helper.add_typeof_edge(param_id, type_id);
1675 }
1676 _ => {}
1677 }
1678 }
1679}
1680
1681fn handle_try_with_resources_declaration(
1682 node: Node,
1683 content: &[u8],
1684 ast_graph: &ASTGraph,
1685 scope_tree: &mut JavaScopeTree,
1686 helper: &mut GraphBuildHelper,
1687) {
1688 let Some(resources) = node.child_by_field_name("resources") else {
1689 return;
1690 };
1691
1692 let mut cursor = resources.walk();
1693 for resource in resources.children(&mut cursor) {
1694 if resource.kind() != "resource" {
1695 continue;
1696 }
1697 let name_node = resource.child_by_field_name("name");
1698 let type_node = resource.child_by_field_name("type");
1699 let value_node = resource.child_by_field_name("value");
1700 if let Some(name_node) = name_node {
1701 if type_node.is_none() && value_node.is_none() {
1702 continue;
1703 }
1704 let name = extract_identifier(name_node, content);
1705 if name.is_empty() {
1706 continue;
1707 }
1708
1709 let qualified_var = format!("{}@{}", name, name_node.start_byte());
1710 let span = Span::from_bytes(resource.start_byte(), resource.end_byte());
1711 let var_id = helper.add_variable(&qualified_var, Some(span));
1712 scope_tree.attach_node_id(&name, name_node.start_byte(), var_id);
1713
1714 if let Some(type_node) = type_node {
1715 let type_text = extract_type_name(type_node, content);
1716 if !type_text.is_empty() {
1717 let resolved_type = ast_graph
1718 .import_map
1719 .get(&type_text)
1720 .cloned()
1721 .unwrap_or(type_text);
1722 let type_id = helper.add_class(&resolved_type, None);
1723 helper.add_typeof_edge(var_id, type_id);
1724 }
1725 }
1726 }
1727 }
1728}
1729
1730fn handle_instanceof_pattern_declaration(
1731 node: Node,
1732 content: &[u8],
1733 ast_graph: &ASTGraph,
1734 scope_tree: &mut JavaScopeTree,
1735 helper: &mut GraphBuildHelper,
1736) {
1737 let mut patterns = Vec::new();
1738 collect_pattern_declarations(node, &mut patterns);
1739 for (name_node, type_node) in patterns {
1740 let name = extract_identifier(name_node, content);
1741 if name.is_empty() {
1742 continue;
1743 }
1744 let qualified_var = format!("{}@{}", name, name_node.start_byte());
1745 let span = Span::from_bytes(name_node.start_byte(), name_node.end_byte());
1746 let var_id = helper.add_variable(&qualified_var, Some(span));
1747 scope_tree.attach_node_id(&name, name_node.start_byte(), var_id);
1748
1749 if let Some(type_node) = type_node {
1750 let type_text = extract_type_name(type_node, content);
1751 if !type_text.is_empty() {
1752 let resolved_type = ast_graph
1753 .import_map
1754 .get(&type_text)
1755 .cloned()
1756 .unwrap_or(type_text);
1757 let type_id = helper.add_class(&resolved_type, None);
1758 helper.add_typeof_edge(var_id, type_id);
1759 }
1760 }
1761 }
1762}
1763
1764fn handle_switch_pattern_declaration(
1765 node: Node,
1766 content: &[u8],
1767 ast_graph: &ASTGraph,
1768 scope_tree: &mut JavaScopeTree,
1769 helper: &mut GraphBuildHelper,
1770) {
1771 let mut patterns = Vec::new();
1772 collect_pattern_declarations(node, &mut patterns);
1773 for (name_node, type_node) in patterns {
1774 let name = extract_identifier(name_node, content);
1775 if name.is_empty() {
1776 continue;
1777 }
1778 let qualified_var = format!("{}@{}", name, name_node.start_byte());
1779 let span = Span::from_bytes(name_node.start_byte(), name_node.end_byte());
1780 let var_id = helper.add_variable(&qualified_var, Some(span));
1781 scope_tree.attach_node_id(&name, name_node.start_byte(), var_id);
1782
1783 if let Some(type_node) = type_node {
1784 let type_text = extract_type_name(type_node, content);
1785 if !type_text.is_empty() {
1786 let resolved_type = ast_graph
1787 .import_map
1788 .get(&type_text)
1789 .cloned()
1790 .unwrap_or(type_text);
1791 let type_id = helper.add_class(&resolved_type, None);
1792 helper.add_typeof_edge(var_id, type_id);
1793 }
1794 }
1795 }
1796}
1797
1798fn handle_compact_constructor_parameters(
1799 node: Node,
1800 content: &[u8],
1801 ast_graph: &ASTGraph,
1802 scope_tree: &mut JavaScopeTree,
1803 helper: &mut GraphBuildHelper,
1804) {
1805 use sqry_core::graph::unified::node::NodeKind;
1806
1807 let Some(record_node) = node
1808 .parent()
1809 .and_then(|parent| find_record_declaration(parent))
1810 else {
1811 return;
1812 };
1813
1814 let Some(record_name_node) = record_node.child_by_field_name("name") else {
1815 return;
1816 };
1817 let record_name = extract_identifier(record_name_node, content);
1818 if record_name.is_empty() {
1819 return;
1820 }
1821
1822 let mut components = Vec::new();
1823 collect_record_components_nodes(record_node, &mut components);
1824 for component in components {
1825 let Some(name_node) = component.child_by_field_name("name") else {
1826 continue;
1827 };
1828 let Some(type_node) = component.child_by_field_name("type") else {
1829 continue;
1830 };
1831 let name = extract_identifier(name_node, content);
1832 if name.is_empty() {
1833 continue;
1834 }
1835
1836 let type_text = extract_type_name(type_node, content);
1837 if type_text.is_empty() {
1838 continue;
1839 }
1840 let resolved_type = ast_graph
1841 .import_map
1842 .get(&type_text)
1843 .cloned()
1844 .unwrap_or(type_text);
1845
1846 let qualified_param = format!("{record_name}.<init>::{name}");
1847 let span = Span::from_bytes(component.start_byte(), component.end_byte());
1848 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
1849 scope_tree.attach_node_id(&name, name_node.start_byte(), param_id);
1850
1851 let type_id = helper.add_class(&resolved_type, None);
1852 helper.add_typeof_edge(param_id, type_id);
1853 }
1854}
1855
1856fn collect_pattern_declarations<'a>(
1857 node: Node<'a>,
1858 output: &mut Vec<(Node<'a>, Option<Node<'a>>)>,
1859) {
1860 if node.kind() == "type_pattern" {
1861 let name_node = node.child_by_field_name("name");
1862 let type_node = node.child_by_field_name("type").or_else(|| {
1863 let mut cursor = node.walk();
1864 for child in node.children(&mut cursor) {
1865 if matches!(
1866 child.kind(),
1867 "type_identifier" | "scoped_type_identifier" | "generic_type"
1868 ) {
1869 return Some(child);
1870 }
1871 }
1872 None
1873 });
1874 if let Some(name_node) = name_node {
1875 output.push((name_node, type_node));
1876 }
1877 }
1878
1879 if node.kind() == "record_pattern_component" {
1880 let mut name_node = None;
1881 let mut type_node = None;
1882 let mut cursor = node.walk();
1883 for child in node.children(&mut cursor) {
1884 if child.kind() == "identifier" {
1885 name_node = Some(child);
1886 } else if matches!(
1887 child.kind(),
1888 "type_identifier" | "scoped_type_identifier" | "generic_type"
1889 ) {
1890 type_node = Some(child);
1891 }
1892 }
1893 if let Some(name_node) = name_node {
1894 output.push((name_node, type_node));
1895 }
1896 }
1897
1898 let mut cursor = node.walk();
1899 for child in node.children(&mut cursor) {
1900 collect_pattern_declarations(child, output);
1901 }
1902}
1903
1904fn find_record_declaration(node: Node) -> Option<Node> {
1905 if node.kind() == "record_declaration" {
1906 return Some(node);
1907 }
1908 node.parent().and_then(find_record_declaration)
1909}
1910
1911fn collect_record_components_nodes<'a>(node: Node<'a>, output: &mut Vec<Node<'a>>) {
1912 let mut cursor = node.walk();
1913 for child in node.children(&mut cursor) {
1914 if child.kind() == "record_component" {
1915 output.push(child);
1916 }
1917 collect_record_components_nodes(child, output);
1918 }
1919}
1920
1921fn process_method_call_unified(
1923 call_node: Node,
1924 content: &[u8],
1925 ast_graph: &ASTGraph,
1926 helper: &mut GraphBuildHelper,
1927) {
1928 let Some(caller_context) = ast_graph.find_enclosing(call_node.start_byte()) else {
1929 return;
1930 };
1931 let Ok(callee_name) = extract_method_invocation_name(call_node, content) else {
1932 return;
1933 };
1934
1935 let callee_qualified =
1936 resolve_callee_qualified(&call_node, content, ast_graph, caller_context, &callee_name);
1937 let caller_method_id = ensure_caller_method(helper, caller_context);
1938 let target_method_id = helper.ensure_method(&callee_qualified, None, false, false);
1939
1940 add_call_edge(helper, caller_method_id, target_method_id, call_node);
1941}
1942
1943fn process_constructor_call_unified(
1945 new_node: Node,
1946 content: &[u8],
1947 ast_graph: &ASTGraph,
1948 helper: &mut GraphBuildHelper,
1949) {
1950 let Some(caller_context) = ast_graph.find_enclosing(new_node.start_byte()) else {
1951 return;
1952 };
1953
1954 let Some(type_node) = new_node.child_by_field_name("type") else {
1955 return;
1956 };
1957
1958 let class_name = extract_type_name(type_node, content);
1959 if class_name.is_empty() {
1960 return;
1961 }
1962
1963 let qualified_class = qualify_constructor_class(&class_name, caller_context);
1964 let constructor_name = format!("{qualified_class}.<init>");
1965
1966 let caller_method_id = ensure_caller_method(helper, caller_context);
1967 let target_method_id = helper.ensure_method(&constructor_name, None, false, false);
1968 add_call_edge(helper, caller_method_id, target_method_id, new_node);
1969}
1970
1971fn count_call_arguments(call_node: Node<'_>) -> u8 {
1972 let Some(args_node) = call_node.child_by_field_name("arguments") else {
1973 return 255;
1974 };
1975 let count = args_node.named_child_count();
1976 if count <= 254 {
1977 u8::try_from(count).unwrap_or(u8::MAX)
1978 } else {
1979 u8::MAX
1980 }
1981}
1982
1983fn process_import_unified(import_node: Node, content: &[u8], helper: &mut GraphBuildHelper) {
1985 let has_asterisk = import_has_wildcard(import_node);
1986 let Some(mut imported_name) = extract_import_name(import_node, content) else {
1987 return;
1988 };
1989 if has_asterisk {
1990 imported_name = format!("{imported_name}.*");
1991 }
1992
1993 let module_id = helper.add_module("<module>", None);
1994 let external_id = helper.add_import(
1995 &imported_name,
1996 Some(Span::from_bytes(
1997 import_node.start_byte(),
1998 import_node.end_byte(),
1999 )),
2000 );
2001
2002 helper.add_import_edge(module_id, external_id);
2003}
2004
2005fn ensure_caller_method(
2006 helper: &mut GraphBuildHelper,
2007 caller_context: &MethodContext,
2008) -> sqry_core::graph::unified::node::NodeId {
2009 helper.ensure_method(
2010 caller_context.qualified_name(),
2011 Some(Span::from_bytes(
2012 caller_context.span.0,
2013 caller_context.span.1,
2014 )),
2015 false,
2016 caller_context.is_static,
2017 )
2018}
2019
2020fn resolve_callee_qualified(
2021 call_node: &Node,
2022 content: &[u8],
2023 ast_graph: &ASTGraph,
2024 caller_context: &MethodContext,
2025 callee_name: &str,
2026) -> String {
2027 if let Some(object_node) = call_node.child_by_field_name("object") {
2028 let object_text = extract_node_text(object_node, content);
2029 return resolve_member_call_target(&object_text, ast_graph, caller_context, callee_name);
2030 }
2031
2032 build_member_symbol(
2033 caller_context.package_name.as_deref(),
2034 &caller_context.class_stack,
2035 callee_name,
2036 )
2037}
2038
2039fn resolve_member_call_target(
2040 object_text: &str,
2041 ast_graph: &ASTGraph,
2042 caller_context: &MethodContext,
2043 callee_name: &str,
2044) -> String {
2045 if object_text.contains('.') {
2046 return format!("{object_text}.{callee_name}");
2047 }
2048 if object_text == "this" {
2049 return build_member_symbol(
2050 caller_context.package_name.as_deref(),
2051 &caller_context.class_stack,
2052 callee_name,
2053 );
2054 }
2055
2056 if let Some(class_name) = caller_context.class_stack.last() {
2058 let qualified_field = format!("{class_name}::{object_text}");
2059 if let Some((field_type, _is_final, _visibility, _is_static)) =
2060 ast_graph.field_types.get(&qualified_field)
2061 {
2062 return format!("{field_type}.{callee_name}");
2063 }
2064 }
2065
2066 if let Some((field_type, _is_final, _visibility, _is_static)) =
2068 ast_graph.field_types.get(object_text)
2069 {
2070 return format!("{field_type}.{callee_name}");
2071 }
2072
2073 if let Some(type_fqn) = ast_graph.import_map.get(object_text) {
2074 return format!("{type_fqn}.{callee_name}");
2075 }
2076
2077 format!("{object_text}.{callee_name}")
2078}
2079
2080fn qualify_constructor_class(class_name: &str, caller_context: &MethodContext) -> String {
2081 if class_name.contains('.') {
2082 class_name.to_string()
2083 } else if let Some(pkg) = caller_context.package_name.as_deref() {
2084 format!("{pkg}.{class_name}")
2085 } else {
2086 class_name.to_string()
2087 }
2088}
2089
2090fn add_call_edge(
2091 helper: &mut GraphBuildHelper,
2092 caller_method_id: sqry_core::graph::unified::node::NodeId,
2093 target_method_id: sqry_core::graph::unified::node::NodeId,
2094 call_node: Node,
2095) {
2096 let argument_count = count_call_arguments(call_node);
2097 let call_span = Span::from_bytes(call_node.start_byte(), call_node.end_byte());
2098 helper.add_call_edge_full_with_span(
2099 caller_method_id,
2100 target_method_id,
2101 argument_count,
2102 false,
2103 vec![call_span],
2104 );
2105}
2106
2107fn import_has_wildcard(import_node: Node) -> bool {
2108 let mut cursor = import_node.walk();
2109 import_node
2110 .children(&mut cursor)
2111 .any(|child| child.kind() == "asterisk")
2112}
2113
2114fn extract_import_name(import_node: Node, content: &[u8]) -> Option<String> {
2115 let mut cursor = import_node.walk();
2116 for child in import_node.children(&mut cursor) {
2117 if child.kind() == "scoped_identifier" || child.kind() == "identifier" {
2118 return Some(extract_full_identifier(child, content));
2119 }
2120 }
2121 None
2122}
2123
2124fn process_inheritance(
2134 class_node: Node,
2135 content: &[u8],
2136 package_name: Option<&str>,
2137 child_class_id: sqry_core::graph::unified::node::NodeId,
2138 helper: &mut GraphBuildHelper,
2139) {
2140 if let Some(superclass_node) = class_node.child_by_field_name("superclass") {
2142 let parent_type_name = extract_type_from_superclass(superclass_node, content);
2144 if !parent_type_name.is_empty() {
2145 let parent_qualified = qualify_type_name(&parent_type_name, package_name);
2147 let parent_id = helper.add_class(&parent_qualified, None);
2148 helper.add_inherits_edge(child_class_id, parent_id);
2149 }
2150 }
2151}
2152
2153fn process_implements(
2159 class_node: Node,
2160 content: &[u8],
2161 package_name: Option<&str>,
2162 class_id: sqry_core::graph::unified::node::NodeId,
2163 helper: &mut GraphBuildHelper,
2164) {
2165 let interfaces_node = class_node
2171 .child_by_field_name("interfaces")
2172 .or_else(|| class_node.child_by_field_name("super_interfaces"));
2173
2174 if let Some(node) = interfaces_node {
2175 extract_interface_types(node, content, package_name, class_id, helper);
2176 return;
2177 }
2178
2179 let mut cursor = class_node.walk();
2181 for child in class_node.children(&mut cursor) {
2182 if child.kind() == "super_interfaces" {
2184 extract_interface_types(child, content, package_name, class_id, helper);
2185 return;
2186 }
2187 }
2188}
2189
2190fn process_interface_extends(
2208 interface_node: Node,
2209 content: &[u8],
2210 package_name: Option<&str>,
2211 interface_id: sqry_core::graph::unified::node::NodeId,
2212 helper: &mut GraphBuildHelper,
2213) {
2214 let mut cursor = interface_node.walk();
2216 for child in interface_node.children(&mut cursor) {
2217 if child.kind() == "extends_interfaces" {
2218 extract_parent_interfaces_for_inherits(
2220 child,
2221 content,
2222 package_name,
2223 interface_id,
2224 helper,
2225 );
2226 return;
2227 }
2228 }
2229}
2230
2231fn extract_parent_interfaces_for_inherits(
2234 extends_node: Node,
2235 content: &[u8],
2236 package_name: Option<&str>,
2237 child_interface_id: sqry_core::graph::unified::node::NodeId,
2238 helper: &mut GraphBuildHelper,
2239) {
2240 let mut cursor = extends_node.walk();
2241 for child in extends_node.children(&mut cursor) {
2242 match child.kind() {
2243 "type_identifier" => {
2244 let type_name = extract_identifier(child, content);
2245 if !type_name.is_empty() {
2246 let parent_qualified = qualify_type_name(&type_name, package_name);
2247 let parent_id = helper.add_interface(&parent_qualified, None);
2248 helper.add_inherits_edge(child_interface_id, parent_id);
2249 }
2250 }
2251 "type_list" => {
2252 let mut type_cursor = child.walk();
2253 for type_child in child.children(&mut type_cursor) {
2254 if let Some(type_name) = extract_type_identifier(type_child, content)
2255 && !type_name.is_empty()
2256 {
2257 let parent_qualified = qualify_type_name(&type_name, package_name);
2258 let parent_id = helper.add_interface(&parent_qualified, None);
2259 helper.add_inherits_edge(child_interface_id, parent_id);
2260 }
2261 }
2262 }
2263 "generic_type" | "scoped_type_identifier" => {
2264 if let Some(type_name) = extract_type_identifier(child, content)
2265 && !type_name.is_empty()
2266 {
2267 let parent_qualified = qualify_type_name(&type_name, package_name);
2268 let parent_id = helper.add_interface(&parent_qualified, None);
2269 helper.add_inherits_edge(child_interface_id, parent_id);
2270 }
2271 }
2272 _ => {}
2273 }
2274 }
2275}
2276
2277fn extract_type_from_superclass(superclass_node: Node, content: &[u8]) -> String {
2279 if superclass_node.kind() == "type_identifier" {
2281 return extract_identifier(superclass_node, content);
2282 }
2283
2284 let mut cursor = superclass_node.walk();
2286 for child in superclass_node.children(&mut cursor) {
2287 if let Some(name) = extract_type_identifier(child, content) {
2288 return name;
2289 }
2290 }
2291
2292 extract_identifier(superclass_node, content)
2294}
2295
2296fn extract_interface_types(
2307 interfaces_node: Node,
2308 content: &[u8],
2309 package_name: Option<&str>,
2310 implementor_id: sqry_core::graph::unified::node::NodeId,
2311 helper: &mut GraphBuildHelper,
2312) {
2313 let mut cursor = interfaces_node.walk();
2315 for child in interfaces_node.children(&mut cursor) {
2316 match child.kind() {
2317 "type_identifier" => {
2319 let type_name = extract_identifier(child, content);
2320 if !type_name.is_empty() {
2321 let interface_qualified = qualify_type_name(&type_name, package_name);
2322 let interface_id = helper.add_interface(&interface_qualified, None);
2323 helper.add_implements_edge(implementor_id, interface_id);
2324 }
2325 }
2326 "type_list" => {
2328 let mut type_cursor = child.walk();
2329 for type_child in child.children(&mut type_cursor) {
2330 if let Some(type_name) = extract_type_identifier(type_child, content)
2331 && !type_name.is_empty()
2332 {
2333 let interface_qualified = qualify_type_name(&type_name, package_name);
2334 let interface_id = helper.add_interface(&interface_qualified, None);
2335 helper.add_implements_edge(implementor_id, interface_id);
2336 }
2337 }
2338 }
2339 "generic_type" | "scoped_type_identifier" => {
2341 if let Some(type_name) = extract_type_identifier(child, content)
2342 && !type_name.is_empty()
2343 {
2344 let interface_qualified = qualify_type_name(&type_name, package_name);
2345 let interface_id = helper.add_interface(&interface_qualified, None);
2346 helper.add_implements_edge(implementor_id, interface_id);
2347 }
2348 }
2349 _ => {}
2350 }
2351 }
2352}
2353
2354fn extract_type_identifier(node: Node, content: &[u8]) -> Option<String> {
2356 match node.kind() {
2357 "type_identifier" => Some(extract_identifier(node, content)),
2358 "generic_type" => {
2359 if let Some(name_node) = node.child_by_field_name("name") {
2361 Some(extract_identifier(name_node, content))
2362 } else {
2363 let mut cursor = node.walk();
2365 for child in node.children(&mut cursor) {
2366 if child.kind() == "type_identifier" {
2367 return Some(extract_identifier(child, content));
2368 }
2369 }
2370 None
2371 }
2372 }
2373 "scoped_type_identifier" => {
2374 Some(extract_full_identifier(node, content))
2376 }
2377 _ => None,
2378 }
2379}
2380
2381fn qualify_type_name(type_name: &str, package_name: Option<&str>) -> String {
2383 if type_name.contains('.') {
2385 return type_name.to_string();
2386 }
2387
2388 if let Some(pkg) = package_name {
2390 format!("{pkg}.{type_name}")
2391 } else {
2392 type_name.to_string()
2393 }
2394}
2395
2396#[allow(clippy::type_complexity)]
2405fn extract_field_and_import_types(
2406 node: Node,
2407 content: &[u8],
2408) -> (
2409 HashMap<String, (String, bool, Option<sqry_core::schema::Visibility>, bool)>,
2410 HashMap<String, String>,
2411) {
2412 let import_map = extract_import_map(node, content);
2414
2415 let mut field_types = HashMap::new();
2416 let mut class_stack = Vec::new();
2417 extract_field_types_recursive(
2418 node,
2419 content,
2420 &import_map,
2421 &mut field_types,
2422 &mut class_stack,
2423 );
2424
2425 (field_types, import_map)
2426}
2427
2428fn extract_import_map(node: Node, content: &[u8]) -> HashMap<String, String> {
2430 let mut import_map = HashMap::new();
2431 collect_import_map_recursive(node, content, &mut import_map);
2432 import_map
2433}
2434
2435fn collect_import_map_recursive(
2436 node: Node,
2437 content: &[u8],
2438 import_map: &mut HashMap<String, String>,
2439) {
2440 if node.kind() == "import_declaration" {
2441 let full_path = node.utf8_text(content).unwrap_or("");
2445
2446 if let Some(path_start) = full_path.find("import ") {
2449 let after_import = &full_path[path_start + 7..].trim();
2450 if let Some(path_end) = after_import.find(';') {
2451 let import_path = &after_import[..path_end].trim();
2452
2453 if let Some(simple_name) = import_path.rsplit('.').next() {
2455 import_map.insert(simple_name.to_string(), (*import_path).to_string());
2456 }
2457 }
2458 }
2459 }
2460
2461 let mut cursor = node.walk();
2463 for child in node.children(&mut cursor) {
2464 collect_import_map_recursive(child, content, import_map);
2465 }
2466}
2467
2468fn extract_field_types_recursive(
2469 node: Node,
2470 content: &[u8],
2471 import_map: &HashMap<String, String>,
2472 field_types: &mut HashMap<String, (String, bool, Option<sqry_core::schema::Visibility>, bool)>,
2473 class_stack: &mut Vec<String>,
2474) {
2475 if matches!(
2477 node.kind(),
2478 "class_declaration" | "interface_declaration" | "enum_declaration"
2479 ) && let Some(name_node) = node.child_by_field_name("name")
2480 {
2481 let class_name = extract_identifier(name_node, content);
2482 class_stack.push(class_name);
2483
2484 if let Some(body_node) = node.child_by_field_name("body") {
2486 let mut cursor = body_node.walk();
2487 for child in body_node.children(&mut cursor) {
2488 extract_field_types_recursive(child, content, import_map, field_types, class_stack);
2489 }
2490 }
2491
2492 class_stack.pop();
2494 return; }
2496
2497 if node.kind() == "field_declaration" {
2504 let is_final = has_modifier(node, "final", content);
2506 let is_static = has_modifier(node, "static", content);
2507
2508 let visibility = if has_modifier(node, "public", content) {
2511 Some(sqry_core::schema::Visibility::Public)
2512 } else {
2513 Some(sqry_core::schema::Visibility::Private)
2515 };
2516
2517 if let Some(type_node) = node.child_by_field_name("type") {
2519 let type_text = extract_type_name_internal(type_node, content);
2520 if !type_text.is_empty() {
2521 let resolved_type = import_map
2523 .get(&type_text)
2524 .cloned()
2525 .unwrap_or(type_text.clone());
2526
2527 let mut cursor = node.walk();
2529 for child in node.children(&mut cursor) {
2530 if child.kind() == "variable_declarator"
2531 && let Some(name_node) = child.child_by_field_name("name")
2532 {
2533 let field_name = extract_identifier(name_node, content);
2534
2535 let qualified_field = if class_stack.is_empty() {
2538 field_name
2539 } else {
2540 let class_path = class_stack.join("::");
2541 format!("{class_path}::{field_name}")
2542 };
2543
2544 field_types.insert(
2545 qualified_field,
2546 (resolved_type.clone(), is_final, visibility, is_static),
2547 );
2548 }
2549 }
2550 }
2551 }
2552 }
2553
2554 let mut cursor = node.walk();
2556 for child in node.children(&mut cursor) {
2557 extract_field_types_recursive(child, content, import_map, field_types, class_stack);
2558 }
2559}
2560
2561fn extract_type_name_internal(type_node: Node, content: &[u8]) -> String {
2563 match type_node.kind() {
2564 "generic_type" => {
2565 if let Some(name_node) = type_node.child_by_field_name("name") {
2567 extract_identifier(name_node, content)
2568 } else {
2569 extract_identifier(type_node, content)
2570 }
2571 }
2572 "scoped_type_identifier" => {
2573 extract_full_identifier(type_node, content)
2575 }
2576 _ => extract_identifier(type_node, content),
2577 }
2578}
2579
2580fn extract_identifier(node: Node, content: &[u8]) -> String {
2585 node.utf8_text(content).unwrap_or("").to_string()
2586}
2587
2588fn extract_node_text(node: Node, content: &[u8]) -> String {
2589 node.utf8_text(content).unwrap_or("").to_string()
2590}
2591
2592fn extract_full_identifier(node: Node, content: &[u8]) -> String {
2593 node.utf8_text(content).unwrap_or("").to_string()
2594}
2595
2596fn first_child_of_kind<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
2597 let mut cursor = node.walk();
2598 node.children(&mut cursor)
2599 .find(|&child| child.kind() == kind)
2600}
2601
2602fn extract_method_invocation_name(call_node: Node, content: &[u8]) -> GraphResult<String> {
2603 if let Some(name_node) = call_node.child_by_field_name("name") {
2605 Ok(extract_identifier(name_node, content))
2606 } else {
2607 let mut cursor = call_node.walk();
2609 for child in call_node.children(&mut cursor) {
2610 if child.kind() == "identifier" {
2611 return Ok(extract_identifier(child, content));
2612 }
2613 }
2614
2615 Err(GraphBuilderError::ParseError {
2616 span: Span::from_bytes(call_node.start_byte(), call_node.end_byte()),
2617 reason: "Method invocation missing name".into(),
2618 })
2619 }
2620}
2621
2622fn extract_type_name(type_node: Node, content: &[u8]) -> String {
2623 match type_node.kind() {
2625 "generic_type" => {
2626 if let Some(name_node) = type_node.child_by_field_name("name") {
2628 extract_identifier(name_node, content)
2629 } else {
2630 extract_identifier(type_node, content)
2631 }
2632 }
2633 "scoped_type_identifier" => {
2634 extract_full_identifier(type_node, content)
2636 }
2637 _ => extract_identifier(type_node, content),
2638 }
2639}
2640
2641fn extract_full_return_type(type_node: Node, content: &[u8]) -> String {
2644 type_node.utf8_text(content).unwrap_or("").to_string()
2647}
2648
2649fn has_modifier(node: Node, modifier: &str, content: &[u8]) -> bool {
2650 let mut cursor = node.walk();
2651 for child in node.children(&mut cursor) {
2652 if child.kind() == "modifiers" {
2653 let mut mod_cursor = child.walk();
2654 for modifier_child in child.children(&mut mod_cursor) {
2655 if extract_identifier(modifier_child, content) == modifier {
2656 return true;
2657 }
2658 }
2659 }
2660 }
2661 false
2662}
2663
2664#[allow(clippy::unnecessary_wraps)]
2667fn extract_visibility(node: Node, content: &[u8]) -> Option<String> {
2668 if has_modifier(node, "public", content) {
2669 Some("public".to_string())
2670 } else if has_modifier(node, "private", content) {
2671 Some("private".to_string())
2672 } else if has_modifier(node, "protected", content) {
2673 Some("protected".to_string())
2674 } else {
2675 Some("package-private".to_string())
2677 }
2678}
2679
2680fn is_public(node: Node, content: &[u8]) -> bool {
2686 has_modifier(node, "public", content)
2687}
2688
2689fn is_private(node: Node, content: &[u8]) -> bool {
2691 has_modifier(node, "private", content)
2692}
2693
2694fn export_from_file_module(
2696 helper: &mut GraphBuildHelper,
2697 exported: sqry_core::graph::unified::node::NodeId,
2698) {
2699 let module_id = helper.add_module(FILE_MODULE_NAME, None);
2700 helper.add_export_edge(module_id, exported);
2701}
2702
2703fn process_class_member_exports(
2708 body_node: Node,
2709 content: &[u8],
2710 class_qualified_name: &str,
2711 helper: &mut GraphBuildHelper,
2712 is_interface: bool,
2713) {
2714 for i in 0..body_node.child_count() {
2715 if let Some(child) = body_node.child(i as u32) {
2716 match child.kind() {
2717 "method_declaration" => {
2718 let should_export = if is_interface {
2721 !is_private(child, content)
2723 } else {
2724 is_public(child, content)
2726 };
2727
2728 if should_export && let Some(name_node) = child.child_by_field_name("name") {
2729 let method_name = extract_identifier(name_node, content);
2730 let qualified_name = format!("{class_qualified_name}.{method_name}");
2731 let span = Span::from_bytes(child.start_byte(), child.end_byte());
2732 let is_static = has_modifier(child, "static", content);
2733 let method_id =
2734 helper.add_method(&qualified_name, Some(span), false, is_static);
2735 export_from_file_module(helper, method_id);
2736 }
2737 }
2738 "constructor_declaration" => {
2739 if is_public(child, content) {
2740 let qualified_name = format!("{class_qualified_name}.<init>");
2741 let span = Span::from_bytes(child.start_byte(), child.end_byte());
2742 let method_id =
2743 helper.add_method(&qualified_name, Some(span), false, false);
2744 export_from_file_module(helper, method_id);
2745 }
2746 }
2747 "field_declaration" => {
2748 if is_public(child, content) {
2749 let mut cursor = child.walk();
2751 for field_child in child.children(&mut cursor) {
2752 if field_child.kind() == "variable_declarator"
2753 && let Some(name_node) = field_child.child_by_field_name("name")
2754 {
2755 let field_name = extract_identifier(name_node, content);
2756 let qualified_name = format!("{class_qualified_name}.{field_name}");
2757 let span = Span::from_bytes(
2758 field_child.start_byte(),
2759 field_child.end_byte(),
2760 );
2761
2762 let is_final = has_modifier(child, "final", content);
2764 let field_id = if is_final {
2765 helper.add_constant(&qualified_name, Some(span))
2766 } else {
2767 helper.add_variable(&qualified_name, Some(span))
2768 };
2769 export_from_file_module(helper, field_id);
2770 }
2771 }
2772 }
2773 }
2774 "constant_declaration" => {
2775 let mut cursor = child.walk();
2777 for const_child in child.children(&mut cursor) {
2778 if const_child.kind() == "variable_declarator"
2779 && let Some(name_node) = const_child.child_by_field_name("name")
2780 {
2781 let const_name = extract_identifier(name_node, content);
2782 let qualified_name = format!("{class_qualified_name}.{const_name}");
2783 let span =
2784 Span::from_bytes(const_child.start_byte(), const_child.end_byte());
2785 let const_id = helper.add_constant(&qualified_name, Some(span));
2786 export_from_file_module(helper, const_id);
2787 }
2788 }
2789 }
2790 "enum_constant" => {
2791 if let Some(name_node) = child.child_by_field_name("name") {
2793 let const_name = extract_identifier(name_node, content);
2794 let qualified_name = format!("{class_qualified_name}.{const_name}");
2795 let span = Span::from_bytes(child.start_byte(), child.end_byte());
2796 let const_id = helper.add_constant(&qualified_name, Some(span));
2797 export_from_file_module(helper, const_id);
2798 }
2799 }
2800 _ => {}
2801 }
2802 }
2803 }
2804}
2805
2806fn detect_ffi_imports(node: Node, content: &[u8]) -> (bool, bool) {
2813 let mut has_jna = false;
2814 let mut has_panama = false;
2815
2816 detect_ffi_imports_recursive(node, content, &mut has_jna, &mut has_panama);
2817
2818 (has_jna, has_panama)
2819}
2820
2821fn detect_ffi_imports_recursive(
2822 node: Node,
2823 content: &[u8],
2824 has_jna: &mut bool,
2825 has_panama: &mut bool,
2826) {
2827 if node.kind() == "import_declaration" {
2828 let import_text = node.utf8_text(content).unwrap_or("");
2829
2830 if import_text.contains("com.sun.jna") || import_text.contains("net.java.dev.jna") {
2832 *has_jna = true;
2833 }
2834
2835 if import_text.contains("java.lang.foreign") {
2837 *has_panama = true;
2838 }
2839 }
2840
2841 let mut cursor = node.walk();
2842 for child in node.children(&mut cursor) {
2843 detect_ffi_imports_recursive(child, content, has_jna, has_panama);
2844 }
2845}
2846
2847fn find_jna_library_interfaces(node: Node, content: &[u8]) -> Vec<String> {
2850 let mut jna_interfaces = Vec::new();
2851 find_jna_library_interfaces_recursive(node, content, &mut jna_interfaces);
2852 jna_interfaces
2853}
2854
2855fn find_jna_library_interfaces_recursive(
2856 node: Node,
2857 content: &[u8],
2858 jna_interfaces: &mut Vec<String>,
2859) {
2860 if node.kind() == "interface_declaration" {
2861 if let Some(name_node) = node.child_by_field_name("name") {
2863 let interface_name = extract_identifier(name_node, content);
2864
2865 let mut cursor = node.walk();
2867 for child in node.children(&mut cursor) {
2868 if child.kind() == "extends_interfaces" {
2869 let extends_text = child.utf8_text(content).unwrap_or("");
2870 if extends_text.contains("Library") {
2872 jna_interfaces.push(interface_name.clone());
2873 }
2874 }
2875 }
2876 }
2877 }
2878
2879 let mut cursor = node.walk();
2880 for child in node.children(&mut cursor) {
2881 find_jna_library_interfaces_recursive(child, content, jna_interfaces);
2882 }
2883}
2884
2885fn build_ffi_call_edge(
2888 call_node: Node,
2889 content: &[u8],
2890 caller_context: &MethodContext,
2891 ast_graph: &ASTGraph,
2892 helper: &mut GraphBuildHelper,
2893) -> bool {
2894 let Ok(method_name) = extract_method_invocation_name(call_node, content) else {
2896 return false;
2897 };
2898
2899 if ast_graph.has_jna_import && is_jna_native_load(call_node, content, &method_name) {
2901 let library_name = extract_jna_library_name(call_node, content);
2902 build_jna_native_load_edge(caller_context, &library_name, call_node, helper);
2903 return true;
2904 }
2905
2906 if ast_graph.has_jna_import
2908 && let Some(object_node) = call_node.child_by_field_name("object")
2909 {
2910 let object_text = extract_node_text(object_node, content);
2911
2912 let field_type = if let Some(class_name) = caller_context.class_stack.last() {
2914 let qualified_field = format!("{class_name}::{object_text}");
2915 ast_graph
2916 .field_types
2917 .get(&qualified_field)
2918 .or_else(|| ast_graph.field_types.get(&object_text))
2919 } else {
2920 ast_graph.field_types.get(&object_text)
2921 };
2922
2923 if let Some((type_name, _is_final, _visibility, _is_static)) = field_type {
2925 let simple_type = simple_type_name(type_name);
2926 if ast_graph.jna_library_interfaces.contains(&simple_type) {
2927 build_jna_method_call_edge(
2928 caller_context,
2929 &simple_type,
2930 &method_name,
2931 call_node,
2932 helper,
2933 );
2934 return true;
2935 }
2936 }
2937 }
2938
2939 if ast_graph.has_panama_import {
2941 if let Some(object_node) = call_node.child_by_field_name("object") {
2942 let object_text = extract_node_text(object_node, content);
2943
2944 if object_text == "Linker" && method_name == "nativeLinker" {
2946 build_panama_linker_edge(caller_context, call_node, helper);
2947 return true;
2948 }
2949
2950 if object_text == "SymbolLookup" && method_name == "libraryLookup" {
2952 let library_name = extract_first_string_arg(call_node, content);
2953 build_panama_library_lookup_edge(caller_context, &library_name, call_node, helper);
2954 return true;
2955 }
2956
2957 if method_name == "invokeExact" || method_name == "invoke" {
2959 if is_potential_panama_invoke(call_node, content) {
2962 build_panama_invoke_edge(caller_context, &method_name, call_node, helper);
2963 return true;
2964 }
2965 }
2966 }
2967
2968 if method_name == "nativeLinker" {
2970 let full_text = call_node.utf8_text(content).unwrap_or("");
2971 if full_text.contains("Linker") {
2972 build_panama_linker_edge(caller_context, call_node, helper);
2973 return true;
2974 }
2975 }
2976 }
2977
2978 false
2979}
2980
2981fn is_jna_native_load(call_node: Node, content: &[u8], method_name: &str) -> bool {
2983 if method_name != "load" && method_name != "loadLibrary" {
2984 return false;
2985 }
2986
2987 if let Some(object_node) = call_node.child_by_field_name("object") {
2988 let object_text = extract_node_text(object_node, content);
2989 return object_text == "Native" || object_text == "com.sun.jna.Native";
2990 }
2991
2992 false
2993}
2994
2995fn extract_jna_library_name(call_node: Node, content: &[u8]) -> String {
2998 if let Some(args_node) = call_node.child_by_field_name("arguments") {
2999 let mut cursor = args_node.walk();
3000 for child in args_node.children(&mut cursor) {
3001 if child.kind() == "string_literal" {
3002 let text = child.utf8_text(content).unwrap_or("\"unknown\"");
3003 return text.trim_matches('"').to_string();
3005 }
3006 }
3007 }
3008 "unknown".to_string()
3009}
3010
3011fn extract_first_string_arg(call_node: Node, content: &[u8]) -> String {
3013 if let Some(args_node) = call_node.child_by_field_name("arguments") {
3014 let mut cursor = args_node.walk();
3015 for child in args_node.children(&mut cursor) {
3016 if child.kind() == "string_literal" {
3017 let text = child.utf8_text(content).unwrap_or("\"unknown\"");
3018 return text.trim_matches('"').to_string();
3019 }
3020 }
3021 }
3022 "unknown".to_string()
3023}
3024
3025fn is_potential_panama_invoke(call_node: Node, content: &[u8]) -> bool {
3027 if let Some(object_node) = call_node.child_by_field_name("object") {
3029 let object_text = extract_node_text(object_node, content);
3030 let lower = object_text.to_lowercase();
3032 return lower.contains("handle")
3033 || lower.contains("downcall")
3034 || lower.contains("mh")
3035 || lower.contains("foreign");
3036 }
3037 false
3038}
3039
3040fn simple_type_name(type_name: &str) -> String {
3042 type_name
3043 .rsplit('.')
3044 .next()
3045 .unwrap_or(type_name)
3046 .to_string()
3047}
3048
3049fn build_jna_native_load_edge(
3051 caller_context: &MethodContext,
3052 library_name: &str,
3053 call_node: Node,
3054 helper: &mut GraphBuildHelper,
3055) {
3056 let caller_id = helper.ensure_method(
3057 caller_context.qualified_name(),
3058 Some(Span::from_bytes(
3059 caller_context.span.0,
3060 caller_context.span.1,
3061 )),
3062 false,
3063 caller_context.is_static,
3064 );
3065
3066 let target_name = format!("native::{library_name}");
3067 let target_id = helper.add_function(
3068 &target_name,
3069 Some(Span::from_bytes(
3070 call_node.start_byte(),
3071 call_node.end_byte(),
3072 )),
3073 false,
3074 false,
3075 );
3076
3077 helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3078}
3079
3080fn build_jna_method_call_edge(
3082 caller_context: &MethodContext,
3083 interface_name: &str,
3084 method_name: &str,
3085 call_node: Node,
3086 helper: &mut GraphBuildHelper,
3087) {
3088 let caller_id = helper.ensure_method(
3089 caller_context.qualified_name(),
3090 Some(Span::from_bytes(
3091 caller_context.span.0,
3092 caller_context.span.1,
3093 )),
3094 false,
3095 caller_context.is_static,
3096 );
3097
3098 let target_name = format!("native::{interface_name}::{method_name}");
3099 let target_id = helper.add_function(
3100 &target_name,
3101 Some(Span::from_bytes(
3102 call_node.start_byte(),
3103 call_node.end_byte(),
3104 )),
3105 false,
3106 false,
3107 );
3108
3109 helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3110}
3111
3112fn build_panama_linker_edge(
3114 caller_context: &MethodContext,
3115 call_node: Node,
3116 helper: &mut GraphBuildHelper,
3117) {
3118 let caller_id = helper.ensure_method(
3119 caller_context.qualified_name(),
3120 Some(Span::from_bytes(
3121 caller_context.span.0,
3122 caller_context.span.1,
3123 )),
3124 false,
3125 caller_context.is_static,
3126 );
3127
3128 let target_name = "native::panama::nativeLinker";
3129 let target_id = helper.add_function(
3130 target_name,
3131 Some(Span::from_bytes(
3132 call_node.start_byte(),
3133 call_node.end_byte(),
3134 )),
3135 false,
3136 false,
3137 );
3138
3139 helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3140}
3141
3142fn build_panama_library_lookup_edge(
3144 caller_context: &MethodContext,
3145 library_name: &str,
3146 call_node: Node,
3147 helper: &mut GraphBuildHelper,
3148) {
3149 let caller_id = helper.ensure_method(
3150 caller_context.qualified_name(),
3151 Some(Span::from_bytes(
3152 caller_context.span.0,
3153 caller_context.span.1,
3154 )),
3155 false,
3156 caller_context.is_static,
3157 );
3158
3159 let target_name = format!("native::panama::{library_name}");
3160 let target_id = helper.add_function(
3161 &target_name,
3162 Some(Span::from_bytes(
3163 call_node.start_byte(),
3164 call_node.end_byte(),
3165 )),
3166 false,
3167 false,
3168 );
3169
3170 helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3171}
3172
3173fn build_panama_invoke_edge(
3175 caller_context: &MethodContext,
3176 method_name: &str,
3177 call_node: Node,
3178 helper: &mut GraphBuildHelper,
3179) {
3180 let caller_id = helper.ensure_method(
3181 caller_context.qualified_name(),
3182 Some(Span::from_bytes(
3183 caller_context.span.0,
3184 caller_context.span.1,
3185 )),
3186 false,
3187 caller_context.is_static,
3188 );
3189
3190 let target_name = format!("native::panama::{method_name}");
3191 let target_id = helper.add_function(
3192 &target_name,
3193 Some(Span::from_bytes(
3194 call_node.start_byte(),
3195 call_node.end_byte(),
3196 )),
3197 false,
3198 false,
3199 );
3200
3201 helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3202}
3203
3204fn build_jni_native_method_edge(method_context: &MethodContext, helper: &mut GraphBuildHelper) {
3207 let method_id = helper.ensure_method(
3209 method_context.qualified_name(),
3210 Some(Span::from_bytes(
3211 method_context.span.0,
3212 method_context.span.1,
3213 )),
3214 false,
3215 method_context.is_static,
3216 );
3217
3218 let native_target = format!("native::jni::{}", method_context.qualified_name());
3221 let target_id = helper.add_function(&native_target, None, false, false);
3222
3223 helper.add_ffi_edge(method_id, target_id, FfiConvention::C);
3224}
3225
3226fn extract_spring_route_info(method_node: Node, content: &[u8]) -> Option<(String, String)> {
3240 let mut cursor = method_node.walk();
3242 let modifiers_node = method_node
3243 .children(&mut cursor)
3244 .find(|child| child.kind() == "modifiers")?;
3245
3246 let mut mod_cursor = modifiers_node.walk();
3248 for annotation_node in modifiers_node.children(&mut mod_cursor) {
3249 if annotation_node.kind() != "annotation" {
3250 continue;
3251 }
3252
3253 let Some(annotation_name) = extract_annotation_name(annotation_node, content) else {
3255 continue;
3256 };
3257
3258 let http_method: String = match annotation_name.as_str() {
3260 "GetMapping" => "GET".to_string(),
3261 "PostMapping" => "POST".to_string(),
3262 "PutMapping" => "PUT".to_string(),
3263 "DeleteMapping" => "DELETE".to_string(),
3264 "PatchMapping" => "PATCH".to_string(),
3265 "RequestMapping" => {
3266 extract_request_mapping_method(annotation_node, content)
3268 .unwrap_or_else(|| "GET".to_string())
3269 }
3270 _ => continue,
3271 };
3272
3273 let Some(path) = extract_annotation_path(annotation_node, content) else {
3275 continue;
3276 };
3277
3278 return Some((http_method, path));
3279 }
3280
3281 None
3282}
3283
3284fn extract_annotation_name(annotation_node: Node, content: &[u8]) -> Option<String> {
3289 let mut cursor = annotation_node.walk();
3290 for child in annotation_node.children(&mut cursor) {
3291 match child.kind() {
3292 "identifier" => {
3293 return Some(extract_identifier(child, content));
3294 }
3295 "scoped_identifier" => {
3296 let full_text = extract_identifier(child, content);
3299 return full_text.rsplit('.').next().map(String::from);
3300 }
3301 _ => {}
3302 }
3303 }
3304 None
3305}
3306
3307fn extract_annotation_path(annotation_node: Node, content: &[u8]) -> Option<String> {
3314 let mut cursor = annotation_node.walk();
3316 let args_node = annotation_node
3317 .children(&mut cursor)
3318 .find(|child| child.kind() == "annotation_argument_list")?;
3319
3320 let mut args_cursor = args_node.walk();
3322 for arg_child in args_node.children(&mut args_cursor) {
3323 match arg_child.kind() {
3324 "string_literal" => {
3326 return extract_string_content(arg_child, content);
3327 }
3328 "element_value_pair" => {
3330 if let Some(path) = extract_path_from_element_value_pair(arg_child, content) {
3331 return Some(path);
3332 }
3333 }
3334 _ => {}
3335 }
3336 }
3337
3338 None
3339}
3340
3341fn extract_request_mapping_method(annotation_node: Node, content: &[u8]) -> Option<String> {
3349 let mut cursor = annotation_node.walk();
3351 let args_node = annotation_node
3352 .children(&mut cursor)
3353 .find(|child| child.kind() == "annotation_argument_list")?;
3354
3355 let mut args_cursor = args_node.walk();
3357 for arg_child in args_node.children(&mut args_cursor) {
3358 if arg_child.kind() != "element_value_pair" {
3359 continue;
3360 }
3361
3362 let Some(key_node) = arg_child.child_by_field_name("key") else {
3364 continue;
3365 };
3366 let key_text = extract_identifier(key_node, content);
3367 if key_text != "method" {
3368 continue;
3369 }
3370
3371 let Some(value_node) = arg_child.child_by_field_name("value") else {
3373 continue;
3374 };
3375 let value_text = extract_identifier(value_node, content);
3376
3377 if let Some(method) = value_text.rsplit('.').next() {
3379 let method_upper = method.to_uppercase();
3380 if matches!(
3381 method_upper.as_str(),
3382 "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS"
3383 ) {
3384 return Some(method_upper);
3385 }
3386 }
3387 }
3388
3389 None
3390}
3391
3392fn extract_path_from_element_value_pair(pair_node: Node, content: &[u8]) -> Option<String> {
3396 let key_node = pair_node.child_by_field_name("key")?;
3397 let key_text = extract_identifier(key_node, content);
3398
3399 if key_text != "path" && key_text != "value" {
3401 return None;
3402 }
3403
3404 let value_node = pair_node.child_by_field_name("value")?;
3405 if value_node.kind() == "string_literal" {
3406 return extract_string_content(value_node, content);
3407 }
3408
3409 None
3410}
3411
3412fn extract_class_request_mapping_path(method_node: Node, content: &[u8]) -> Option<String> {
3429 let mut current = method_node.parent()?;
3431 loop {
3432 if current.kind() == "class_declaration" {
3433 break;
3434 }
3435 current = current.parent()?;
3436 }
3437
3438 let mut cursor = current.walk();
3440 let modifiers = current
3441 .children(&mut cursor)
3442 .find(|child| child.kind() == "modifiers")?;
3443
3444 let mut mod_cursor = modifiers.walk();
3445 for annotation in modifiers.children(&mut mod_cursor) {
3446 if annotation.kind() != "annotation" {
3447 continue;
3448 }
3449 let Some(name) = extract_annotation_name(annotation, content) else {
3450 continue;
3451 };
3452 if name == "RequestMapping" {
3453 return extract_annotation_path(annotation, content);
3454 }
3455 }
3456
3457 None
3458}
3459
3460fn extract_string_content(string_node: Node, content: &[u8]) -> Option<String> {
3464 let text = string_node.utf8_text(content).ok()?;
3465 let trimmed = text.trim();
3466
3467 if trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() >= 2 {
3469 Some(trimmed[1..trimmed.len() - 1].to_string())
3470 } else {
3471 None
3472 }
3473}