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 #[allow(clippy::cast_possible_truncation)]
300 if let Some(child) = body_node.child(i as u32) {
302 extract_java_contexts(
303 child,
304 content,
305 contexts,
306 class_stack,
307 package_name,
308 depth + 1,
309 max_depth,
310 guard,
311 )?;
312 }
313 }
314 }
315
316 class_stack.pop();
318
319 guard.exit();
320 return Ok(());
321 }
322 }
323 _ => {}
324 }
325
326 for i in 0..node.child_count() {
328 #[allow(clippy::cast_possible_truncation)]
329 if let Some(child) = node.child(i as u32) {
331 extract_java_contexts(
332 child,
333 content,
334 contexts,
335 class_stack,
336 package_name,
337 depth,
338 max_depth,
339 guard,
340 )?;
341 }
342 }
343
344 guard.exit();
345 Ok(())
346}
347
348#[allow(clippy::unnecessary_wraps)]
352fn extract_methods_from_body(
353 body_node: Node,
354 content: &[u8],
355 class_stack: &[String],
356 package_name: Option<&str>,
357 contexts: &mut Vec<MethodContext>,
358 depth: usize,
359 _max_depth: usize,
360 _guard: &mut sqry_core::query::security::RecursionGuard,
361) -> Result<(), sqry_core::query::security::RecursionError> {
362 for i in 0..body_node.child_count() {
363 #[allow(clippy::cast_possible_truncation)]
364 if let Some(child) = body_node.child(i as u32) {
366 match child.kind() {
367 "method_declaration" => {
368 if let Some(method_context) =
369 extract_method_context(child, content, class_stack, package_name, depth)
370 {
371 contexts.push(method_context);
372 }
373 }
374 "constructor_declaration" => {
375 let constructor_context = extract_constructor_context(
376 child,
377 content,
378 class_stack,
379 package_name,
380 depth,
381 );
382 contexts.push(constructor_context);
383 }
384 _ => {}
385 }
386 }
387 }
388 Ok(())
389}
390
391fn extract_method_context(
392 method_node: Node,
393 content: &[u8],
394 class_stack: &[String],
395 package_name: Option<&str>,
396 depth: usize,
397) -> Option<MethodContext> {
398 let name_node = method_node.child_by_field_name("name")?;
399 let method_name = extract_identifier(name_node, content);
400
401 let is_static = has_modifier(method_node, "static", content);
402 let is_synchronized = has_modifier(method_node, "synchronized", content);
403 let is_native = has_modifier(method_node, "native", content);
404 let visibility = extract_visibility(method_node, content);
405
406 let return_type = method_node
409 .child_by_field_name("type")
410 .map(|type_node| extract_full_return_type(type_node, content));
411
412 let qualified_name = build_member_symbol(package_name, class_stack, &method_name);
414
415 Some(MethodContext {
416 qualified_name,
417 span: (method_node.start_byte(), method_node.end_byte()),
418 depth,
419 is_static,
420 is_synchronized,
421 is_constructor: false,
422 is_native,
423 package_name: package_name.map(std::string::ToString::to_string),
424 class_stack: class_stack.to_vec(),
425 return_type,
426 visibility,
427 })
428}
429
430fn extract_constructor_context(
431 constructor_node: Node,
432 content: &[u8],
433 class_stack: &[String],
434 package_name: Option<&str>,
435 depth: usize,
436) -> MethodContext {
437 let qualified_name = build_member_symbol(package_name, class_stack, "<init>");
439 let visibility = extract_visibility(constructor_node, content);
440
441 MethodContext {
442 qualified_name,
443 span: (constructor_node.start_byte(), constructor_node.end_byte()),
444 depth,
445 is_static: false,
446 is_synchronized: false,
447 is_constructor: true,
448 is_native: false,
449 package_name: package_name.map(std::string::ToString::to_string),
450 class_stack: class_stack.to_vec(),
451 return_type: None, visibility,
453 }
454}
455
456fn walk_tree_for_edges(
462 node: Node,
463 content: &[u8],
464 ast_graph: &ASTGraph,
465 scope_tree: &mut JavaScopeTree,
466 helper: &mut GraphBuildHelper,
467 tree: &Tree,
468) -> GraphResult<()> {
469 match node.kind() {
470 "class_declaration" | "interface_declaration" | "enum_declaration" => {
471 return handle_type_declaration(node, content, ast_graph, scope_tree, helper, tree);
473 }
474 "method_declaration" | "constructor_declaration" => {
475 handle_method_declaration_parameters(node, content, ast_graph, scope_tree, helper);
477
478 if node.kind() == "method_declaration"
480 && let Some((http_method, path)) = extract_spring_route_info(node, content)
481 {
482 let full_path =
484 if let Some(class_prefix) = extract_class_request_mapping_path(node, content) {
485 let prefix = class_prefix.trim_end_matches('/');
486 let suffix = path.trim_start_matches('/');
487 if suffix.is_empty() {
488 class_prefix
489 } else {
490 format!("{prefix}/{suffix}")
491 }
492 } else {
493 path
494 };
495 let qualified_name = format!("route::{http_method}::{full_path}");
496 let span = Span::from_bytes(node.start_byte(), node.end_byte());
497 let endpoint_id = helper.add_endpoint(&qualified_name, Some(span));
498
499 let byte_pos = node.start_byte();
501 if let Some(context) = ast_graph.find_enclosing(byte_pos) {
502 let method_id = helper.ensure_method(
503 context.qualified_name(),
504 Some(Span::from_bytes(context.span.0, context.span.1)),
505 false,
506 context.is_static,
507 );
508 helper.add_contains_edge(endpoint_id, method_id);
509 }
510 }
511 }
512 "compact_constructor_declaration" => {
513 handle_compact_constructor_parameters(node, content, ast_graph, scope_tree, helper);
514 }
515 "method_invocation" => {
516 handle_method_invocation(node, content, ast_graph, helper);
517 }
518 "object_creation_expression" => {
519 handle_constructor_call(node, content, ast_graph, helper);
520 }
521 "import_declaration" => {
522 handle_import_declaration(node, content, helper);
523 }
524 "local_variable_declaration" => {
525 handle_local_variable_declaration(node, content, ast_graph, scope_tree, helper);
526 }
527 "enhanced_for_statement" => {
528 handle_enhanced_for_declaration(node, content, ast_graph, scope_tree, helper);
529 }
530 "catch_clause" => {
531 handle_catch_parameter_declaration(node, content, ast_graph, scope_tree, helper);
532 }
533 "lambda_expression" => {
534 handle_lambda_parameter_declaration(node, content, ast_graph, scope_tree, helper);
535 }
536 "try_with_resources_statement" => {
537 handle_try_with_resources_declaration(node, content, ast_graph, scope_tree, helper);
538 }
539 "instanceof_expression" => {
540 handle_instanceof_pattern_declaration(node, content, ast_graph, scope_tree, helper);
541 }
542 "switch_label" => {
543 handle_switch_pattern_declaration(node, content, ast_graph, scope_tree, helper);
544 }
545 "identifier" => {
546 handle_identifier_for_reference(node, content, ast_graph, scope_tree, helper);
547 }
548 _ => {}
549 }
550
551 for i in 0..node.child_count() {
553 #[allow(clippy::cast_possible_truncation)]
554 if let Some(child) = node.child(i as u32) {
556 walk_tree_for_edges(child, content, ast_graph, scope_tree, helper, tree)?;
557 }
558 }
559
560 Ok(())
561}
562
563fn handle_type_declaration(
564 node: Node,
565 content: &[u8],
566 ast_graph: &ASTGraph,
567 scope_tree: &mut JavaScopeTree,
568 helper: &mut GraphBuildHelper,
569 tree: &Tree,
570) -> GraphResult<()> {
571 let Some(name_node) = node.child_by_field_name("name") else {
572 return Ok(());
573 };
574 let class_name = extract_identifier(name_node, content);
575 let span = Span::from_bytes(node.start_byte(), node.end_byte());
576
577 let package = PackageResolver::package_from_ast(tree, content);
578 let class_stack = extract_declaration_class_stack(node, content);
579 let qualified_name = qualify_class_name(&class_name, &class_stack, package.as_deref());
580 let class_node_id = add_type_node(helper, node.kind(), &qualified_name, span);
581
582 if is_public(node, content) {
583 export_from_file_module(helper, class_node_id);
584 }
585
586 process_inheritance(node, content, package.as_deref(), class_node_id, helper);
587 if node.kind() == "class_declaration" {
588 process_implements(node, content, package.as_deref(), class_node_id, helper);
589 }
590 if node.kind() == "interface_declaration" {
591 process_interface_extends(node, content, package.as_deref(), class_node_id, helper);
592 }
593
594 if let Some(body_node) = node.child_by_field_name("body") {
595 let is_interface = node.kind() == "interface_declaration";
596 process_class_member_exports(body_node, content, &qualified_name, helper, is_interface);
597
598 for i in 0..body_node.child_count() {
599 #[allow(clippy::cast_possible_truncation)]
600 if let Some(child) = body_node.child(i as u32) {
602 walk_tree_for_edges(child, content, ast_graph, scope_tree, helper, tree)?;
603 }
604 }
605 }
606
607 Ok(())
608}
609
610fn extract_declaration_class_stack(node: Node, content: &[u8]) -> Vec<String> {
611 let mut class_stack = Vec::new();
612 let mut current_node = Some(node);
613
614 while let Some(current) = current_node {
615 if matches!(
616 current.kind(),
617 "class_declaration" | "interface_declaration" | "enum_declaration"
618 ) && let Some(name_node) = current.child_by_field_name("name")
619 {
620 class_stack.push(extract_identifier(name_node, content));
621 }
622
623 current_node = current.parent();
624 }
625
626 class_stack.reverse();
627 class_stack
628}
629
630fn qualify_class_name(class_name: &str, class_stack: &[String], package: Option<&str>) -> String {
631 let scope = class_stack
632 .split_last()
633 .map_or(&[][..], |(_, parent_stack)| parent_stack);
634 build_symbol(package, scope, class_name)
635}
636
637fn add_type_node(
638 helper: &mut GraphBuildHelper,
639 kind: &str,
640 qualified_name: &str,
641 span: Span,
642) -> sqry_core::graph::unified::node::NodeId {
643 match kind {
644 "interface_declaration" => helper.add_interface(qualified_name, Some(span)),
645 _ => helper.add_class(qualified_name, Some(span)),
646 }
647}
648
649fn handle_method_invocation(
650 node: Node,
651 content: &[u8],
652 ast_graph: &ASTGraph,
653 helper: &mut GraphBuildHelper,
654) {
655 if let Some(caller_context) = ast_graph.find_enclosing(node.start_byte()) {
656 let is_ffi = build_ffi_call_edge(node, content, caller_context, ast_graph, helper);
657 if is_ffi {
658 return;
659 }
660 }
661
662 process_method_call_unified(node, content, ast_graph, helper);
663}
664
665fn handle_constructor_call(
666 node: Node,
667 content: &[u8],
668 ast_graph: &ASTGraph,
669 helper: &mut GraphBuildHelper,
670) {
671 process_constructor_call_unified(node, content, ast_graph, helper);
672}
673
674fn handle_import_declaration(node: Node, content: &[u8], helper: &mut GraphBuildHelper) {
675 process_import_unified(node, content, helper);
676}
677
678fn add_field_typeof_edges(ast_graph: &ASTGraph, helper: &mut GraphBuildHelper) {
681 for (field_name, (type_fqn, is_final, visibility, is_static)) in &ast_graph.field_types {
682 let field_id = if *is_final {
684 if let Some(vis) = visibility {
686 helper.add_constant_with_static_and_visibility(
687 field_name,
688 None,
689 *is_static,
690 Some(vis.as_str()),
691 )
692 } else {
693 helper.add_constant_with_static_and_visibility(field_name, None, *is_static, None)
694 }
695 } else {
696 if let Some(vis) = visibility {
698 helper.add_property_with_static_and_visibility(
699 field_name,
700 None,
701 *is_static,
702 Some(vis.as_str()),
703 )
704 } else {
705 helper.add_property_with_static_and_visibility(field_name, None, *is_static, None)
706 }
707 };
708
709 let type_id = helper.add_class(type_fqn, None);
711
712 helper.add_typeof_edge(field_id, type_id);
714 }
715}
716
717fn extract_method_parameters(
720 method_node: Node,
721 content: &[u8],
722 qualified_method_name: &str,
723 helper: &mut GraphBuildHelper,
724 import_map: &HashMap<String, String>,
725 scope_tree: &mut JavaScopeTree,
726) {
727 let mut cursor = method_node.walk();
729 for child in method_node.children(&mut cursor) {
730 if child.kind() == "formal_parameters" {
731 let mut param_cursor = child.walk();
733 for param_child in child.children(&mut param_cursor) {
734 match param_child.kind() {
735 "formal_parameter" => {
736 handle_formal_parameter(
737 param_child,
738 content,
739 qualified_method_name,
740 helper,
741 import_map,
742 scope_tree,
743 );
744 }
745 "spread_parameter" => {
746 handle_spread_parameter(
747 param_child,
748 content,
749 qualified_method_name,
750 helper,
751 import_map,
752 scope_tree,
753 );
754 }
755 "receiver_parameter" => {
756 handle_receiver_parameter(
757 param_child,
758 content,
759 qualified_method_name,
760 helper,
761 import_map,
762 scope_tree,
763 );
764 }
765 _ => {}
766 }
767 }
768 }
769 }
770}
771
772fn handle_formal_parameter(
774 param_node: Node,
775 content: &[u8],
776 method_name: &str,
777 helper: &mut GraphBuildHelper,
778 import_map: &HashMap<String, String>,
779 scope_tree: &mut JavaScopeTree,
780) {
781 use sqry_core::graph::unified::node::NodeKind;
782
783 let Some(type_node) = param_node.child_by_field_name("type") else {
785 return;
786 };
787
788 let Some(name_node) = param_node.child_by_field_name("name") else {
790 return;
791 };
792
793 let type_text = extract_type_name(type_node, content);
795 let param_name = extract_identifier(name_node, content);
796
797 if type_text.is_empty() || param_name.is_empty() {
798 return;
799 }
800
801 let resolved_type = import_map.get(&type_text).cloned().unwrap_or(type_text);
803
804 let qualified_param = format!("{method_name}::{param_name}");
806 let span = Span::from_bytes(param_node.start_byte(), param_node.end_byte());
807
808 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
810
811 scope_tree.attach_node_id(¶m_name, name_node.start_byte(), param_id);
812
813 let type_id = helper.add_class(&resolved_type, None);
815
816 helper.add_typeof_edge(param_id, type_id);
818}
819
820fn handle_spread_parameter(
822 param_node: Node,
823 content: &[u8],
824 method_name: &str,
825 helper: &mut GraphBuildHelper,
826 import_map: &HashMap<String, String>,
827 scope_tree: &mut JavaScopeTree,
828) {
829 use sqry_core::graph::unified::node::NodeKind;
830
831 let mut type_text = String::new();
840 let mut param_name = String::new();
841 let mut param_name_node = None;
842
843 let mut cursor = param_node.walk();
844 for child in param_node.children(&mut cursor) {
845 match child.kind() {
846 "type_identifier" | "generic_type" | "scoped_type_identifier" => {
847 type_text = extract_type_name(child, content);
848 }
849 "variable_declarator" => {
850 if let Some(name_node) = child.child_by_field_name("name") {
852 param_name = extract_identifier(name_node, content);
853 param_name_node = Some(name_node);
854 }
855 }
856 _ => {}
857 }
858 }
859
860 if type_text.is_empty() || param_name.is_empty() {
861 return;
862 }
863
864 let resolved_type = import_map.get(&type_text).cloned().unwrap_or(type_text);
866
867 let qualified_param = format!("{method_name}::{param_name}");
869 let span = Span::from_bytes(param_node.start_byte(), param_node.end_byte());
870
871 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
873
874 if let Some(name_node) = param_name_node {
875 scope_tree.attach_node_id(¶m_name, name_node.start_byte(), param_id);
876 }
877
878 let type_id = helper.add_class(&resolved_type, None);
881
882 helper.add_typeof_edge(param_id, type_id);
884}
885
886fn handle_receiver_parameter(
888 param_node: Node,
889 content: &[u8],
890 method_name: &str,
891 helper: &mut GraphBuildHelper,
892 import_map: &HashMap<String, String>,
893 _scope_tree: &mut JavaScopeTree,
894) {
895 use sqry_core::graph::unified::node::NodeKind;
896
897 let mut type_text = String::new();
905 let mut cursor = param_node.walk();
906
907 for child in param_node.children(&mut cursor) {
909 if matches!(
910 child.kind(),
911 "type_identifier" | "generic_type" | "scoped_type_identifier"
912 ) {
913 type_text = extract_type_name(child, content);
914 break;
915 }
916 }
917
918 if type_text.is_empty() {
919 return;
920 }
921
922 let param_name = "this";
924
925 let resolved_type = import_map.get(&type_text).cloned().unwrap_or(type_text);
927
928 let qualified_param = format!("{method_name}::{param_name}");
930 let span = Span::from_bytes(param_node.start_byte(), param_node.end_byte());
931
932 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
934
935 let type_id = helper.add_class(&resolved_type, None);
937
938 helper.add_typeof_edge(param_id, type_id);
940}
941
942#[derive(Debug, Clone, Copy, Eq, PartialEq)]
943enum FieldAccessRole {
944 Default,
945 ExplicitThisOrSuper,
946 Skip,
947}
948
949#[derive(Debug, Clone, Copy, Eq, PartialEq)]
950enum FieldResolutionMode {
951 Default,
952 CurrentOnly,
953}
954
955fn field_access_role(
956 node: Node,
957 content: &[u8],
958 ast_graph: &ASTGraph,
959 scope_tree: &JavaScopeTree,
960 identifier_text: &str,
961) -> FieldAccessRole {
962 let Some(parent) = node.parent() else {
963 return FieldAccessRole::Default;
964 };
965
966 if parent.kind() == "field_access" {
967 if let Some(field_node) = parent.child_by_field_name("field")
968 && field_node.id() == node.id()
969 && let Some(object_node) = parent.child_by_field_name("object")
970 {
971 if is_explicit_this_or_super(object_node, content) {
972 return FieldAccessRole::ExplicitThisOrSuper;
973 }
974 return FieldAccessRole::Skip;
975 }
976
977 if 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
986 if parent.kind() == "method_invocation"
987 && let Some(object_node) = parent.child_by_field_name("object")
988 && object_node.id() == node.id()
989 && !scope_tree.has_local_binding(identifier_text, node.start_byte())
990 && is_static_type_identifier(identifier_text, ast_graph, scope_tree)
991 {
992 return FieldAccessRole::Skip;
993 }
994
995 if parent.kind() == "method_reference"
996 && let Some(object_node) = parent.child_by_field_name("object")
997 && object_node.id() == node.id()
998 && !scope_tree.has_local_binding(identifier_text, node.start_byte())
999 && is_static_type_identifier(identifier_text, ast_graph, scope_tree)
1000 {
1001 return FieldAccessRole::Skip;
1002 }
1003
1004 FieldAccessRole::Default
1005}
1006
1007fn is_static_type_identifier(
1008 identifier_text: &str,
1009 ast_graph: &ASTGraph,
1010 scope_tree: &JavaScopeTree,
1011) -> bool {
1012 ast_graph.import_map.contains_key(identifier_text)
1013 || scope_tree.is_known_type_name(identifier_text)
1014}
1015
1016fn is_explicit_this_or_super(node: Node, content: &[u8]) -> bool {
1017 if matches!(node.kind(), "this" | "super") {
1018 return true;
1019 }
1020 if node.kind() == "identifier" {
1021 let text = extract_identifier(node, content);
1022 return matches!(text.as_str(), "this" | "super");
1023 }
1024 if node.kind() == "field_access"
1025 && let Some(field) = node.child_by_field_name("field")
1026 {
1027 let text = extract_identifier(field, content);
1028 if matches!(text.as_str(), "this" | "super") {
1029 return true;
1030 }
1031 }
1032 false
1033}
1034
1035#[allow(clippy::too_many_lines)]
1038fn is_declaration_context(node: Node) -> bool {
1039 let Some(parent) = node.parent() else {
1041 return false;
1042 };
1043
1044 if parent.kind() == "variable_declarator" {
1049 let mut cursor = parent.walk();
1051 for (idx, child) in parent.children(&mut cursor).enumerate() {
1052 if child.id() == node.id() {
1053 #[allow(clippy::cast_possible_truncation)]
1054 if let Some(field_name) = parent.field_name_for_child(idx as u32) {
1055 return field_name == "name";
1057 }
1058 break;
1059 }
1060 }
1061
1062 if let Some(grandparent) = parent.parent()
1064 && grandparent.kind() == "spread_parameter"
1065 {
1066 return true;
1067 }
1068
1069 return false;
1070 }
1071
1072 if parent.kind() == "formal_parameter" {
1074 let mut cursor = parent.walk();
1075 for (idx, child) in parent.children(&mut cursor).enumerate() {
1076 if child.id() == node.id() {
1077 #[allow(clippy::cast_possible_truncation)]
1078 if let Some(field_name) = parent.field_name_for_child(idx as u32) {
1079 return field_name == "name";
1080 }
1081 break;
1082 }
1083 }
1084 return false;
1085 }
1086
1087 if parent.kind() == "enhanced_for_statement" {
1090 let mut cursor = parent.walk();
1092 for (idx, child) in parent.children(&mut cursor).enumerate() {
1093 if child.id() == node.id() {
1094 #[allow(clippy::cast_possible_truncation)]
1095 if let Some(field_name) = parent.field_name_for_child(idx as u32) {
1096 return field_name == "name";
1098 }
1099 break;
1100 }
1101 }
1102 return false;
1103 }
1104
1105 if parent.kind() == "lambda_expression" {
1106 if let Some(params) = parent.child_by_field_name("parameters") {
1107 return params.id() == node.id();
1108 }
1109 return false;
1110 }
1111
1112 if parent.kind() == "inferred_parameters" {
1113 return true;
1114 }
1115
1116 if parent.kind() == "resource" {
1117 if let Some(name_node) = parent.child_by_field_name("name")
1118 && name_node.id() == node.id()
1119 {
1120 let has_type = parent.child_by_field_name("type").is_some();
1121 let has_value = parent.child_by_field_name("value").is_some();
1122 return has_type || has_value;
1123 }
1124 return false;
1125 }
1126
1127 if parent.kind() == "type_pattern" {
1131 if let Some(name_node) = parent.child_by_field_name("name")
1132 && name_node.id() == node.id()
1133 {
1134 return true;
1135 }
1136 return false;
1137 }
1138
1139 if parent.kind() == "instanceof_expression" {
1141 let mut cursor = parent.walk();
1142 for (idx, child) in parent.children(&mut cursor).enumerate() {
1143 if child.id() == node.id() {
1144 #[allow(clippy::cast_possible_truncation)]
1145 if let Some(field_name) = parent.field_name_for_child(idx as u32) {
1146 return field_name == "name";
1148 }
1149 break;
1150 }
1151 }
1152 return false;
1153 }
1154
1155 if parent.kind() == "record_pattern_component" {
1158 let mut cursor = parent.walk();
1160 for child in parent.children(&mut cursor) {
1161 if child.id() == node.id() && child.kind() == "identifier" {
1162 return true;
1164 }
1165 }
1166 return false;
1167 }
1168
1169 if parent.kind() == "record_component" {
1170 if let Some(name_node) = parent.child_by_field_name("name") {
1171 return name_node.id() == node.id();
1172 }
1173 return false;
1174 }
1175
1176 matches!(
1178 parent.kind(),
1179 "method_declaration"
1180 | "constructor_declaration"
1181 | "compact_constructor_declaration"
1182 | "class_declaration"
1183 | "interface_declaration"
1184 | "enum_declaration"
1185 | "field_declaration"
1186 | "catch_formal_parameter"
1187 )
1188}
1189
1190fn is_method_invocation_name(node: Node) -> bool {
1191 let Some(parent) = node.parent() else {
1192 return false;
1193 };
1194 if parent.kind() != "method_invocation" {
1195 return false;
1196 }
1197 parent
1198 .child_by_field_name("name")
1199 .is_some_and(|name_node| name_node.id() == node.id())
1200}
1201
1202fn is_method_reference_name(node: Node) -> bool {
1203 let Some(parent) = node.parent() else {
1204 return false;
1205 };
1206 if parent.kind() != "method_reference" {
1207 return false;
1208 }
1209 parent
1210 .child_by_field_name("name")
1211 .is_some_and(|name_node| name_node.id() == node.id())
1212}
1213
1214fn is_label_identifier(node: Node) -> bool {
1215 let Some(parent) = node.parent() else {
1216 return false;
1217 };
1218 if parent.kind() == "labeled_statement" {
1219 return true;
1220 }
1221 if matches!(parent.kind(), "break_statement" | "continue_statement")
1222 && let Some(label) = parent.child_by_field_name("label")
1223 {
1224 return label.id() == node.id();
1225 }
1226 false
1227}
1228
1229fn is_class_literal(node: Node) -> bool {
1230 let Some(parent) = node.parent() else {
1231 return false;
1232 };
1233 parent.kind() == "class_literal"
1234}
1235
1236fn is_type_identifier_context(node: Node) -> bool {
1237 let Some(parent) = node.parent() else {
1238 return false;
1239 };
1240 matches!(
1241 parent.kind(),
1242 "type_identifier"
1243 | "scoped_type_identifier"
1244 | "scoped_identifier"
1245 | "generic_type"
1246 | "type_argument"
1247 | "type_bound"
1248 )
1249}
1250
1251fn add_reference_edge_for_target(
1252 usage_node: Node,
1253 identifier_text: &str,
1254 target_id: sqry_core::graph::unified::node::NodeId,
1255 helper: &mut GraphBuildHelper,
1256) {
1257 let usage_span = Span::from_bytes(usage_node.start_byte(), usage_node.end_byte());
1258 let usage_id = helper.add_node(
1259 &format!("{}@{}", identifier_text, usage_node.start_byte()),
1260 Some(usage_span),
1261 sqry_core::graph::unified::node::NodeKind::Variable,
1262 );
1263 helper.add_reference_edge(usage_id, target_id);
1264}
1265
1266fn resolve_field_reference(
1267 node: Node,
1268 identifier_text: &str,
1269 ast_graph: &ASTGraph,
1270 helper: &mut GraphBuildHelper,
1271 mode: FieldResolutionMode,
1272) {
1273 let context = ast_graph.find_enclosing(node.start_byte());
1274 let mut candidates = Vec::new();
1275 if let Some(ctx) = context
1276 && !ctx.class_stack.is_empty()
1277 {
1278 if mode == FieldResolutionMode::CurrentOnly {
1279 let class_path = ctx.class_stack.join("::");
1280 candidates.push(format!("{class_path}::{identifier_text}"));
1281 } else {
1282 let stack_len = ctx.class_stack.len();
1283 for idx in (1..=stack_len).rev() {
1284 let class_path = ctx.class_stack[..idx].join("::");
1285 candidates.push(format!("{class_path}::{identifier_text}"));
1286 }
1287 }
1288 }
1289
1290 if mode != FieldResolutionMode::CurrentOnly {
1291 candidates.push(identifier_text.to_string());
1292 }
1293
1294 for candidate in candidates {
1295 if ast_graph.field_types.contains_key(&candidate) {
1296 add_field_reference(node, identifier_text, &candidate, ast_graph, helper);
1297 return;
1298 }
1299 }
1300}
1301
1302fn add_field_reference(
1303 node: Node,
1304 identifier_text: &str,
1305 field_name: &str,
1306 ast_graph: &ASTGraph,
1307 helper: &mut GraphBuildHelper,
1308) {
1309 let usage_span = Span::from_bytes(node.start_byte(), node.end_byte());
1310 let usage_id = helper.add_node(
1311 &format!("{}@{}", identifier_text, node.start_byte()),
1312 Some(usage_span),
1313 sqry_core::graph::unified::node::NodeKind::Variable,
1314 );
1315
1316 let field_metadata = ast_graph.field_types.get(field_name);
1317 let field_id = if let Some((_, is_final, visibility, is_static)) = field_metadata {
1318 if *is_final {
1319 if let Some(vis) = visibility {
1320 helper.add_constant_with_static_and_visibility(
1321 field_name,
1322 None,
1323 *is_static,
1324 Some(vis.as_str()),
1325 )
1326 } else {
1327 helper.add_constant_with_static_and_visibility(field_name, None, *is_static, None)
1328 }
1329 } else if let Some(vis) = visibility {
1330 helper.add_property_with_static_and_visibility(
1331 field_name,
1332 None,
1333 *is_static,
1334 Some(vis.as_str()),
1335 )
1336 } else {
1337 helper.add_property_with_static_and_visibility(field_name, None, *is_static, None)
1338 }
1339 } else {
1340 helper.add_property_with_static_and_visibility(field_name, None, false, None)
1341 };
1342
1343 helper.add_reference_edge(usage_id, field_id);
1344}
1345
1346#[allow(clippy::similar_names)]
1348fn handle_identifier_for_reference(
1349 node: Node,
1350 content: &[u8],
1351 ast_graph: &ASTGraph,
1352 scope_tree: &mut JavaScopeTree,
1353 helper: &mut GraphBuildHelper,
1354) {
1355 let identifier_text = extract_identifier(node, content);
1356
1357 if identifier_text.is_empty() {
1358 return;
1359 }
1360
1361 if is_declaration_context(node) {
1363 return;
1364 }
1365
1366 if is_method_invocation_name(node)
1367 || is_method_reference_name(node)
1368 || is_label_identifier(node)
1369 || is_class_literal(node)
1370 {
1371 return;
1372 }
1373
1374 if is_type_identifier_context(node) {
1375 return;
1376 }
1377
1378 let field_access_role =
1379 field_access_role(node, content, ast_graph, scope_tree, &identifier_text);
1380 if matches!(field_access_role, FieldAccessRole::Skip) {
1381 return;
1382 }
1383
1384 let allow_local = matches!(field_access_role, FieldAccessRole::Default);
1385 let allow_field = !matches!(field_access_role, FieldAccessRole::Skip);
1386 let field_mode = if matches!(field_access_role, FieldAccessRole::ExplicitThisOrSuper) {
1387 FieldResolutionMode::CurrentOnly
1388 } else {
1389 FieldResolutionMode::Default
1390 };
1391
1392 if allow_local {
1393 match scope_tree.resolve_identifier(node.start_byte(), &identifier_text) {
1394 ResolutionOutcome::Local(binding) => {
1395 let target_id = if let Some(node_id) = binding.node_id {
1396 node_id
1397 } else {
1398 let span = Span::from_bytes(binding.decl_start_byte, binding.decl_end_byte);
1399 let qualified_var = format!("{}@{}", identifier_text, binding.decl_start_byte);
1400 let var_id = helper.add_variable(&qualified_var, Some(span));
1401 scope_tree.attach_node_id(&identifier_text, binding.decl_start_byte, var_id);
1402 var_id
1403 };
1404 add_reference_edge_for_target(node, &identifier_text, target_id, helper);
1405 return;
1406 }
1407 ResolutionOutcome::Member { qualified_name } => {
1408 if let Some(field_name) = qualified_name {
1409 add_field_reference(node, &identifier_text, &field_name, ast_graph, helper);
1410 }
1411 return;
1412 }
1413 ResolutionOutcome::Ambiguous => {
1414 return;
1415 }
1416 ResolutionOutcome::NoMatch => {}
1417 }
1418 }
1419
1420 if !allow_field {
1421 return;
1422 }
1423
1424 resolve_field_reference(node, &identifier_text, ast_graph, helper, field_mode);
1425}
1426
1427fn handle_method_declaration_parameters(
1429 node: Node,
1430 content: &[u8],
1431 ast_graph: &ASTGraph,
1432 scope_tree: &mut JavaScopeTree,
1433 helper: &mut GraphBuildHelper,
1434) {
1435 let byte_pos = node.start_byte();
1437 if let Some(context) = ast_graph.find_enclosing(byte_pos) {
1438 let qualified_method_name = &context.qualified_name;
1439
1440 extract_method_parameters(
1442 node,
1443 content,
1444 qualified_method_name,
1445 helper,
1446 &ast_graph.import_map,
1447 scope_tree,
1448 );
1449 }
1450}
1451
1452fn handle_local_variable_declaration(
1454 node: Node,
1455 content: &[u8],
1456 ast_graph: &ASTGraph,
1457 scope_tree: &mut JavaScopeTree,
1458 helper: &mut GraphBuildHelper,
1459) {
1460 let Some(type_node) = node.child_by_field_name("type") else {
1462 return;
1463 };
1464
1465 let type_text = extract_type_name(type_node, content);
1466 if type_text.is_empty() {
1467 return;
1468 }
1469
1470 let resolved_type = ast_graph
1472 .import_map
1473 .get(&type_text)
1474 .cloned()
1475 .unwrap_or_else(|| type_text.clone());
1476
1477 let mut cursor = node.walk();
1479 for child in node.children(&mut cursor) {
1480 if child.kind() == "variable_declarator"
1481 && let Some(name_node) = child.child_by_field_name("name")
1482 {
1483 let var_name = extract_identifier(name_node, content);
1484
1485 let qualified_var = format!("{}@{}", var_name, name_node.start_byte());
1487
1488 let span = Span::from_bytes(child.start_byte(), child.end_byte());
1490 let var_id = helper.add_variable(&qualified_var, Some(span));
1491 scope_tree.attach_node_id(&var_name, name_node.start_byte(), var_id);
1492
1493 let type_id = helper.add_class(&resolved_type, None);
1495
1496 helper.add_typeof_edge(var_id, type_id);
1498 }
1499 }
1500}
1501
1502fn handle_enhanced_for_declaration(
1503 node: Node,
1504 content: &[u8],
1505 ast_graph: &ASTGraph,
1506 scope_tree: &mut JavaScopeTree,
1507 helper: &mut GraphBuildHelper,
1508) {
1509 let Some(type_node) = node.child_by_field_name("type") else {
1510 return;
1511 };
1512 let Some(name_node) = node.child_by_field_name("name") else {
1513 return;
1514 };
1515 let Some(body_node) = node.child_by_field_name("body") else {
1516 return;
1517 };
1518
1519 let type_text = extract_type_name(type_node, content);
1520 let var_name = extract_identifier(name_node, content);
1521 if type_text.is_empty() || var_name.is_empty() {
1522 return;
1523 }
1524
1525 let resolved_type = ast_graph
1526 .import_map
1527 .get(&type_text)
1528 .cloned()
1529 .unwrap_or(type_text);
1530
1531 let qualified_var = format!("{}@{}", var_name, name_node.start_byte());
1532 let span = Span::from_bytes(name_node.start_byte(), name_node.end_byte());
1533 let var_id = helper.add_variable(&qualified_var, Some(span));
1534 scope_tree.attach_node_id(&var_name, body_node.start_byte(), var_id);
1535
1536 let type_id = helper.add_class(&resolved_type, None);
1537 helper.add_typeof_edge(var_id, type_id);
1538}
1539
1540fn handle_catch_parameter_declaration(
1541 node: Node,
1542 content: &[u8],
1543 ast_graph: &ASTGraph,
1544 scope_tree: &mut JavaScopeTree,
1545 helper: &mut GraphBuildHelper,
1546) {
1547 let Some(param_node) = node
1548 .child_by_field_name("parameter")
1549 .or_else(|| first_child_of_kind(node, "catch_formal_parameter"))
1550 .or_else(|| first_child_of_kind(node, "formal_parameter"))
1551 else {
1552 return;
1553 };
1554 let Some(name_node) = param_node
1555 .child_by_field_name("name")
1556 .or_else(|| first_child_of_kind(param_node, "identifier"))
1557 else {
1558 return;
1559 };
1560
1561 let var_name = extract_identifier(name_node, content);
1562 if var_name.is_empty() {
1563 return;
1564 }
1565
1566 let qualified_var = format!("{}@{}", var_name, name_node.start_byte());
1567 let span = Span::from_bytes(param_node.start_byte(), param_node.end_byte());
1568 let var_id = helper.add_variable(&qualified_var, Some(span));
1569 scope_tree.attach_node_id(&var_name, name_node.start_byte(), var_id);
1570
1571 if let Some(type_node) = param_node
1572 .child_by_field_name("type")
1573 .or_else(|| first_child_of_kind(param_node, "type_identifier"))
1574 .or_else(|| first_child_of_kind(param_node, "scoped_type_identifier"))
1575 .or_else(|| first_child_of_kind(param_node, "generic_type"))
1576 {
1577 add_typeof_for_catch_type(type_node, content, ast_graph, helper, var_id);
1578 }
1579}
1580
1581fn add_typeof_for_catch_type(
1582 type_node: Node,
1583 content: &[u8],
1584 ast_graph: &ASTGraph,
1585 helper: &mut GraphBuildHelper,
1586 var_id: sqry_core::graph::unified::node::NodeId,
1587) {
1588 if type_node.kind() == "union_type" {
1589 let mut cursor = type_node.walk();
1590 for child in type_node.children(&mut cursor) {
1591 if matches!(
1592 child.kind(),
1593 "type_identifier" | "scoped_type_identifier" | "generic_type"
1594 ) {
1595 let type_text = extract_type_name(child, content);
1596 if !type_text.is_empty() {
1597 let resolved_type = ast_graph
1598 .import_map
1599 .get(&type_text)
1600 .cloned()
1601 .unwrap_or(type_text);
1602 let type_id = helper.add_class(&resolved_type, None);
1603 helper.add_typeof_edge(var_id, type_id);
1604 }
1605 }
1606 }
1607 return;
1608 }
1609
1610 let type_text = extract_type_name(type_node, content);
1611 if type_text.is_empty() {
1612 return;
1613 }
1614 let resolved_type = ast_graph
1615 .import_map
1616 .get(&type_text)
1617 .cloned()
1618 .unwrap_or(type_text);
1619 let type_id = helper.add_class(&resolved_type, None);
1620 helper.add_typeof_edge(var_id, type_id);
1621}
1622
1623fn handle_lambda_parameter_declaration(
1624 node: Node,
1625 content: &[u8],
1626 ast_graph: &ASTGraph,
1627 scope_tree: &mut JavaScopeTree,
1628 helper: &mut GraphBuildHelper,
1629) {
1630 use sqry_core::graph::unified::node::NodeKind;
1631
1632 let Some(params_node) = node.child_by_field_name("parameters") else {
1633 return;
1634 };
1635 let lambda_prefix = format!("lambda@{}", node.start_byte());
1636
1637 if params_node.kind() == "identifier" {
1638 let name = extract_identifier(params_node, content);
1639 if name.is_empty() {
1640 return;
1641 }
1642 let qualified_param = format!("{lambda_prefix}::{name}");
1643 let span = Span::from_bytes(params_node.start_byte(), params_node.end_byte());
1644 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
1645 scope_tree.attach_node_id(&name, params_node.start_byte(), param_id);
1646 return;
1647 }
1648
1649 let mut cursor = params_node.walk();
1650 for child in params_node.children(&mut cursor) {
1651 match child.kind() {
1652 "identifier" => {
1653 let name = extract_identifier(child, content);
1654 if name.is_empty() {
1655 continue;
1656 }
1657 let qualified_param = format!("{lambda_prefix}::{name}");
1658 let span = Span::from_bytes(child.start_byte(), child.end_byte());
1659 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
1660 scope_tree.attach_node_id(&name, child.start_byte(), param_id);
1661 }
1662 "formal_parameter" => {
1663 let Some(name_node) = child.child_by_field_name("name") else {
1664 continue;
1665 };
1666 let Some(type_node) = child.child_by_field_name("type") else {
1667 continue;
1668 };
1669 let name = extract_identifier(name_node, content);
1670 if name.is_empty() {
1671 continue;
1672 }
1673 let type_text = extract_type_name(type_node, content);
1674 let resolved_type = ast_graph
1675 .import_map
1676 .get(&type_text)
1677 .cloned()
1678 .unwrap_or(type_text);
1679 let qualified_param = format!("{lambda_prefix}::{name}");
1680 let span = Span::from_bytes(child.start_byte(), child.end_byte());
1681 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
1682 scope_tree.attach_node_id(&name, name_node.start_byte(), param_id);
1683 let type_id = helper.add_class(&resolved_type, None);
1684 helper.add_typeof_edge(param_id, type_id);
1685 }
1686 _ => {}
1687 }
1688 }
1689}
1690
1691fn handle_try_with_resources_declaration(
1692 node: Node,
1693 content: &[u8],
1694 ast_graph: &ASTGraph,
1695 scope_tree: &mut JavaScopeTree,
1696 helper: &mut GraphBuildHelper,
1697) {
1698 let Some(resources) = node.child_by_field_name("resources") else {
1699 return;
1700 };
1701
1702 let mut cursor = resources.walk();
1703 for resource in resources.children(&mut cursor) {
1704 if resource.kind() != "resource" {
1705 continue;
1706 }
1707 let name_node = resource.child_by_field_name("name");
1708 let type_node = resource.child_by_field_name("type");
1709 let value_node = resource.child_by_field_name("value");
1710 if let Some(name_node) = name_node {
1711 if type_node.is_none() && value_node.is_none() {
1712 continue;
1713 }
1714 let name = extract_identifier(name_node, content);
1715 if name.is_empty() {
1716 continue;
1717 }
1718
1719 let qualified_var = format!("{}@{}", name, name_node.start_byte());
1720 let span = Span::from_bytes(resource.start_byte(), resource.end_byte());
1721 let var_id = helper.add_variable(&qualified_var, Some(span));
1722 scope_tree.attach_node_id(&name, name_node.start_byte(), var_id);
1723
1724 if let Some(type_node) = type_node {
1725 let type_text = extract_type_name(type_node, content);
1726 if !type_text.is_empty() {
1727 let resolved_type = ast_graph
1728 .import_map
1729 .get(&type_text)
1730 .cloned()
1731 .unwrap_or(type_text);
1732 let type_id = helper.add_class(&resolved_type, None);
1733 helper.add_typeof_edge(var_id, type_id);
1734 }
1735 }
1736 }
1737 }
1738}
1739
1740fn handle_instanceof_pattern_declaration(
1741 node: Node,
1742 content: &[u8],
1743 ast_graph: &ASTGraph,
1744 scope_tree: &mut JavaScopeTree,
1745 helper: &mut GraphBuildHelper,
1746) {
1747 let mut patterns = Vec::new();
1748 collect_pattern_declarations(node, &mut patterns);
1749 for (name_node, type_node) in patterns {
1750 let name = extract_identifier(name_node, content);
1751 if name.is_empty() {
1752 continue;
1753 }
1754 let qualified_var = format!("{}@{}", name, name_node.start_byte());
1755 let span = Span::from_bytes(name_node.start_byte(), name_node.end_byte());
1756 let var_id = helper.add_variable(&qualified_var, Some(span));
1757 scope_tree.attach_node_id(&name, name_node.start_byte(), var_id);
1758
1759 if let Some(type_node) = type_node {
1760 let type_text = extract_type_name(type_node, content);
1761 if !type_text.is_empty() {
1762 let resolved_type = ast_graph
1763 .import_map
1764 .get(&type_text)
1765 .cloned()
1766 .unwrap_or(type_text);
1767 let type_id = helper.add_class(&resolved_type, None);
1768 helper.add_typeof_edge(var_id, type_id);
1769 }
1770 }
1771 }
1772}
1773
1774fn handle_switch_pattern_declaration(
1775 node: Node,
1776 content: &[u8],
1777 ast_graph: &ASTGraph,
1778 scope_tree: &mut JavaScopeTree,
1779 helper: &mut GraphBuildHelper,
1780) {
1781 let mut patterns = Vec::new();
1782 collect_pattern_declarations(node, &mut patterns);
1783 for (name_node, type_node) in patterns {
1784 let name = extract_identifier(name_node, content);
1785 if name.is_empty() {
1786 continue;
1787 }
1788 let qualified_var = format!("{}@{}", name, name_node.start_byte());
1789 let span = Span::from_bytes(name_node.start_byte(), name_node.end_byte());
1790 let var_id = helper.add_variable(&qualified_var, Some(span));
1791 scope_tree.attach_node_id(&name, name_node.start_byte(), var_id);
1792
1793 if let Some(type_node) = type_node {
1794 let type_text = extract_type_name(type_node, content);
1795 if !type_text.is_empty() {
1796 let resolved_type = ast_graph
1797 .import_map
1798 .get(&type_text)
1799 .cloned()
1800 .unwrap_or(type_text);
1801 let type_id = helper.add_class(&resolved_type, None);
1802 helper.add_typeof_edge(var_id, type_id);
1803 }
1804 }
1805 }
1806}
1807
1808fn handle_compact_constructor_parameters(
1809 node: Node,
1810 content: &[u8],
1811 ast_graph: &ASTGraph,
1812 scope_tree: &mut JavaScopeTree,
1813 helper: &mut GraphBuildHelper,
1814) {
1815 use sqry_core::graph::unified::node::NodeKind;
1816
1817 let Some(record_node) = node
1818 .parent()
1819 .and_then(|parent| find_record_declaration(parent))
1820 else {
1821 return;
1822 };
1823
1824 let Some(record_name_node) = record_node.child_by_field_name("name") else {
1825 return;
1826 };
1827 let record_name = extract_identifier(record_name_node, content);
1828 if record_name.is_empty() {
1829 return;
1830 }
1831
1832 let mut components = Vec::new();
1833 collect_record_components_nodes(record_node, &mut components);
1834 for component in components {
1835 let Some(name_node) = component.child_by_field_name("name") else {
1836 continue;
1837 };
1838 let Some(type_node) = component.child_by_field_name("type") else {
1839 continue;
1840 };
1841 let name = extract_identifier(name_node, content);
1842 if name.is_empty() {
1843 continue;
1844 }
1845
1846 let type_text = extract_type_name(type_node, content);
1847 if type_text.is_empty() {
1848 continue;
1849 }
1850 let resolved_type = ast_graph
1851 .import_map
1852 .get(&type_text)
1853 .cloned()
1854 .unwrap_or(type_text);
1855
1856 let qualified_param = format!("{record_name}.<init>::{name}");
1857 let span = Span::from_bytes(component.start_byte(), component.end_byte());
1858 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
1859 scope_tree.attach_node_id(&name, name_node.start_byte(), param_id);
1860
1861 let type_id = helper.add_class(&resolved_type, None);
1862 helper.add_typeof_edge(param_id, type_id);
1863 }
1864}
1865
1866fn collect_pattern_declarations<'a>(
1867 node: Node<'a>,
1868 output: &mut Vec<(Node<'a>, Option<Node<'a>>)>,
1869) {
1870 if node.kind() == "type_pattern" {
1871 let name_node = node.child_by_field_name("name");
1872 let type_node = node.child_by_field_name("type").or_else(|| {
1873 let mut cursor = node.walk();
1874 for child in node.children(&mut cursor) {
1875 if matches!(
1876 child.kind(),
1877 "type_identifier" | "scoped_type_identifier" | "generic_type"
1878 ) {
1879 return Some(child);
1880 }
1881 }
1882 None
1883 });
1884 if let Some(name_node) = name_node {
1885 output.push((name_node, type_node));
1886 }
1887 }
1888
1889 if node.kind() == "record_pattern_component" {
1890 let mut name_node = None;
1891 let mut type_node = None;
1892 let mut cursor = node.walk();
1893 for child in node.children(&mut cursor) {
1894 if child.kind() == "identifier" {
1895 name_node = Some(child);
1896 } else if matches!(
1897 child.kind(),
1898 "type_identifier" | "scoped_type_identifier" | "generic_type"
1899 ) {
1900 type_node = Some(child);
1901 }
1902 }
1903 if let Some(name_node) = name_node {
1904 output.push((name_node, type_node));
1905 }
1906 }
1907
1908 let mut cursor = node.walk();
1909 for child in node.children(&mut cursor) {
1910 collect_pattern_declarations(child, output);
1911 }
1912}
1913
1914fn find_record_declaration(node: Node) -> Option<Node> {
1915 if node.kind() == "record_declaration" {
1916 return Some(node);
1917 }
1918 node.parent().and_then(find_record_declaration)
1919}
1920
1921fn collect_record_components_nodes<'a>(node: Node<'a>, output: &mut Vec<Node<'a>>) {
1922 let mut cursor = node.walk();
1923 for child in node.children(&mut cursor) {
1924 if child.kind() == "record_component" {
1925 output.push(child);
1926 }
1927 collect_record_components_nodes(child, output);
1928 }
1929}
1930
1931fn process_method_call_unified(
1933 call_node: Node,
1934 content: &[u8],
1935 ast_graph: &ASTGraph,
1936 helper: &mut GraphBuildHelper,
1937) {
1938 let Some(caller_context) = ast_graph.find_enclosing(call_node.start_byte()) else {
1939 return;
1940 };
1941 let Ok(callee_name) = extract_method_invocation_name(call_node, content) else {
1942 return;
1943 };
1944
1945 let callee_qualified =
1946 resolve_callee_qualified(&call_node, content, ast_graph, caller_context, &callee_name);
1947 let caller_method_id = ensure_caller_method(helper, caller_context);
1948 let target_method_id = helper.ensure_method(&callee_qualified, None, false, false);
1949
1950 add_call_edge(helper, caller_method_id, target_method_id, call_node);
1951}
1952
1953fn process_constructor_call_unified(
1955 new_node: Node,
1956 content: &[u8],
1957 ast_graph: &ASTGraph,
1958 helper: &mut GraphBuildHelper,
1959) {
1960 let Some(caller_context) = ast_graph.find_enclosing(new_node.start_byte()) else {
1961 return;
1962 };
1963
1964 let Some(type_node) = new_node.child_by_field_name("type") else {
1965 return;
1966 };
1967
1968 let class_name = extract_type_name(type_node, content);
1969 if class_name.is_empty() {
1970 return;
1971 }
1972
1973 let qualified_class = qualify_constructor_class(&class_name, caller_context);
1974 let constructor_name = format!("{qualified_class}.<init>");
1975
1976 let caller_method_id = ensure_caller_method(helper, caller_context);
1977 let target_method_id = helper.ensure_method(&constructor_name, None, false, false);
1978 add_call_edge(helper, caller_method_id, target_method_id, new_node);
1979}
1980
1981fn count_call_arguments(call_node: Node<'_>) -> u8 {
1982 let Some(args_node) = call_node.child_by_field_name("arguments") else {
1983 return 255;
1984 };
1985 let count = args_node.named_child_count();
1986 if count <= 254 {
1987 u8::try_from(count).unwrap_or(u8::MAX)
1988 } else {
1989 u8::MAX
1990 }
1991}
1992
1993fn process_import_unified(import_node: Node, content: &[u8], helper: &mut GraphBuildHelper) {
1995 let has_asterisk = import_has_wildcard(import_node);
1996 let Some(mut imported_name) = extract_import_name(import_node, content) else {
1997 return;
1998 };
1999 if has_asterisk {
2000 imported_name = format!("{imported_name}.*");
2001 }
2002
2003 let module_id = helper.add_module("<module>", None);
2004 let external_id = helper.add_import(
2005 &imported_name,
2006 Some(Span::from_bytes(
2007 import_node.start_byte(),
2008 import_node.end_byte(),
2009 )),
2010 );
2011
2012 helper.add_import_edge(module_id, external_id);
2013}
2014
2015fn ensure_caller_method(
2016 helper: &mut GraphBuildHelper,
2017 caller_context: &MethodContext,
2018) -> sqry_core::graph::unified::node::NodeId {
2019 helper.ensure_method(
2020 caller_context.qualified_name(),
2021 Some(Span::from_bytes(
2022 caller_context.span.0,
2023 caller_context.span.1,
2024 )),
2025 false,
2026 caller_context.is_static,
2027 )
2028}
2029
2030fn resolve_callee_qualified(
2031 call_node: &Node,
2032 content: &[u8],
2033 ast_graph: &ASTGraph,
2034 caller_context: &MethodContext,
2035 callee_name: &str,
2036) -> String {
2037 if let Some(object_node) = call_node.child_by_field_name("object") {
2038 let object_text = extract_node_text(object_node, content);
2039 return resolve_member_call_target(&object_text, ast_graph, caller_context, callee_name);
2040 }
2041
2042 build_member_symbol(
2043 caller_context.package_name.as_deref(),
2044 &caller_context.class_stack,
2045 callee_name,
2046 )
2047}
2048
2049fn resolve_member_call_target(
2050 object_text: &str,
2051 ast_graph: &ASTGraph,
2052 caller_context: &MethodContext,
2053 callee_name: &str,
2054) -> String {
2055 if object_text.contains('.') {
2056 return format!("{object_text}.{callee_name}");
2057 }
2058 if object_text == "this" {
2059 return build_member_symbol(
2060 caller_context.package_name.as_deref(),
2061 &caller_context.class_stack,
2062 callee_name,
2063 );
2064 }
2065
2066 if let Some(class_name) = caller_context.class_stack.last() {
2068 let qualified_field = format!("{class_name}::{object_text}");
2069 if let Some((field_type, _is_final, _visibility, _is_static)) =
2070 ast_graph.field_types.get(&qualified_field)
2071 {
2072 return format!("{field_type}.{callee_name}");
2073 }
2074 }
2075
2076 if let Some((field_type, _is_final, _visibility, _is_static)) =
2078 ast_graph.field_types.get(object_text)
2079 {
2080 return format!("{field_type}.{callee_name}");
2081 }
2082
2083 if let Some(type_fqn) = ast_graph.import_map.get(object_text) {
2084 return format!("{type_fqn}.{callee_name}");
2085 }
2086
2087 format!("{object_text}.{callee_name}")
2088}
2089
2090fn qualify_constructor_class(class_name: &str, caller_context: &MethodContext) -> String {
2091 if class_name.contains('.') {
2092 class_name.to_string()
2093 } else if let Some(pkg) = caller_context.package_name.as_deref() {
2094 format!("{pkg}.{class_name}")
2095 } else {
2096 class_name.to_string()
2097 }
2098}
2099
2100fn add_call_edge(
2101 helper: &mut GraphBuildHelper,
2102 caller_method_id: sqry_core::graph::unified::node::NodeId,
2103 target_method_id: sqry_core::graph::unified::node::NodeId,
2104 call_node: Node,
2105) {
2106 let argument_count = count_call_arguments(call_node);
2107 let call_span = Span::from_bytes(call_node.start_byte(), call_node.end_byte());
2108 helper.add_call_edge_full_with_span(
2109 caller_method_id,
2110 target_method_id,
2111 argument_count,
2112 false,
2113 vec![call_span],
2114 );
2115}
2116
2117fn import_has_wildcard(import_node: Node) -> bool {
2118 let mut cursor = import_node.walk();
2119 import_node
2120 .children(&mut cursor)
2121 .any(|child| child.kind() == "asterisk")
2122}
2123
2124fn extract_import_name(import_node: Node, content: &[u8]) -> Option<String> {
2125 let mut cursor = import_node.walk();
2126 for child in import_node.children(&mut cursor) {
2127 if child.kind() == "scoped_identifier" || child.kind() == "identifier" {
2128 return Some(extract_full_identifier(child, content));
2129 }
2130 }
2131 None
2132}
2133
2134fn process_inheritance(
2144 class_node: Node,
2145 content: &[u8],
2146 package_name: Option<&str>,
2147 child_class_id: sqry_core::graph::unified::node::NodeId,
2148 helper: &mut GraphBuildHelper,
2149) {
2150 if let Some(superclass_node) = class_node.child_by_field_name("superclass") {
2152 let parent_type_name = extract_type_from_superclass(superclass_node, content);
2154 if !parent_type_name.is_empty() {
2155 let parent_qualified = qualify_type_name(&parent_type_name, package_name);
2157 let parent_id = helper.add_class(&parent_qualified, None);
2158 helper.add_inherits_edge(child_class_id, parent_id);
2159 }
2160 }
2161}
2162
2163fn process_implements(
2169 class_node: Node,
2170 content: &[u8],
2171 package_name: Option<&str>,
2172 class_id: sqry_core::graph::unified::node::NodeId,
2173 helper: &mut GraphBuildHelper,
2174) {
2175 let interfaces_node = class_node
2181 .child_by_field_name("interfaces")
2182 .or_else(|| class_node.child_by_field_name("super_interfaces"));
2183
2184 if let Some(node) = interfaces_node {
2185 extract_interface_types(node, content, package_name, class_id, helper);
2186 return;
2187 }
2188
2189 let mut cursor = class_node.walk();
2191 for child in class_node.children(&mut cursor) {
2192 if child.kind() == "super_interfaces" {
2194 extract_interface_types(child, content, package_name, class_id, helper);
2195 return;
2196 }
2197 }
2198}
2199
2200fn process_interface_extends(
2218 interface_node: Node,
2219 content: &[u8],
2220 package_name: Option<&str>,
2221 interface_id: sqry_core::graph::unified::node::NodeId,
2222 helper: &mut GraphBuildHelper,
2223) {
2224 let mut cursor = interface_node.walk();
2226 for child in interface_node.children(&mut cursor) {
2227 if child.kind() == "extends_interfaces" {
2228 extract_parent_interfaces_for_inherits(
2230 child,
2231 content,
2232 package_name,
2233 interface_id,
2234 helper,
2235 );
2236 return;
2237 }
2238 }
2239}
2240
2241fn extract_parent_interfaces_for_inherits(
2244 extends_node: Node,
2245 content: &[u8],
2246 package_name: Option<&str>,
2247 child_interface_id: sqry_core::graph::unified::node::NodeId,
2248 helper: &mut GraphBuildHelper,
2249) {
2250 let mut cursor = extends_node.walk();
2251 for child in extends_node.children(&mut cursor) {
2252 match child.kind() {
2253 "type_identifier" => {
2254 let type_name = extract_identifier(child, content);
2255 if !type_name.is_empty() {
2256 let parent_qualified = qualify_type_name(&type_name, package_name);
2257 let parent_id = helper.add_interface(&parent_qualified, None);
2258 helper.add_inherits_edge(child_interface_id, parent_id);
2259 }
2260 }
2261 "type_list" => {
2262 let mut type_cursor = child.walk();
2263 for type_child in child.children(&mut type_cursor) {
2264 if let Some(type_name) = extract_type_identifier(type_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 "generic_type" | "scoped_type_identifier" => {
2274 if let Some(type_name) = extract_type_identifier(child, content)
2275 && !type_name.is_empty()
2276 {
2277 let parent_qualified = qualify_type_name(&type_name, package_name);
2278 let parent_id = helper.add_interface(&parent_qualified, None);
2279 helper.add_inherits_edge(child_interface_id, parent_id);
2280 }
2281 }
2282 _ => {}
2283 }
2284 }
2285}
2286
2287fn extract_type_from_superclass(superclass_node: Node, content: &[u8]) -> String {
2289 if superclass_node.kind() == "type_identifier" {
2291 return extract_identifier(superclass_node, content);
2292 }
2293
2294 let mut cursor = superclass_node.walk();
2296 for child in superclass_node.children(&mut cursor) {
2297 if let Some(name) = extract_type_identifier(child, content) {
2298 return name;
2299 }
2300 }
2301
2302 extract_identifier(superclass_node, content)
2304}
2305
2306fn extract_interface_types(
2317 interfaces_node: Node,
2318 content: &[u8],
2319 package_name: Option<&str>,
2320 implementor_id: sqry_core::graph::unified::node::NodeId,
2321 helper: &mut GraphBuildHelper,
2322) {
2323 let mut cursor = interfaces_node.walk();
2325 for child in interfaces_node.children(&mut cursor) {
2326 match child.kind() {
2327 "type_identifier" => {
2329 let type_name = extract_identifier(child, content);
2330 if !type_name.is_empty() {
2331 let interface_qualified = qualify_type_name(&type_name, package_name);
2332 let interface_id = helper.add_interface(&interface_qualified, None);
2333 helper.add_implements_edge(implementor_id, interface_id);
2334 }
2335 }
2336 "type_list" => {
2338 let mut type_cursor = child.walk();
2339 for type_child in child.children(&mut type_cursor) {
2340 if let Some(type_name) = extract_type_identifier(type_child, content)
2341 && !type_name.is_empty()
2342 {
2343 let interface_qualified = qualify_type_name(&type_name, package_name);
2344 let interface_id = helper.add_interface(&interface_qualified, None);
2345 helper.add_implements_edge(implementor_id, interface_id);
2346 }
2347 }
2348 }
2349 "generic_type" | "scoped_type_identifier" => {
2351 if let Some(type_name) = extract_type_identifier(child, content)
2352 && !type_name.is_empty()
2353 {
2354 let interface_qualified = qualify_type_name(&type_name, package_name);
2355 let interface_id = helper.add_interface(&interface_qualified, None);
2356 helper.add_implements_edge(implementor_id, interface_id);
2357 }
2358 }
2359 _ => {}
2360 }
2361 }
2362}
2363
2364fn extract_type_identifier(node: Node, content: &[u8]) -> Option<String> {
2366 match node.kind() {
2367 "type_identifier" => Some(extract_identifier(node, content)),
2368 "generic_type" => {
2369 if let Some(name_node) = node.child_by_field_name("name") {
2371 Some(extract_identifier(name_node, content))
2372 } else {
2373 let mut cursor = node.walk();
2375 for child in node.children(&mut cursor) {
2376 if child.kind() == "type_identifier" {
2377 return Some(extract_identifier(child, content));
2378 }
2379 }
2380 None
2381 }
2382 }
2383 "scoped_type_identifier" => {
2384 Some(extract_full_identifier(node, content))
2386 }
2387 _ => None,
2388 }
2389}
2390
2391fn qualify_type_name(type_name: &str, package_name: Option<&str>) -> String {
2393 if type_name.contains('.') {
2395 return type_name.to_string();
2396 }
2397
2398 if let Some(pkg) = package_name {
2400 format!("{pkg}.{type_name}")
2401 } else {
2402 type_name.to_string()
2403 }
2404}
2405
2406#[allow(clippy::type_complexity)]
2415fn extract_field_and_import_types(
2416 node: Node,
2417 content: &[u8],
2418) -> (
2419 HashMap<String, (String, bool, Option<sqry_core::schema::Visibility>, bool)>,
2420 HashMap<String, String>,
2421) {
2422 let import_map = extract_import_map(node, content);
2424
2425 let mut field_types = HashMap::new();
2426 let mut class_stack = Vec::new();
2427 extract_field_types_recursive(
2428 node,
2429 content,
2430 &import_map,
2431 &mut field_types,
2432 &mut class_stack,
2433 );
2434
2435 (field_types, import_map)
2436}
2437
2438fn extract_import_map(node: Node, content: &[u8]) -> HashMap<String, String> {
2440 let mut import_map = HashMap::new();
2441 collect_import_map_recursive(node, content, &mut import_map);
2442 import_map
2443}
2444
2445fn collect_import_map_recursive(
2446 node: Node,
2447 content: &[u8],
2448 import_map: &mut HashMap<String, String>,
2449) {
2450 if node.kind() == "import_declaration" {
2451 let full_path = node.utf8_text(content).unwrap_or("");
2455
2456 if let Some(path_start) = full_path.find("import ") {
2459 let after_import = &full_path[path_start + 7..].trim();
2460 if let Some(path_end) = after_import.find(';') {
2461 let import_path = &after_import[..path_end].trim();
2462
2463 if let Some(simple_name) = import_path.rsplit('.').next() {
2465 import_map.insert(simple_name.to_string(), (*import_path).to_string());
2466 }
2467 }
2468 }
2469 }
2470
2471 let mut cursor = node.walk();
2473 for child in node.children(&mut cursor) {
2474 collect_import_map_recursive(child, content, import_map);
2475 }
2476}
2477
2478fn extract_field_types_recursive(
2479 node: Node,
2480 content: &[u8],
2481 import_map: &HashMap<String, String>,
2482 field_types: &mut HashMap<String, (String, bool, Option<sqry_core::schema::Visibility>, bool)>,
2483 class_stack: &mut Vec<String>,
2484) {
2485 if matches!(
2487 node.kind(),
2488 "class_declaration" | "interface_declaration" | "enum_declaration"
2489 ) && let Some(name_node) = node.child_by_field_name("name")
2490 {
2491 let class_name = extract_identifier(name_node, content);
2492 class_stack.push(class_name);
2493
2494 if let Some(body_node) = node.child_by_field_name("body") {
2496 let mut cursor = body_node.walk();
2497 for child in body_node.children(&mut cursor) {
2498 extract_field_types_recursive(child, content, import_map, field_types, class_stack);
2499 }
2500 }
2501
2502 class_stack.pop();
2504 return; }
2506
2507 if node.kind() == "field_declaration" {
2514 let is_final = has_modifier(node, "final", content);
2516 let is_static = has_modifier(node, "static", content);
2517
2518 let visibility = if has_modifier(node, "public", content) {
2521 Some(sqry_core::schema::Visibility::Public)
2522 } else {
2523 Some(sqry_core::schema::Visibility::Private)
2525 };
2526
2527 if let Some(type_node) = node.child_by_field_name("type") {
2529 let type_text = extract_type_name_internal(type_node, content);
2530 if !type_text.is_empty() {
2531 let resolved_type = import_map
2533 .get(&type_text)
2534 .cloned()
2535 .unwrap_or(type_text.clone());
2536
2537 let mut cursor = node.walk();
2539 for child in node.children(&mut cursor) {
2540 if child.kind() == "variable_declarator"
2541 && let Some(name_node) = child.child_by_field_name("name")
2542 {
2543 let field_name = extract_identifier(name_node, content);
2544
2545 let qualified_field = if class_stack.is_empty() {
2548 field_name
2549 } else {
2550 let class_path = class_stack.join("::");
2551 format!("{class_path}::{field_name}")
2552 };
2553
2554 field_types.insert(
2555 qualified_field,
2556 (resolved_type.clone(), is_final, visibility, is_static),
2557 );
2558 }
2559 }
2560 }
2561 }
2562 }
2563
2564 let mut cursor = node.walk();
2566 for child in node.children(&mut cursor) {
2567 extract_field_types_recursive(child, content, import_map, field_types, class_stack);
2568 }
2569}
2570
2571fn extract_type_name_internal(type_node: Node, content: &[u8]) -> String {
2573 match type_node.kind() {
2574 "generic_type" => {
2575 if let Some(name_node) = type_node.child_by_field_name("name") {
2577 extract_identifier(name_node, content)
2578 } else {
2579 extract_identifier(type_node, content)
2580 }
2581 }
2582 "scoped_type_identifier" => {
2583 extract_full_identifier(type_node, content)
2585 }
2586 _ => extract_identifier(type_node, content),
2587 }
2588}
2589
2590fn extract_identifier(node: Node, content: &[u8]) -> String {
2595 node.utf8_text(content).unwrap_or("").to_string()
2596}
2597
2598fn extract_node_text(node: Node, content: &[u8]) -> String {
2599 node.utf8_text(content).unwrap_or("").to_string()
2600}
2601
2602fn extract_full_identifier(node: Node, content: &[u8]) -> String {
2603 node.utf8_text(content).unwrap_or("").to_string()
2604}
2605
2606fn first_child_of_kind<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
2607 let mut cursor = node.walk();
2608 node.children(&mut cursor)
2609 .find(|&child| child.kind() == kind)
2610}
2611
2612fn extract_method_invocation_name(call_node: Node, content: &[u8]) -> GraphResult<String> {
2613 if let Some(name_node) = call_node.child_by_field_name("name") {
2615 Ok(extract_identifier(name_node, content))
2616 } else {
2617 let mut cursor = call_node.walk();
2619 for child in call_node.children(&mut cursor) {
2620 if child.kind() == "identifier" {
2621 return Ok(extract_identifier(child, content));
2622 }
2623 }
2624
2625 Err(GraphBuilderError::ParseError {
2626 span: Span::from_bytes(call_node.start_byte(), call_node.end_byte()),
2627 reason: "Method invocation missing name".into(),
2628 })
2629 }
2630}
2631
2632fn extract_type_name(type_node: Node, content: &[u8]) -> String {
2633 match type_node.kind() {
2635 "generic_type" => {
2636 if let Some(name_node) = type_node.child_by_field_name("name") {
2638 extract_identifier(name_node, content)
2639 } else {
2640 extract_identifier(type_node, content)
2641 }
2642 }
2643 "scoped_type_identifier" => {
2644 extract_full_identifier(type_node, content)
2646 }
2647 _ => extract_identifier(type_node, content),
2648 }
2649}
2650
2651fn extract_full_return_type(type_node: Node, content: &[u8]) -> String {
2654 type_node.utf8_text(content).unwrap_or("").to_string()
2657}
2658
2659fn has_modifier(node: Node, modifier: &str, content: &[u8]) -> bool {
2660 let mut cursor = node.walk();
2661 for child in node.children(&mut cursor) {
2662 if child.kind() == "modifiers" {
2663 let mut mod_cursor = child.walk();
2664 for modifier_child in child.children(&mut mod_cursor) {
2665 if extract_identifier(modifier_child, content) == modifier {
2666 return true;
2667 }
2668 }
2669 }
2670 }
2671 false
2672}
2673
2674#[allow(clippy::unnecessary_wraps)]
2677fn extract_visibility(node: Node, content: &[u8]) -> Option<String> {
2678 if has_modifier(node, "public", content) {
2679 Some("public".to_string())
2680 } else if has_modifier(node, "private", content) {
2681 Some("private".to_string())
2682 } else if has_modifier(node, "protected", content) {
2683 Some("protected".to_string())
2684 } else {
2685 Some("package-private".to_string())
2687 }
2688}
2689
2690fn is_public(node: Node, content: &[u8]) -> bool {
2696 has_modifier(node, "public", content)
2697}
2698
2699fn is_private(node: Node, content: &[u8]) -> bool {
2701 has_modifier(node, "private", content)
2702}
2703
2704fn export_from_file_module(
2706 helper: &mut GraphBuildHelper,
2707 exported: sqry_core::graph::unified::node::NodeId,
2708) {
2709 let module_id = helper.add_module(FILE_MODULE_NAME, None);
2710 helper.add_export_edge(module_id, exported);
2711}
2712
2713fn process_class_member_exports(
2718 body_node: Node,
2719 content: &[u8],
2720 class_qualified_name: &str,
2721 helper: &mut GraphBuildHelper,
2722 is_interface: bool,
2723) {
2724 for i in 0..body_node.child_count() {
2725 #[allow(clippy::cast_possible_truncation)]
2726 if let Some(child) = body_node.child(i as u32) {
2728 match child.kind() {
2729 "method_declaration" => {
2730 let should_export = if is_interface {
2733 !is_private(child, content)
2735 } else {
2736 is_public(child, content)
2738 };
2739
2740 if should_export && let Some(name_node) = child.child_by_field_name("name") {
2741 let method_name = extract_identifier(name_node, content);
2742 let qualified_name = format!("{class_qualified_name}.{method_name}");
2743 let span = Span::from_bytes(child.start_byte(), child.end_byte());
2744 let is_static = has_modifier(child, "static", content);
2745 let method_id =
2746 helper.add_method(&qualified_name, Some(span), false, is_static);
2747 export_from_file_module(helper, method_id);
2748 }
2749 }
2750 "constructor_declaration" => {
2751 if is_public(child, content) {
2752 let qualified_name = format!("{class_qualified_name}.<init>");
2753 let span = Span::from_bytes(child.start_byte(), child.end_byte());
2754 let method_id =
2755 helper.add_method(&qualified_name, Some(span), false, false);
2756 export_from_file_module(helper, method_id);
2757 }
2758 }
2759 "field_declaration" => {
2760 if is_public(child, content) {
2761 let mut cursor = child.walk();
2763 for field_child in child.children(&mut cursor) {
2764 if field_child.kind() == "variable_declarator"
2765 && let Some(name_node) = field_child.child_by_field_name("name")
2766 {
2767 let field_name = extract_identifier(name_node, content);
2768 let qualified_name = format!("{class_qualified_name}.{field_name}");
2769 let span = Span::from_bytes(
2770 field_child.start_byte(),
2771 field_child.end_byte(),
2772 );
2773
2774 let is_final = has_modifier(child, "final", content);
2776 let field_id = if is_final {
2777 helper.add_constant(&qualified_name, Some(span))
2778 } else {
2779 helper.add_variable(&qualified_name, Some(span))
2780 };
2781 export_from_file_module(helper, field_id);
2782 }
2783 }
2784 }
2785 }
2786 "constant_declaration" => {
2787 let mut cursor = child.walk();
2789 for const_child in child.children(&mut cursor) {
2790 if const_child.kind() == "variable_declarator"
2791 && let Some(name_node) = const_child.child_by_field_name("name")
2792 {
2793 let const_name = extract_identifier(name_node, content);
2794 let qualified_name = format!("{class_qualified_name}.{const_name}");
2795 let span =
2796 Span::from_bytes(const_child.start_byte(), const_child.end_byte());
2797 let const_id = helper.add_constant(&qualified_name, Some(span));
2798 export_from_file_module(helper, const_id);
2799 }
2800 }
2801 }
2802 "enum_constant" => {
2803 if let Some(name_node) = child.child_by_field_name("name") {
2805 let const_name = extract_identifier(name_node, content);
2806 let qualified_name = format!("{class_qualified_name}.{const_name}");
2807 let span = Span::from_bytes(child.start_byte(), child.end_byte());
2808 let const_id = helper.add_constant(&qualified_name, Some(span));
2809 export_from_file_module(helper, const_id);
2810 }
2811 }
2812 _ => {}
2813 }
2814 }
2815 }
2816}
2817
2818fn detect_ffi_imports(node: Node, content: &[u8]) -> (bool, bool) {
2825 let mut has_jna = false;
2826 let mut has_panama = false;
2827
2828 detect_ffi_imports_recursive(node, content, &mut has_jna, &mut has_panama);
2829
2830 (has_jna, has_panama)
2831}
2832
2833fn detect_ffi_imports_recursive(
2834 node: Node,
2835 content: &[u8],
2836 has_jna: &mut bool,
2837 has_panama: &mut bool,
2838) {
2839 if node.kind() == "import_declaration" {
2840 let import_text = node.utf8_text(content).unwrap_or("");
2841
2842 if import_text.contains("com.sun.jna") || import_text.contains("net.java.dev.jna") {
2844 *has_jna = true;
2845 }
2846
2847 if import_text.contains("java.lang.foreign") {
2849 *has_panama = true;
2850 }
2851 }
2852
2853 let mut cursor = node.walk();
2854 for child in node.children(&mut cursor) {
2855 detect_ffi_imports_recursive(child, content, has_jna, has_panama);
2856 }
2857}
2858
2859fn find_jna_library_interfaces(node: Node, content: &[u8]) -> Vec<String> {
2862 let mut jna_interfaces = Vec::new();
2863 find_jna_library_interfaces_recursive(node, content, &mut jna_interfaces);
2864 jna_interfaces
2865}
2866
2867fn find_jna_library_interfaces_recursive(
2868 node: Node,
2869 content: &[u8],
2870 jna_interfaces: &mut Vec<String>,
2871) {
2872 if node.kind() == "interface_declaration" {
2873 if let Some(name_node) = node.child_by_field_name("name") {
2875 let interface_name = extract_identifier(name_node, content);
2876
2877 let mut cursor = node.walk();
2879 for child in node.children(&mut cursor) {
2880 if child.kind() == "extends_interfaces" {
2881 let extends_text = child.utf8_text(content).unwrap_or("");
2882 if extends_text.contains("Library") {
2884 jna_interfaces.push(interface_name.clone());
2885 }
2886 }
2887 }
2888 }
2889 }
2890
2891 let mut cursor = node.walk();
2892 for child in node.children(&mut cursor) {
2893 find_jna_library_interfaces_recursive(child, content, jna_interfaces);
2894 }
2895}
2896
2897fn build_ffi_call_edge(
2900 call_node: Node,
2901 content: &[u8],
2902 caller_context: &MethodContext,
2903 ast_graph: &ASTGraph,
2904 helper: &mut GraphBuildHelper,
2905) -> bool {
2906 let Ok(method_name) = extract_method_invocation_name(call_node, content) else {
2908 return false;
2909 };
2910
2911 if ast_graph.has_jna_import && is_jna_native_load(call_node, content, &method_name) {
2913 let library_name = extract_jna_library_name(call_node, content);
2914 build_jna_native_load_edge(caller_context, &library_name, call_node, helper);
2915 return true;
2916 }
2917
2918 if ast_graph.has_jna_import
2920 && let Some(object_node) = call_node.child_by_field_name("object")
2921 {
2922 let object_text = extract_node_text(object_node, content);
2923
2924 let field_type = if let Some(class_name) = caller_context.class_stack.last() {
2926 let qualified_field = format!("{class_name}::{object_text}");
2927 ast_graph
2928 .field_types
2929 .get(&qualified_field)
2930 .or_else(|| ast_graph.field_types.get(&object_text))
2931 } else {
2932 ast_graph.field_types.get(&object_text)
2933 };
2934
2935 if let Some((type_name, _is_final, _visibility, _is_static)) = field_type {
2937 let simple_type = simple_type_name(type_name);
2938 if ast_graph.jna_library_interfaces.contains(&simple_type) {
2939 build_jna_method_call_edge(
2940 caller_context,
2941 &simple_type,
2942 &method_name,
2943 call_node,
2944 helper,
2945 );
2946 return true;
2947 }
2948 }
2949 }
2950
2951 if ast_graph.has_panama_import {
2953 if let Some(object_node) = call_node.child_by_field_name("object") {
2954 let object_text = extract_node_text(object_node, content);
2955
2956 if object_text == "Linker" && method_name == "nativeLinker" {
2958 build_panama_linker_edge(caller_context, call_node, helper);
2959 return true;
2960 }
2961
2962 if object_text == "SymbolLookup" && method_name == "libraryLookup" {
2964 let library_name = extract_first_string_arg(call_node, content);
2965 build_panama_library_lookup_edge(caller_context, &library_name, call_node, helper);
2966 return true;
2967 }
2968
2969 if method_name == "invokeExact" || method_name == "invoke" {
2971 if is_potential_panama_invoke(call_node, content) {
2974 build_panama_invoke_edge(caller_context, &method_name, call_node, helper);
2975 return true;
2976 }
2977 }
2978 }
2979
2980 if method_name == "nativeLinker" {
2982 let full_text = call_node.utf8_text(content).unwrap_or("");
2983 if full_text.contains("Linker") {
2984 build_panama_linker_edge(caller_context, call_node, helper);
2985 return true;
2986 }
2987 }
2988 }
2989
2990 false
2991}
2992
2993fn is_jna_native_load(call_node: Node, content: &[u8], method_name: &str) -> bool {
2995 if method_name != "load" && method_name != "loadLibrary" {
2996 return false;
2997 }
2998
2999 if let Some(object_node) = call_node.child_by_field_name("object") {
3000 let object_text = extract_node_text(object_node, content);
3001 return object_text == "Native" || object_text == "com.sun.jna.Native";
3002 }
3003
3004 false
3005}
3006
3007fn extract_jna_library_name(call_node: Node, content: &[u8]) -> String {
3010 if let Some(args_node) = call_node.child_by_field_name("arguments") {
3011 let mut cursor = args_node.walk();
3012 for child in args_node.children(&mut cursor) {
3013 if child.kind() == "string_literal" {
3014 let text = child.utf8_text(content).unwrap_or("\"unknown\"");
3015 return text.trim_matches('"').to_string();
3017 }
3018 }
3019 }
3020 "unknown".to_string()
3021}
3022
3023fn extract_first_string_arg(call_node: Node, content: &[u8]) -> String {
3025 if let Some(args_node) = call_node.child_by_field_name("arguments") {
3026 let mut cursor = args_node.walk();
3027 for child in args_node.children(&mut cursor) {
3028 if child.kind() == "string_literal" {
3029 let text = child.utf8_text(content).unwrap_or("\"unknown\"");
3030 return text.trim_matches('"').to_string();
3031 }
3032 }
3033 }
3034 "unknown".to_string()
3035}
3036
3037fn is_potential_panama_invoke(call_node: Node, content: &[u8]) -> bool {
3039 if let Some(object_node) = call_node.child_by_field_name("object") {
3041 let object_text = extract_node_text(object_node, content);
3042 let lower = object_text.to_lowercase();
3044 return lower.contains("handle")
3045 || lower.contains("downcall")
3046 || lower.contains("mh")
3047 || lower.contains("foreign");
3048 }
3049 false
3050}
3051
3052fn simple_type_name(type_name: &str) -> String {
3054 type_name
3055 .rsplit('.')
3056 .next()
3057 .unwrap_or(type_name)
3058 .to_string()
3059}
3060
3061fn build_jna_native_load_edge(
3063 caller_context: &MethodContext,
3064 library_name: &str,
3065 call_node: Node,
3066 helper: &mut GraphBuildHelper,
3067) {
3068 let caller_id = helper.ensure_method(
3069 caller_context.qualified_name(),
3070 Some(Span::from_bytes(
3071 caller_context.span.0,
3072 caller_context.span.1,
3073 )),
3074 false,
3075 caller_context.is_static,
3076 );
3077
3078 let target_name = format!("native::{library_name}");
3079 let target_id = helper.add_function(
3080 &target_name,
3081 Some(Span::from_bytes(
3082 call_node.start_byte(),
3083 call_node.end_byte(),
3084 )),
3085 false,
3086 false,
3087 );
3088
3089 helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3090}
3091
3092fn build_jna_method_call_edge(
3094 caller_context: &MethodContext,
3095 interface_name: &str,
3096 method_name: &str,
3097 call_node: Node,
3098 helper: &mut GraphBuildHelper,
3099) {
3100 let caller_id = helper.ensure_method(
3101 caller_context.qualified_name(),
3102 Some(Span::from_bytes(
3103 caller_context.span.0,
3104 caller_context.span.1,
3105 )),
3106 false,
3107 caller_context.is_static,
3108 );
3109
3110 let target_name = format!("native::{interface_name}::{method_name}");
3111 let target_id = helper.add_function(
3112 &target_name,
3113 Some(Span::from_bytes(
3114 call_node.start_byte(),
3115 call_node.end_byte(),
3116 )),
3117 false,
3118 false,
3119 );
3120
3121 helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3122}
3123
3124fn build_panama_linker_edge(
3126 caller_context: &MethodContext,
3127 call_node: Node,
3128 helper: &mut GraphBuildHelper,
3129) {
3130 let caller_id = helper.ensure_method(
3131 caller_context.qualified_name(),
3132 Some(Span::from_bytes(
3133 caller_context.span.0,
3134 caller_context.span.1,
3135 )),
3136 false,
3137 caller_context.is_static,
3138 );
3139
3140 let target_name = "native::panama::nativeLinker";
3141 let target_id = helper.add_function(
3142 target_name,
3143 Some(Span::from_bytes(
3144 call_node.start_byte(),
3145 call_node.end_byte(),
3146 )),
3147 false,
3148 false,
3149 );
3150
3151 helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3152}
3153
3154fn build_panama_library_lookup_edge(
3156 caller_context: &MethodContext,
3157 library_name: &str,
3158 call_node: Node,
3159 helper: &mut GraphBuildHelper,
3160) {
3161 let caller_id = helper.ensure_method(
3162 caller_context.qualified_name(),
3163 Some(Span::from_bytes(
3164 caller_context.span.0,
3165 caller_context.span.1,
3166 )),
3167 false,
3168 caller_context.is_static,
3169 );
3170
3171 let target_name = format!("native::panama::{library_name}");
3172 let target_id = helper.add_function(
3173 &target_name,
3174 Some(Span::from_bytes(
3175 call_node.start_byte(),
3176 call_node.end_byte(),
3177 )),
3178 false,
3179 false,
3180 );
3181
3182 helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3183}
3184
3185fn build_panama_invoke_edge(
3187 caller_context: &MethodContext,
3188 method_name: &str,
3189 call_node: Node,
3190 helper: &mut GraphBuildHelper,
3191) {
3192 let caller_id = helper.ensure_method(
3193 caller_context.qualified_name(),
3194 Some(Span::from_bytes(
3195 caller_context.span.0,
3196 caller_context.span.1,
3197 )),
3198 false,
3199 caller_context.is_static,
3200 );
3201
3202 let target_name = format!("native::panama::{method_name}");
3203 let target_id = helper.add_function(
3204 &target_name,
3205 Some(Span::from_bytes(
3206 call_node.start_byte(),
3207 call_node.end_byte(),
3208 )),
3209 false,
3210 false,
3211 );
3212
3213 helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3214}
3215
3216fn build_jni_native_method_edge(method_context: &MethodContext, helper: &mut GraphBuildHelper) {
3219 let method_id = helper.ensure_method(
3221 method_context.qualified_name(),
3222 Some(Span::from_bytes(
3223 method_context.span.0,
3224 method_context.span.1,
3225 )),
3226 false,
3227 method_context.is_static,
3228 );
3229
3230 let native_target = format!("native::jni::{}", method_context.qualified_name());
3233 let target_id = helper.add_function(&native_target, None, false, false);
3234
3235 helper.add_ffi_edge(method_id, target_id, FfiConvention::C);
3236}
3237
3238fn extract_spring_route_info(method_node: Node, content: &[u8]) -> Option<(String, String)> {
3252 let mut cursor = method_node.walk();
3254 let modifiers_node = method_node
3255 .children(&mut cursor)
3256 .find(|child| child.kind() == "modifiers")?;
3257
3258 let mut mod_cursor = modifiers_node.walk();
3260 for annotation_node in modifiers_node.children(&mut mod_cursor) {
3261 if annotation_node.kind() != "annotation" {
3262 continue;
3263 }
3264
3265 let Some(annotation_name) = extract_annotation_name(annotation_node, content) else {
3267 continue;
3268 };
3269
3270 let http_method: String = match annotation_name.as_str() {
3272 "GetMapping" => "GET".to_string(),
3273 "PostMapping" => "POST".to_string(),
3274 "PutMapping" => "PUT".to_string(),
3275 "DeleteMapping" => "DELETE".to_string(),
3276 "PatchMapping" => "PATCH".to_string(),
3277 "RequestMapping" => {
3278 extract_request_mapping_method(annotation_node, content)
3280 .unwrap_or_else(|| "GET".to_string())
3281 }
3282 _ => continue,
3283 };
3284
3285 let Some(path) = extract_annotation_path(annotation_node, content) else {
3287 continue;
3288 };
3289
3290 return Some((http_method, path));
3291 }
3292
3293 None
3294}
3295
3296fn extract_annotation_name(annotation_node: Node, content: &[u8]) -> Option<String> {
3301 let mut cursor = annotation_node.walk();
3302 for child in annotation_node.children(&mut cursor) {
3303 match child.kind() {
3304 "identifier" => {
3305 return Some(extract_identifier(child, content));
3306 }
3307 "scoped_identifier" => {
3308 let full_text = extract_identifier(child, content);
3311 return full_text.rsplit('.').next().map(String::from);
3312 }
3313 _ => {}
3314 }
3315 }
3316 None
3317}
3318
3319fn extract_annotation_path(annotation_node: Node, content: &[u8]) -> Option<String> {
3326 let mut cursor = annotation_node.walk();
3328 let args_node = annotation_node
3329 .children(&mut cursor)
3330 .find(|child| child.kind() == "annotation_argument_list")?;
3331
3332 let mut args_cursor = args_node.walk();
3334 for arg_child in args_node.children(&mut args_cursor) {
3335 match arg_child.kind() {
3336 "string_literal" => {
3338 return extract_string_content(arg_child, content);
3339 }
3340 "element_value_pair" => {
3342 if let Some(path) = extract_path_from_element_value_pair(arg_child, content) {
3343 return Some(path);
3344 }
3345 }
3346 _ => {}
3347 }
3348 }
3349
3350 None
3351}
3352
3353fn extract_request_mapping_method(annotation_node: Node, content: &[u8]) -> Option<String> {
3361 let mut cursor = annotation_node.walk();
3363 let args_node = annotation_node
3364 .children(&mut cursor)
3365 .find(|child| child.kind() == "annotation_argument_list")?;
3366
3367 let mut args_cursor = args_node.walk();
3369 for arg_child in args_node.children(&mut args_cursor) {
3370 if arg_child.kind() != "element_value_pair" {
3371 continue;
3372 }
3373
3374 let Some(key_node) = arg_child.child_by_field_name("key") else {
3376 continue;
3377 };
3378 let key_text = extract_identifier(key_node, content);
3379 if key_text != "method" {
3380 continue;
3381 }
3382
3383 let Some(value_node) = arg_child.child_by_field_name("value") else {
3385 continue;
3386 };
3387 let value_text = extract_identifier(value_node, content);
3388
3389 if let Some(method) = value_text.rsplit('.').next() {
3391 let method_upper = method.to_uppercase();
3392 if matches!(
3393 method_upper.as_str(),
3394 "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS"
3395 ) {
3396 return Some(method_upper);
3397 }
3398 }
3399 }
3400
3401 None
3402}
3403
3404fn extract_path_from_element_value_pair(pair_node: Node, content: &[u8]) -> Option<String> {
3408 let key_node = pair_node.child_by_field_name("key")?;
3409 let key_text = extract_identifier(key_node, content);
3410
3411 if key_text != "path" && key_text != "value" {
3413 return None;
3414 }
3415
3416 let value_node = pair_node.child_by_field_name("value")?;
3417 if value_node.kind() == "string_literal" {
3418 return extract_string_content(value_node, content);
3419 }
3420
3421 None
3422}
3423
3424fn extract_class_request_mapping_path(method_node: Node, content: &[u8]) -> Option<String> {
3441 let mut current = method_node.parent()?;
3443 loop {
3444 if current.kind() == "class_declaration" {
3445 break;
3446 }
3447 current = current.parent()?;
3448 }
3449
3450 let mut cursor = current.walk();
3452 let modifiers = current
3453 .children(&mut cursor)
3454 .find(|child| child.kind() == "modifiers")?;
3455
3456 let mut mod_cursor = modifiers.walk();
3457 for annotation in modifiers.children(&mut mod_cursor) {
3458 if annotation.kind() != "annotation" {
3459 continue;
3460 }
3461 let Some(name) = extract_annotation_name(annotation, content) else {
3462 continue;
3463 };
3464 if name == "RequestMapping" {
3465 return extract_annotation_path(annotation, content);
3466 }
3467 }
3468
3469 None
3470}
3471
3472fn extract_string_content(string_node: Node, content: &[u8]) -> Option<String> {
3476 let text = string_node.utf8_text(content).ok()?;
3477 let trimmed = text.trim();
3478
3479 if trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() >= 2 {
3481 Some(trimmed[1..trimmed.len() - 1].to_string())
3482 } else {
3483 None
3484 }
3485}