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