1use std::{collections::HashMap, path::Path};
2
3use crate::relations::java_common::{PackageResolver, build_member_symbol, build_symbol};
4use crate::relations::local_scopes::{self, JavaScopeTree, ResolutionOutcome};
5use sqry_core::graph::unified::StagingGraph;
6use sqry_core::graph::unified::build::helper::GraphBuildHelper;
7use sqry_core::graph::unified::edge::FfiConvention;
8use sqry_core::graph::{GraphBuilder, GraphBuilderError, GraphResult, Language, Span};
9use tree_sitter::{Node, Tree};
10
11const DEFAULT_SCOPE_DEPTH: usize = 4;
12
13const FILE_MODULE_NAME: &str = "<file_module>";
16
17#[derive(Debug, Clone, Copy)]
38pub struct JavaGraphBuilder {
39 max_scope_depth: usize,
40}
41
42impl Default for JavaGraphBuilder {
43 fn default() -> Self {
44 Self {
45 max_scope_depth: DEFAULT_SCOPE_DEPTH,
46 }
47 }
48}
49
50impl JavaGraphBuilder {
51 #[must_use]
52 pub fn new(max_scope_depth: usize) -> Self {
53 Self { max_scope_depth }
54 }
55}
56
57impl GraphBuilder for JavaGraphBuilder {
58 fn build_graph(
59 &self,
60 tree: &Tree,
61 content: &[u8],
62 file: &Path,
63 staging: &mut StagingGraph,
64 ) -> GraphResult<()> {
65 let mut helper = GraphBuildHelper::new(staging, file, Language::Java);
66
67 let ast_graph = ASTGraph::from_tree(tree, content, self.max_scope_depth);
69 let mut scope_tree = local_scopes::build(tree.root_node(), content, Some(file))?;
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"
277 | "interface_declaration"
278 | "enum_declaration"
279 | "record_declaration" => {
280 if let Some(name_node) = node.child_by_field_name("name") {
282 let class_name = extract_identifier(name_node, content);
283
284 class_stack.push(class_name.clone());
286
287 if let Some(body_node) = node.child_by_field_name("body") {
289 extract_methods_from_body(
290 body_node,
291 content,
292 class_stack,
293 package_name,
294 contexts,
295 depth + 1,
296 max_depth,
297 guard,
298 )?;
299
300 for i in 0..body_node.child_count() {
302 #[allow(clippy::cast_possible_truncation)]
303 if let Some(child) = body_node.child(i as u32) {
305 extract_java_contexts(
306 child,
307 content,
308 contexts,
309 class_stack,
310 package_name,
311 depth + 1,
312 max_depth,
313 guard,
314 )?;
315 }
316 }
317 }
318
319 class_stack.pop();
321
322 guard.exit();
323 return Ok(());
324 }
325 }
326 _ => {}
327 }
328
329 for i in 0..node.child_count() {
331 #[allow(clippy::cast_possible_truncation)]
332 if let Some(child) = node.child(i as u32) {
334 extract_java_contexts(
335 child,
336 content,
337 contexts,
338 class_stack,
339 package_name,
340 depth,
341 max_depth,
342 guard,
343 )?;
344 }
345 }
346
347 guard.exit();
348 Ok(())
349}
350
351#[allow(clippy::unnecessary_wraps)]
355fn extract_methods_from_body(
356 body_node: Node,
357 content: &[u8],
358 class_stack: &[String],
359 package_name: Option<&str>,
360 contexts: &mut Vec<MethodContext>,
361 depth: usize,
362 _max_depth: usize,
363 _guard: &mut sqry_core::query::security::RecursionGuard,
364) -> Result<(), sqry_core::query::security::RecursionError> {
365 for i in 0..body_node.child_count() {
366 #[allow(clippy::cast_possible_truncation)]
367 if let Some(child) = body_node.child(i as u32) {
369 match child.kind() {
370 "method_declaration" => {
371 if let Some(method_context) =
372 extract_method_context(child, content, class_stack, package_name, depth)
373 {
374 contexts.push(method_context);
375 }
376 }
377 "constructor_declaration" | "compact_constructor_declaration" => {
378 let constructor_context = extract_constructor_context(
379 child,
380 content,
381 class_stack,
382 package_name,
383 depth,
384 );
385 contexts.push(constructor_context);
386 }
387 _ => {}
388 }
389 }
390 }
391 Ok(())
392}
393
394fn extract_method_context(
395 method_node: Node,
396 content: &[u8],
397 class_stack: &[String],
398 package_name: Option<&str>,
399 depth: usize,
400) -> Option<MethodContext> {
401 let name_node = method_node.child_by_field_name("name")?;
402 let method_name = extract_identifier(name_node, content);
403
404 let is_static = has_modifier(method_node, "static", content);
405 let is_synchronized = has_modifier(method_node, "synchronized", content);
406 let is_native = has_modifier(method_node, "native", content);
407 let visibility = extract_visibility(method_node, content);
408
409 let return_type = method_node
412 .child_by_field_name("type")
413 .map(|type_node| extract_full_return_type(type_node, content));
414
415 let qualified_name = build_member_symbol(package_name, class_stack, &method_name);
417
418 Some(MethodContext {
419 qualified_name,
420 span: (method_node.start_byte(), method_node.end_byte()),
421 depth,
422 is_static,
423 is_synchronized,
424 is_constructor: false,
425 is_native,
426 package_name: package_name.map(std::string::ToString::to_string),
427 class_stack: class_stack.to_vec(),
428 return_type,
429 visibility,
430 })
431}
432
433fn extract_constructor_context(
434 constructor_node: Node,
435 content: &[u8],
436 class_stack: &[String],
437 package_name: Option<&str>,
438 depth: usize,
439) -> MethodContext {
440 let qualified_name = build_member_symbol(package_name, class_stack, "<init>");
442 let visibility = extract_visibility(constructor_node, content);
443
444 MethodContext {
445 qualified_name,
446 span: (constructor_node.start_byte(), constructor_node.end_byte()),
447 depth,
448 is_static: false,
449 is_synchronized: false,
450 is_constructor: true,
451 is_native: false,
452 package_name: package_name.map(std::string::ToString::to_string),
453 class_stack: class_stack.to_vec(),
454 return_type: None, visibility,
456 }
457}
458
459fn walk_tree_for_edges(
465 node: Node,
466 content: &[u8],
467 ast_graph: &ASTGraph,
468 scope_tree: &mut JavaScopeTree,
469 helper: &mut GraphBuildHelper,
470 tree: &Tree,
471) -> GraphResult<()> {
472 match node.kind() {
473 "class_declaration"
474 | "interface_declaration"
475 | "enum_declaration"
476 | "record_declaration" => {
477 return handle_type_declaration(node, content, ast_graph, scope_tree, helper, tree);
479 }
480 "method_declaration" | "constructor_declaration" => {
481 handle_method_declaration_parameters(node, content, ast_graph, scope_tree, helper);
483
484 if node.kind() == "method_declaration"
486 && let Some((http_method, path)) = extract_spring_route_info(node, content)
487 {
488 let full_path =
490 if let Some(class_prefix) = extract_class_request_mapping_path(node, content) {
491 let prefix = class_prefix.trim_end_matches('/');
492 let suffix = path.trim_start_matches('/');
493 if suffix.is_empty() {
494 class_prefix
495 } else {
496 format!("{prefix}/{suffix}")
497 }
498 } else {
499 path
500 };
501 let qualified_name = format!("route::{http_method}::{full_path}");
502 let span = Span::from_bytes(node.start_byte(), node.end_byte());
503 let endpoint_id = helper.add_endpoint(&qualified_name, Some(span));
504
505 let byte_pos = node.start_byte();
507 if let Some(context) = ast_graph.find_enclosing(byte_pos) {
508 let method_id = helper.ensure_method(
509 context.qualified_name(),
510 Some(Span::from_bytes(context.span.0, context.span.1)),
511 false,
512 context.is_static,
513 );
514 helper.add_contains_edge(endpoint_id, method_id);
515 }
516 }
517 }
518 "compact_constructor_declaration" => {
519 handle_compact_constructor_parameters(node, content, ast_graph, scope_tree, helper);
520 }
521 "method_invocation" => {
522 handle_method_invocation(node, content, ast_graph, helper);
523 }
524 "object_creation_expression" => {
525 handle_constructor_call(node, content, ast_graph, helper);
526 }
527 "import_declaration" => {
528 handle_import_declaration(node, content, helper);
529 }
530 "local_variable_declaration" => {
531 handle_local_variable_declaration(node, content, ast_graph, scope_tree, helper);
532 }
533 "enhanced_for_statement" => {
534 handle_enhanced_for_declaration(node, content, ast_graph, scope_tree, helper);
535 }
536 "catch_clause" => {
537 handle_catch_parameter_declaration(node, content, ast_graph, scope_tree, helper);
538 }
539 "lambda_expression" => {
540 handle_lambda_parameter_declaration(node, content, ast_graph, scope_tree, helper);
541 }
542 "try_with_resources_statement" => {
543 handle_try_with_resources_declaration(node, content, ast_graph, scope_tree, helper);
544 }
545 "instanceof_expression" => {
546 handle_instanceof_pattern_declaration(node, content, ast_graph, scope_tree, helper);
547 }
548 "switch_label" => {
549 handle_switch_pattern_declaration(node, content, ast_graph, scope_tree, helper);
550 }
551 "identifier" => {
552 handle_identifier_for_reference(node, content, ast_graph, scope_tree, helper);
553 }
554 _ => {}
555 }
556
557 for i in 0..node.child_count() {
559 #[allow(clippy::cast_possible_truncation)]
560 if let Some(child) = node.child(i as u32) {
562 walk_tree_for_edges(child, content, ast_graph, scope_tree, helper, tree)?;
563 }
564 }
565
566 Ok(())
567}
568
569fn handle_type_declaration(
570 node: Node,
571 content: &[u8],
572 ast_graph: &ASTGraph,
573 scope_tree: &mut JavaScopeTree,
574 helper: &mut GraphBuildHelper,
575 tree: &Tree,
576) -> GraphResult<()> {
577 let Some(name_node) = node.child_by_field_name("name") else {
578 return Ok(());
579 };
580 let class_name = extract_identifier(name_node, content);
581 let span = Span::from_bytes(node.start_byte(), node.end_byte());
582
583 let package = PackageResolver::package_from_ast(tree, content);
584 let class_stack = extract_declaration_class_stack(node, content);
585 let qualified_name = qualify_class_name(&class_name, &class_stack, package.as_deref());
586 let class_node_id = add_type_node(helper, node.kind(), &qualified_name, span);
587
588 if is_public(node, content) {
589 export_from_file_module(helper, class_node_id);
590 }
591
592 process_inheritance(node, content, package.as_deref(), class_node_id, helper);
593 if node.kind() == "class_declaration" {
594 process_implements(node, content, package.as_deref(), class_node_id, helper);
595 }
596 if node.kind() == "interface_declaration" {
597 process_interface_extends(node, content, package.as_deref(), class_node_id, helper);
598 }
599
600 if let Some(body_node) = node.child_by_field_name("body") {
601 let is_interface = node.kind() == "interface_declaration";
602 process_class_member_exports(body_node, content, &qualified_name, helper, is_interface);
603
604 for i in 0..body_node.child_count() {
605 #[allow(clippy::cast_possible_truncation)]
606 if let Some(child) = body_node.child(i as u32) {
608 walk_tree_for_edges(child, content, ast_graph, scope_tree, helper, tree)?;
609 }
610 }
611 }
612
613 Ok(())
614}
615
616fn extract_declaration_class_stack(node: Node, content: &[u8]) -> Vec<String> {
617 let mut class_stack = Vec::new();
618 let mut current_node = Some(node);
619
620 while let Some(current) = current_node {
621 if matches!(
622 current.kind(),
623 "class_declaration"
624 | "interface_declaration"
625 | "enum_declaration"
626 | "record_declaration"
627 ) && let Some(name_node) = current.child_by_field_name("name")
628 {
629 class_stack.push(extract_identifier(name_node, content));
630 }
631
632 current_node = current.parent();
633 }
634
635 class_stack.reverse();
636 class_stack
637}
638
639fn qualify_class_name(class_name: &str, class_stack: &[String], package: Option<&str>) -> String {
640 let scope = class_stack
641 .split_last()
642 .map_or(&[][..], |(_, parent_stack)| parent_stack);
643 build_symbol(package, scope, class_name)
644}
645
646fn add_type_node(
647 helper: &mut GraphBuildHelper,
648 kind: &str,
649 qualified_name: &str,
650 span: Span,
651) -> sqry_core::graph::unified::node::NodeId {
652 match kind {
653 "interface_declaration" => helper.add_interface(qualified_name, Some(span)),
654 _ => helper.add_class(qualified_name, Some(span)),
655 }
656}
657
658fn handle_method_invocation(
659 node: Node,
660 content: &[u8],
661 ast_graph: &ASTGraph,
662 helper: &mut GraphBuildHelper,
663) {
664 if let Some(caller_context) = ast_graph.find_enclosing(node.start_byte()) {
665 let is_ffi = build_ffi_call_edge(node, content, caller_context, ast_graph, helper);
666 if is_ffi {
667 return;
668 }
669 }
670
671 process_method_call_unified(node, content, ast_graph, helper);
672}
673
674fn handle_constructor_call(
675 node: Node,
676 content: &[u8],
677 ast_graph: &ASTGraph,
678 helper: &mut GraphBuildHelper,
679) {
680 process_constructor_call_unified(node, content, ast_graph, helper);
681}
682
683fn handle_import_declaration(node: Node, content: &[u8], helper: &mut GraphBuildHelper) {
684 process_import_unified(node, content, helper);
685}
686
687fn add_field_typeof_edges(ast_graph: &ASTGraph, helper: &mut GraphBuildHelper) {
690 for (field_name, (type_fqn, is_final, visibility, is_static)) in &ast_graph.field_types {
691 let field_id = if *is_final {
693 if let Some(vis) = visibility {
695 helper.add_constant_with_static_and_visibility(
696 field_name,
697 None,
698 *is_static,
699 Some(vis.as_str()),
700 )
701 } else {
702 helper.add_constant_with_static_and_visibility(field_name, None, *is_static, None)
703 }
704 } else {
705 if let Some(vis) = visibility {
707 helper.add_property_with_static_and_visibility(
708 field_name,
709 None,
710 *is_static,
711 Some(vis.as_str()),
712 )
713 } else {
714 helper.add_property_with_static_and_visibility(field_name, None, *is_static, None)
715 }
716 };
717
718 let type_id = helper.add_class(type_fqn, None);
720
721 helper.add_typeof_edge(field_id, type_id);
723 }
724}
725
726fn extract_method_parameters(
729 method_node: Node,
730 content: &[u8],
731 qualified_method_name: &str,
732 helper: &mut GraphBuildHelper,
733 import_map: &HashMap<String, String>,
734 scope_tree: &mut JavaScopeTree,
735) {
736 let mut cursor = method_node.walk();
738 for child in method_node.children(&mut cursor) {
739 if child.kind() == "formal_parameters" {
740 let mut param_cursor = child.walk();
742 for param_child in child.children(&mut param_cursor) {
743 match param_child.kind() {
744 "formal_parameter" => {
745 handle_formal_parameter(
746 param_child,
747 content,
748 qualified_method_name,
749 helper,
750 import_map,
751 scope_tree,
752 );
753 }
754 "spread_parameter" => {
755 handle_spread_parameter(
756 param_child,
757 content,
758 qualified_method_name,
759 helper,
760 import_map,
761 scope_tree,
762 );
763 }
764 "receiver_parameter" => {
765 handle_receiver_parameter(
766 param_child,
767 content,
768 qualified_method_name,
769 helper,
770 import_map,
771 scope_tree,
772 );
773 }
774 _ => {}
775 }
776 }
777 }
778 }
779}
780
781fn handle_formal_parameter(
783 param_node: Node,
784 content: &[u8],
785 method_name: &str,
786 helper: &mut GraphBuildHelper,
787 import_map: &HashMap<String, String>,
788 scope_tree: &mut JavaScopeTree,
789) {
790 use sqry_core::graph::unified::node::NodeKind;
791
792 let Some(type_node) = param_node.child_by_field_name("type") else {
794 return;
795 };
796
797 let Some(name_node) = param_node.child_by_field_name("name") else {
799 return;
800 };
801
802 let type_text = extract_type_name(type_node, content);
804 let param_name = extract_identifier(name_node, content);
805
806 if type_text.is_empty() || param_name.is_empty() {
807 return;
808 }
809
810 let resolved_type = import_map.get(&type_text).cloned().unwrap_or(type_text);
812
813 let qualified_param = format!("{method_name}::{param_name}");
815 let span = Span::from_bytes(param_node.start_byte(), param_node.end_byte());
816
817 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
819
820 scope_tree.attach_node_id(¶m_name, name_node.start_byte(), param_id);
821
822 let type_id = helper.add_class(&resolved_type, None);
824
825 helper.add_typeof_edge(param_id, type_id);
827}
828
829fn handle_spread_parameter(
831 param_node: Node,
832 content: &[u8],
833 method_name: &str,
834 helper: &mut GraphBuildHelper,
835 import_map: &HashMap<String, String>,
836 scope_tree: &mut JavaScopeTree,
837) {
838 use sqry_core::graph::unified::node::NodeKind;
839
840 let mut type_text = String::new();
849 let mut param_name = String::new();
850 let mut param_name_node = None;
851
852 let mut cursor = param_node.walk();
853 for child in param_node.children(&mut cursor) {
854 match child.kind() {
855 "type_identifier" | "generic_type" | "scoped_type_identifier" => {
856 type_text = extract_type_name(child, content);
857 }
858 "variable_declarator" => {
859 if let Some(name_node) = child.child_by_field_name("name") {
861 param_name = extract_identifier(name_node, content);
862 param_name_node = Some(name_node);
863 }
864 }
865 _ => {}
866 }
867 }
868
869 if type_text.is_empty() || param_name.is_empty() {
870 return;
871 }
872
873 let resolved_type = import_map.get(&type_text).cloned().unwrap_or(type_text);
875
876 let qualified_param = format!("{method_name}::{param_name}");
878 let span = Span::from_bytes(param_node.start_byte(), param_node.end_byte());
879
880 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
882
883 if let Some(name_node) = param_name_node {
884 scope_tree.attach_node_id(¶m_name, name_node.start_byte(), param_id);
885 }
886
887 let type_id = helper.add_class(&resolved_type, None);
890
891 helper.add_typeof_edge(param_id, type_id);
893}
894
895fn handle_receiver_parameter(
897 param_node: Node,
898 content: &[u8],
899 method_name: &str,
900 helper: &mut GraphBuildHelper,
901 import_map: &HashMap<String, String>,
902 _scope_tree: &mut JavaScopeTree,
903) {
904 use sqry_core::graph::unified::node::NodeKind;
905
906 let mut type_text = String::new();
914 let mut cursor = param_node.walk();
915
916 for child in param_node.children(&mut cursor) {
918 if matches!(
919 child.kind(),
920 "type_identifier" | "generic_type" | "scoped_type_identifier"
921 ) {
922 type_text = extract_type_name(child, content);
923 break;
924 }
925 }
926
927 if type_text.is_empty() {
928 return;
929 }
930
931 let param_name = "this";
933
934 let resolved_type = import_map.get(&type_text).cloned().unwrap_or(type_text);
936
937 let qualified_param = format!("{method_name}::{param_name}");
939 let span = Span::from_bytes(param_node.start_byte(), param_node.end_byte());
940
941 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
943
944 let type_id = helper.add_class(&resolved_type, None);
946
947 helper.add_typeof_edge(param_id, type_id);
949}
950
951#[derive(Debug, Clone, Copy, Eq, PartialEq)]
952enum FieldAccessRole {
953 Default,
954 ExplicitThisOrSuper,
955 Skip,
956}
957
958#[derive(Debug, Clone, Copy, Eq, PartialEq)]
959enum FieldResolutionMode {
960 Default,
961 CurrentOnly,
962}
963
964fn field_access_role(
965 node: Node,
966 content: &[u8],
967 ast_graph: &ASTGraph,
968 scope_tree: &JavaScopeTree,
969 identifier_text: &str,
970) -> FieldAccessRole {
971 let Some(parent) = node.parent() else {
972 return FieldAccessRole::Default;
973 };
974
975 if parent.kind() == "field_access" {
976 if let Some(field_node) = parent.child_by_field_name("field")
977 && field_node.id() == node.id()
978 && let Some(object_node) = parent.child_by_field_name("object")
979 {
980 if is_explicit_this_or_super(object_node, content) {
981 return FieldAccessRole::ExplicitThisOrSuper;
982 }
983 return FieldAccessRole::Skip;
984 }
985
986 if let Some(object_node) = parent.child_by_field_name("object")
987 && object_node.id() == node.id()
988 && !scope_tree.has_local_binding(identifier_text, node.start_byte())
989 && is_static_type_identifier(identifier_text, ast_graph, scope_tree)
990 {
991 return FieldAccessRole::Skip;
992 }
993 }
994
995 if parent.kind() == "method_invocation"
996 && let Some(object_node) = parent.child_by_field_name("object")
997 && object_node.id() == node.id()
998 && !scope_tree.has_local_binding(identifier_text, node.start_byte())
999 && is_static_type_identifier(identifier_text, ast_graph, scope_tree)
1000 {
1001 return FieldAccessRole::Skip;
1002 }
1003
1004 if parent.kind() == "method_reference"
1005 && let Some(object_node) = parent.child_by_field_name("object")
1006 && object_node.id() == node.id()
1007 && !scope_tree.has_local_binding(identifier_text, node.start_byte())
1008 && is_static_type_identifier(identifier_text, ast_graph, scope_tree)
1009 {
1010 return FieldAccessRole::Skip;
1011 }
1012
1013 FieldAccessRole::Default
1014}
1015
1016fn is_static_type_identifier(
1017 identifier_text: &str,
1018 ast_graph: &ASTGraph,
1019 scope_tree: &JavaScopeTree,
1020) -> bool {
1021 ast_graph.import_map.contains_key(identifier_text)
1022 || scope_tree.is_known_type_name(identifier_text)
1023}
1024
1025fn is_explicit_this_or_super(node: Node, content: &[u8]) -> bool {
1026 if matches!(node.kind(), "this" | "super") {
1027 return true;
1028 }
1029 if node.kind() == "identifier" {
1030 let text = extract_identifier(node, content);
1031 return matches!(text.as_str(), "this" | "super");
1032 }
1033 if node.kind() == "field_access"
1034 && let Some(field) = node.child_by_field_name("field")
1035 {
1036 let text = extract_identifier(field, content);
1037 if matches!(text.as_str(), "this" | "super") {
1038 return true;
1039 }
1040 }
1041 false
1042}
1043
1044#[allow(clippy::too_many_lines)]
1047fn is_declaration_context(node: Node) -> bool {
1048 let Some(parent) = node.parent() else {
1050 return false;
1051 };
1052
1053 if parent.kind() == "variable_declarator" {
1058 let mut cursor = parent.walk();
1060 for (idx, child) in parent.children(&mut cursor).enumerate() {
1061 if child.id() == node.id() {
1062 #[allow(clippy::cast_possible_truncation)]
1063 if let Some(field_name) = parent.field_name_for_child(idx as u32) {
1064 return field_name == "name";
1066 }
1067 break;
1068 }
1069 }
1070
1071 if let Some(grandparent) = parent.parent()
1073 && grandparent.kind() == "spread_parameter"
1074 {
1075 return true;
1076 }
1077
1078 return false;
1079 }
1080
1081 if parent.kind() == "formal_parameter" {
1083 let mut cursor = parent.walk();
1084 for (idx, child) in parent.children(&mut cursor).enumerate() {
1085 if child.id() == node.id() {
1086 #[allow(clippy::cast_possible_truncation)]
1087 if let Some(field_name) = parent.field_name_for_child(idx as u32) {
1088 return field_name == "name";
1089 }
1090 break;
1091 }
1092 }
1093 return false;
1094 }
1095
1096 if parent.kind() == "enhanced_for_statement" {
1099 let mut cursor = parent.walk();
1101 for (idx, child) in parent.children(&mut cursor).enumerate() {
1102 if child.id() == node.id() {
1103 #[allow(clippy::cast_possible_truncation)]
1104 if let Some(field_name) = parent.field_name_for_child(idx as u32) {
1105 return field_name == "name";
1107 }
1108 break;
1109 }
1110 }
1111 return false;
1112 }
1113
1114 if parent.kind() == "lambda_expression" {
1115 if let Some(params) = parent.child_by_field_name("parameters") {
1116 return params.id() == node.id();
1117 }
1118 return false;
1119 }
1120
1121 if parent.kind() == "inferred_parameters" {
1122 return true;
1123 }
1124
1125 if parent.kind() == "resource" {
1126 if let Some(name_node) = parent.child_by_field_name("name")
1127 && name_node.id() == node.id()
1128 {
1129 let has_type = parent.child_by_field_name("type").is_some();
1130 let has_value = parent.child_by_field_name("value").is_some();
1131 return has_type || has_value;
1132 }
1133 return false;
1134 }
1135
1136 if parent.kind() == "type_pattern" {
1140 if let Some((name_node, _type_node)) = typed_pattern_parts(parent) {
1141 return name_node.id() == node.id();
1142 }
1143 return false;
1144 }
1145
1146 if parent.kind() == "instanceof_expression" {
1148 let mut cursor = parent.walk();
1149 for (idx, child) in parent.children(&mut cursor).enumerate() {
1150 if child.id() == node.id() {
1151 #[allow(clippy::cast_possible_truncation)]
1152 if let Some(field_name) = parent.field_name_for_child(idx as u32) {
1153 return field_name == "name";
1155 }
1156 break;
1157 }
1158 }
1159 return false;
1160 }
1161
1162 if parent.kind() == "record_pattern_component" {
1165 let mut cursor = parent.walk();
1167 for child in parent.children(&mut cursor) {
1168 if child.id() == node.id() && child.kind() == "identifier" {
1169 return true;
1171 }
1172 }
1173 return false;
1174 }
1175
1176 if parent.kind() == "record_component" {
1177 if let Some(name_node) = parent.child_by_field_name("name") {
1178 return name_node.id() == node.id();
1179 }
1180 return false;
1181 }
1182
1183 matches!(
1185 parent.kind(),
1186 "method_declaration"
1187 | "constructor_declaration"
1188 | "compact_constructor_declaration"
1189 | "class_declaration"
1190 | "interface_declaration"
1191 | "enum_declaration"
1192 | "field_declaration"
1193 | "catch_formal_parameter"
1194 )
1195}
1196
1197fn is_method_invocation_name(node: Node) -> bool {
1198 let Some(parent) = node.parent() else {
1199 return false;
1200 };
1201 if parent.kind() != "method_invocation" {
1202 return false;
1203 }
1204 parent
1205 .child_by_field_name("name")
1206 .is_some_and(|name_node| name_node.id() == node.id())
1207}
1208
1209fn is_method_reference_name(node: Node) -> bool {
1210 let Some(parent) = node.parent() else {
1211 return false;
1212 };
1213 if parent.kind() != "method_reference" {
1214 return false;
1215 }
1216 parent
1217 .child_by_field_name("name")
1218 .is_some_and(|name_node| name_node.id() == node.id())
1219}
1220
1221fn is_label_identifier(node: Node) -> bool {
1222 let Some(parent) = node.parent() else {
1223 return false;
1224 };
1225 if parent.kind() == "labeled_statement" {
1226 return true;
1227 }
1228 if matches!(parent.kind(), "break_statement" | "continue_statement")
1229 && let Some(label) = parent.child_by_field_name("label")
1230 {
1231 return label.id() == node.id();
1232 }
1233 false
1234}
1235
1236fn is_class_literal(node: Node) -> bool {
1237 let Some(parent) = node.parent() else {
1238 return false;
1239 };
1240 parent.kind() == "class_literal"
1241}
1242
1243fn is_type_identifier_context(node: Node) -> bool {
1244 let Some(parent) = node.parent() else {
1245 return false;
1246 };
1247 matches!(
1248 parent.kind(),
1249 "type_identifier"
1250 | "scoped_type_identifier"
1251 | "scoped_identifier"
1252 | "generic_type"
1253 | "type_argument"
1254 | "type_bound"
1255 )
1256}
1257
1258fn add_reference_edge_for_target(
1259 usage_node: Node,
1260 identifier_text: &str,
1261 target_id: sqry_core::graph::unified::node::NodeId,
1262 helper: &mut GraphBuildHelper,
1263) {
1264 let usage_span = Span::from_bytes(usage_node.start_byte(), usage_node.end_byte());
1265 let usage_id = helper.add_node(
1266 &format!("{}@{}", identifier_text, usage_node.start_byte()),
1267 Some(usage_span),
1268 sqry_core::graph::unified::node::NodeKind::Variable,
1269 );
1270 helper.add_reference_edge(usage_id, target_id);
1271}
1272
1273fn resolve_field_reference(
1274 node: Node,
1275 identifier_text: &str,
1276 ast_graph: &ASTGraph,
1277 helper: &mut GraphBuildHelper,
1278 mode: FieldResolutionMode,
1279) {
1280 let context = ast_graph.find_enclosing(node.start_byte());
1281 let mut candidates = Vec::new();
1282 if let Some(ctx) = context
1283 && !ctx.class_stack.is_empty()
1284 {
1285 if mode == FieldResolutionMode::CurrentOnly {
1286 let class_path = ctx.class_stack.join("::");
1287 candidates.push(format!("{class_path}::{identifier_text}"));
1288 } else {
1289 let stack_len = ctx.class_stack.len();
1290 for idx in (1..=stack_len).rev() {
1291 let class_path = ctx.class_stack[..idx].join("::");
1292 candidates.push(format!("{class_path}::{identifier_text}"));
1293 }
1294 }
1295 }
1296
1297 if mode != FieldResolutionMode::CurrentOnly {
1298 candidates.push(identifier_text.to_string());
1299 }
1300
1301 for candidate in candidates {
1302 if ast_graph.field_types.contains_key(&candidate) {
1303 add_field_reference(node, identifier_text, &candidate, ast_graph, helper);
1304 return;
1305 }
1306 }
1307}
1308
1309fn add_field_reference(
1310 node: Node,
1311 identifier_text: &str,
1312 field_name: &str,
1313 ast_graph: &ASTGraph,
1314 helper: &mut GraphBuildHelper,
1315) {
1316 let usage_span = Span::from_bytes(node.start_byte(), node.end_byte());
1317 let usage_id = helper.add_node(
1318 &format!("{}@{}", identifier_text, node.start_byte()),
1319 Some(usage_span),
1320 sqry_core::graph::unified::node::NodeKind::Variable,
1321 );
1322
1323 let field_metadata = ast_graph.field_types.get(field_name);
1324 let field_id = if let Some((_, is_final, visibility, is_static)) = field_metadata {
1325 if *is_final {
1326 if let Some(vis) = visibility {
1327 helper.add_constant_with_static_and_visibility(
1328 field_name,
1329 None,
1330 *is_static,
1331 Some(vis.as_str()),
1332 )
1333 } else {
1334 helper.add_constant_with_static_and_visibility(field_name, None, *is_static, None)
1335 }
1336 } else if let Some(vis) = visibility {
1337 helper.add_property_with_static_and_visibility(
1338 field_name,
1339 None,
1340 *is_static,
1341 Some(vis.as_str()),
1342 )
1343 } else {
1344 helper.add_property_with_static_and_visibility(field_name, None, *is_static, None)
1345 }
1346 } else {
1347 helper.add_property_with_static_and_visibility(field_name, None, false, None)
1348 };
1349
1350 helper.add_reference_edge(usage_id, field_id);
1351}
1352
1353#[allow(clippy::similar_names)]
1355fn handle_identifier_for_reference(
1356 node: Node,
1357 content: &[u8],
1358 ast_graph: &ASTGraph,
1359 scope_tree: &mut JavaScopeTree,
1360 helper: &mut GraphBuildHelper,
1361) {
1362 let identifier_text = extract_identifier(node, content);
1363
1364 if identifier_text.is_empty() {
1365 return;
1366 }
1367
1368 if is_declaration_context(node) {
1370 return;
1371 }
1372
1373 if is_method_invocation_name(node)
1374 || is_method_reference_name(node)
1375 || is_label_identifier(node)
1376 || is_class_literal(node)
1377 {
1378 return;
1379 }
1380
1381 if is_type_identifier_context(node) {
1382 return;
1383 }
1384
1385 let field_access_role =
1386 field_access_role(node, content, ast_graph, scope_tree, &identifier_text);
1387 if matches!(field_access_role, FieldAccessRole::Skip) {
1388 return;
1389 }
1390
1391 let allow_local = matches!(field_access_role, FieldAccessRole::Default);
1392 let allow_field = !matches!(field_access_role, FieldAccessRole::Skip);
1393 let field_mode = if matches!(field_access_role, FieldAccessRole::ExplicitThisOrSuper) {
1394 FieldResolutionMode::CurrentOnly
1395 } else {
1396 FieldResolutionMode::Default
1397 };
1398
1399 if allow_local {
1400 match scope_tree.resolve_identifier(node.start_byte(), &identifier_text) {
1401 ResolutionOutcome::Local(binding) => {
1402 let target_id = if let Some(node_id) = binding.node_id {
1403 node_id
1404 } else {
1405 let span = Span::from_bytes(binding.decl_start_byte, binding.decl_end_byte);
1406 let qualified_var = format!("{}@{}", identifier_text, binding.decl_start_byte);
1407 let var_id = helper.add_variable(&qualified_var, Some(span));
1408 scope_tree.attach_node_id(&identifier_text, binding.decl_start_byte, var_id);
1409 var_id
1410 };
1411 add_reference_edge_for_target(node, &identifier_text, target_id, helper);
1412 return;
1413 }
1414 ResolutionOutcome::Member { qualified_name } => {
1415 if let Some(field_name) = qualified_name {
1416 add_field_reference(node, &identifier_text, &field_name, ast_graph, helper);
1417 }
1418 return;
1419 }
1420 ResolutionOutcome::Ambiguous => {
1421 return;
1422 }
1423 ResolutionOutcome::NoMatch => {}
1424 }
1425 }
1426
1427 if !allow_field {
1428 return;
1429 }
1430
1431 resolve_field_reference(node, &identifier_text, ast_graph, helper, field_mode);
1432}
1433
1434fn handle_method_declaration_parameters(
1436 node: Node,
1437 content: &[u8],
1438 ast_graph: &ASTGraph,
1439 scope_tree: &mut JavaScopeTree,
1440 helper: &mut GraphBuildHelper,
1441) {
1442 let byte_pos = node.start_byte();
1444 if let Some(context) = ast_graph.find_enclosing(byte_pos) {
1445 let qualified_method_name = &context.qualified_name;
1446
1447 extract_method_parameters(
1449 node,
1450 content,
1451 qualified_method_name,
1452 helper,
1453 &ast_graph.import_map,
1454 scope_tree,
1455 );
1456 }
1457}
1458
1459fn handle_local_variable_declaration(
1461 node: Node,
1462 content: &[u8],
1463 ast_graph: &ASTGraph,
1464 scope_tree: &mut JavaScopeTree,
1465 helper: &mut GraphBuildHelper,
1466) {
1467 let Some(type_node) = node.child_by_field_name("type") else {
1469 return;
1470 };
1471
1472 let type_text = extract_type_name(type_node, content);
1473 if type_text.is_empty() {
1474 return;
1475 }
1476
1477 let resolved_type = ast_graph
1479 .import_map
1480 .get(&type_text)
1481 .cloned()
1482 .unwrap_or_else(|| type_text.clone());
1483
1484 let mut cursor = node.walk();
1486 for child in node.children(&mut cursor) {
1487 if child.kind() == "variable_declarator"
1488 && let Some(name_node) = child.child_by_field_name("name")
1489 {
1490 let var_name = extract_identifier(name_node, content);
1491
1492 let qualified_var = format!("{}@{}", var_name, name_node.start_byte());
1494
1495 let span = Span::from_bytes(child.start_byte(), child.end_byte());
1497 let var_id = helper.add_variable(&qualified_var, Some(span));
1498 scope_tree.attach_node_id(&var_name, name_node.start_byte(), var_id);
1499
1500 let type_id = helper.add_class(&resolved_type, None);
1502
1503 helper.add_typeof_edge(var_id, type_id);
1505 }
1506 }
1507}
1508
1509fn handle_enhanced_for_declaration(
1510 node: Node,
1511 content: &[u8],
1512 ast_graph: &ASTGraph,
1513 scope_tree: &mut JavaScopeTree,
1514 helper: &mut GraphBuildHelper,
1515) {
1516 let Some(type_node) = node.child_by_field_name("type") else {
1517 return;
1518 };
1519 let Some(name_node) = node.child_by_field_name("name") else {
1520 return;
1521 };
1522 let Some(body_node) = node.child_by_field_name("body") else {
1523 return;
1524 };
1525
1526 let type_text = extract_type_name(type_node, content);
1527 let var_name = extract_identifier(name_node, content);
1528 if type_text.is_empty() || var_name.is_empty() {
1529 return;
1530 }
1531
1532 let resolved_type = ast_graph
1533 .import_map
1534 .get(&type_text)
1535 .cloned()
1536 .unwrap_or(type_text);
1537
1538 let qualified_var = format!("{}@{}", var_name, name_node.start_byte());
1539 let span = Span::from_bytes(name_node.start_byte(), name_node.end_byte());
1540 let var_id = helper.add_variable(&qualified_var, Some(span));
1541 scope_tree.attach_node_id(&var_name, body_node.start_byte(), var_id);
1542
1543 let type_id = helper.add_class(&resolved_type, None);
1544 helper.add_typeof_edge(var_id, type_id);
1545}
1546
1547fn handle_catch_parameter_declaration(
1548 node: Node,
1549 content: &[u8],
1550 ast_graph: &ASTGraph,
1551 scope_tree: &mut JavaScopeTree,
1552 helper: &mut GraphBuildHelper,
1553) {
1554 let Some(param_node) = node
1555 .child_by_field_name("parameter")
1556 .or_else(|| first_child_of_kind(node, "catch_formal_parameter"))
1557 .or_else(|| first_child_of_kind(node, "formal_parameter"))
1558 else {
1559 return;
1560 };
1561 let Some(name_node) = param_node
1562 .child_by_field_name("name")
1563 .or_else(|| first_child_of_kind(param_node, "identifier"))
1564 else {
1565 return;
1566 };
1567
1568 let var_name = extract_identifier(name_node, content);
1569 if var_name.is_empty() {
1570 return;
1571 }
1572
1573 let qualified_var = format!("{}@{}", var_name, name_node.start_byte());
1574 let span = Span::from_bytes(param_node.start_byte(), param_node.end_byte());
1575 let var_id = helper.add_variable(&qualified_var, Some(span));
1576 scope_tree.attach_node_id(&var_name, name_node.start_byte(), var_id);
1577
1578 if let Some(type_node) = param_node
1579 .child_by_field_name("type")
1580 .or_else(|| first_child_of_kind(param_node, "type_identifier"))
1581 .or_else(|| first_child_of_kind(param_node, "scoped_type_identifier"))
1582 .or_else(|| first_child_of_kind(param_node, "generic_type"))
1583 {
1584 add_typeof_for_catch_type(type_node, content, ast_graph, helper, var_id);
1585 }
1586}
1587
1588fn add_typeof_for_catch_type(
1589 type_node: Node,
1590 content: &[u8],
1591 ast_graph: &ASTGraph,
1592 helper: &mut GraphBuildHelper,
1593 var_id: sqry_core::graph::unified::node::NodeId,
1594) {
1595 if type_node.kind() == "union_type" {
1596 let mut cursor = type_node.walk();
1597 for child in type_node.children(&mut cursor) {
1598 if matches!(
1599 child.kind(),
1600 "type_identifier" | "scoped_type_identifier" | "generic_type"
1601 ) {
1602 let type_text = extract_type_name(child, content);
1603 if !type_text.is_empty() {
1604 let resolved_type = ast_graph
1605 .import_map
1606 .get(&type_text)
1607 .cloned()
1608 .unwrap_or(type_text);
1609 let type_id = helper.add_class(&resolved_type, None);
1610 helper.add_typeof_edge(var_id, type_id);
1611 }
1612 }
1613 }
1614 return;
1615 }
1616
1617 let type_text = extract_type_name(type_node, content);
1618 if type_text.is_empty() {
1619 return;
1620 }
1621 let resolved_type = ast_graph
1622 .import_map
1623 .get(&type_text)
1624 .cloned()
1625 .unwrap_or(type_text);
1626 let type_id = helper.add_class(&resolved_type, None);
1627 helper.add_typeof_edge(var_id, type_id);
1628}
1629
1630fn handle_lambda_parameter_declaration(
1631 node: Node,
1632 content: &[u8],
1633 ast_graph: &ASTGraph,
1634 scope_tree: &mut JavaScopeTree,
1635 helper: &mut GraphBuildHelper,
1636) {
1637 use sqry_core::graph::unified::node::NodeKind;
1638
1639 let Some(params_node) = node.child_by_field_name("parameters") else {
1640 return;
1641 };
1642 let lambda_prefix = format!("lambda@{}", node.start_byte());
1643
1644 if params_node.kind() == "identifier" {
1645 let name = extract_identifier(params_node, content);
1646 if name.is_empty() {
1647 return;
1648 }
1649 let qualified_param = format!("{lambda_prefix}::{name}");
1650 let span = Span::from_bytes(params_node.start_byte(), params_node.end_byte());
1651 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
1652 scope_tree.attach_node_id(&name, params_node.start_byte(), param_id);
1653 return;
1654 }
1655
1656 let mut cursor = params_node.walk();
1657 for child in params_node.children(&mut cursor) {
1658 match child.kind() {
1659 "identifier" => {
1660 let name = extract_identifier(child, content);
1661 if name.is_empty() {
1662 continue;
1663 }
1664 let qualified_param = format!("{lambda_prefix}::{name}");
1665 let span = Span::from_bytes(child.start_byte(), child.end_byte());
1666 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
1667 scope_tree.attach_node_id(&name, child.start_byte(), param_id);
1668 }
1669 "formal_parameter" => {
1670 let Some(name_node) = child.child_by_field_name("name") else {
1671 continue;
1672 };
1673 let Some(type_node) = child.child_by_field_name("type") else {
1674 continue;
1675 };
1676 let name = extract_identifier(name_node, content);
1677 if name.is_empty() {
1678 continue;
1679 }
1680 let type_text = extract_type_name(type_node, content);
1681 let resolved_type = ast_graph
1682 .import_map
1683 .get(&type_text)
1684 .cloned()
1685 .unwrap_or(type_text);
1686 let qualified_param = format!("{lambda_prefix}::{name}");
1687 let span = Span::from_bytes(child.start_byte(), child.end_byte());
1688 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
1689 scope_tree.attach_node_id(&name, name_node.start_byte(), param_id);
1690 let type_id = helper.add_class(&resolved_type, None);
1691 helper.add_typeof_edge(param_id, type_id);
1692 }
1693 _ => {}
1694 }
1695 }
1696}
1697
1698fn handle_try_with_resources_declaration(
1699 node: Node,
1700 content: &[u8],
1701 ast_graph: &ASTGraph,
1702 scope_tree: &mut JavaScopeTree,
1703 helper: &mut GraphBuildHelper,
1704) {
1705 let Some(resources) = node.child_by_field_name("resources") else {
1706 return;
1707 };
1708
1709 let mut cursor = resources.walk();
1710 for resource in resources.children(&mut cursor) {
1711 if resource.kind() != "resource" {
1712 continue;
1713 }
1714 let name_node = resource.child_by_field_name("name");
1715 let type_node = resource.child_by_field_name("type");
1716 let value_node = resource.child_by_field_name("value");
1717 if let Some(name_node) = name_node {
1718 if type_node.is_none() && value_node.is_none() {
1719 continue;
1720 }
1721 let name = extract_identifier(name_node, content);
1722 if name.is_empty() {
1723 continue;
1724 }
1725
1726 let qualified_var = format!("{}@{}", name, name_node.start_byte());
1727 let span = Span::from_bytes(resource.start_byte(), resource.end_byte());
1728 let var_id = helper.add_variable(&qualified_var, Some(span));
1729 scope_tree.attach_node_id(&name, name_node.start_byte(), var_id);
1730
1731 if let Some(type_node) = type_node {
1732 let type_text = extract_type_name(type_node, content);
1733 if !type_text.is_empty() {
1734 let resolved_type = ast_graph
1735 .import_map
1736 .get(&type_text)
1737 .cloned()
1738 .unwrap_or(type_text);
1739 let type_id = helper.add_class(&resolved_type, None);
1740 helper.add_typeof_edge(var_id, type_id);
1741 }
1742 }
1743 }
1744 }
1745}
1746
1747fn handle_instanceof_pattern_declaration(
1748 node: Node,
1749 content: &[u8],
1750 ast_graph: &ASTGraph,
1751 scope_tree: &mut JavaScopeTree,
1752 helper: &mut GraphBuildHelper,
1753) {
1754 let mut patterns = Vec::new();
1755 collect_pattern_declarations(node, &mut patterns);
1756 for (name_node, type_node) in patterns {
1757 let name = extract_identifier(name_node, content);
1758 if name.is_empty() {
1759 continue;
1760 }
1761 let qualified_var = format!("{}@{}", name, name_node.start_byte());
1762 let span = Span::from_bytes(name_node.start_byte(), name_node.end_byte());
1763 let var_id = helper.add_variable(&qualified_var, Some(span));
1764 scope_tree.attach_node_id(&name, name_node.start_byte(), var_id);
1765
1766 if let Some(type_node) = type_node {
1767 let type_text = extract_type_name(type_node, content);
1768 if !type_text.is_empty() {
1769 let resolved_type = ast_graph
1770 .import_map
1771 .get(&type_text)
1772 .cloned()
1773 .unwrap_or(type_text);
1774 let type_id = helper.add_class(&resolved_type, None);
1775 helper.add_typeof_edge(var_id, type_id);
1776 }
1777 }
1778 }
1779}
1780
1781fn handle_switch_pattern_declaration(
1782 node: Node,
1783 content: &[u8],
1784 ast_graph: &ASTGraph,
1785 scope_tree: &mut JavaScopeTree,
1786 helper: &mut GraphBuildHelper,
1787) {
1788 let mut patterns = Vec::new();
1789 collect_pattern_declarations(node, &mut patterns);
1790 for (name_node, type_node) in patterns {
1791 let name = extract_identifier(name_node, content);
1792 if name.is_empty() {
1793 continue;
1794 }
1795 let qualified_var = format!("{}@{}", name, name_node.start_byte());
1796 let span = Span::from_bytes(name_node.start_byte(), name_node.end_byte());
1797 let var_id = helper.add_variable(&qualified_var, Some(span));
1798 scope_tree.attach_node_id(&name, name_node.start_byte(), var_id);
1799
1800 if let Some(type_node) = type_node {
1801 let type_text = extract_type_name(type_node, content);
1802 if !type_text.is_empty() {
1803 let resolved_type = ast_graph
1804 .import_map
1805 .get(&type_text)
1806 .cloned()
1807 .unwrap_or(type_text);
1808 let type_id = helper.add_class(&resolved_type, None);
1809 helper.add_typeof_edge(var_id, type_id);
1810 }
1811 }
1812 }
1813}
1814
1815fn handle_compact_constructor_parameters(
1816 node: Node,
1817 content: &[u8],
1818 ast_graph: &ASTGraph,
1819 scope_tree: &mut JavaScopeTree,
1820 helper: &mut GraphBuildHelper,
1821) {
1822 use sqry_core::graph::unified::node::NodeKind;
1823
1824 let Some(record_node) = node
1825 .parent()
1826 .and_then(|parent| find_record_declaration(parent))
1827 else {
1828 return;
1829 };
1830
1831 let Some(record_name_node) = record_node.child_by_field_name("name") else {
1832 return;
1833 };
1834 let record_name = extract_identifier(record_name_node, content);
1835 if record_name.is_empty() {
1836 return;
1837 }
1838
1839 let mut components = Vec::new();
1840 collect_record_components_nodes(record_node, &mut components);
1841 for component in components {
1842 let Some(name_node) = component.child_by_field_name("name") else {
1843 continue;
1844 };
1845 let Some(type_node) = component.child_by_field_name("type") else {
1846 continue;
1847 };
1848 let name = extract_identifier(name_node, content);
1849 if name.is_empty() {
1850 continue;
1851 }
1852
1853 let type_text = extract_type_name(type_node, content);
1854 if type_text.is_empty() {
1855 continue;
1856 }
1857 let resolved_type = ast_graph
1858 .import_map
1859 .get(&type_text)
1860 .cloned()
1861 .unwrap_or(type_text);
1862
1863 let qualified_param = format!("{record_name}.<init>::{name}");
1864 let span = Span::from_bytes(component.start_byte(), component.end_byte());
1865 let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
1866 scope_tree.attach_node_id(&name, name_node.start_byte(), param_id);
1867
1868 let type_id = helper.add_class(&resolved_type, None);
1869 helper.add_typeof_edge(param_id, type_id);
1870 }
1871}
1872
1873fn collect_pattern_declarations<'a>(
1874 node: Node<'a>,
1875 output: &mut Vec<(Node<'a>, Option<Node<'a>>)>,
1876) {
1877 if node.kind() == "instanceof_expression"
1878 && !node_has_direct_child_kind(node, "type_pattern")
1879 && let Some(name_node) = node.child_by_field_name("name")
1880 {
1881 let type_node = first_type_like_child(node);
1882 output.push((name_node, type_node));
1883 }
1884
1885 if node.kind() == "type_pattern"
1886 && let Some((name_node, type_node)) = typed_pattern_parts(node)
1887 {
1888 output.push((name_node, type_node));
1889 }
1890
1891 if node.kind() == "record_pattern_component"
1892 && let Some((name_node, type_node)) = typed_pattern_parts(node)
1893 {
1894 output.push((name_node, type_node));
1895 }
1896
1897 let mut cursor = node.walk();
1898 for child in node.children(&mut cursor) {
1899 collect_pattern_declarations(child, output);
1900 }
1901}
1902
1903fn node_has_direct_child_kind(node: Node, kind: &str) -> bool {
1904 let mut cursor = node.walk();
1905 node.children(&mut cursor).any(|child| child.kind() == kind)
1906}
1907
1908fn typed_pattern_parts(node: Node) -> Option<(Node, Option<Node>)> {
1909 let mut name_node = None;
1910 let mut type_node = None;
1911 let mut cursor = node.walk();
1912 for child in node.children(&mut cursor) {
1913 if matches!(child.kind(), "identifier" | "_reserved_identifier") {
1914 name_node = Some(child);
1915 } else if matches!(
1916 child.kind(),
1917 "type_identifier" | "scoped_type_identifier" | "generic_type"
1918 ) {
1919 type_node = Some(child);
1920 }
1921 }
1922 name_node.map(|name| (name, type_node))
1923}
1924
1925fn first_type_like_child(node: Node) -> Option<Node> {
1926 let mut cursor = node.walk();
1927 for child in node.children(&mut cursor) {
1928 if matches!(
1929 child.kind(),
1930 "type_identifier" | "scoped_type_identifier" | "generic_type"
1931 ) {
1932 return Some(child);
1933 }
1934 }
1935 None
1936}
1937
1938fn find_record_declaration(node: Node) -> Option<Node> {
1939 if node.kind() == "record_declaration" {
1940 return Some(node);
1941 }
1942 node.parent().and_then(find_record_declaration)
1943}
1944
1945fn collect_record_components_nodes<'a>(node: Node<'a>, output: &mut Vec<Node<'a>>) {
1946 if let Some(parameters) = node.child_by_field_name("parameters") {
1947 let mut cursor = parameters.walk();
1948 for child in parameters.children(&mut cursor) {
1949 if matches!(child.kind(), "formal_parameter" | "record_component") {
1950 output.push(child);
1951 }
1952 }
1953 return;
1954 }
1955
1956 let mut cursor = node.walk();
1957 for child in node.children(&mut cursor) {
1958 if child.kind() == "record_component" {
1959 output.push(child);
1960 }
1961 }
1962}
1963
1964fn process_method_call_unified(
1966 call_node: Node,
1967 content: &[u8],
1968 ast_graph: &ASTGraph,
1969 helper: &mut GraphBuildHelper,
1970) {
1971 let Some(caller_context) = ast_graph.find_enclosing(call_node.start_byte()) else {
1972 return;
1973 };
1974 let Ok(callee_name) = extract_method_invocation_name(call_node, content) else {
1975 return;
1976 };
1977
1978 let callee_qualified =
1979 resolve_callee_qualified(&call_node, content, ast_graph, caller_context, &callee_name);
1980 let caller_method_id = ensure_caller_method(helper, caller_context);
1981 let target_method_id = helper.ensure_method(&callee_qualified, None, false, false);
1982
1983 add_call_edge(helper, caller_method_id, target_method_id, call_node);
1984}
1985
1986fn process_constructor_call_unified(
1988 new_node: Node,
1989 content: &[u8],
1990 ast_graph: &ASTGraph,
1991 helper: &mut GraphBuildHelper,
1992) {
1993 let Some(caller_context) = ast_graph.find_enclosing(new_node.start_byte()) else {
1994 return;
1995 };
1996
1997 let Some(type_node) = new_node.child_by_field_name("type") else {
1998 return;
1999 };
2000
2001 let class_name = extract_type_name(type_node, content);
2002 if class_name.is_empty() {
2003 return;
2004 }
2005
2006 let qualified_class = qualify_constructor_class(&class_name, caller_context);
2007 let constructor_name = format!("{qualified_class}.<init>");
2008
2009 let caller_method_id = ensure_caller_method(helper, caller_context);
2010 let target_method_id = helper.ensure_method(&constructor_name, None, false, false);
2011 add_call_edge(helper, caller_method_id, target_method_id, new_node);
2012}
2013
2014fn count_call_arguments(call_node: Node<'_>) -> u8 {
2015 let Some(args_node) = call_node.child_by_field_name("arguments") else {
2016 return 255;
2017 };
2018 let count = args_node.named_child_count();
2019 if count <= 254 {
2020 u8::try_from(count).unwrap_or(u8::MAX)
2021 } else {
2022 u8::MAX
2023 }
2024}
2025
2026fn process_import_unified(import_node: Node, content: &[u8], helper: &mut GraphBuildHelper) {
2028 let has_asterisk = import_has_wildcard(import_node);
2029 let Some(mut imported_name) = extract_import_name(import_node, content) else {
2030 return;
2031 };
2032 if has_asterisk {
2033 imported_name = format!("{imported_name}.*");
2034 }
2035
2036 let module_id = helper.add_module("<module>", None);
2037 let external_id = helper.add_import(
2038 &imported_name,
2039 Some(Span::from_bytes(
2040 import_node.start_byte(),
2041 import_node.end_byte(),
2042 )),
2043 );
2044
2045 helper.add_import_edge(module_id, external_id);
2046}
2047
2048fn ensure_caller_method(
2049 helper: &mut GraphBuildHelper,
2050 caller_context: &MethodContext,
2051) -> sqry_core::graph::unified::node::NodeId {
2052 helper.ensure_method(
2053 caller_context.qualified_name(),
2054 Some(Span::from_bytes(
2055 caller_context.span.0,
2056 caller_context.span.1,
2057 )),
2058 false,
2059 caller_context.is_static,
2060 )
2061}
2062
2063fn resolve_callee_qualified(
2064 call_node: &Node,
2065 content: &[u8],
2066 ast_graph: &ASTGraph,
2067 caller_context: &MethodContext,
2068 callee_name: &str,
2069) -> String {
2070 if let Some(object_node) = call_node.child_by_field_name("object") {
2071 let object_text = extract_node_text(object_node, content);
2072 return resolve_member_call_target(&object_text, ast_graph, caller_context, callee_name);
2073 }
2074
2075 build_member_symbol(
2076 caller_context.package_name.as_deref(),
2077 &caller_context.class_stack,
2078 callee_name,
2079 )
2080}
2081
2082fn resolve_member_call_target(
2083 object_text: &str,
2084 ast_graph: &ASTGraph,
2085 caller_context: &MethodContext,
2086 callee_name: &str,
2087) -> String {
2088 if object_text.contains('.') {
2089 return format!("{object_text}.{callee_name}");
2090 }
2091 if object_text == "this" {
2092 return build_member_symbol(
2093 caller_context.package_name.as_deref(),
2094 &caller_context.class_stack,
2095 callee_name,
2096 );
2097 }
2098
2099 if let Some(class_name) = caller_context.class_stack.last() {
2101 let qualified_field = format!("{class_name}::{object_text}");
2102 if let Some((field_type, _is_final, _visibility, _is_static)) =
2103 ast_graph.field_types.get(&qualified_field)
2104 {
2105 return format!("{field_type}.{callee_name}");
2106 }
2107 }
2108
2109 if let Some((field_type, _is_final, _visibility, _is_static)) =
2111 ast_graph.field_types.get(object_text)
2112 {
2113 return format!("{field_type}.{callee_name}");
2114 }
2115
2116 if let Some(type_fqn) = ast_graph.import_map.get(object_text) {
2117 return format!("{type_fqn}.{callee_name}");
2118 }
2119
2120 format!("{object_text}.{callee_name}")
2121}
2122
2123fn qualify_constructor_class(class_name: &str, caller_context: &MethodContext) -> String {
2124 if class_name.contains('.') {
2125 class_name.to_string()
2126 } else if let Some(pkg) = caller_context.package_name.as_deref() {
2127 format!("{pkg}.{class_name}")
2128 } else {
2129 class_name.to_string()
2130 }
2131}
2132
2133fn add_call_edge(
2134 helper: &mut GraphBuildHelper,
2135 caller_method_id: sqry_core::graph::unified::node::NodeId,
2136 target_method_id: sqry_core::graph::unified::node::NodeId,
2137 call_node: Node,
2138) {
2139 let argument_count = count_call_arguments(call_node);
2140 let call_span = Span::from_bytes(call_node.start_byte(), call_node.end_byte());
2141 helper.add_call_edge_full_with_span(
2142 caller_method_id,
2143 target_method_id,
2144 argument_count,
2145 false,
2146 vec![call_span],
2147 );
2148}
2149
2150fn import_has_wildcard(import_node: Node) -> bool {
2151 let mut cursor = import_node.walk();
2152 import_node
2153 .children(&mut cursor)
2154 .any(|child| child.kind() == "asterisk")
2155}
2156
2157fn extract_import_name(import_node: Node, content: &[u8]) -> Option<String> {
2158 let mut cursor = import_node.walk();
2159 for child in import_node.children(&mut cursor) {
2160 if child.kind() == "scoped_identifier" || child.kind() == "identifier" {
2161 return Some(extract_full_identifier(child, content));
2162 }
2163 }
2164 None
2165}
2166
2167fn process_inheritance(
2177 class_node: Node,
2178 content: &[u8],
2179 package_name: Option<&str>,
2180 child_class_id: sqry_core::graph::unified::node::NodeId,
2181 helper: &mut GraphBuildHelper,
2182) {
2183 if let Some(superclass_node) = class_node.child_by_field_name("superclass") {
2185 let parent_type_name = extract_type_from_superclass(superclass_node, content);
2187 if !parent_type_name.is_empty() {
2188 let parent_qualified = qualify_type_name(&parent_type_name, package_name);
2190 let parent_id = helper.add_class(&parent_qualified, None);
2191 helper.add_inherits_edge(child_class_id, parent_id);
2192 }
2193 }
2194}
2195
2196fn process_implements(
2202 class_node: Node,
2203 content: &[u8],
2204 package_name: Option<&str>,
2205 class_id: sqry_core::graph::unified::node::NodeId,
2206 helper: &mut GraphBuildHelper,
2207) {
2208 let interfaces_node = class_node
2214 .child_by_field_name("interfaces")
2215 .or_else(|| class_node.child_by_field_name("super_interfaces"));
2216
2217 if let Some(node) = interfaces_node {
2218 extract_interface_types(node, content, package_name, class_id, helper);
2219 return;
2220 }
2221
2222 let mut cursor = class_node.walk();
2224 for child in class_node.children(&mut cursor) {
2225 if child.kind() == "super_interfaces" {
2227 extract_interface_types(child, content, package_name, class_id, helper);
2228 return;
2229 }
2230 }
2231}
2232
2233fn process_interface_extends(
2251 interface_node: Node,
2252 content: &[u8],
2253 package_name: Option<&str>,
2254 interface_id: sqry_core::graph::unified::node::NodeId,
2255 helper: &mut GraphBuildHelper,
2256) {
2257 let mut cursor = interface_node.walk();
2259 for child in interface_node.children(&mut cursor) {
2260 if child.kind() == "extends_interfaces" {
2261 extract_parent_interfaces_for_inherits(
2263 child,
2264 content,
2265 package_name,
2266 interface_id,
2267 helper,
2268 );
2269 return;
2270 }
2271 }
2272}
2273
2274fn extract_parent_interfaces_for_inherits(
2277 extends_node: Node,
2278 content: &[u8],
2279 package_name: Option<&str>,
2280 child_interface_id: sqry_core::graph::unified::node::NodeId,
2281 helper: &mut GraphBuildHelper,
2282) {
2283 let mut cursor = extends_node.walk();
2284 for child in extends_node.children(&mut cursor) {
2285 match child.kind() {
2286 "type_identifier" => {
2287 let type_name = extract_identifier(child, content);
2288 if !type_name.is_empty() {
2289 let parent_qualified = qualify_type_name(&type_name, package_name);
2290 let parent_id = helper.add_interface(&parent_qualified, None);
2291 helper.add_inherits_edge(child_interface_id, parent_id);
2292 }
2293 }
2294 "type_list" => {
2295 let mut type_cursor = child.walk();
2296 for type_child in child.children(&mut type_cursor) {
2297 if let Some(type_name) = extract_type_identifier(type_child, content)
2298 && !type_name.is_empty()
2299 {
2300 let parent_qualified = qualify_type_name(&type_name, package_name);
2301 let parent_id = helper.add_interface(&parent_qualified, None);
2302 helper.add_inherits_edge(child_interface_id, parent_id);
2303 }
2304 }
2305 }
2306 "generic_type" | "scoped_type_identifier" => {
2307 if let Some(type_name) = extract_type_identifier(child, content)
2308 && !type_name.is_empty()
2309 {
2310 let parent_qualified = qualify_type_name(&type_name, package_name);
2311 let parent_id = helper.add_interface(&parent_qualified, None);
2312 helper.add_inherits_edge(child_interface_id, parent_id);
2313 }
2314 }
2315 _ => {}
2316 }
2317 }
2318}
2319
2320fn extract_type_from_superclass(superclass_node: Node, content: &[u8]) -> String {
2322 if superclass_node.kind() == "type_identifier" {
2324 return extract_identifier(superclass_node, content);
2325 }
2326
2327 let mut cursor = superclass_node.walk();
2329 for child in superclass_node.children(&mut cursor) {
2330 if let Some(name) = extract_type_identifier(child, content) {
2331 return name;
2332 }
2333 }
2334
2335 extract_identifier(superclass_node, content)
2337}
2338
2339fn extract_interface_types(
2350 interfaces_node: Node,
2351 content: &[u8],
2352 package_name: Option<&str>,
2353 implementor_id: sqry_core::graph::unified::node::NodeId,
2354 helper: &mut GraphBuildHelper,
2355) {
2356 let mut cursor = interfaces_node.walk();
2358 for child in interfaces_node.children(&mut cursor) {
2359 match child.kind() {
2360 "type_identifier" => {
2362 let type_name = extract_identifier(child, content);
2363 if !type_name.is_empty() {
2364 let interface_qualified = qualify_type_name(&type_name, package_name);
2365 let interface_id = helper.add_interface(&interface_qualified, None);
2366 helper.add_implements_edge(implementor_id, interface_id);
2367 }
2368 }
2369 "type_list" => {
2371 let mut type_cursor = child.walk();
2372 for type_child in child.children(&mut type_cursor) {
2373 if let Some(type_name) = extract_type_identifier(type_child, content)
2374 && !type_name.is_empty()
2375 {
2376 let interface_qualified = qualify_type_name(&type_name, package_name);
2377 let interface_id = helper.add_interface(&interface_qualified, None);
2378 helper.add_implements_edge(implementor_id, interface_id);
2379 }
2380 }
2381 }
2382 "generic_type" | "scoped_type_identifier" => {
2384 if let Some(type_name) = extract_type_identifier(child, content)
2385 && !type_name.is_empty()
2386 {
2387 let interface_qualified = qualify_type_name(&type_name, package_name);
2388 let interface_id = helper.add_interface(&interface_qualified, None);
2389 helper.add_implements_edge(implementor_id, interface_id);
2390 }
2391 }
2392 _ => {}
2393 }
2394 }
2395}
2396
2397fn extract_type_identifier(node: Node, content: &[u8]) -> Option<String> {
2399 match node.kind() {
2400 "type_identifier" => Some(extract_identifier(node, content)),
2401 "generic_type" => {
2402 if let Some(name_node) = node.child_by_field_name("name") {
2404 Some(extract_identifier(name_node, content))
2405 } else {
2406 let mut cursor = node.walk();
2408 for child in node.children(&mut cursor) {
2409 if child.kind() == "type_identifier" {
2410 return Some(extract_identifier(child, content));
2411 }
2412 }
2413 None
2414 }
2415 }
2416 "scoped_type_identifier" => {
2417 Some(extract_full_identifier(node, content))
2419 }
2420 _ => None,
2421 }
2422}
2423
2424fn qualify_type_name(type_name: &str, package_name: Option<&str>) -> String {
2426 if type_name.contains('.') {
2428 return type_name.to_string();
2429 }
2430
2431 if let Some(pkg) = package_name {
2433 format!("{pkg}.{type_name}")
2434 } else {
2435 type_name.to_string()
2436 }
2437}
2438
2439#[allow(clippy::type_complexity)]
2448fn extract_field_and_import_types(
2449 node: Node,
2450 content: &[u8],
2451) -> (
2452 HashMap<String, (String, bool, Option<sqry_core::schema::Visibility>, bool)>,
2453 HashMap<String, String>,
2454) {
2455 let import_map = extract_import_map(node, content);
2457
2458 let mut field_types = HashMap::new();
2459 let mut class_stack = Vec::new();
2460 extract_field_types_recursive(
2461 node,
2462 content,
2463 &import_map,
2464 &mut field_types,
2465 &mut class_stack,
2466 );
2467
2468 (field_types, import_map)
2469}
2470
2471fn extract_import_map(node: Node, content: &[u8]) -> HashMap<String, String> {
2473 let mut import_map = HashMap::new();
2474 collect_import_map_recursive(node, content, &mut import_map);
2475 import_map
2476}
2477
2478fn collect_import_map_recursive(
2479 node: Node,
2480 content: &[u8],
2481 import_map: &mut HashMap<String, String>,
2482) {
2483 if node.kind() == "import_declaration" {
2484 let full_path = node.utf8_text(content).unwrap_or("");
2488
2489 if let Some(path_start) = full_path.find("import ") {
2492 let after_import = &full_path[path_start + 7..].trim();
2493 if let Some(path_end) = after_import.find(';') {
2494 let import_path = &after_import[..path_end].trim();
2495
2496 if let Some(simple_name) = import_path.rsplit('.').next() {
2498 import_map.insert(simple_name.to_string(), (*import_path).to_string());
2499 }
2500 }
2501 }
2502 }
2503
2504 let mut cursor = node.walk();
2506 for child in node.children(&mut cursor) {
2507 collect_import_map_recursive(child, content, import_map);
2508 }
2509}
2510
2511fn extract_field_types_recursive(
2512 node: Node,
2513 content: &[u8],
2514 import_map: &HashMap<String, String>,
2515 field_types: &mut HashMap<String, (String, bool, Option<sqry_core::schema::Visibility>, bool)>,
2516 class_stack: &mut Vec<String>,
2517) {
2518 if matches!(
2520 node.kind(),
2521 "class_declaration" | "interface_declaration" | "enum_declaration" | "record_declaration"
2522 ) && let Some(name_node) = node.child_by_field_name("name")
2523 {
2524 let class_name = extract_identifier(name_node, content);
2525 class_stack.push(class_name);
2526
2527 if let Some(body_node) = node.child_by_field_name("body") {
2529 let mut cursor = body_node.walk();
2530 for child in body_node.children(&mut cursor) {
2531 extract_field_types_recursive(child, content, import_map, field_types, class_stack);
2532 }
2533 }
2534
2535 class_stack.pop();
2537 return; }
2539
2540 if node.kind() == "field_declaration" {
2547 let is_final = has_modifier(node, "final", content);
2549 let is_static = has_modifier(node, "static", content);
2550
2551 let visibility = if has_modifier(node, "public", content) {
2554 Some(sqry_core::schema::Visibility::Public)
2555 } else {
2556 Some(sqry_core::schema::Visibility::Private)
2558 };
2559
2560 if let Some(type_node) = node.child_by_field_name("type") {
2562 let type_text = extract_type_name_internal(type_node, content);
2563 if !type_text.is_empty() {
2564 let resolved_type = import_map
2566 .get(&type_text)
2567 .cloned()
2568 .unwrap_or(type_text.clone());
2569
2570 let mut cursor = node.walk();
2572 for child in node.children(&mut cursor) {
2573 if child.kind() == "variable_declarator"
2574 && let Some(name_node) = child.child_by_field_name("name")
2575 {
2576 let field_name = extract_identifier(name_node, content);
2577
2578 let qualified_field = if class_stack.is_empty() {
2581 field_name
2582 } else {
2583 let class_path = class_stack.join("::");
2584 format!("{class_path}::{field_name}")
2585 };
2586
2587 field_types.insert(
2588 qualified_field,
2589 (resolved_type.clone(), is_final, visibility, is_static),
2590 );
2591 }
2592 }
2593 }
2594 }
2595 }
2596
2597 let mut cursor = node.walk();
2599 for child in node.children(&mut cursor) {
2600 extract_field_types_recursive(child, content, import_map, field_types, class_stack);
2601 }
2602}
2603
2604fn extract_type_name_internal(type_node: Node, content: &[u8]) -> String {
2606 match type_node.kind() {
2607 "generic_type" => {
2608 if let Some(name_node) = type_node.child_by_field_name("name") {
2610 extract_identifier(name_node, content)
2611 } else {
2612 extract_identifier(type_node, content)
2613 }
2614 }
2615 "scoped_type_identifier" => {
2616 extract_full_identifier(type_node, content)
2618 }
2619 _ => extract_identifier(type_node, content),
2620 }
2621}
2622
2623fn extract_identifier(node: Node, content: &[u8]) -> String {
2628 node.utf8_text(content).unwrap_or("").to_string()
2629}
2630
2631fn extract_node_text(node: Node, content: &[u8]) -> String {
2632 node.utf8_text(content).unwrap_or("").to_string()
2633}
2634
2635fn extract_full_identifier(node: Node, content: &[u8]) -> String {
2636 node.utf8_text(content).unwrap_or("").to_string()
2637}
2638
2639fn first_child_of_kind<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
2640 let mut cursor = node.walk();
2641 node.children(&mut cursor)
2642 .find(|&child| child.kind() == kind)
2643}
2644
2645fn extract_method_invocation_name(call_node: Node, content: &[u8]) -> GraphResult<String> {
2646 if let Some(name_node) = call_node.child_by_field_name("name") {
2648 Ok(extract_identifier(name_node, content))
2649 } else {
2650 let mut cursor = call_node.walk();
2652 for child in call_node.children(&mut cursor) {
2653 if child.kind() == "identifier" {
2654 return Ok(extract_identifier(child, content));
2655 }
2656 }
2657
2658 Err(GraphBuilderError::ParseError {
2659 span: Span::from_bytes(call_node.start_byte(), call_node.end_byte()),
2660 reason: "Method invocation missing name".into(),
2661 })
2662 }
2663}
2664
2665fn extract_type_name(type_node: Node, content: &[u8]) -> String {
2666 match type_node.kind() {
2668 "generic_type" => {
2669 if let Some(name_node) = type_node.child_by_field_name("name") {
2671 extract_identifier(name_node, content)
2672 } else {
2673 extract_identifier(type_node, content)
2674 }
2675 }
2676 "scoped_type_identifier" => {
2677 extract_full_identifier(type_node, content)
2679 }
2680 _ => extract_identifier(type_node, content),
2681 }
2682}
2683
2684fn extract_full_return_type(type_node: Node, content: &[u8]) -> String {
2687 type_node.utf8_text(content).unwrap_or("").to_string()
2690}
2691
2692fn has_modifier(node: Node, modifier: &str, content: &[u8]) -> bool {
2693 let mut cursor = node.walk();
2694 for child in node.children(&mut cursor) {
2695 if child.kind() == "modifiers" {
2696 let mut mod_cursor = child.walk();
2697 for modifier_child in child.children(&mut mod_cursor) {
2698 if extract_identifier(modifier_child, content) == modifier {
2699 return true;
2700 }
2701 }
2702 }
2703 }
2704 false
2705}
2706
2707#[allow(clippy::unnecessary_wraps)]
2710fn extract_visibility(node: Node, content: &[u8]) -> Option<String> {
2711 if has_modifier(node, "public", content) {
2712 Some("public".to_string())
2713 } else if has_modifier(node, "private", content) {
2714 Some("private".to_string())
2715 } else if has_modifier(node, "protected", content) {
2716 Some("protected".to_string())
2717 } else {
2718 Some("package-private".to_string())
2720 }
2721}
2722
2723fn is_public(node: Node, content: &[u8]) -> bool {
2729 has_modifier(node, "public", content)
2730}
2731
2732fn is_private(node: Node, content: &[u8]) -> bool {
2734 has_modifier(node, "private", content)
2735}
2736
2737fn export_from_file_module(
2739 helper: &mut GraphBuildHelper,
2740 exported: sqry_core::graph::unified::node::NodeId,
2741) {
2742 let module_id = helper.add_module(FILE_MODULE_NAME, None);
2743 helper.add_export_edge(module_id, exported);
2744}
2745
2746fn process_class_member_exports(
2751 body_node: Node,
2752 content: &[u8],
2753 class_qualified_name: &str,
2754 helper: &mut GraphBuildHelper,
2755 is_interface: bool,
2756) {
2757 for i in 0..body_node.child_count() {
2758 #[allow(clippy::cast_possible_truncation)]
2759 if let Some(child) = body_node.child(i as u32) {
2761 match child.kind() {
2762 "method_declaration" => {
2763 let should_export = if is_interface {
2766 !is_private(child, content)
2768 } else {
2769 is_public(child, content)
2771 };
2772
2773 if should_export && let Some(name_node) = child.child_by_field_name("name") {
2774 let method_name = extract_identifier(name_node, content);
2775 let qualified_name = format!("{class_qualified_name}.{method_name}");
2776 let span = Span::from_bytes(child.start_byte(), child.end_byte());
2777 let is_static = has_modifier(child, "static", content);
2778 let method_id =
2779 helper.add_method(&qualified_name, Some(span), false, is_static);
2780 export_from_file_module(helper, method_id);
2781 }
2782 }
2783 "constructor_declaration" => {
2784 if is_public(child, content) {
2785 let qualified_name = format!("{class_qualified_name}.<init>");
2786 let span = Span::from_bytes(child.start_byte(), child.end_byte());
2787 let method_id =
2788 helper.add_method(&qualified_name, Some(span), false, false);
2789 export_from_file_module(helper, method_id);
2790 }
2791 }
2792 "field_declaration" => {
2793 if is_public(child, content) {
2794 let mut cursor = child.walk();
2796 for field_child in child.children(&mut cursor) {
2797 if field_child.kind() == "variable_declarator"
2798 && let Some(name_node) = field_child.child_by_field_name("name")
2799 {
2800 let field_name = extract_identifier(name_node, content);
2801 let qualified_name = format!("{class_qualified_name}.{field_name}");
2802 let span = Span::from_bytes(
2803 field_child.start_byte(),
2804 field_child.end_byte(),
2805 );
2806
2807 let is_final = has_modifier(child, "final", content);
2809 let field_id = if is_final {
2810 helper.add_constant(&qualified_name, Some(span))
2811 } else {
2812 helper.add_variable(&qualified_name, Some(span))
2813 };
2814 export_from_file_module(helper, field_id);
2815 }
2816 }
2817 }
2818 }
2819 "constant_declaration" => {
2820 let mut cursor = child.walk();
2822 for const_child in child.children(&mut cursor) {
2823 if const_child.kind() == "variable_declarator"
2824 && let Some(name_node) = const_child.child_by_field_name("name")
2825 {
2826 let const_name = extract_identifier(name_node, content);
2827 let qualified_name = format!("{class_qualified_name}.{const_name}");
2828 let span =
2829 Span::from_bytes(const_child.start_byte(), const_child.end_byte());
2830 let const_id = helper.add_constant(&qualified_name, Some(span));
2831 export_from_file_module(helper, const_id);
2832 }
2833 }
2834 }
2835 "enum_constant" => {
2836 if let Some(name_node) = child.child_by_field_name("name") {
2838 let const_name = extract_identifier(name_node, content);
2839 let qualified_name = format!("{class_qualified_name}.{const_name}");
2840 let span = Span::from_bytes(child.start_byte(), child.end_byte());
2841 let const_id = helper.add_constant(&qualified_name, Some(span));
2842 export_from_file_module(helper, const_id);
2843 }
2844 }
2845 _ => {}
2846 }
2847 }
2848 }
2849}
2850
2851fn detect_ffi_imports(node: Node, content: &[u8]) -> (bool, bool) {
2858 let mut has_jna = false;
2859 let mut has_panama = false;
2860
2861 detect_ffi_imports_recursive(node, content, &mut has_jna, &mut has_panama);
2862
2863 (has_jna, has_panama)
2864}
2865
2866fn detect_ffi_imports_recursive(
2867 node: Node,
2868 content: &[u8],
2869 has_jna: &mut bool,
2870 has_panama: &mut bool,
2871) {
2872 if node.kind() == "import_declaration" {
2873 let import_text = node.utf8_text(content).unwrap_or("");
2874
2875 if import_text.contains("com.sun.jna") || import_text.contains("net.java.dev.jna") {
2877 *has_jna = true;
2878 }
2879
2880 if import_text.contains("java.lang.foreign") {
2882 *has_panama = true;
2883 }
2884 }
2885
2886 let mut cursor = node.walk();
2887 for child in node.children(&mut cursor) {
2888 detect_ffi_imports_recursive(child, content, has_jna, has_panama);
2889 }
2890}
2891
2892fn find_jna_library_interfaces(node: Node, content: &[u8]) -> Vec<String> {
2895 let mut jna_interfaces = Vec::new();
2896 find_jna_library_interfaces_recursive(node, content, &mut jna_interfaces);
2897 jna_interfaces
2898}
2899
2900fn find_jna_library_interfaces_recursive(
2901 node: Node,
2902 content: &[u8],
2903 jna_interfaces: &mut Vec<String>,
2904) {
2905 if node.kind() == "interface_declaration" {
2906 if let Some(name_node) = node.child_by_field_name("name") {
2908 let interface_name = extract_identifier(name_node, content);
2909
2910 let mut cursor = node.walk();
2912 for child in node.children(&mut cursor) {
2913 if child.kind() == "extends_interfaces" {
2914 let extends_text = child.utf8_text(content).unwrap_or("");
2915 if extends_text.contains("Library") {
2917 jna_interfaces.push(interface_name.clone());
2918 }
2919 }
2920 }
2921 }
2922 }
2923
2924 let mut cursor = node.walk();
2925 for child in node.children(&mut cursor) {
2926 find_jna_library_interfaces_recursive(child, content, jna_interfaces);
2927 }
2928}
2929
2930fn build_ffi_call_edge(
2933 call_node: Node,
2934 content: &[u8],
2935 caller_context: &MethodContext,
2936 ast_graph: &ASTGraph,
2937 helper: &mut GraphBuildHelper,
2938) -> bool {
2939 let Ok(method_name) = extract_method_invocation_name(call_node, content) else {
2941 return false;
2942 };
2943
2944 if ast_graph.has_jna_import && is_jna_native_load(call_node, content, &method_name) {
2946 let library_name = extract_jna_library_name(call_node, content);
2947 build_jna_native_load_edge(caller_context, &library_name, call_node, helper);
2948 return true;
2949 }
2950
2951 if ast_graph.has_jna_import
2953 && let Some(object_node) = call_node.child_by_field_name("object")
2954 {
2955 let object_text = extract_node_text(object_node, content);
2956
2957 let field_type = if let Some(class_name) = caller_context.class_stack.last() {
2959 let qualified_field = format!("{class_name}::{object_text}");
2960 ast_graph
2961 .field_types
2962 .get(&qualified_field)
2963 .or_else(|| ast_graph.field_types.get(&object_text))
2964 } else {
2965 ast_graph.field_types.get(&object_text)
2966 };
2967
2968 if let Some((type_name, _is_final, _visibility, _is_static)) = field_type {
2970 let simple_type = simple_type_name(type_name);
2971 if ast_graph.jna_library_interfaces.contains(&simple_type) {
2972 build_jna_method_call_edge(
2973 caller_context,
2974 &simple_type,
2975 &method_name,
2976 call_node,
2977 helper,
2978 );
2979 return true;
2980 }
2981 }
2982 }
2983
2984 if ast_graph.has_panama_import {
2986 if let Some(object_node) = call_node.child_by_field_name("object") {
2987 let object_text = extract_node_text(object_node, content);
2988
2989 if object_text == "Linker" && method_name == "nativeLinker" {
2991 build_panama_linker_edge(caller_context, call_node, helper);
2992 return true;
2993 }
2994
2995 if object_text == "SymbolLookup" && method_name == "libraryLookup" {
2997 let library_name = extract_first_string_arg(call_node, content);
2998 build_panama_library_lookup_edge(caller_context, &library_name, call_node, helper);
2999 return true;
3000 }
3001
3002 if method_name == "invokeExact" || method_name == "invoke" {
3004 if is_potential_panama_invoke(call_node, content) {
3007 build_panama_invoke_edge(caller_context, &method_name, call_node, helper);
3008 return true;
3009 }
3010 }
3011 }
3012
3013 if method_name == "nativeLinker" {
3015 let full_text = call_node.utf8_text(content).unwrap_or("");
3016 if full_text.contains("Linker") {
3017 build_panama_linker_edge(caller_context, call_node, helper);
3018 return true;
3019 }
3020 }
3021 }
3022
3023 false
3024}
3025
3026fn is_jna_native_load(call_node: Node, content: &[u8], method_name: &str) -> bool {
3028 if method_name != "load" && method_name != "loadLibrary" {
3029 return false;
3030 }
3031
3032 if let Some(object_node) = call_node.child_by_field_name("object") {
3033 let object_text = extract_node_text(object_node, content);
3034 return object_text == "Native" || object_text == "com.sun.jna.Native";
3035 }
3036
3037 false
3038}
3039
3040fn extract_jna_library_name(call_node: Node, content: &[u8]) -> String {
3043 if let Some(args_node) = call_node.child_by_field_name("arguments") {
3044 let mut cursor = args_node.walk();
3045 for child in args_node.children(&mut cursor) {
3046 if child.kind() == "string_literal" {
3047 let text = child.utf8_text(content).unwrap_or("\"unknown\"");
3048 return text.trim_matches('"').to_string();
3050 }
3051 }
3052 }
3053 "unknown".to_string()
3054}
3055
3056fn extract_first_string_arg(call_node: Node, content: &[u8]) -> String {
3058 if let Some(args_node) = call_node.child_by_field_name("arguments") {
3059 let mut cursor = args_node.walk();
3060 for child in args_node.children(&mut cursor) {
3061 if child.kind() == "string_literal" {
3062 let text = child.utf8_text(content).unwrap_or("\"unknown\"");
3063 return text.trim_matches('"').to_string();
3064 }
3065 }
3066 }
3067 "unknown".to_string()
3068}
3069
3070fn is_potential_panama_invoke(call_node: Node, content: &[u8]) -> bool {
3072 if let Some(object_node) = call_node.child_by_field_name("object") {
3074 let object_text = extract_node_text(object_node, content);
3075 let lower = object_text.to_lowercase();
3077 return lower.contains("handle")
3078 || lower.contains("downcall")
3079 || lower.contains("mh")
3080 || lower.contains("foreign");
3081 }
3082 false
3083}
3084
3085fn simple_type_name(type_name: &str) -> String {
3087 type_name
3088 .rsplit('.')
3089 .next()
3090 .unwrap_or(type_name)
3091 .to_string()
3092}
3093
3094fn build_jna_native_load_edge(
3096 caller_context: &MethodContext,
3097 library_name: &str,
3098 call_node: Node,
3099 helper: &mut GraphBuildHelper,
3100) {
3101 let caller_id = helper.ensure_method(
3102 caller_context.qualified_name(),
3103 Some(Span::from_bytes(
3104 caller_context.span.0,
3105 caller_context.span.1,
3106 )),
3107 false,
3108 caller_context.is_static,
3109 );
3110
3111 let target_name = format!("native::{library_name}");
3112 let target_id = helper.add_function(
3113 &target_name,
3114 Some(Span::from_bytes(
3115 call_node.start_byte(),
3116 call_node.end_byte(),
3117 )),
3118 false,
3119 false,
3120 );
3121
3122 helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3123}
3124
3125fn build_jna_method_call_edge(
3127 caller_context: &MethodContext,
3128 interface_name: &str,
3129 method_name: &str,
3130 call_node: Node,
3131 helper: &mut GraphBuildHelper,
3132) {
3133 let caller_id = helper.ensure_method(
3134 caller_context.qualified_name(),
3135 Some(Span::from_bytes(
3136 caller_context.span.0,
3137 caller_context.span.1,
3138 )),
3139 false,
3140 caller_context.is_static,
3141 );
3142
3143 let target_name = format!("native::{interface_name}::{method_name}");
3144 let target_id = helper.add_function(
3145 &target_name,
3146 Some(Span::from_bytes(
3147 call_node.start_byte(),
3148 call_node.end_byte(),
3149 )),
3150 false,
3151 false,
3152 );
3153
3154 helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3155}
3156
3157fn build_panama_linker_edge(
3159 caller_context: &MethodContext,
3160 call_node: Node,
3161 helper: &mut GraphBuildHelper,
3162) {
3163 let caller_id = helper.ensure_method(
3164 caller_context.qualified_name(),
3165 Some(Span::from_bytes(
3166 caller_context.span.0,
3167 caller_context.span.1,
3168 )),
3169 false,
3170 caller_context.is_static,
3171 );
3172
3173 let target_name = "native::panama::nativeLinker";
3174 let target_id = helper.add_function(
3175 target_name,
3176 Some(Span::from_bytes(
3177 call_node.start_byte(),
3178 call_node.end_byte(),
3179 )),
3180 false,
3181 false,
3182 );
3183
3184 helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3185}
3186
3187fn build_panama_library_lookup_edge(
3189 caller_context: &MethodContext,
3190 library_name: &str,
3191 call_node: Node,
3192 helper: &mut GraphBuildHelper,
3193) {
3194 let caller_id = helper.ensure_method(
3195 caller_context.qualified_name(),
3196 Some(Span::from_bytes(
3197 caller_context.span.0,
3198 caller_context.span.1,
3199 )),
3200 false,
3201 caller_context.is_static,
3202 );
3203
3204 let target_name = format!("native::panama::{library_name}");
3205 let target_id = helper.add_function(
3206 &target_name,
3207 Some(Span::from_bytes(
3208 call_node.start_byte(),
3209 call_node.end_byte(),
3210 )),
3211 false,
3212 false,
3213 );
3214
3215 helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3216}
3217
3218fn build_panama_invoke_edge(
3220 caller_context: &MethodContext,
3221 method_name: &str,
3222 call_node: Node,
3223 helper: &mut GraphBuildHelper,
3224) {
3225 let caller_id = helper.ensure_method(
3226 caller_context.qualified_name(),
3227 Some(Span::from_bytes(
3228 caller_context.span.0,
3229 caller_context.span.1,
3230 )),
3231 false,
3232 caller_context.is_static,
3233 );
3234
3235 let target_name = format!("native::panama::{method_name}");
3236 let target_id = helper.add_function(
3237 &target_name,
3238 Some(Span::from_bytes(
3239 call_node.start_byte(),
3240 call_node.end_byte(),
3241 )),
3242 false,
3243 false,
3244 );
3245
3246 helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3247}
3248
3249fn build_jni_native_method_edge(method_context: &MethodContext, helper: &mut GraphBuildHelper) {
3252 let method_id = helper.ensure_method(
3254 method_context.qualified_name(),
3255 Some(Span::from_bytes(
3256 method_context.span.0,
3257 method_context.span.1,
3258 )),
3259 false,
3260 method_context.is_static,
3261 );
3262
3263 let native_target = format!("native::jni::{}", method_context.qualified_name());
3266 let target_id = helper.add_function(&native_target, None, false, false);
3267
3268 helper.add_ffi_edge(method_id, target_id, FfiConvention::C);
3269}
3270
3271fn extract_spring_route_info(method_node: Node, content: &[u8]) -> Option<(String, String)> {
3285 let mut cursor = method_node.walk();
3287 let modifiers_node = method_node
3288 .children(&mut cursor)
3289 .find(|child| child.kind() == "modifiers")?;
3290
3291 let mut mod_cursor = modifiers_node.walk();
3293 for annotation_node in modifiers_node.children(&mut mod_cursor) {
3294 if annotation_node.kind() != "annotation" {
3295 continue;
3296 }
3297
3298 let Some(annotation_name) = extract_annotation_name(annotation_node, content) else {
3300 continue;
3301 };
3302
3303 let http_method: String = match annotation_name.as_str() {
3305 "GetMapping" => "GET".to_string(),
3306 "PostMapping" => "POST".to_string(),
3307 "PutMapping" => "PUT".to_string(),
3308 "DeleteMapping" => "DELETE".to_string(),
3309 "PatchMapping" => "PATCH".to_string(),
3310 "RequestMapping" => {
3311 extract_request_mapping_method(annotation_node, content)
3313 .unwrap_or_else(|| "GET".to_string())
3314 }
3315 _ => continue,
3316 };
3317
3318 let Some(path) = extract_annotation_path(annotation_node, content) else {
3320 continue;
3321 };
3322
3323 return Some((http_method, path));
3324 }
3325
3326 None
3327}
3328
3329fn extract_annotation_name(annotation_node: Node, content: &[u8]) -> Option<String> {
3334 let mut cursor = annotation_node.walk();
3335 for child in annotation_node.children(&mut cursor) {
3336 match child.kind() {
3337 "identifier" => {
3338 return Some(extract_identifier(child, content));
3339 }
3340 "scoped_identifier" => {
3341 let full_text = extract_identifier(child, content);
3344 return full_text.rsplit('.').next().map(String::from);
3345 }
3346 _ => {}
3347 }
3348 }
3349 None
3350}
3351
3352fn extract_annotation_path(annotation_node: Node, content: &[u8]) -> Option<String> {
3359 let mut cursor = annotation_node.walk();
3361 let args_node = annotation_node
3362 .children(&mut cursor)
3363 .find(|child| child.kind() == "annotation_argument_list")?;
3364
3365 let mut args_cursor = args_node.walk();
3367 for arg_child in args_node.children(&mut args_cursor) {
3368 match arg_child.kind() {
3369 "string_literal" => {
3371 return extract_string_content(arg_child, content);
3372 }
3373 "element_value_pair" => {
3375 if let Some(path) = extract_path_from_element_value_pair(arg_child, content) {
3376 return Some(path);
3377 }
3378 }
3379 _ => {}
3380 }
3381 }
3382
3383 None
3384}
3385
3386fn extract_request_mapping_method(annotation_node: Node, content: &[u8]) -> Option<String> {
3394 let mut cursor = annotation_node.walk();
3396 let args_node = annotation_node
3397 .children(&mut cursor)
3398 .find(|child| child.kind() == "annotation_argument_list")?;
3399
3400 let mut args_cursor = args_node.walk();
3402 for arg_child in args_node.children(&mut args_cursor) {
3403 if arg_child.kind() != "element_value_pair" {
3404 continue;
3405 }
3406
3407 let Some(key_node) = arg_child.child_by_field_name("key") else {
3409 continue;
3410 };
3411 let key_text = extract_identifier(key_node, content);
3412 if key_text != "method" {
3413 continue;
3414 }
3415
3416 let Some(value_node) = arg_child.child_by_field_name("value") else {
3418 continue;
3419 };
3420 let value_text = extract_identifier(value_node, content);
3421
3422 if let Some(method) = value_text.rsplit('.').next() {
3424 let method_upper = method.to_uppercase();
3425 if matches!(
3426 method_upper.as_str(),
3427 "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS"
3428 ) {
3429 return Some(method_upper);
3430 }
3431 }
3432 }
3433
3434 None
3435}
3436
3437fn extract_path_from_element_value_pair(pair_node: Node, content: &[u8]) -> Option<String> {
3441 let key_node = pair_node.child_by_field_name("key")?;
3442 let key_text = extract_identifier(key_node, content);
3443
3444 if key_text != "path" && key_text != "value" {
3446 return None;
3447 }
3448
3449 let value_node = pair_node.child_by_field_name("value")?;
3450 if value_node.kind() == "string_literal" {
3451 return extract_string_content(value_node, content);
3452 }
3453
3454 None
3455}
3456
3457fn extract_class_request_mapping_path(method_node: Node, content: &[u8]) -> Option<String> {
3474 let mut current = method_node.parent()?;
3476 loop {
3477 if current.kind() == "class_declaration" {
3478 break;
3479 }
3480 current = current.parent()?;
3481 }
3482
3483 let mut cursor = current.walk();
3485 let modifiers = current
3486 .children(&mut cursor)
3487 .find(|child| child.kind() == "modifiers")?;
3488
3489 let mut mod_cursor = modifiers.walk();
3490 for annotation in modifiers.children(&mut mod_cursor) {
3491 if annotation.kind() != "annotation" {
3492 continue;
3493 }
3494 let Some(name) = extract_annotation_name(annotation, content) else {
3495 continue;
3496 };
3497 if name == "RequestMapping" {
3498 return extract_annotation_path(annotation, content);
3499 }
3500 }
3501
3502 None
3503}
3504
3505fn extract_string_content(string_node: Node, content: &[u8]) -> Option<String> {
3509 let text = string_node.utf8_text(content).ok()?;
3510 let trimmed = text.trim();
3511
3512 if trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() >= 2 {
3514 Some(trimmed[1..trimmed.len() - 1].to_string())
3515 } else {
3516 None
3517 }
3518}