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