Skip to main content

sqry_lang_java/relations/
graph_builder.rs

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
13/// File-level module name for exports/imports.
14/// Distinct from `<module>` to avoid node kind collision in `GraphBuildHelper` cache.
15const FILE_MODULE_NAME: &str = "<file_module>";
16
17/// Graph builder for Java files using unified `CodeGraph` architecture.
18///
19/// This implementation follows the two-phase `ASTGraph` architecture introduced
20/// in JavaScript and Rust for O(1) context lookups during call edge detection.
21///
22/// # Supported Features
23///
24/// - Class and interface definitions
25/// - Method definitions (instance, static, constructors)
26/// - Method call expressions
27/// - Constructor calls (new expressions)
28/// - Static method calls (`Class.method()`)
29/// - Import declarations (single and wildcard)
30/// - Export edges (public classes, interfaces, methods, fields)
31/// - Package declarations
32/// - JNI detection (native methods)
33/// - Anonymous classes and lambda expressions
34/// - Nested classes
35/// - Synchronized detection
36/// - Proper argument counting
37#[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        // Build AST context for O(1) method lookups
68        let ast_graph = ASTGraph::from_tree(tree, content, self.max_scope_depth);
69        let mut scope_tree = local_scopes::build(tree.root_node(), content)?;
70
71        // Phase 1: Create method/constructor nodes and JNI FFI edges for native methods
72        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                // Use add_method_with_signature to store return type for `returns:` queries
86                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                // JNI: Create FFI edge for native methods
96                if context.is_native {
97                    build_jni_native_method_edge(context, &mut helper);
98                }
99            }
100        }
101
102        // Phase 1.5: Add TypeOf edges for fields
103        add_field_typeof_edges(&ast_graph, &mut helper);
104
105        // Phase 2: Walk the tree to find calls, imports, classes, interfaces
106        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// ================================
125// ASTGraph: In-memory function context index
126// ================================
127
128#[derive(Debug)]
129struct ASTGraph {
130    contexts: Vec<MethodContext>,
131    /// Maps qualified field names to their metadata: (`type_fqn`, `is_final`, visibility, `is_static`)
132    /// - Key: Qualified field name (e.g., `ClassName::fieldName`)
133    /// - Tuple: (`type_fqn`, `is_final`, visibility, `is_static`)
134    ///   - `type_fqn`: Fully qualified type name (e.g., `com.example.service.UserService`)
135    ///   - `is_final`: true if field has `final` modifier (determines Constant vs Property node)
136    ///   - visibility: Public or Private (public fields are Public, others are Private)
137    ///   - `is_static`: true if field has `static` modifier
138    ///
139    /// Used to resolve method calls on fields and create appropriate node types with metadata
140    field_types: HashMap<String, (String, bool, Option<sqry_core::schema::Visibility>, bool)>,
141    /// Maps simple type names to FQNs (e.g., `UserService` -> `com.example.service.UserService`)
142    /// Used to resolve static method calls (e.g., `UserRepository.method` ->
143    /// `com.example.repository.UserRepository.method`)
144    import_map: HashMap<String, String>,
145    /// Whether this file imports JNA (`com.sun.jna.*`)
146    has_jna_import: bool,
147    /// Whether this file imports Panama Foreign Function API (`java.lang.foreign.*`)
148    has_panama_import: bool,
149    /// Interfaces that extend JNA Library (simple names)
150    jna_library_interfaces: Vec<String>,
151}
152
153impl ASTGraph {
154    fn from_tree(tree: &Tree, content: &[u8], max_depth: usize) -> Self {
155        // Extract package name from AST
156        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        // Create recursion guard
162        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        // Extract field declarations and imports to enable type resolution
184        let (field_types, import_map) = extract_field_and_import_types(tree.root_node(), content);
185
186        // Detect FFI-related imports
187        let (has_jna_import, has_panama_import) = detect_ffi_imports(tree.root_node(), content);
188
189        // Find interfaces extending JNA Library
190        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    /// Find the enclosing method context for a given byte position
207    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)] // Captures explicit method traits for graph resolution.
217struct MethodContext {
218    /// Fully qualified name: `com.example.Class.method` or `com.example.Class.<init>`
219    qualified_name: String,
220    /// Byte span of the method body
221    span: (usize, usize),
222    /// Nesting depth (for resolving ambiguity)
223    depth: usize,
224    /// Whether this is a static method
225    is_static: bool,
226    /// Whether this is synchronized
227    #[allow(dead_code)] // Reserved for threading analysis
228    is_synchronized: bool,
229    /// Whether this is a constructor
230    is_constructor: bool,
231    /// Whether this is a native method (JNI)
232    #[allow(dead_code)] // Reserved for JNI bridge analysis
233    is_native: bool,
234    /// Package name for use in call resolution (e.g., `com.example`)
235    package_name: Option<String>,
236    /// Class stack for use in call resolution (e.g., `["Outer", "Inner"]`)
237    class_stack: Vec<String>,
238    /// Return type of the method (e.g., `Optional<User>`, `void`)
239    return_type: Option<String>,
240    /// Visibility modifier (e.g., "public", "private", "protected", "package-private")
241    visibility: Option<String>,
242}
243
244impl MethodContext {
245    fn qualified_name(&self) -> &str {
246        &self.qualified_name
247    }
248}
249
250// ================================
251// Context Extraction
252// ================================
253
254/// Recursively extract method contexts from Java AST
255/// # Errors
256///
257/// Returns [`RecursionError::DepthLimitExceeded`] if recursion depth exceeds the guard's limit.
258fn extract_java_contexts(
259    node: Node,
260    content: &[u8],
261    contexts: &mut Vec<MethodContext>,
262    class_stack: &mut Vec<String>,
263    package_name: Option<&str>,
264    depth: usize,
265    max_depth: usize,
266    guard: &mut sqry_core::query::security::RecursionGuard,
267) -> Result<(), sqry_core::query::security::RecursionError> {
268    guard.enter()?;
269
270    if depth > max_depth {
271        guard.exit();
272        return Ok(());
273    }
274
275    match node.kind() {
276        "class_declaration" | "interface_declaration" | "enum_declaration" => {
277            // Extract class/interface name
278            if let Some(name_node) = node.child_by_field_name("name") {
279                let class_name = extract_identifier(name_node, content);
280
281                // Push class onto stack for nested context
282                class_stack.push(class_name.clone());
283
284                // Extract methods within this class
285                if let Some(body_node) = node.child_by_field_name("body") {
286                    extract_methods_from_body(
287                        body_node,
288                        content,
289                        class_stack,
290                        package_name,
291                        contexts,
292                        depth + 1,
293                        max_depth,
294                        guard,
295                    )?;
296
297                    // Handle nested classes (recursively)
298                    for i in 0..body_node.child_count() {
299                        if let Some(child) = body_node.child(i as u32) {
300                            extract_java_contexts(
301                                child,
302                                content,
303                                contexts,
304                                class_stack,
305                                package_name,
306                                depth + 1,
307                                max_depth,
308                                guard,
309                            )?;
310                        }
311                    }
312                }
313
314                // Pop class from stack when exiting
315                class_stack.pop();
316
317                guard.exit();
318                return Ok(());
319            }
320        }
321        _ => {}
322    }
323
324    // Continue traversing for top-level declarations
325    for i in 0..node.child_count() {
326        if let Some(child) = node.child(i as u32) {
327            extract_java_contexts(
328                child,
329                content,
330                contexts,
331                class_stack,
332                package_name,
333                depth,
334                max_depth,
335                guard,
336            )?;
337        }
338    }
339
340    guard.exit();
341    Ok(())
342}
343
344/// # Errors
345///
346/// Returns [`RecursionError::DepthLimitExceeded`] if recursion depth exceeds the guard's limit.
347#[allow(clippy::unnecessary_wraps)]
348fn extract_methods_from_body(
349    body_node: Node,
350    content: &[u8],
351    class_stack: &[String],
352    package_name: Option<&str>,
353    contexts: &mut Vec<MethodContext>,
354    depth: usize,
355    _max_depth: usize,
356    _guard: &mut sqry_core::query::security::RecursionGuard,
357) -> Result<(), sqry_core::query::security::RecursionError> {
358    for i in 0..body_node.child_count() {
359        if let Some(child) = body_node.child(i as u32) {
360            match child.kind() {
361                "method_declaration" => {
362                    if let Some(method_context) =
363                        extract_method_context(child, content, class_stack, package_name, depth)
364                    {
365                        contexts.push(method_context);
366                    }
367                }
368                "constructor_declaration" => {
369                    let constructor_context = extract_constructor_context(
370                        child,
371                        content,
372                        class_stack,
373                        package_name,
374                        depth,
375                    );
376                    contexts.push(constructor_context);
377                }
378                _ => {}
379            }
380        }
381    }
382    Ok(())
383}
384
385fn extract_method_context(
386    method_node: Node,
387    content: &[u8],
388    class_stack: &[String],
389    package_name: Option<&str>,
390    depth: usize,
391) -> Option<MethodContext> {
392    let name_node = method_node.child_by_field_name("name")?;
393    let method_name = extract_identifier(name_node, content);
394
395    let is_static = has_modifier(method_node, "static", content);
396    let is_synchronized = has_modifier(method_node, "synchronized", content);
397    let is_native = has_modifier(method_node, "native", content);
398    let visibility = extract_visibility(method_node, content);
399
400    // Extract return type from method_declaration
401    // tree-sitter-java structure: (method_declaration type: <type_node> name: identifier ...)
402    let return_type = method_node
403        .child_by_field_name("type")
404        .map(|type_node| extract_full_return_type(type_node, content));
405
406    // Use build_member_symbol to create fully qualified name
407    let qualified_name = build_member_symbol(package_name, class_stack, &method_name);
408
409    Some(MethodContext {
410        qualified_name,
411        span: (method_node.start_byte(), method_node.end_byte()),
412        depth,
413        is_static,
414        is_synchronized,
415        is_constructor: false,
416        is_native,
417        package_name: package_name.map(std::string::ToString::to_string),
418        class_stack: class_stack.to_vec(),
419        return_type,
420        visibility,
421    })
422}
423
424fn extract_constructor_context(
425    constructor_node: Node,
426    content: &[u8],
427    class_stack: &[String],
428    package_name: Option<&str>,
429    depth: usize,
430) -> MethodContext {
431    // Use build_member_symbol with "<init>" as method name
432    let qualified_name = build_member_symbol(package_name, class_stack, "<init>");
433    let visibility = extract_visibility(constructor_node, content);
434
435    MethodContext {
436        qualified_name,
437        span: (constructor_node.start_byte(), constructor_node.end_byte()),
438        depth,
439        is_static: false,
440        is_synchronized: false,
441        is_constructor: true,
442        is_native: false,
443        package_name: package_name.map(std::string::ToString::to_string),
444        class_stack: class_stack.to_vec(),
445        return_type: None, // Constructors don't have return types
446        visibility,
447    }
448}
449
450// ================================
451// Edge Building with GraphBuildHelper
452// ================================
453
454/// Walk the AST tree and build edges using `GraphBuildHelper`
455fn walk_tree_for_edges(
456    node: Node,
457    content: &[u8],
458    ast_graph: &ASTGraph,
459    scope_tree: &mut JavaScopeTree,
460    helper: &mut GraphBuildHelper,
461    tree: &Tree,
462) -> GraphResult<()> {
463    match node.kind() {
464        "class_declaration" | "interface_declaration" | "enum_declaration" => {
465            // handle_type_declaration already walks the body children, so return early
466            return handle_type_declaration(node, content, ast_graph, scope_tree, helper, tree);
467        }
468        "method_declaration" | "constructor_declaration" => {
469            // Handle both method and constructor parameters
470            handle_method_declaration_parameters(node, content, ast_graph, scope_tree, helper);
471
472            // Detect Spring MVC route annotations on method declarations
473            if node.kind() == "method_declaration"
474                && let Some((http_method, path)) = extract_spring_route_info(node, content)
475            {
476                // Compose class-level @RequestMapping prefix with method path
477                let full_path =
478                    if let Some(class_prefix) = extract_class_request_mapping_path(node, content) {
479                        let prefix = class_prefix.trim_end_matches('/');
480                        let suffix = path.trim_start_matches('/');
481                        if suffix.is_empty() {
482                            class_prefix
483                        } else {
484                            format!("{prefix}/{suffix}")
485                        }
486                    } else {
487                        path
488                    };
489                let qualified_name = format!("route::{http_method}::{full_path}");
490                let span = Span::from_bytes(node.start_byte(), node.end_byte());
491                let endpoint_id = helper.add_endpoint(&qualified_name, Some(span));
492
493                // Link endpoint to the handler method via Contains edge
494                let byte_pos = node.start_byte();
495                if let Some(context) = ast_graph.find_enclosing(byte_pos) {
496                    let method_id = helper.ensure_method(
497                        context.qualified_name(),
498                        Some(Span::from_bytes(context.span.0, context.span.1)),
499                        false,
500                        context.is_static,
501                    );
502                    helper.add_contains_edge(endpoint_id, method_id);
503                }
504            }
505        }
506        "compact_constructor_declaration" => {
507            handle_compact_constructor_parameters(node, content, ast_graph, scope_tree, helper);
508        }
509        "method_invocation" => {
510            handle_method_invocation(node, content, ast_graph, helper);
511        }
512        "object_creation_expression" => {
513            handle_constructor_call(node, content, ast_graph, helper);
514        }
515        "import_declaration" => {
516            handle_import_declaration(node, content, helper);
517        }
518        "local_variable_declaration" => {
519            handle_local_variable_declaration(node, content, ast_graph, scope_tree, helper);
520        }
521        "enhanced_for_statement" => {
522            handle_enhanced_for_declaration(node, content, ast_graph, scope_tree, helper);
523        }
524        "catch_clause" => {
525            handle_catch_parameter_declaration(node, content, ast_graph, scope_tree, helper);
526        }
527        "lambda_expression" => {
528            handle_lambda_parameter_declaration(node, content, ast_graph, scope_tree, helper);
529        }
530        "try_with_resources_statement" => {
531            handle_try_with_resources_declaration(node, content, ast_graph, scope_tree, helper);
532        }
533        "instanceof_expression" => {
534            handle_instanceof_pattern_declaration(node, content, ast_graph, scope_tree, helper);
535        }
536        "switch_label" => {
537            handle_switch_pattern_declaration(node, content, ast_graph, scope_tree, helper);
538        }
539        "identifier" => {
540            handle_identifier_for_reference(node, content, ast_graph, scope_tree, helper);
541        }
542        _ => {}
543    }
544
545    // Recurse to children
546    for i in 0..node.child_count() {
547        if let Some(child) = node.child(i as u32) {
548            walk_tree_for_edges(child, content, ast_graph, scope_tree, helper, tree)?;
549        }
550    }
551
552    Ok(())
553}
554
555fn handle_type_declaration(
556    node: Node,
557    content: &[u8],
558    ast_graph: &ASTGraph,
559    scope_tree: &mut JavaScopeTree,
560    helper: &mut GraphBuildHelper,
561    tree: &Tree,
562) -> GraphResult<()> {
563    let Some(name_node) = node.child_by_field_name("name") else {
564        return Ok(());
565    };
566    let class_name = extract_identifier(name_node, content);
567    let span = Span::from_bytes(node.start_byte(), node.end_byte());
568
569    let package = PackageResolver::package_from_ast(tree, content);
570    let class_stack = extract_declaration_class_stack(node, content);
571    let qualified_name = qualify_class_name(&class_name, &class_stack, package.as_deref());
572    let class_node_id = add_type_node(helper, node.kind(), &qualified_name, span);
573
574    if is_public(node, content) {
575        export_from_file_module(helper, class_node_id);
576    }
577
578    process_inheritance(node, content, package.as_deref(), class_node_id, helper);
579    if node.kind() == "class_declaration" {
580        process_implements(node, content, package.as_deref(), class_node_id, helper);
581    }
582    if node.kind() == "interface_declaration" {
583        process_interface_extends(node, content, package.as_deref(), class_node_id, helper);
584    }
585
586    if let Some(body_node) = node.child_by_field_name("body") {
587        let is_interface = node.kind() == "interface_declaration";
588        process_class_member_exports(body_node, content, &qualified_name, helper, is_interface);
589
590        for i in 0..body_node.child_count() {
591            if let Some(child) = body_node.child(i as u32) {
592                walk_tree_for_edges(child, content, ast_graph, scope_tree, helper, tree)?;
593            }
594        }
595    }
596
597    Ok(())
598}
599
600fn extract_declaration_class_stack(node: Node, content: &[u8]) -> Vec<String> {
601    let mut class_stack = Vec::new();
602    let mut current_node = Some(node);
603
604    while let Some(current) = current_node {
605        if matches!(
606            current.kind(),
607            "class_declaration" | "interface_declaration" | "enum_declaration"
608        ) && let Some(name_node) = current.child_by_field_name("name")
609        {
610            class_stack.push(extract_identifier(name_node, content));
611        }
612
613        current_node = current.parent();
614    }
615
616    class_stack.reverse();
617    class_stack
618}
619
620fn qualify_class_name(class_name: &str, class_stack: &[String], package: Option<&str>) -> String {
621    let scope = class_stack
622        .split_last()
623        .map_or(&[][..], |(_, parent_stack)| parent_stack);
624    build_symbol(package, scope, class_name)
625}
626
627fn add_type_node(
628    helper: &mut GraphBuildHelper,
629    kind: &str,
630    qualified_name: &str,
631    span: Span,
632) -> sqry_core::graph::unified::node::NodeId {
633    match kind {
634        "interface_declaration" => helper.add_interface(qualified_name, Some(span)),
635        _ => helper.add_class(qualified_name, Some(span)),
636    }
637}
638
639fn handle_method_invocation(
640    node: Node,
641    content: &[u8],
642    ast_graph: &ASTGraph,
643    helper: &mut GraphBuildHelper,
644) {
645    if let Some(caller_context) = ast_graph.find_enclosing(node.start_byte()) {
646        let is_ffi = build_ffi_call_edge(node, content, caller_context, ast_graph, helper);
647        if is_ffi {
648            return;
649        }
650    }
651
652    process_method_call_unified(node, content, ast_graph, helper);
653}
654
655fn handle_constructor_call(
656    node: Node,
657    content: &[u8],
658    ast_graph: &ASTGraph,
659    helper: &mut GraphBuildHelper,
660) {
661    process_constructor_call_unified(node, content, ast_graph, helper);
662}
663
664fn handle_import_declaration(node: Node, content: &[u8], helper: &mut GraphBuildHelper) {
665    process_import_unified(node, content, helper);
666}
667
668/// Add `TypeOf` edges for all field declarations
669/// Creates Property nodes for mutable fields and Constant nodes for final fields
670fn add_field_typeof_edges(ast_graph: &ASTGraph, helper: &mut GraphBuildHelper) {
671    for (field_name, (type_fqn, is_final, visibility, is_static)) in &ast_graph.field_types {
672        // Create appropriate node type based on 'final' modifier, with visibility and static metadata
673        let field_id = if *is_final {
674            // final fields are constants
675            if let Some(vis) = visibility {
676                helper.add_constant_with_static_and_visibility(
677                    field_name,
678                    None,
679                    *is_static,
680                    Some(vis.as_str()),
681                )
682            } else {
683                helper.add_constant_with_static_and_visibility(field_name, None, *is_static, None)
684            }
685        } else {
686            // non-final fields are properties
687            if let Some(vis) = visibility {
688                helper.add_property_with_static_and_visibility(
689                    field_name,
690                    None,
691                    *is_static,
692                    Some(vis.as_str()),
693                )
694            } else {
695                helper.add_property_with_static_and_visibility(field_name, None, *is_static, None)
696            }
697        };
698
699        // Create class node for the type
700        let type_id = helper.add_class(type_fqn, None);
701
702        // Create TypeOf edge from field to its type
703        helper.add_typeof_edge(field_id, type_id);
704    }
705}
706
707/// Extract method parameters and create Parameter nodes with `TypeOf` edges
708/// Should be called during method context creation
709fn extract_method_parameters(
710    method_node: Node,
711    content: &[u8],
712    qualified_method_name: &str,
713    helper: &mut GraphBuildHelper,
714    import_map: &HashMap<String, String>,
715    scope_tree: &mut JavaScopeTree,
716) {
717    // Find formal_parameters node in the method declaration
718    let mut cursor = method_node.walk();
719    for child in method_node.children(&mut cursor) {
720        if child.kind() == "formal_parameters" {
721            // Iterate through each parameter (formal, varargs, receiver)
722            let mut param_cursor = child.walk();
723            for param_child in child.children(&mut param_cursor) {
724                match param_child.kind() {
725                    "formal_parameter" => {
726                        handle_formal_parameter(
727                            param_child,
728                            content,
729                            qualified_method_name,
730                            helper,
731                            import_map,
732                            scope_tree,
733                        );
734                    }
735                    "spread_parameter" => {
736                        handle_spread_parameter(
737                            param_child,
738                            content,
739                            qualified_method_name,
740                            helper,
741                            import_map,
742                            scope_tree,
743                        );
744                    }
745                    "receiver_parameter" => {
746                        handle_receiver_parameter(
747                            param_child,
748                            content,
749                            qualified_method_name,
750                            helper,
751                            import_map,
752                            scope_tree,
753                        );
754                    }
755                    _ => {}
756                }
757            }
758        }
759    }
760}
761
762/// Handle a single formal parameter and create Parameter node with `TypeOf` edge
763fn handle_formal_parameter(
764    param_node: Node,
765    content: &[u8],
766    method_name: &str,
767    helper: &mut GraphBuildHelper,
768    import_map: &HashMap<String, String>,
769    scope_tree: &mut JavaScopeTree,
770) {
771    use sqry_core::graph::unified::node::NodeKind;
772
773    // Extract type from formal_parameter
774    let Some(type_node) = param_node.child_by_field_name("type") else {
775        return;
776    };
777
778    // Extract parameter name
779    let Some(name_node) = param_node.child_by_field_name("name") else {
780        return;
781    };
782
783    // Get type and parameter name texts
784    let type_text = extract_type_name(type_node, content);
785    let param_name = extract_identifier(name_node, content);
786
787    if type_text.is_empty() || param_name.is_empty() {
788        return;
789    }
790
791    // Resolve type to FQN using import map
792    let resolved_type = import_map.get(&type_text).cloned().unwrap_or(type_text);
793
794    // Create qualified parameter name (method::param)
795    let qualified_param = format!("{method_name}::{param_name}");
796    let span = Span::from_bytes(param_node.start_byte(), param_node.end_byte());
797
798    // Create parameter node
799    let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
800
801    scope_tree.attach_node_id(&param_name, name_node.start_byte(), param_id);
802
803    // Create type node (class/interface)
804    let type_id = helper.add_class(&resolved_type, None);
805
806    // Add TypeOf edge from parameter to its type
807    helper.add_typeof_edge(param_id, type_id);
808}
809
810/// Handle a spread parameter (varargs like String... args)
811fn handle_spread_parameter(
812    param_node: Node,
813    content: &[u8],
814    method_name: &str,
815    helper: &mut GraphBuildHelper,
816    import_map: &HashMap<String, String>,
817    scope_tree: &mut JavaScopeTree,
818) {
819    use sqry_core::graph::unified::node::NodeKind;
820
821    // spread_parameter structure:
822    // (spread_parameter
823    //   type_identifier
824    //   ...
825    //   variable_declarator
826    //     identifier)
827
828    // Find type node (first type_identifier child)
829    let mut type_text = String::new();
830    let mut param_name = String::new();
831    let mut param_name_node = None;
832
833    let mut cursor = param_node.walk();
834    for child in param_node.children(&mut cursor) {
835        match child.kind() {
836            "type_identifier" | "generic_type" | "scoped_type_identifier" => {
837                type_text = extract_type_name(child, content);
838            }
839            "variable_declarator" => {
840                // Name is inside variable_declarator
841                if let Some(name_node) = child.child_by_field_name("name") {
842                    param_name = extract_identifier(name_node, content);
843                    param_name_node = Some(name_node);
844                }
845            }
846            _ => {}
847        }
848    }
849
850    if type_text.is_empty() || param_name.is_empty() {
851        return;
852    }
853
854    // Resolve type to FQN using import map
855    let resolved_type = import_map.get(&type_text).cloned().unwrap_or(type_text);
856
857    // Create qualified parameter name (method::param)
858    let qualified_param = format!("{method_name}::{param_name}");
859    let span = Span::from_bytes(param_node.start_byte(), param_node.end_byte());
860
861    // Create parameter node
862    let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
863
864    if let Some(name_node) = param_name_node {
865        scope_tree.attach_node_id(&param_name, name_node.start_byte(), param_id);
866    }
867
868    // Create type node for the array type (resolved_type represents the element type)
869    // For varargs, the actual type is an array of the base type
870    let type_id = helper.add_class(&resolved_type, None);
871
872    // Add TypeOf edge from parameter to its type
873    helper.add_typeof_edge(param_id, type_id);
874}
875
876/// Handle a receiver parameter (e.g., Outer.this in inner class methods)
877fn handle_receiver_parameter(
878    param_node: Node,
879    content: &[u8],
880    method_name: &str,
881    helper: &mut GraphBuildHelper,
882    import_map: &HashMap<String, String>,
883    _scope_tree: &mut JavaScopeTree,
884) {
885    use sqry_core::graph::unified::node::NodeKind;
886
887    // receiver_parameter structure:
888    // (receiver_parameter
889    //   type_identifier
890    //   identifier (optional - class name)
891    //   .
892    //   this)
893
894    let mut type_text = String::new();
895    let mut cursor = param_node.walk();
896
897    // Find the type_identifier child
898    for child in param_node.children(&mut cursor) {
899        if matches!(
900            child.kind(),
901            "type_identifier" | "generic_type" | "scoped_type_identifier"
902        ) {
903            type_text = extract_type_name(child, content);
904            break;
905        }
906    }
907
908    if type_text.is_empty() {
909        return;
910    }
911
912    // Receiver parameter name is always "this"
913    let param_name = "this";
914
915    // Resolve type to FQN using import map
916    let resolved_type = import_map.get(&type_text).cloned().unwrap_or(type_text);
917
918    // Create qualified parameter name (method::this)
919    let qualified_param = format!("{method_name}::{param_name}");
920    let span = Span::from_bytes(param_node.start_byte(), param_node.end_byte());
921
922    // Create parameter node
923    let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
924
925    // Create type node (class)
926    let type_id = helper.add_class(&resolved_type, None);
927
928    // Add TypeOf edge from parameter to its type
929    helper.add_typeof_edge(param_id, type_id);
930}
931
932#[derive(Debug, Clone, Copy, Eq, PartialEq)]
933enum FieldAccessRole {
934    Default,
935    ExplicitThisOrSuper,
936    Skip,
937}
938
939#[derive(Debug, Clone, Copy, Eq, PartialEq)]
940enum FieldResolutionMode {
941    Default,
942    CurrentOnly,
943}
944
945fn field_access_role(
946    node: Node,
947    content: &[u8],
948    ast_graph: &ASTGraph,
949    scope_tree: &JavaScopeTree,
950    identifier_text: &str,
951) -> FieldAccessRole {
952    let Some(parent) = node.parent() else {
953        return FieldAccessRole::Default;
954    };
955
956    if parent.kind() == "field_access" {
957        if let Some(field_node) = parent.child_by_field_name("field")
958            && field_node.id() == node.id()
959            && let Some(object_node) = parent.child_by_field_name("object")
960        {
961            if is_explicit_this_or_super(object_node, content) {
962                return FieldAccessRole::ExplicitThisOrSuper;
963            }
964            return FieldAccessRole::Skip;
965        }
966
967        if let Some(object_node) = parent.child_by_field_name("object")
968            && object_node.id() == node.id()
969            && !scope_tree.has_local_binding(identifier_text, node.start_byte())
970            && is_static_type_identifier(identifier_text, ast_graph, scope_tree)
971        {
972            return FieldAccessRole::Skip;
973        }
974    }
975
976    if parent.kind() == "method_invocation"
977        && let Some(object_node) = parent.child_by_field_name("object")
978        && object_node.id() == node.id()
979        && !scope_tree.has_local_binding(identifier_text, node.start_byte())
980        && is_static_type_identifier(identifier_text, ast_graph, scope_tree)
981    {
982        return FieldAccessRole::Skip;
983    }
984
985    if parent.kind() == "method_reference"
986        && let Some(object_node) = parent.child_by_field_name("object")
987        && object_node.id() == node.id()
988        && !scope_tree.has_local_binding(identifier_text, node.start_byte())
989        && is_static_type_identifier(identifier_text, ast_graph, scope_tree)
990    {
991        return FieldAccessRole::Skip;
992    }
993
994    FieldAccessRole::Default
995}
996
997fn is_static_type_identifier(
998    identifier_text: &str,
999    ast_graph: &ASTGraph,
1000    scope_tree: &JavaScopeTree,
1001) -> bool {
1002    ast_graph.import_map.contains_key(identifier_text)
1003        || scope_tree.is_known_type_name(identifier_text)
1004}
1005
1006fn is_explicit_this_or_super(node: Node, content: &[u8]) -> bool {
1007    if matches!(node.kind(), "this" | "super") {
1008        return true;
1009    }
1010    if node.kind() == "identifier" {
1011        let text = extract_identifier(node, content);
1012        return matches!(text.as_str(), "this" | "super");
1013    }
1014    if node.kind() == "field_access"
1015        && let Some(field) = node.child_by_field_name("field")
1016    {
1017        let text = extract_identifier(field, content);
1018        if matches!(text.as_str(), "this" | "super") {
1019            return true;
1020        }
1021    }
1022    false
1023}
1024
1025/// Check if an identifier node is part of a declaration context
1026/// Returns true if the identifier is being declared (not referenced)
1027#[allow(clippy::too_many_lines)]
1028fn is_declaration_context(node: Node) -> bool {
1029    // Check if parent is a declaration node
1030    let Some(parent) = node.parent() else {
1031        return false;
1032    };
1033
1034    // For variable_declarator, only the 'name' field is a declaration, not 'value'
1035    // Example: `String key = API_KEY`
1036    //   - 'key' has parent variable_declarator with field 'name' (declaration)
1037    //   - 'API_KEY' has parent variable_declarator with field 'value' (NOT declaration)
1038    if parent.kind() == "variable_declarator" {
1039        // Check if this identifier is the 'name' field
1040        let mut cursor = parent.walk();
1041        for (idx, child) in parent.children(&mut cursor).enumerate() {
1042            if child.id() == node.id() {
1043                #[allow(clippy::cast_possible_truncation)]
1044                if let Some(field_name) = parent.field_name_for_child(idx as u32) {
1045                    // Only 'name' field is declaration context, not 'value'
1046                    return field_name == "name";
1047                }
1048                break;
1049            }
1050        }
1051
1052        // If inside variable_declarator that's inside spread_parameter, it's a declaration
1053        if let Some(grandparent) = parent.parent()
1054            && grandparent.kind() == "spread_parameter"
1055        {
1056            return true;
1057        }
1058
1059        return false;
1060    }
1061
1062    // For formal_parameter, only the 'name' field is a declaration
1063    if parent.kind() == "formal_parameter" {
1064        let mut cursor = parent.walk();
1065        for (idx, child) in parent.children(&mut cursor).enumerate() {
1066            if child.id() == node.id() {
1067                #[allow(clippy::cast_possible_truncation)]
1068                if let Some(field_name) = parent.field_name_for_child(idx as u32) {
1069                    return field_name == "name";
1070                }
1071                break;
1072            }
1073        }
1074        return false;
1075    }
1076
1077    // For enhanced_for_statement, only the loop variable 'name' field is a declaration
1078    // Example: `for (String item : items)` - 'item' is declaration, 'items' is not
1079    if parent.kind() == "enhanced_for_statement" {
1080        // Check if this identifier is the loop variable name field
1081        let mut cursor = parent.walk();
1082        for (idx, child) in parent.children(&mut cursor).enumerate() {
1083            if child.id() == node.id() {
1084                #[allow(clippy::cast_possible_truncation)]
1085                if let Some(field_name) = parent.field_name_for_child(idx as u32) {
1086                    // Only the 'name' field is declaration, not the iterable expression
1087                    return field_name == "name";
1088                }
1089                break;
1090            }
1091        }
1092        return false;
1093    }
1094
1095    if parent.kind() == "lambda_expression" {
1096        if let Some(params) = parent.child_by_field_name("parameters") {
1097            return params.id() == node.id();
1098        }
1099        return false;
1100    }
1101
1102    if parent.kind() == "inferred_parameters" {
1103        return true;
1104    }
1105
1106    if parent.kind() == "resource" {
1107        if let Some(name_node) = parent.child_by_field_name("name")
1108            && name_node.id() == node.id()
1109        {
1110            let has_type = parent.child_by_field_name("type").is_some();
1111            let has_value = parent.child_by_field_name("value").is_some();
1112            return has_type || has_value;
1113        }
1114        return false;
1115    }
1116
1117    // Pattern variables (Java 16+)
1118    // Type pattern: case String s -> ...; if (obj instanceof String s)
1119    // The 'name' field is the pattern variable declaration
1120    if parent.kind() == "type_pattern" {
1121        if let Some(name_node) = parent.child_by_field_name("name")
1122            && name_node.id() == node.id()
1123        {
1124            return true;
1125        }
1126        return false;
1127    }
1128
1129    // instanceof pattern: if (obj instanceof String value)
1130    if parent.kind() == "instanceof_expression" {
1131        let mut cursor = parent.walk();
1132        for (idx, child) in parent.children(&mut cursor).enumerate() {
1133            if child.id() == node.id() {
1134                #[allow(clippy::cast_possible_truncation)]
1135                if let Some(field_name) = parent.field_name_for_child(idx as u32) {
1136                    // The 'name' field in instanceof_expression is the pattern variable
1137                    return field_name == "name";
1138                }
1139                break;
1140            }
1141        }
1142        return false;
1143    }
1144
1145    // Record pattern components: case Point(int x, int y)
1146    // The identifiers in record_pattern_component are declarations
1147    if parent.kind() == "record_pattern_component" {
1148        // In record pattern component, the second child (after type) is the identifier declaration
1149        let mut cursor = parent.walk();
1150        for child in parent.children(&mut cursor) {
1151            if child.id() == node.id() && child.kind() == "identifier" {
1152                // This is a pattern variable declaration
1153                return true;
1154            }
1155        }
1156        return false;
1157    }
1158
1159    if parent.kind() == "record_component" {
1160        if let Some(name_node) = parent.child_by_field_name("name") {
1161            return name_node.id() == node.id();
1162        }
1163        return false;
1164    }
1165
1166    // For other declaration contexts, any direct child identifier is considered a declaration
1167    matches!(
1168        parent.kind(),
1169        "method_declaration"
1170            | "constructor_declaration"
1171            | "compact_constructor_declaration"
1172            | "class_declaration"
1173            | "interface_declaration"
1174            | "enum_declaration"
1175            | "field_declaration"
1176            | "catch_formal_parameter"
1177    )
1178}
1179
1180fn is_method_invocation_name(node: Node) -> bool {
1181    let Some(parent) = node.parent() else {
1182        return false;
1183    };
1184    if parent.kind() != "method_invocation" {
1185        return false;
1186    }
1187    parent
1188        .child_by_field_name("name")
1189        .is_some_and(|name_node| name_node.id() == node.id())
1190}
1191
1192fn is_method_reference_name(node: Node) -> bool {
1193    let Some(parent) = node.parent() else {
1194        return false;
1195    };
1196    if parent.kind() != "method_reference" {
1197        return false;
1198    }
1199    parent
1200        .child_by_field_name("name")
1201        .is_some_and(|name_node| name_node.id() == node.id())
1202}
1203
1204fn is_label_identifier(node: Node) -> bool {
1205    let Some(parent) = node.parent() else {
1206        return false;
1207    };
1208    if parent.kind() == "labeled_statement" {
1209        return true;
1210    }
1211    if matches!(parent.kind(), "break_statement" | "continue_statement")
1212        && let Some(label) = parent.child_by_field_name("label")
1213    {
1214        return label.id() == node.id();
1215    }
1216    false
1217}
1218
1219fn is_class_literal(node: Node) -> bool {
1220    let Some(parent) = node.parent() else {
1221        return false;
1222    };
1223    parent.kind() == "class_literal"
1224}
1225
1226fn is_type_identifier_context(node: Node) -> bool {
1227    let Some(parent) = node.parent() else {
1228        return false;
1229    };
1230    matches!(
1231        parent.kind(),
1232        "type_identifier"
1233            | "scoped_type_identifier"
1234            | "scoped_identifier"
1235            | "generic_type"
1236            | "type_argument"
1237            | "type_bound"
1238    )
1239}
1240
1241fn add_reference_edge_for_target(
1242    usage_node: Node,
1243    identifier_text: &str,
1244    target_id: sqry_core::graph::unified::node::NodeId,
1245    helper: &mut GraphBuildHelper,
1246) {
1247    let usage_span = Span::from_bytes(usage_node.start_byte(), usage_node.end_byte());
1248    let usage_id = helper.add_node(
1249        &format!("{}@{}", identifier_text, usage_node.start_byte()),
1250        Some(usage_span),
1251        sqry_core::graph::unified::node::NodeKind::Variable,
1252    );
1253    helper.add_reference_edge(usage_id, target_id);
1254}
1255
1256fn resolve_field_reference(
1257    node: Node,
1258    identifier_text: &str,
1259    ast_graph: &ASTGraph,
1260    helper: &mut GraphBuildHelper,
1261    mode: FieldResolutionMode,
1262) {
1263    let context = ast_graph.find_enclosing(node.start_byte());
1264    let mut candidates = Vec::new();
1265    if let Some(ctx) = context
1266        && !ctx.class_stack.is_empty()
1267    {
1268        if mode == FieldResolutionMode::CurrentOnly {
1269            let class_path = ctx.class_stack.join("::");
1270            candidates.push(format!("{class_path}::{identifier_text}"));
1271        } else {
1272            let stack_len = ctx.class_stack.len();
1273            for idx in (1..=stack_len).rev() {
1274                let class_path = ctx.class_stack[..idx].join("::");
1275                candidates.push(format!("{class_path}::{identifier_text}"));
1276            }
1277        }
1278    }
1279
1280    if mode != FieldResolutionMode::CurrentOnly {
1281        candidates.push(identifier_text.to_string());
1282    }
1283
1284    for candidate in candidates {
1285        if ast_graph.field_types.contains_key(&candidate) {
1286            add_field_reference(node, identifier_text, &candidate, ast_graph, helper);
1287            return;
1288        }
1289    }
1290}
1291
1292fn add_field_reference(
1293    node: Node,
1294    identifier_text: &str,
1295    field_name: &str,
1296    ast_graph: &ASTGraph,
1297    helper: &mut GraphBuildHelper,
1298) {
1299    let usage_span = Span::from_bytes(node.start_byte(), node.end_byte());
1300    let usage_id = helper.add_node(
1301        &format!("{}@{}", identifier_text, node.start_byte()),
1302        Some(usage_span),
1303        sqry_core::graph::unified::node::NodeKind::Variable,
1304    );
1305
1306    let field_metadata = ast_graph.field_types.get(field_name);
1307    let field_id = if let Some((_, is_final, visibility, is_static)) = field_metadata {
1308        if *is_final {
1309            if let Some(vis) = visibility {
1310                helper.add_constant_with_static_and_visibility(
1311                    field_name,
1312                    None,
1313                    *is_static,
1314                    Some(vis.as_str()),
1315                )
1316            } else {
1317                helper.add_constant_with_static_and_visibility(field_name, None, *is_static, None)
1318            }
1319        } else if let Some(vis) = visibility {
1320            helper.add_property_with_static_and_visibility(
1321                field_name,
1322                None,
1323                *is_static,
1324                Some(vis.as_str()),
1325            )
1326        } else {
1327            helper.add_property_with_static_and_visibility(field_name, None, *is_static, None)
1328        }
1329    } else {
1330        helper.add_property_with_static_and_visibility(field_name, None, false, None)
1331    };
1332
1333    helper.add_reference_edge(usage_id, field_id);
1334}
1335
1336/// Handle identifier nodes to create Reference edges for variable/field accesses
1337#[allow(clippy::similar_names)]
1338fn handle_identifier_for_reference(
1339    node: Node,
1340    content: &[u8],
1341    ast_graph: &ASTGraph,
1342    scope_tree: &mut JavaScopeTree,
1343    helper: &mut GraphBuildHelper,
1344) {
1345    let identifier_text = extract_identifier(node, content);
1346
1347    if identifier_text.is_empty() {
1348        return;
1349    }
1350
1351    // Skip if this identifier is part of a declaration
1352    if is_declaration_context(node) {
1353        return;
1354    }
1355
1356    if is_method_invocation_name(node)
1357        || is_method_reference_name(node)
1358        || is_label_identifier(node)
1359        || is_class_literal(node)
1360    {
1361        return;
1362    }
1363
1364    if is_type_identifier_context(node) {
1365        return;
1366    }
1367
1368    let field_access_role =
1369        field_access_role(node, content, ast_graph, scope_tree, &identifier_text);
1370    if matches!(field_access_role, FieldAccessRole::Skip) {
1371        return;
1372    }
1373
1374    let allow_local = matches!(field_access_role, FieldAccessRole::Default);
1375    let allow_field = !matches!(field_access_role, FieldAccessRole::Skip);
1376    let field_mode = if matches!(field_access_role, FieldAccessRole::ExplicitThisOrSuper) {
1377        FieldResolutionMode::CurrentOnly
1378    } else {
1379        FieldResolutionMode::Default
1380    };
1381
1382    if allow_local {
1383        match scope_tree.resolve_identifier(node.start_byte(), &identifier_text) {
1384            ResolutionOutcome::Local(binding) => {
1385                let target_id = if let Some(node_id) = binding.node_id {
1386                    node_id
1387                } else {
1388                    let span = Span::from_bytes(binding.decl_start_byte, binding.decl_end_byte);
1389                    let qualified_var = format!("{}@{}", identifier_text, binding.decl_start_byte);
1390                    let var_id = helper.add_variable(&qualified_var, Some(span));
1391                    scope_tree.attach_node_id(&identifier_text, binding.decl_start_byte, var_id);
1392                    var_id
1393                };
1394                add_reference_edge_for_target(node, &identifier_text, target_id, helper);
1395                return;
1396            }
1397            ResolutionOutcome::Member { qualified_name } => {
1398                if let Some(field_name) = qualified_name {
1399                    add_field_reference(node, &identifier_text, &field_name, ast_graph, helper);
1400                }
1401                return;
1402            }
1403            ResolutionOutcome::Ambiguous => {
1404                return;
1405            }
1406            ResolutionOutcome::NoMatch => {}
1407        }
1408    }
1409
1410    if !allow_field {
1411        return;
1412    }
1413
1414    resolve_field_reference(node, &identifier_text, ast_graph, helper, field_mode);
1415}
1416
1417/// Handle method declarations to extract parameter `TypeOf` edges
1418fn handle_method_declaration_parameters(
1419    node: Node,
1420    content: &[u8],
1421    ast_graph: &ASTGraph,
1422    scope_tree: &mut JavaScopeTree,
1423    helper: &mut GraphBuildHelper,
1424) {
1425    // Find the enclosing method context to get the qualified name
1426    let byte_pos = node.start_byte();
1427    if let Some(context) = ast_graph.find_enclosing(byte_pos) {
1428        let qualified_method_name = &context.qualified_name;
1429
1430        // Extract parameters from this method
1431        extract_method_parameters(
1432            node,
1433            content,
1434            qualified_method_name,
1435            helper,
1436            &ast_graph.import_map,
1437            scope_tree,
1438        );
1439    }
1440}
1441
1442/// Handle local variable declarations and create `TypeOf` edges
1443fn handle_local_variable_declaration(
1444    node: Node,
1445    content: &[u8],
1446    ast_graph: &ASTGraph,
1447    scope_tree: &mut JavaScopeTree,
1448    helper: &mut GraphBuildHelper,
1449) {
1450    // Extract the type from the local variable declaration
1451    let Some(type_node) = node.child_by_field_name("type") else {
1452        return;
1453    };
1454
1455    let type_text = extract_type_name(type_node, content);
1456    if type_text.is_empty() {
1457        return;
1458    }
1459
1460    // Resolve type through import map (e.g., Optional<User> -> java.util.Optional)
1461    let resolved_type = ast_graph
1462        .import_map
1463        .get(&type_text)
1464        .cloned()
1465        .unwrap_or_else(|| type_text.clone());
1466
1467    // Process all variable declarators (handles cases like: String a, b, c;)
1468    let mut cursor = node.walk();
1469    for child in node.children(&mut cursor) {
1470        if child.kind() == "variable_declarator"
1471            && let Some(name_node) = child.child_by_field_name("name")
1472        {
1473            let var_name = extract_identifier(name_node, content);
1474
1475            // Create unique variable name using byte position to avoid conflicts
1476            let qualified_var = format!("{}@{}", var_name, name_node.start_byte());
1477
1478            // Create variable node
1479            let span = Span::from_bytes(child.start_byte(), child.end_byte());
1480            let var_id = helper.add_variable(&qualified_var, Some(span));
1481            scope_tree.attach_node_id(&var_name, name_node.start_byte(), var_id);
1482
1483            // Create type node
1484            let type_id = helper.add_class(&resolved_type, None);
1485
1486            // Create TypeOf edge
1487            helper.add_typeof_edge(var_id, type_id);
1488        }
1489    }
1490}
1491
1492fn handle_enhanced_for_declaration(
1493    node: Node,
1494    content: &[u8],
1495    ast_graph: &ASTGraph,
1496    scope_tree: &mut JavaScopeTree,
1497    helper: &mut GraphBuildHelper,
1498) {
1499    let Some(type_node) = node.child_by_field_name("type") else {
1500        return;
1501    };
1502    let Some(name_node) = node.child_by_field_name("name") else {
1503        return;
1504    };
1505    let Some(body_node) = node.child_by_field_name("body") else {
1506        return;
1507    };
1508
1509    let type_text = extract_type_name(type_node, content);
1510    let var_name = extract_identifier(name_node, content);
1511    if type_text.is_empty() || var_name.is_empty() {
1512        return;
1513    }
1514
1515    let resolved_type = ast_graph
1516        .import_map
1517        .get(&type_text)
1518        .cloned()
1519        .unwrap_or(type_text);
1520
1521    let qualified_var = format!("{}@{}", var_name, name_node.start_byte());
1522    let span = Span::from_bytes(name_node.start_byte(), name_node.end_byte());
1523    let var_id = helper.add_variable(&qualified_var, Some(span));
1524    scope_tree.attach_node_id(&var_name, body_node.start_byte(), var_id);
1525
1526    let type_id = helper.add_class(&resolved_type, None);
1527    helper.add_typeof_edge(var_id, type_id);
1528}
1529
1530fn handle_catch_parameter_declaration(
1531    node: Node,
1532    content: &[u8],
1533    ast_graph: &ASTGraph,
1534    scope_tree: &mut JavaScopeTree,
1535    helper: &mut GraphBuildHelper,
1536) {
1537    let Some(param_node) = node
1538        .child_by_field_name("parameter")
1539        .or_else(|| first_child_of_kind(node, "catch_formal_parameter"))
1540        .or_else(|| first_child_of_kind(node, "formal_parameter"))
1541    else {
1542        return;
1543    };
1544    let Some(name_node) = param_node
1545        .child_by_field_name("name")
1546        .or_else(|| first_child_of_kind(param_node, "identifier"))
1547    else {
1548        return;
1549    };
1550
1551    let var_name = extract_identifier(name_node, content);
1552    if var_name.is_empty() {
1553        return;
1554    }
1555
1556    let qualified_var = format!("{}@{}", var_name, name_node.start_byte());
1557    let span = Span::from_bytes(param_node.start_byte(), param_node.end_byte());
1558    let var_id = helper.add_variable(&qualified_var, Some(span));
1559    scope_tree.attach_node_id(&var_name, name_node.start_byte(), var_id);
1560
1561    if let Some(type_node) = param_node
1562        .child_by_field_name("type")
1563        .or_else(|| first_child_of_kind(param_node, "type_identifier"))
1564        .or_else(|| first_child_of_kind(param_node, "scoped_type_identifier"))
1565        .or_else(|| first_child_of_kind(param_node, "generic_type"))
1566    {
1567        add_typeof_for_catch_type(type_node, content, ast_graph, helper, var_id);
1568    }
1569}
1570
1571fn add_typeof_for_catch_type(
1572    type_node: Node,
1573    content: &[u8],
1574    ast_graph: &ASTGraph,
1575    helper: &mut GraphBuildHelper,
1576    var_id: sqry_core::graph::unified::node::NodeId,
1577) {
1578    if type_node.kind() == "union_type" {
1579        let mut cursor = type_node.walk();
1580        for child in type_node.children(&mut cursor) {
1581            if matches!(
1582                child.kind(),
1583                "type_identifier" | "scoped_type_identifier" | "generic_type"
1584            ) {
1585                let type_text = extract_type_name(child, content);
1586                if !type_text.is_empty() {
1587                    let resolved_type = ast_graph
1588                        .import_map
1589                        .get(&type_text)
1590                        .cloned()
1591                        .unwrap_or(type_text);
1592                    let type_id = helper.add_class(&resolved_type, None);
1593                    helper.add_typeof_edge(var_id, type_id);
1594                }
1595            }
1596        }
1597        return;
1598    }
1599
1600    let type_text = extract_type_name(type_node, content);
1601    if type_text.is_empty() {
1602        return;
1603    }
1604    let resolved_type = ast_graph
1605        .import_map
1606        .get(&type_text)
1607        .cloned()
1608        .unwrap_or(type_text);
1609    let type_id = helper.add_class(&resolved_type, None);
1610    helper.add_typeof_edge(var_id, type_id);
1611}
1612
1613fn handle_lambda_parameter_declaration(
1614    node: Node,
1615    content: &[u8],
1616    ast_graph: &ASTGraph,
1617    scope_tree: &mut JavaScopeTree,
1618    helper: &mut GraphBuildHelper,
1619) {
1620    use sqry_core::graph::unified::node::NodeKind;
1621
1622    let Some(params_node) = node.child_by_field_name("parameters") else {
1623        return;
1624    };
1625    let lambda_prefix = format!("lambda@{}", node.start_byte());
1626
1627    if params_node.kind() == "identifier" {
1628        let name = extract_identifier(params_node, content);
1629        if name.is_empty() {
1630            return;
1631        }
1632        let qualified_param = format!("{lambda_prefix}::{name}");
1633        let span = Span::from_bytes(params_node.start_byte(), params_node.end_byte());
1634        let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
1635        scope_tree.attach_node_id(&name, params_node.start_byte(), param_id);
1636        return;
1637    }
1638
1639    let mut cursor = params_node.walk();
1640    for child in params_node.children(&mut cursor) {
1641        match child.kind() {
1642            "identifier" => {
1643                let name = extract_identifier(child, content);
1644                if name.is_empty() {
1645                    continue;
1646                }
1647                let qualified_param = format!("{lambda_prefix}::{name}");
1648                let span = Span::from_bytes(child.start_byte(), child.end_byte());
1649                let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
1650                scope_tree.attach_node_id(&name, child.start_byte(), param_id);
1651            }
1652            "formal_parameter" => {
1653                let Some(name_node) = child.child_by_field_name("name") else {
1654                    continue;
1655                };
1656                let Some(type_node) = child.child_by_field_name("type") else {
1657                    continue;
1658                };
1659                let name = extract_identifier(name_node, content);
1660                if name.is_empty() {
1661                    continue;
1662                }
1663                let type_text = extract_type_name(type_node, content);
1664                let resolved_type = ast_graph
1665                    .import_map
1666                    .get(&type_text)
1667                    .cloned()
1668                    .unwrap_or(type_text);
1669                let qualified_param = format!("{lambda_prefix}::{name}");
1670                let span = Span::from_bytes(child.start_byte(), child.end_byte());
1671                let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
1672                scope_tree.attach_node_id(&name, name_node.start_byte(), param_id);
1673                let type_id = helper.add_class(&resolved_type, None);
1674                helper.add_typeof_edge(param_id, type_id);
1675            }
1676            _ => {}
1677        }
1678    }
1679}
1680
1681fn handle_try_with_resources_declaration(
1682    node: Node,
1683    content: &[u8],
1684    ast_graph: &ASTGraph,
1685    scope_tree: &mut JavaScopeTree,
1686    helper: &mut GraphBuildHelper,
1687) {
1688    let Some(resources) = node.child_by_field_name("resources") else {
1689        return;
1690    };
1691
1692    let mut cursor = resources.walk();
1693    for resource in resources.children(&mut cursor) {
1694        if resource.kind() != "resource" {
1695            continue;
1696        }
1697        let name_node = resource.child_by_field_name("name");
1698        let type_node = resource.child_by_field_name("type");
1699        let value_node = resource.child_by_field_name("value");
1700        if let Some(name_node) = name_node {
1701            if type_node.is_none() && value_node.is_none() {
1702                continue;
1703            }
1704            let name = extract_identifier(name_node, content);
1705            if name.is_empty() {
1706                continue;
1707            }
1708
1709            let qualified_var = format!("{}@{}", name, name_node.start_byte());
1710            let span = Span::from_bytes(resource.start_byte(), resource.end_byte());
1711            let var_id = helper.add_variable(&qualified_var, Some(span));
1712            scope_tree.attach_node_id(&name, name_node.start_byte(), var_id);
1713
1714            if let Some(type_node) = type_node {
1715                let type_text = extract_type_name(type_node, content);
1716                if !type_text.is_empty() {
1717                    let resolved_type = ast_graph
1718                        .import_map
1719                        .get(&type_text)
1720                        .cloned()
1721                        .unwrap_or(type_text);
1722                    let type_id = helper.add_class(&resolved_type, None);
1723                    helper.add_typeof_edge(var_id, type_id);
1724                }
1725            }
1726        }
1727    }
1728}
1729
1730fn handle_instanceof_pattern_declaration(
1731    node: Node,
1732    content: &[u8],
1733    ast_graph: &ASTGraph,
1734    scope_tree: &mut JavaScopeTree,
1735    helper: &mut GraphBuildHelper,
1736) {
1737    let mut patterns = Vec::new();
1738    collect_pattern_declarations(node, &mut patterns);
1739    for (name_node, type_node) in patterns {
1740        let name = extract_identifier(name_node, content);
1741        if name.is_empty() {
1742            continue;
1743        }
1744        let qualified_var = format!("{}@{}", name, name_node.start_byte());
1745        let span = Span::from_bytes(name_node.start_byte(), name_node.end_byte());
1746        let var_id = helper.add_variable(&qualified_var, Some(span));
1747        scope_tree.attach_node_id(&name, name_node.start_byte(), var_id);
1748
1749        if let Some(type_node) = type_node {
1750            let type_text = extract_type_name(type_node, content);
1751            if !type_text.is_empty() {
1752                let resolved_type = ast_graph
1753                    .import_map
1754                    .get(&type_text)
1755                    .cloned()
1756                    .unwrap_or(type_text);
1757                let type_id = helper.add_class(&resolved_type, None);
1758                helper.add_typeof_edge(var_id, type_id);
1759            }
1760        }
1761    }
1762}
1763
1764fn handle_switch_pattern_declaration(
1765    node: Node,
1766    content: &[u8],
1767    ast_graph: &ASTGraph,
1768    scope_tree: &mut JavaScopeTree,
1769    helper: &mut GraphBuildHelper,
1770) {
1771    let mut patterns = Vec::new();
1772    collect_pattern_declarations(node, &mut patterns);
1773    for (name_node, type_node) in patterns {
1774        let name = extract_identifier(name_node, content);
1775        if name.is_empty() {
1776            continue;
1777        }
1778        let qualified_var = format!("{}@{}", name, name_node.start_byte());
1779        let span = Span::from_bytes(name_node.start_byte(), name_node.end_byte());
1780        let var_id = helper.add_variable(&qualified_var, Some(span));
1781        scope_tree.attach_node_id(&name, name_node.start_byte(), var_id);
1782
1783        if let Some(type_node) = type_node {
1784            let type_text = extract_type_name(type_node, content);
1785            if !type_text.is_empty() {
1786                let resolved_type = ast_graph
1787                    .import_map
1788                    .get(&type_text)
1789                    .cloned()
1790                    .unwrap_or(type_text);
1791                let type_id = helper.add_class(&resolved_type, None);
1792                helper.add_typeof_edge(var_id, type_id);
1793            }
1794        }
1795    }
1796}
1797
1798fn handle_compact_constructor_parameters(
1799    node: Node,
1800    content: &[u8],
1801    ast_graph: &ASTGraph,
1802    scope_tree: &mut JavaScopeTree,
1803    helper: &mut GraphBuildHelper,
1804) {
1805    use sqry_core::graph::unified::node::NodeKind;
1806
1807    let Some(record_node) = node
1808        .parent()
1809        .and_then(|parent| find_record_declaration(parent))
1810    else {
1811        return;
1812    };
1813
1814    let Some(record_name_node) = record_node.child_by_field_name("name") else {
1815        return;
1816    };
1817    let record_name = extract_identifier(record_name_node, content);
1818    if record_name.is_empty() {
1819        return;
1820    }
1821
1822    let mut components = Vec::new();
1823    collect_record_components_nodes(record_node, &mut components);
1824    for component in components {
1825        let Some(name_node) = component.child_by_field_name("name") else {
1826            continue;
1827        };
1828        let Some(type_node) = component.child_by_field_name("type") else {
1829            continue;
1830        };
1831        let name = extract_identifier(name_node, content);
1832        if name.is_empty() {
1833            continue;
1834        }
1835
1836        let type_text = extract_type_name(type_node, content);
1837        if type_text.is_empty() {
1838            continue;
1839        }
1840        let resolved_type = ast_graph
1841            .import_map
1842            .get(&type_text)
1843            .cloned()
1844            .unwrap_or(type_text);
1845
1846        let qualified_param = format!("{record_name}.<init>::{name}");
1847        let span = Span::from_bytes(component.start_byte(), component.end_byte());
1848        let param_id = helper.add_node(&qualified_param, Some(span), NodeKind::Parameter);
1849        scope_tree.attach_node_id(&name, name_node.start_byte(), param_id);
1850
1851        let type_id = helper.add_class(&resolved_type, None);
1852        helper.add_typeof_edge(param_id, type_id);
1853    }
1854}
1855
1856fn collect_pattern_declarations<'a>(
1857    node: Node<'a>,
1858    output: &mut Vec<(Node<'a>, Option<Node<'a>>)>,
1859) {
1860    if node.kind() == "type_pattern" {
1861        let name_node = node.child_by_field_name("name");
1862        let type_node = node.child_by_field_name("type").or_else(|| {
1863            let mut cursor = node.walk();
1864            for child in node.children(&mut cursor) {
1865                if matches!(
1866                    child.kind(),
1867                    "type_identifier" | "scoped_type_identifier" | "generic_type"
1868                ) {
1869                    return Some(child);
1870                }
1871            }
1872            None
1873        });
1874        if let Some(name_node) = name_node {
1875            output.push((name_node, type_node));
1876        }
1877    }
1878
1879    if node.kind() == "record_pattern_component" {
1880        let mut name_node = None;
1881        let mut type_node = None;
1882        let mut cursor = node.walk();
1883        for child in node.children(&mut cursor) {
1884            if child.kind() == "identifier" {
1885                name_node = Some(child);
1886            } else if matches!(
1887                child.kind(),
1888                "type_identifier" | "scoped_type_identifier" | "generic_type"
1889            ) {
1890                type_node = Some(child);
1891            }
1892        }
1893        if let Some(name_node) = name_node {
1894            output.push((name_node, type_node));
1895        }
1896    }
1897
1898    let mut cursor = node.walk();
1899    for child in node.children(&mut cursor) {
1900        collect_pattern_declarations(child, output);
1901    }
1902}
1903
1904fn find_record_declaration(node: Node) -> Option<Node> {
1905    if node.kind() == "record_declaration" {
1906        return Some(node);
1907    }
1908    node.parent().and_then(find_record_declaration)
1909}
1910
1911fn collect_record_components_nodes<'a>(node: Node<'a>, output: &mut Vec<Node<'a>>) {
1912    let mut cursor = node.walk();
1913    for child in node.children(&mut cursor) {
1914        if child.kind() == "record_component" {
1915            output.push(child);
1916        }
1917        collect_record_components_nodes(child, output);
1918    }
1919}
1920
1921/// Process method invocation using `GraphBuildHelper`
1922fn process_method_call_unified(
1923    call_node: Node,
1924    content: &[u8],
1925    ast_graph: &ASTGraph,
1926    helper: &mut GraphBuildHelper,
1927) {
1928    let Some(caller_context) = ast_graph.find_enclosing(call_node.start_byte()) else {
1929        return;
1930    };
1931    let Ok(callee_name) = extract_method_invocation_name(call_node, content) else {
1932        return;
1933    };
1934
1935    let callee_qualified =
1936        resolve_callee_qualified(&call_node, content, ast_graph, caller_context, &callee_name);
1937    let caller_method_id = ensure_caller_method(helper, caller_context);
1938    let target_method_id = helper.ensure_method(&callee_qualified, None, false, false);
1939
1940    add_call_edge(helper, caller_method_id, target_method_id, call_node);
1941}
1942
1943/// Process constructor call (new expression) using `GraphBuildHelper`
1944fn process_constructor_call_unified(
1945    new_node: Node,
1946    content: &[u8],
1947    ast_graph: &ASTGraph,
1948    helper: &mut GraphBuildHelper,
1949) {
1950    let Some(caller_context) = ast_graph.find_enclosing(new_node.start_byte()) else {
1951        return;
1952    };
1953
1954    let Some(type_node) = new_node.child_by_field_name("type") else {
1955        return;
1956    };
1957
1958    let class_name = extract_type_name(type_node, content);
1959    if class_name.is_empty() {
1960        return;
1961    }
1962
1963    let qualified_class = qualify_constructor_class(&class_name, caller_context);
1964    let constructor_name = format!("{qualified_class}.<init>");
1965
1966    let caller_method_id = ensure_caller_method(helper, caller_context);
1967    let target_method_id = helper.ensure_method(&constructor_name, None, false, false);
1968    add_call_edge(helper, caller_method_id, target_method_id, new_node);
1969}
1970
1971fn count_call_arguments(call_node: Node<'_>) -> u8 {
1972    let Some(args_node) = call_node.child_by_field_name("arguments") else {
1973        return 255;
1974    };
1975    let count = args_node.named_child_count();
1976    if count <= 254 {
1977        u8::try_from(count).unwrap_or(u8::MAX)
1978    } else {
1979        u8::MAX
1980    }
1981}
1982
1983/// Process import declaration using `GraphBuildHelper`
1984fn process_import_unified(import_node: Node, content: &[u8], helper: &mut GraphBuildHelper) {
1985    let has_asterisk = import_has_wildcard(import_node);
1986    let Some(mut imported_name) = extract_import_name(import_node, content) else {
1987        return;
1988    };
1989    if has_asterisk {
1990        imported_name = format!("{imported_name}.*");
1991    }
1992
1993    let module_id = helper.add_module("<module>", None);
1994    let external_id = helper.add_import(
1995        &imported_name,
1996        Some(Span::from_bytes(
1997            import_node.start_byte(),
1998            import_node.end_byte(),
1999        )),
2000    );
2001
2002    helper.add_import_edge(module_id, external_id);
2003}
2004
2005fn ensure_caller_method(
2006    helper: &mut GraphBuildHelper,
2007    caller_context: &MethodContext,
2008) -> sqry_core::graph::unified::node::NodeId {
2009    helper.ensure_method(
2010        caller_context.qualified_name(),
2011        Some(Span::from_bytes(
2012            caller_context.span.0,
2013            caller_context.span.1,
2014        )),
2015        false,
2016        caller_context.is_static,
2017    )
2018}
2019
2020fn resolve_callee_qualified(
2021    call_node: &Node,
2022    content: &[u8],
2023    ast_graph: &ASTGraph,
2024    caller_context: &MethodContext,
2025    callee_name: &str,
2026) -> String {
2027    if let Some(object_node) = call_node.child_by_field_name("object") {
2028        let object_text = extract_node_text(object_node, content);
2029        return resolve_member_call_target(&object_text, ast_graph, caller_context, callee_name);
2030    }
2031
2032    build_member_symbol(
2033        caller_context.package_name.as_deref(),
2034        &caller_context.class_stack,
2035        callee_name,
2036    )
2037}
2038
2039fn resolve_member_call_target(
2040    object_text: &str,
2041    ast_graph: &ASTGraph,
2042    caller_context: &MethodContext,
2043    callee_name: &str,
2044) -> String {
2045    if object_text.contains('.') {
2046        return format!("{object_text}.{callee_name}");
2047    }
2048    if object_text == "this" {
2049        return build_member_symbol(
2050            caller_context.package_name.as_deref(),
2051            &caller_context.class_stack,
2052            callee_name,
2053        );
2054    }
2055
2056    // Try qualified field lookup (ClassName::fieldName)
2057    if let Some(class_name) = caller_context.class_stack.last() {
2058        let qualified_field = format!("{class_name}::{object_text}");
2059        if let Some((field_type, _is_final, _visibility, _is_static)) =
2060            ast_graph.field_types.get(&qualified_field)
2061        {
2062            return format!("{field_type}.{callee_name}");
2063        }
2064    }
2065
2066    // Fallback: try unqualified field lookup (for backwards compatibility)
2067    if let Some((field_type, _is_final, _visibility, _is_static)) =
2068        ast_graph.field_types.get(object_text)
2069    {
2070        return format!("{field_type}.{callee_name}");
2071    }
2072
2073    if let Some(type_fqn) = ast_graph.import_map.get(object_text) {
2074        return format!("{type_fqn}.{callee_name}");
2075    }
2076
2077    format!("{object_text}.{callee_name}")
2078}
2079
2080fn qualify_constructor_class(class_name: &str, caller_context: &MethodContext) -> String {
2081    if class_name.contains('.') {
2082        class_name.to_string()
2083    } else if let Some(pkg) = caller_context.package_name.as_deref() {
2084        format!("{pkg}.{class_name}")
2085    } else {
2086        class_name.to_string()
2087    }
2088}
2089
2090fn add_call_edge(
2091    helper: &mut GraphBuildHelper,
2092    caller_method_id: sqry_core::graph::unified::node::NodeId,
2093    target_method_id: sqry_core::graph::unified::node::NodeId,
2094    call_node: Node,
2095) {
2096    let argument_count = count_call_arguments(call_node);
2097    let call_span = Span::from_bytes(call_node.start_byte(), call_node.end_byte());
2098    helper.add_call_edge_full_with_span(
2099        caller_method_id,
2100        target_method_id,
2101        argument_count,
2102        false,
2103        vec![call_span],
2104    );
2105}
2106
2107fn import_has_wildcard(import_node: Node) -> bool {
2108    let mut cursor = import_node.walk();
2109    import_node
2110        .children(&mut cursor)
2111        .any(|child| child.kind() == "asterisk")
2112}
2113
2114fn extract_import_name(import_node: Node, content: &[u8]) -> Option<String> {
2115    let mut cursor = import_node.walk();
2116    for child in import_node.children(&mut cursor) {
2117        if child.kind() == "scoped_identifier" || child.kind() == "identifier" {
2118            return Some(extract_full_identifier(child, content));
2119        }
2120    }
2121    None
2122}
2123
2124// ================================
2125// Inheritance and Interface Implementation
2126// ================================
2127
2128/// Process class inheritance (extends clause).
2129///
2130/// Handles patterns like:
2131/// - `class Child extends Parent`
2132/// - `class Dog extends Animal`
2133fn process_inheritance(
2134    class_node: Node,
2135    content: &[u8],
2136    package_name: Option<&str>,
2137    child_class_id: sqry_core::graph::unified::node::NodeId,
2138    helper: &mut GraphBuildHelper,
2139) {
2140    // In tree-sitter-java, the superclass is in a "superclass" field
2141    if let Some(superclass_node) = class_node.child_by_field_name("superclass") {
2142        // The superclass node typically wraps a type_identifier
2143        let parent_type_name = extract_type_from_superclass(superclass_node, content);
2144        if !parent_type_name.is_empty() {
2145            // Build qualified name for parent (may be in same package or imported)
2146            let parent_qualified = qualify_type_name(&parent_type_name, package_name);
2147            let parent_id = helper.add_class(&parent_qualified, None);
2148            helper.add_inherits_edge(child_class_id, parent_id);
2149        }
2150    }
2151}
2152
2153/// Process implements clause for classes.
2154///
2155/// Handles patterns like:
2156/// - `class Foo implements IBar`
2157/// - `class Foo implements IBar, IBaz`
2158fn process_implements(
2159    class_node: Node,
2160    content: &[u8],
2161    package_name: Option<&str>,
2162    class_id: sqry_core::graph::unified::node::NodeId,
2163    helper: &mut GraphBuildHelper,
2164) {
2165    // In tree-sitter-java, the implements clause may be:
2166    // - Field named "interfaces" or "super_interfaces"
2167    // - A child node with kind "super_interfaces"
2168
2169    // First try field-based access
2170    let interfaces_node = class_node
2171        .child_by_field_name("interfaces")
2172        .or_else(|| class_node.child_by_field_name("super_interfaces"));
2173
2174    if let Some(node) = interfaces_node {
2175        extract_interface_types(node, content, package_name, class_id, helper);
2176        return;
2177    }
2178
2179    // Walk children to find super_interfaces node by kind
2180    let mut cursor = class_node.walk();
2181    for child in class_node.children(&mut cursor) {
2182        // tree-sitter-java uses "super_interfaces" node kind for implements clause
2183        if child.kind() == "super_interfaces" {
2184            extract_interface_types(child, content, package_name, class_id, helper);
2185            return;
2186        }
2187    }
2188}
2189
2190/// Process interface inheritance (extends clause for interfaces).
2191///
2192/// Handles patterns like:
2193/// - `interface IChild extends IParent`
2194/// - `interface IChild extends IParent, IOther`
2195///
2196/// tree-sitter-java structure:
2197/// ```text
2198/// interface_declaration
2199///   interface (keyword)
2200///   identifier "Stream"
2201///   extends_interfaces  <- not a field, but a child node by kind
2202///     extends (keyword)
2203///     type_list
2204///       type_identifier "Readable"
2205///       type_identifier "Closeable"
2206/// ```
2207fn process_interface_extends(
2208    interface_node: Node,
2209    content: &[u8],
2210    package_name: Option<&str>,
2211    interface_id: sqry_core::graph::unified::node::NodeId,
2212    helper: &mut GraphBuildHelper,
2213) {
2214    // Walk children to find extends_interfaces by node kind
2215    let mut cursor = interface_node.walk();
2216    for child in interface_node.children(&mut cursor) {
2217        if child.kind() == "extends_interfaces" {
2218            // Found the extends clause - extract parent interfaces using same logic as implements
2219            extract_parent_interfaces_for_inherits(
2220                child,
2221                content,
2222                package_name,
2223                interface_id,
2224                helper,
2225            );
2226            return;
2227        }
2228    }
2229}
2230
2231/// Extract parent interfaces for Inherits edges (interface extends).
2232/// Reuses the same tree structure as `extract_interface_types` but creates Inherits edges.
2233fn extract_parent_interfaces_for_inherits(
2234    extends_node: Node,
2235    content: &[u8],
2236    package_name: Option<&str>,
2237    child_interface_id: sqry_core::graph::unified::node::NodeId,
2238    helper: &mut GraphBuildHelper,
2239) {
2240    let mut cursor = extends_node.walk();
2241    for child in extends_node.children(&mut cursor) {
2242        match child.kind() {
2243            "type_identifier" => {
2244                let type_name = extract_identifier(child, content);
2245                if !type_name.is_empty() {
2246                    let parent_qualified = qualify_type_name(&type_name, package_name);
2247                    let parent_id = helper.add_interface(&parent_qualified, None);
2248                    helper.add_inherits_edge(child_interface_id, parent_id);
2249                }
2250            }
2251            "type_list" => {
2252                let mut type_cursor = child.walk();
2253                for type_child in child.children(&mut type_cursor) {
2254                    if let Some(type_name) = extract_type_identifier(type_child, content)
2255                        && !type_name.is_empty()
2256                    {
2257                        let parent_qualified = qualify_type_name(&type_name, package_name);
2258                        let parent_id = helper.add_interface(&parent_qualified, None);
2259                        helper.add_inherits_edge(child_interface_id, parent_id);
2260                    }
2261                }
2262            }
2263            "generic_type" | "scoped_type_identifier" => {
2264                if let Some(type_name) = extract_type_identifier(child, content)
2265                    && !type_name.is_empty()
2266                {
2267                    let parent_qualified = qualify_type_name(&type_name, package_name);
2268                    let parent_id = helper.add_interface(&parent_qualified, None);
2269                    helper.add_inherits_edge(child_interface_id, parent_id);
2270                }
2271            }
2272            _ => {}
2273        }
2274    }
2275}
2276
2277/// Extract type name from superclass node.
2278fn extract_type_from_superclass(superclass_node: Node, content: &[u8]) -> String {
2279    // The superclass node may directly be a type_identifier or contain one
2280    if superclass_node.kind() == "type_identifier" {
2281        return extract_identifier(superclass_node, content);
2282    }
2283
2284    // Look for type_identifier among children
2285    let mut cursor = superclass_node.walk();
2286    for child in superclass_node.children(&mut cursor) {
2287        if let Some(name) = extract_type_identifier(child, content) {
2288            return name;
2289        }
2290    }
2291
2292    // Fallback: try to extract the entire text
2293    extract_identifier(superclass_node, content)
2294}
2295
2296/// Extract all interface types from a `super_interfaces` or `extends_interfaces` node.
2297///
2298/// tree-sitter-java structure:
2299/// ```text
2300/// super_interfaces
2301///   implements (keyword)
2302///   type_list
2303///     type_identifier "Runnable"
2304///     type_identifier "Serializable" (if multiple)
2305/// ```
2306fn extract_interface_types(
2307    interfaces_node: Node,
2308    content: &[u8],
2309    package_name: Option<&str>,
2310    implementor_id: sqry_core::graph::unified::node::NodeId,
2311    helper: &mut GraphBuildHelper,
2312) {
2313    // Walk all children to find type_list or direct type identifiers
2314    let mut cursor = interfaces_node.walk();
2315    for child in interfaces_node.children(&mut cursor) {
2316        match child.kind() {
2317            // Direct type identifiers at this level
2318            "type_identifier" => {
2319                let type_name = extract_identifier(child, content);
2320                if !type_name.is_empty() {
2321                    let interface_qualified = qualify_type_name(&type_name, package_name);
2322                    let interface_id = helper.add_interface(&interface_qualified, None);
2323                    helper.add_implements_edge(implementor_id, interface_id);
2324                }
2325            }
2326            // type_list contains the actual interfaces
2327            "type_list" => {
2328                let mut type_cursor = child.walk();
2329                for type_child in child.children(&mut type_cursor) {
2330                    if let Some(type_name) = extract_type_identifier(type_child, content)
2331                        && !type_name.is_empty()
2332                    {
2333                        let interface_qualified = qualify_type_name(&type_name, package_name);
2334                        let interface_id = helper.add_interface(&interface_qualified, None);
2335                        helper.add_implements_edge(implementor_id, interface_id);
2336                    }
2337                }
2338            }
2339            // Generic type at this level
2340            "generic_type" | "scoped_type_identifier" => {
2341                if let Some(type_name) = extract_type_identifier(child, content)
2342                    && !type_name.is_empty()
2343                {
2344                    let interface_qualified = qualify_type_name(&type_name, package_name);
2345                    let interface_id = helper.add_interface(&interface_qualified, None);
2346                    helper.add_implements_edge(implementor_id, interface_id);
2347                }
2348            }
2349            _ => {}
2350        }
2351    }
2352}
2353
2354/// Extract type identifier from a node (handles `type_identifier` and `generic_type`).
2355fn extract_type_identifier(node: Node, content: &[u8]) -> Option<String> {
2356    match node.kind() {
2357        "type_identifier" => Some(extract_identifier(node, content)),
2358        "generic_type" => {
2359            // For generic types like `List<String>`, extract base type
2360            if let Some(name_node) = node.child_by_field_name("name") {
2361                Some(extract_identifier(name_node, content))
2362            } else {
2363                // Fallback: get first child if it's a type_identifier
2364                let mut cursor = node.walk();
2365                for child in node.children(&mut cursor) {
2366                    if child.kind() == "type_identifier" {
2367                        return Some(extract_identifier(child, content));
2368                    }
2369                }
2370                None
2371            }
2372        }
2373        "scoped_type_identifier" => {
2374            // Fully qualified type like `java.util.List`
2375            Some(extract_full_identifier(node, content))
2376        }
2377        _ => None,
2378    }
2379}
2380
2381/// Qualify a type name with package prefix if not already qualified.
2382fn qualify_type_name(type_name: &str, package_name: Option<&str>) -> String {
2383    // If already qualified (contains '.'), keep as-is
2384    if type_name.contains('.') {
2385        return type_name.to_string();
2386    }
2387
2388    // Otherwise, prefix with package if available
2389    if let Some(pkg) = package_name {
2390        format!("{pkg}.{type_name}")
2391    } else {
2392        type_name.to_string()
2393    }
2394}
2395
2396// ================================
2397// Field Type Extraction
2398// ================================
2399
2400/// Extract field declarations and imports to build type resolution maps.
2401/// Returns (`field_types`, `import_map`) where:
2402/// - `field_types` maps field names to (`type_fqn`, `is_final`) tuples
2403/// - `import_map` maps simple type names to FQNs (e.g., "`UserService`" -> "com.example.service.UserService")
2404#[allow(clippy::type_complexity)]
2405fn extract_field_and_import_types(
2406    node: Node,
2407    content: &[u8],
2408) -> (
2409    HashMap<String, (String, bool, Option<sqry_core::schema::Visibility>, bool)>,
2410    HashMap<String, String>,
2411) {
2412    // First, build import map (simple name -> FQN)
2413    let import_map = extract_import_map(node, content);
2414
2415    let mut field_types = HashMap::new();
2416    let mut class_stack = Vec::new();
2417    extract_field_types_recursive(
2418        node,
2419        content,
2420        &import_map,
2421        &mut field_types,
2422        &mut class_stack,
2423    );
2424
2425    (field_types, import_map)
2426}
2427
2428/// Build a map from simple type names to their FQNs based on import declarations
2429fn extract_import_map(node: Node, content: &[u8]) -> HashMap<String, String> {
2430    let mut import_map = HashMap::new();
2431    collect_import_map_recursive(node, content, &mut import_map);
2432    import_map
2433}
2434
2435fn collect_import_map_recursive(
2436    node: Node,
2437    content: &[u8],
2438    import_map: &mut HashMap<String, String>,
2439) {
2440    if node.kind() == "import_declaration" {
2441        // import com.example.service.UserService;
2442        // Tree structure: (import_declaration (scoped_identifier ...))
2443        // Try to get the full import path
2444        let full_path = node.utf8_text(content).unwrap_or("");
2445
2446        // Parse out the class name from the import statement
2447        // "import com.example.service.UserService;" -> "com.example.service.UserService"
2448        if let Some(path_start) = full_path.find("import ") {
2449            let after_import = &full_path[path_start + 7..].trim();
2450            if let Some(path_end) = after_import.find(';') {
2451                let import_path = &after_import[..path_end].trim();
2452
2453                // Get the simple name (last part)
2454                if let Some(simple_name) = import_path.rsplit('.').next() {
2455                    import_map.insert(simple_name.to_string(), (*import_path).to_string());
2456                }
2457            }
2458        }
2459    }
2460
2461    // Recurse into children
2462    let mut cursor = node.walk();
2463    for child in node.children(&mut cursor) {
2464        collect_import_map_recursive(child, content, import_map);
2465    }
2466}
2467
2468fn extract_field_types_recursive(
2469    node: Node,
2470    content: &[u8],
2471    import_map: &HashMap<String, String>,
2472    field_types: &mut HashMap<String, (String, bool, Option<sqry_core::schema::Visibility>, bool)>,
2473    class_stack: &mut Vec<String>,
2474) {
2475    // Handle class/interface/enum declarations - push onto stack
2476    if matches!(
2477        node.kind(),
2478        "class_declaration" | "interface_declaration" | "enum_declaration"
2479    ) && let Some(name_node) = node.child_by_field_name("name")
2480    {
2481        let class_name = extract_identifier(name_node, content);
2482        class_stack.push(class_name);
2483
2484        // Recurse into body
2485        if let Some(body_node) = node.child_by_field_name("body") {
2486            let mut cursor = body_node.walk();
2487            for child in body_node.children(&mut cursor) {
2488                extract_field_types_recursive(child, content, import_map, field_types, class_stack);
2489            }
2490        }
2491
2492        // Pop class from stack
2493        class_stack.pop();
2494        return; // Already recursed into body
2495    }
2496
2497    // field_declaration node structure:
2498    // (field_declaration
2499    //   modifiers?: (modifiers) - may contain "final", "static", "public", etc.
2500    //   type: (type_identifier) @type
2501    //   declarator: (variable_declarator
2502    //     name: (identifier) @name))
2503    if node.kind() == "field_declaration" {
2504        // Check for modifiers using the helper function
2505        let is_final = has_modifier(node, "final", content);
2506        let is_static = has_modifier(node, "static", content);
2507
2508        // Extract visibility (Java has: public, private, protected, package-private)
2509        // Map to sqry Visibility: public -> Public, others -> Private
2510        let visibility = if has_modifier(node, "public", content) {
2511            Some(sqry_core::schema::Visibility::Public)
2512        } else {
2513            // private, protected, or package-private (default) all map to Private
2514            Some(sqry_core::schema::Visibility::Private)
2515        };
2516
2517        // Extract type
2518        if let Some(type_node) = node.child_by_field_name("type") {
2519            let type_text = extract_type_name_internal(type_node, content);
2520            if !type_text.is_empty() {
2521                // Resolve simple type name to FQN using imports
2522                let resolved_type = import_map
2523                    .get(&type_text)
2524                    .cloned()
2525                    .unwrap_or(type_text.clone());
2526
2527                // Extract all declarators (there can be multiple: "String a, b;")
2528                let mut cursor = node.walk();
2529                for child in node.children(&mut cursor) {
2530                    if child.kind() == "variable_declarator"
2531                        && let Some(name_node) = child.child_by_field_name("name")
2532                    {
2533                        let field_name = extract_identifier(name_node, content);
2534
2535                        // Create qualified field name using full class path (OuterClass::InnerClass::fieldName)
2536                        // This prevents collisions for fields with same name in different nested classes
2537                        let qualified_field = if class_stack.is_empty() {
2538                            field_name
2539                        } else {
2540                            let class_path = class_stack.join("::");
2541                            format!("{class_path}::{field_name}")
2542                        };
2543
2544                        field_types.insert(
2545                            qualified_field,
2546                            (resolved_type.clone(), is_final, visibility, is_static),
2547                        );
2548                    }
2549                }
2550            }
2551        }
2552    }
2553
2554    // Recurse into children (for non-class nodes)
2555    let mut cursor = node.walk();
2556    for child in node.children(&mut cursor) {
2557        extract_field_types_recursive(child, content, import_map, field_types, class_stack);
2558    }
2559}
2560
2561/// Helper to extract type names for field extraction.
2562fn extract_type_name_internal(type_node: Node, content: &[u8]) -> String {
2563    match type_node.kind() {
2564        "generic_type" => {
2565            // Extract base type (e.g., "List" from "List<String>")
2566            if let Some(name_node) = type_node.child_by_field_name("name") {
2567                extract_identifier(name_node, content)
2568            } else {
2569                extract_identifier(type_node, content)
2570            }
2571        }
2572        "scoped_type_identifier" => {
2573            // e.g., "java.util.List"
2574            extract_full_identifier(type_node, content)
2575        }
2576        _ => extract_identifier(type_node, content),
2577    }
2578}
2579
2580// ================================
2581// AST Extraction Helpers
2582// ================================
2583
2584fn extract_identifier(node: Node, content: &[u8]) -> String {
2585    node.utf8_text(content).unwrap_or("").to_string()
2586}
2587
2588fn extract_node_text(node: Node, content: &[u8]) -> String {
2589    node.utf8_text(content).unwrap_or("").to_string()
2590}
2591
2592fn extract_full_identifier(node: Node, content: &[u8]) -> String {
2593    node.utf8_text(content).unwrap_or("").to_string()
2594}
2595
2596fn first_child_of_kind<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
2597    let mut cursor = node.walk();
2598    node.children(&mut cursor)
2599        .find(|&child| child.kind() == kind)
2600}
2601
2602fn extract_method_invocation_name(call_node: Node, content: &[u8]) -> GraphResult<String> {
2603    // method_invocation has a "name" field
2604    if let Some(name_node) = call_node.child_by_field_name("name") {
2605        Ok(extract_identifier(name_node, content))
2606    } else {
2607        // Fallback: try to find identifier
2608        let mut cursor = call_node.walk();
2609        for child in call_node.children(&mut cursor) {
2610            if child.kind() == "identifier" {
2611                return Ok(extract_identifier(child, content));
2612            }
2613        }
2614
2615        Err(GraphBuilderError::ParseError {
2616            span: Span::from_bytes(call_node.start_byte(), call_node.end_byte()),
2617            reason: "Method invocation missing name".into(),
2618        })
2619    }
2620}
2621
2622fn extract_type_name(type_node: Node, content: &[u8]) -> String {
2623    // Type can be simple identifier or generic type
2624    match type_node.kind() {
2625        "generic_type" => {
2626            // Extract base type (e.g., "List" from "List<String>")
2627            if let Some(name_node) = type_node.child_by_field_name("name") {
2628                extract_identifier(name_node, content)
2629            } else {
2630                extract_identifier(type_node, content)
2631            }
2632        }
2633        "scoped_type_identifier" => {
2634            // e.g., "java.util.List"
2635            extract_full_identifier(type_node, content)
2636        }
2637        _ => extract_identifier(type_node, content),
2638    }
2639}
2640
2641/// Extract the full return type including generics (e.g., `Optional<User>`, `List<String>`).
2642/// Unlike `extract_type_name` which extracts just the base type, this preserves the full type signature.
2643fn extract_full_return_type(type_node: Node, content: &[u8]) -> String {
2644    // For the `returns:` predicate, we need the full type representation
2645    // including generic parameters like Optional<User>, List<Map<String, Integer>>
2646    type_node.utf8_text(content).unwrap_or("").to_string()
2647}
2648
2649fn has_modifier(node: Node, modifier: &str, content: &[u8]) -> bool {
2650    let mut cursor = node.walk();
2651    for child in node.children(&mut cursor) {
2652        if child.kind() == "modifiers" {
2653            let mut mod_cursor = child.walk();
2654            for modifier_child in child.children(&mut mod_cursor) {
2655                if extract_identifier(modifier_child, content) == modifier {
2656                    return true;
2657                }
2658            }
2659        }
2660    }
2661    false
2662}
2663
2664/// Extract visibility modifier from a method or constructor node.
2665/// Returns "public", "private", "protected", or "package-private" (no explicit modifier).
2666#[allow(clippy::unnecessary_wraps)]
2667fn extract_visibility(node: Node, content: &[u8]) -> Option<String> {
2668    if has_modifier(node, "public", content) {
2669        Some("public".to_string())
2670    } else if has_modifier(node, "private", content) {
2671        Some("private".to_string())
2672    } else if has_modifier(node, "protected", content) {
2673        Some("protected".to_string())
2674    } else {
2675        // No explicit modifier means package-private in Java
2676        Some("package-private".to_string())
2677    }
2678}
2679
2680// ================================
2681// Export Detection (public visibility)
2682// ================================
2683
2684/// Check if a node has the `public` visibility modifier.
2685fn is_public(node: Node, content: &[u8]) -> bool {
2686    has_modifier(node, "public", content)
2687}
2688
2689/// Check if a node has the `private` visibility modifier.
2690fn is_private(node: Node, content: &[u8]) -> bool {
2691    has_modifier(node, "private", content)
2692}
2693
2694/// Create an export edge from the file module to the exported node.
2695fn export_from_file_module(
2696    helper: &mut GraphBuildHelper,
2697    exported: sqry_core::graph::unified::node::NodeId,
2698) {
2699    let module_id = helper.add_module(FILE_MODULE_NAME, None);
2700    helper.add_export_edge(module_id, exported);
2701}
2702
2703/// Process public methods, constructors, and fields within a class body for export edges.
2704///
2705/// For interfaces, methods are implicitly public UNLESS explicitly marked private (Java 9+).
2706/// For classes, only explicitly public members are exported.
2707fn process_class_member_exports(
2708    body_node: Node,
2709    content: &[u8],
2710    class_qualified_name: &str,
2711    helper: &mut GraphBuildHelper,
2712    is_interface: bool,
2713) {
2714    for i in 0..body_node.child_count() {
2715        if let Some(child) = body_node.child(i as u32) {
2716            match child.kind() {
2717                "method_declaration" => {
2718                    // Interface methods are implicitly public UNLESS explicitly private (Java 9+)
2719                    // Class methods need explicit public modifier
2720                    let should_export = if is_interface {
2721                        // Export interface method if NOT explicitly private
2722                        !is_private(child, content)
2723                    } else {
2724                        // Export class method only if explicitly public
2725                        is_public(child, content)
2726                    };
2727
2728                    if should_export && let Some(name_node) = child.child_by_field_name("name") {
2729                        let method_name = extract_identifier(name_node, content);
2730                        let qualified_name = format!("{class_qualified_name}.{method_name}");
2731                        let span = Span::from_bytes(child.start_byte(), child.end_byte());
2732                        let is_static = has_modifier(child, "static", content);
2733                        let method_id =
2734                            helper.add_method(&qualified_name, Some(span), false, is_static);
2735                        export_from_file_module(helper, method_id);
2736                    }
2737                }
2738                "constructor_declaration" => {
2739                    if is_public(child, content) {
2740                        let qualified_name = format!("{class_qualified_name}.<init>");
2741                        let span = Span::from_bytes(child.start_byte(), child.end_byte());
2742                        let method_id =
2743                            helper.add_method(&qualified_name, Some(span), false, false);
2744                        export_from_file_module(helper, method_id);
2745                    }
2746                }
2747                "field_declaration" => {
2748                    if is_public(child, content) {
2749                        // Extract all field names from the declaration
2750                        let mut cursor = child.walk();
2751                        for field_child in child.children(&mut cursor) {
2752                            if field_child.kind() == "variable_declarator"
2753                                && let Some(name_node) = field_child.child_by_field_name("name")
2754                            {
2755                                let field_name = extract_identifier(name_node, content);
2756                                let qualified_name = format!("{class_qualified_name}.{field_name}");
2757                                let span = Span::from_bytes(
2758                                    field_child.start_byte(),
2759                                    field_child.end_byte(),
2760                                );
2761
2762                                // Use constant for final fields, variable otherwise
2763                                let is_final = has_modifier(child, "final", content);
2764                                let field_id = if is_final {
2765                                    helper.add_constant(&qualified_name, Some(span))
2766                                } else {
2767                                    helper.add_variable(&qualified_name, Some(span))
2768                                };
2769                                export_from_file_module(helper, field_id);
2770                            }
2771                        }
2772                    }
2773                }
2774                "constant_declaration" => {
2775                    // Constants in interfaces are always public
2776                    let mut cursor = child.walk();
2777                    for const_child in child.children(&mut cursor) {
2778                        if const_child.kind() == "variable_declarator"
2779                            && let Some(name_node) = const_child.child_by_field_name("name")
2780                        {
2781                            let const_name = extract_identifier(name_node, content);
2782                            let qualified_name = format!("{class_qualified_name}.{const_name}");
2783                            let span =
2784                                Span::from_bytes(const_child.start_byte(), const_child.end_byte());
2785                            let const_id = helper.add_constant(&qualified_name, Some(span));
2786                            export_from_file_module(helper, const_id);
2787                        }
2788                    }
2789                }
2790                "enum_constant" => {
2791                    // Enum constants are always public
2792                    if let Some(name_node) = child.child_by_field_name("name") {
2793                        let const_name = extract_identifier(name_node, content);
2794                        let qualified_name = format!("{class_qualified_name}.{const_name}");
2795                        let span = Span::from_bytes(child.start_byte(), child.end_byte());
2796                        let const_id = helper.add_constant(&qualified_name, Some(span));
2797                        export_from_file_module(helper, const_id);
2798                    }
2799                }
2800                _ => {}
2801            }
2802        }
2803    }
2804}
2805
2806// ================================
2807// FFI Detection (JNI, JNA, Panama)
2808// ================================
2809
2810/// Detect FFI-related imports in the file.
2811/// Returns (`has_jna_import`, `has_panama_import`).
2812fn detect_ffi_imports(node: Node, content: &[u8]) -> (bool, bool) {
2813    let mut has_jna = false;
2814    let mut has_panama = false;
2815
2816    detect_ffi_imports_recursive(node, content, &mut has_jna, &mut has_panama);
2817
2818    (has_jna, has_panama)
2819}
2820
2821fn detect_ffi_imports_recursive(
2822    node: Node,
2823    content: &[u8],
2824    has_jna: &mut bool,
2825    has_panama: &mut bool,
2826) {
2827    if node.kind() == "import_declaration" {
2828        let import_text = node.utf8_text(content).unwrap_or("");
2829
2830        // JNA: com.sun.jna.* or net.java.dev.jna.*
2831        if import_text.contains("com.sun.jna") || import_text.contains("net.java.dev.jna") {
2832            *has_jna = true;
2833        }
2834
2835        // Panama Foreign Function API: java.lang.foreign.*
2836        if import_text.contains("java.lang.foreign") {
2837            *has_panama = true;
2838        }
2839    }
2840
2841    let mut cursor = node.walk();
2842    for child in node.children(&mut cursor) {
2843        detect_ffi_imports_recursive(child, content, has_jna, has_panama);
2844    }
2845}
2846
2847/// Find interfaces that extend JNA Library.
2848/// These interfaces define native function signatures.
2849fn find_jna_library_interfaces(node: Node, content: &[u8]) -> Vec<String> {
2850    let mut jna_interfaces = Vec::new();
2851    find_jna_library_interfaces_recursive(node, content, &mut jna_interfaces);
2852    jna_interfaces
2853}
2854
2855fn find_jna_library_interfaces_recursive(
2856    node: Node,
2857    content: &[u8],
2858    jna_interfaces: &mut Vec<String>,
2859) {
2860    if node.kind() == "interface_declaration" {
2861        // Check if this interface extends Library
2862        if let Some(name_node) = node.child_by_field_name("name") {
2863            let interface_name = extract_identifier(name_node, content);
2864
2865            // Look for extends clause
2866            let mut cursor = node.walk();
2867            for child in node.children(&mut cursor) {
2868                if child.kind() == "extends_interfaces" {
2869                    let extends_text = child.utf8_text(content).unwrap_or("");
2870                    // Check if extends Library or com.sun.jna.Library
2871                    if extends_text.contains("Library") {
2872                        jna_interfaces.push(interface_name.clone());
2873                    }
2874                }
2875            }
2876        }
2877    }
2878
2879    let mut cursor = node.walk();
2880    for child in node.children(&mut cursor) {
2881        find_jna_library_interfaces_recursive(child, content, jna_interfaces);
2882    }
2883}
2884
2885/// Check if a method call is an FFI call and build the appropriate edge.
2886/// Returns true if an FFI edge was created.
2887fn build_ffi_call_edge(
2888    call_node: Node,
2889    content: &[u8],
2890    caller_context: &MethodContext,
2891    ast_graph: &ASTGraph,
2892    helper: &mut GraphBuildHelper,
2893) -> bool {
2894    // Extract method name
2895    let Ok(method_name) = extract_method_invocation_name(call_node, content) else {
2896        return false;
2897    };
2898
2899    // Check for JNA Native.load() call
2900    if ast_graph.has_jna_import && is_jna_native_load(call_node, content, &method_name) {
2901        let library_name = extract_jna_library_name(call_node, content);
2902        build_jna_native_load_edge(caller_context, &library_name, call_node, helper);
2903        return true;
2904    }
2905
2906    // Check for JNA interface method call (calling methods on loaded library)
2907    if ast_graph.has_jna_import
2908        && let Some(object_node) = call_node.child_by_field_name("object")
2909    {
2910        let object_text = extract_node_text(object_node, content);
2911
2912        // Try qualified field lookup first (ClassName::fieldName)
2913        let field_type = if let Some(class_name) = caller_context.class_stack.last() {
2914            let qualified_field = format!("{class_name}::{object_text}");
2915            ast_graph
2916                .field_types
2917                .get(&qualified_field)
2918                .or_else(|| ast_graph.field_types.get(&object_text))
2919        } else {
2920            ast_graph.field_types.get(&object_text)
2921        };
2922
2923        // Check if the object type is a JNA Library interface
2924        if let Some((type_name, _is_final, _visibility, _is_static)) = field_type {
2925            let simple_type = simple_type_name(type_name);
2926            if ast_graph.jna_library_interfaces.contains(&simple_type) {
2927                build_jna_method_call_edge(
2928                    caller_context,
2929                    &simple_type,
2930                    &method_name,
2931                    call_node,
2932                    helper,
2933                );
2934                return true;
2935            }
2936        }
2937    }
2938
2939    // Check for Panama Foreign Function API calls
2940    if ast_graph.has_panama_import {
2941        if let Some(object_node) = call_node.child_by_field_name("object") {
2942            let object_text = extract_node_text(object_node, content);
2943
2944            // Linker.nativeLinker() and downcallHandle()
2945            if object_text == "Linker" && method_name == "nativeLinker" {
2946                build_panama_linker_edge(caller_context, call_node, helper);
2947                return true;
2948            }
2949
2950            // SymbolLookup.libraryLookup()
2951            if object_text == "SymbolLookup" && method_name == "libraryLookup" {
2952                let library_name = extract_first_string_arg(call_node, content);
2953                build_panama_library_lookup_edge(caller_context, &library_name, call_node, helper);
2954                return true;
2955            }
2956
2957            // MethodHandle.invokeExact() on a downcall handle
2958            if method_name == "invokeExact" || method_name == "invoke" {
2959                // Check if this might be a foreign function call
2960                // This is a heuristic - we mark it as FFI if in Panama context
2961                if is_potential_panama_invoke(call_node, content) {
2962                    build_panama_invoke_edge(caller_context, &method_name, call_node, helper);
2963                    return true;
2964                }
2965            }
2966        }
2967
2968        // Direct Linker.nativeLinker() static call
2969        if method_name == "nativeLinker" {
2970            let full_text = call_node.utf8_text(content).unwrap_or("");
2971            if full_text.contains("Linker") {
2972                build_panama_linker_edge(caller_context, call_node, helper);
2973                return true;
2974            }
2975        }
2976    }
2977
2978    false
2979}
2980
2981/// Check if this is a JNA `Native.load()` or `Native.loadLibrary()` call.
2982fn is_jna_native_load(call_node: Node, content: &[u8], method_name: &str) -> bool {
2983    if method_name != "load" && method_name != "loadLibrary" {
2984        return false;
2985    }
2986
2987    if let Some(object_node) = call_node.child_by_field_name("object") {
2988        let object_text = extract_node_text(object_node, content);
2989        return object_text == "Native" || object_text == "com.sun.jna.Native";
2990    }
2991
2992    false
2993}
2994
2995/// Extract the library name from JNA `Native.load()` call.
2996/// Native.load("c", CLibrary.class) -> "c"
2997fn extract_jna_library_name(call_node: Node, content: &[u8]) -> String {
2998    if let Some(args_node) = call_node.child_by_field_name("arguments") {
2999        let mut cursor = args_node.walk();
3000        for child in args_node.children(&mut cursor) {
3001            if child.kind() == "string_literal" {
3002                let text = child.utf8_text(content).unwrap_or("\"unknown\"");
3003                // Remove quotes
3004                return text.trim_matches('"').to_string();
3005            }
3006        }
3007    }
3008    "unknown".to_string()
3009}
3010
3011/// Extract the first string argument from a method call.
3012fn extract_first_string_arg(call_node: Node, content: &[u8]) -> String {
3013    if let Some(args_node) = call_node.child_by_field_name("arguments") {
3014        let mut cursor = args_node.walk();
3015        for child in args_node.children(&mut cursor) {
3016            if child.kind() == "string_literal" {
3017                let text = child.utf8_text(content).unwrap_or("\"unknown\"");
3018                return text.trim_matches('"').to_string();
3019            }
3020        }
3021    }
3022    "unknown".to_string()
3023}
3024
3025/// Check if this is potentially a Panama foreign function invoke.
3026fn is_potential_panama_invoke(call_node: Node, content: &[u8]) -> bool {
3027    // Check if the call is on a MethodHandle that might be a downcall
3028    if let Some(object_node) = call_node.child_by_field_name("object") {
3029        let object_text = extract_node_text(object_node, content);
3030        // Heuristics: variable names often contain "handle", "downcall", or "mh"
3031        let lower = object_text.to_lowercase();
3032        return lower.contains("handle")
3033            || lower.contains("downcall")
3034            || lower.contains("mh")
3035            || lower.contains("foreign");
3036    }
3037    false
3038}
3039
3040/// Get simple type name from potentially qualified name.
3041fn simple_type_name(type_name: &str) -> String {
3042    type_name
3043        .rsplit('.')
3044        .next()
3045        .unwrap_or(type_name)
3046        .to_string()
3047}
3048
3049/// Build FFI edge for JNA `Native.load()` call.
3050fn build_jna_native_load_edge(
3051    caller_context: &MethodContext,
3052    library_name: &str,
3053    call_node: Node,
3054    helper: &mut GraphBuildHelper,
3055) {
3056    let caller_id = helper.ensure_method(
3057        caller_context.qualified_name(),
3058        Some(Span::from_bytes(
3059            caller_context.span.0,
3060            caller_context.span.1,
3061        )),
3062        false,
3063        caller_context.is_static,
3064    );
3065
3066    let target_name = format!("native::{library_name}");
3067    let target_id = helper.add_function(
3068        &target_name,
3069        Some(Span::from_bytes(
3070            call_node.start_byte(),
3071            call_node.end_byte(),
3072        )),
3073        false,
3074        false,
3075    );
3076
3077    helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3078}
3079
3080/// Build FFI edge for JNA interface method call.
3081fn build_jna_method_call_edge(
3082    caller_context: &MethodContext,
3083    interface_name: &str,
3084    method_name: &str,
3085    call_node: Node,
3086    helper: &mut GraphBuildHelper,
3087) {
3088    let caller_id = helper.ensure_method(
3089        caller_context.qualified_name(),
3090        Some(Span::from_bytes(
3091            caller_context.span.0,
3092            caller_context.span.1,
3093        )),
3094        false,
3095        caller_context.is_static,
3096    );
3097
3098    let target_name = format!("native::{interface_name}::{method_name}");
3099    let target_id = helper.add_function(
3100        &target_name,
3101        Some(Span::from_bytes(
3102            call_node.start_byte(),
3103            call_node.end_byte(),
3104        )),
3105        false,
3106        false,
3107    );
3108
3109    helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3110}
3111
3112/// Build FFI edge for Panama `Linker.nativeLinker()` call.
3113fn build_panama_linker_edge(
3114    caller_context: &MethodContext,
3115    call_node: Node,
3116    helper: &mut GraphBuildHelper,
3117) {
3118    let caller_id = helper.ensure_method(
3119        caller_context.qualified_name(),
3120        Some(Span::from_bytes(
3121            caller_context.span.0,
3122            caller_context.span.1,
3123        )),
3124        false,
3125        caller_context.is_static,
3126    );
3127
3128    let target_name = "native::panama::nativeLinker";
3129    let target_id = helper.add_function(
3130        target_name,
3131        Some(Span::from_bytes(
3132            call_node.start_byte(),
3133            call_node.end_byte(),
3134        )),
3135        false,
3136        false,
3137    );
3138
3139    helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3140}
3141
3142/// Build FFI edge for Panama `SymbolLookup.libraryLookup()` call.
3143fn build_panama_library_lookup_edge(
3144    caller_context: &MethodContext,
3145    library_name: &str,
3146    call_node: Node,
3147    helper: &mut GraphBuildHelper,
3148) {
3149    let caller_id = helper.ensure_method(
3150        caller_context.qualified_name(),
3151        Some(Span::from_bytes(
3152            caller_context.span.0,
3153            caller_context.span.1,
3154        )),
3155        false,
3156        caller_context.is_static,
3157    );
3158
3159    let target_name = format!("native::panama::{library_name}");
3160    let target_id = helper.add_function(
3161        &target_name,
3162        Some(Span::from_bytes(
3163            call_node.start_byte(),
3164            call_node.end_byte(),
3165        )),
3166        false,
3167        false,
3168    );
3169
3170    helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3171}
3172
3173/// Build FFI edge for Panama `MethodHandle` invoke.
3174fn build_panama_invoke_edge(
3175    caller_context: &MethodContext,
3176    method_name: &str,
3177    call_node: Node,
3178    helper: &mut GraphBuildHelper,
3179) {
3180    let caller_id = helper.ensure_method(
3181        caller_context.qualified_name(),
3182        Some(Span::from_bytes(
3183            caller_context.span.0,
3184            caller_context.span.1,
3185        )),
3186        false,
3187        caller_context.is_static,
3188    );
3189
3190    let target_name = format!("native::panama::{method_name}");
3191    let target_id = helper.add_function(
3192        &target_name,
3193        Some(Span::from_bytes(
3194            call_node.start_byte(),
3195            call_node.end_byte(),
3196        )),
3197        false,
3198        false,
3199    );
3200
3201    helper.add_ffi_edge(caller_id, target_id, FfiConvention::C);
3202}
3203
3204/// Build FFI edge for JNI native method declaration.
3205/// This is called when we encounter a native method declaration.
3206fn build_jni_native_method_edge(method_context: &MethodContext, helper: &mut GraphBuildHelper) {
3207    // The method itself is the caller (conceptually, calling into native code)
3208    let method_id = helper.ensure_method(
3209        method_context.qualified_name(),
3210        Some(Span::from_bytes(
3211            method_context.span.0,
3212            method_context.span.1,
3213        )),
3214        false,
3215        method_context.is_static,
3216    );
3217
3218    // Create a synthetic target representing the native implementation
3219    // Convention: Java_<package>_<class>_<method>
3220    let native_target = format!("native::jni::{}", method_context.qualified_name());
3221    let target_id = helper.add_function(&native_target, None, false, false);
3222
3223    helper.add_ffi_edge(method_id, target_id, FfiConvention::C);
3224}
3225
3226// ================================
3227// Spring MVC Route Endpoint Detection
3228// ================================
3229
3230/// Extract Spring MVC route information from a `method_declaration` node.
3231///
3232/// Detects annotations like `@GetMapping("/api/users")`, `@PostMapping("/api/items")`,
3233/// `@RequestMapping(path="/api/users", method=RequestMethod.GET)`, etc.
3234///
3235/// # Returns
3236///
3237/// `Some((http_method, path))` if a Spring route annotation is found, `None` otherwise.
3238/// For example: `Some(("GET", "/api/users"))`.
3239fn extract_spring_route_info(method_node: Node, content: &[u8]) -> Option<(String, String)> {
3240    // Navigate to the modifiers child node (contains annotations)
3241    let mut cursor = method_node.walk();
3242    let modifiers_node = method_node
3243        .children(&mut cursor)
3244        .find(|child| child.kind() == "modifiers")?;
3245
3246    // Iterate through children of modifiers looking for annotation nodes
3247    let mut mod_cursor = modifiers_node.walk();
3248    for annotation_node in modifiers_node.children(&mut mod_cursor) {
3249        if annotation_node.kind() != "annotation" {
3250            continue;
3251        }
3252
3253        // Extract the annotation name (identifier or scoped_identifier)
3254        let Some(annotation_name) = extract_annotation_name(annotation_node, content) else {
3255            continue;
3256        };
3257
3258        // Map annotation name to HTTP method
3259        let http_method: String = match annotation_name.as_str() {
3260            "GetMapping" => "GET".to_string(),
3261            "PostMapping" => "POST".to_string(),
3262            "PutMapping" => "PUT".to_string(),
3263            "DeleteMapping" => "DELETE".to_string(),
3264            "PatchMapping" => "PATCH".to_string(),
3265            "RequestMapping" => {
3266                // For @RequestMapping, extract method from arguments or default to GET
3267                extract_request_mapping_method(annotation_node, content)
3268                    .unwrap_or_else(|| "GET".to_string())
3269            }
3270            _ => continue,
3271        };
3272
3273        // Extract the path from the annotation arguments
3274        let Some(path) = extract_annotation_path(annotation_node, content) else {
3275            continue;
3276        };
3277
3278        return Some((http_method, path));
3279    }
3280
3281    None
3282}
3283
3284/// Extract the simple name from an annotation node.
3285///
3286/// Handles both `@GetMapping` (identifier) and `@org.springframework...GetMapping`
3287/// (`scoped_identifier`) by returning just the final identifier segment.
3288fn extract_annotation_name(annotation_node: Node, content: &[u8]) -> Option<String> {
3289    let mut cursor = annotation_node.walk();
3290    for child in annotation_node.children(&mut cursor) {
3291        match child.kind() {
3292            "identifier" => {
3293                return Some(extract_identifier(child, content));
3294            }
3295            "scoped_identifier" => {
3296                // For scoped identifiers like org.springframework.web.bind.annotation.GetMapping,
3297                // extract just the last segment (the actual annotation name)
3298                let full_text = extract_identifier(child, content);
3299                return full_text.rsplit('.').next().map(String::from);
3300            }
3301            _ => {}
3302        }
3303    }
3304    None
3305}
3306
3307/// Extract the path string from a Spring annotation's argument list.
3308///
3309/// Handles these patterns:
3310/// - `@GetMapping("/api/users")` -> `/api/users`
3311/// - `@RequestMapping(path = "/api/users")` -> `/api/users`
3312/// - `@RequestMapping(value = "/api/users")` -> `/api/users`
3313fn extract_annotation_path(annotation_node: Node, content: &[u8]) -> Option<String> {
3314    // Find the annotation_argument_list child
3315    let mut cursor = annotation_node.walk();
3316    let args_node = annotation_node
3317        .children(&mut cursor)
3318        .find(|child| child.kind() == "annotation_argument_list")?;
3319
3320    // Iterate through the argument list children
3321    let mut args_cursor = args_node.walk();
3322    for arg_child in args_node.children(&mut args_cursor) {
3323        match arg_child.kind() {
3324            // Direct string literal: @GetMapping("/api/users")
3325            "string_literal" => {
3326                return extract_string_content(arg_child, content);
3327            }
3328            // Named argument: @RequestMapping(path = "/api/users") or value = "/api/users"
3329            "element_value_pair" => {
3330                if let Some(path) = extract_path_from_element_value_pair(arg_child, content) {
3331                    return Some(path);
3332                }
3333            }
3334            _ => {}
3335        }
3336    }
3337
3338    None
3339}
3340
3341/// Extract the HTTP method from a `@RequestMapping` annotation's `method` argument.
3342///
3343/// Handles patterns like:
3344/// - `@RequestMapping(method = RequestMethod.POST)` -> `Some("POST")`
3345/// - `@RequestMapping(method = RequestMethod.GET)` -> `Some("GET")`
3346///
3347/// Returns `None` if no method argument is found (caller defaults to GET).
3348fn extract_request_mapping_method(annotation_node: Node, content: &[u8]) -> Option<String> {
3349    // Find the annotation_argument_list child
3350    let mut cursor = annotation_node.walk();
3351    let args_node = annotation_node
3352        .children(&mut cursor)
3353        .find(|child| child.kind() == "annotation_argument_list")?;
3354
3355    // Look for element_value_pair with key "method"
3356    let mut args_cursor = args_node.walk();
3357    for arg_child in args_node.children(&mut args_cursor) {
3358        if arg_child.kind() != "element_value_pair" {
3359            continue;
3360        }
3361
3362        // Check if the key is "method"
3363        let Some(key_node) = arg_child.child_by_field_name("key") else {
3364            continue;
3365        };
3366        let key_text = extract_identifier(key_node, content);
3367        if key_text != "method" {
3368            continue;
3369        }
3370
3371        // Extract the value — expect something like RequestMethod.GET
3372        let Some(value_node) = arg_child.child_by_field_name("value") else {
3373            continue;
3374        };
3375        let value_text = extract_identifier(value_node, content);
3376
3377        // Handle RequestMethod.GET, RequestMethod.POST, etc.
3378        if let Some(method) = value_text.rsplit('.').next() {
3379            let method_upper = method.to_uppercase();
3380            if matches!(
3381                method_upper.as_str(),
3382                "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS"
3383            ) {
3384                return Some(method_upper);
3385            }
3386        }
3387    }
3388
3389    None
3390}
3391
3392/// Extract a path string from an `element_value_pair` node.
3393///
3394/// Matches `path = "/api/users"` or `value = "/api/users"` patterns.
3395fn extract_path_from_element_value_pair(pair_node: Node, content: &[u8]) -> Option<String> {
3396    let key_node = pair_node.child_by_field_name("key")?;
3397    let key_text = extract_identifier(key_node, content);
3398
3399    // Only extract from "path" or "value" keys
3400    if key_text != "path" && key_text != "value" {
3401        return None;
3402    }
3403
3404    let value_node = pair_node.child_by_field_name("value")?;
3405    if value_node.kind() == "string_literal" {
3406        return extract_string_content(value_node, content);
3407    }
3408
3409    None
3410}
3411
3412/// Extract the class-level `@RequestMapping` path prefix from the enclosing class.
3413///
3414/// Walks up the AST from a `method_declaration` to find the enclosing `class_declaration`,
3415/// then checks for a `@RequestMapping` annotation with a path value.
3416///
3417/// # Example
3418///
3419/// ```java
3420/// @RequestMapping("/api")
3421/// public class UserController {
3422///     @GetMapping("/users")
3423///     public List<User> getUsers() { ... }
3424/// }
3425/// ```
3426///
3427/// For the `getUsers` method node, returns `Some("/api")`.
3428fn extract_class_request_mapping_path(method_node: Node, content: &[u8]) -> Option<String> {
3429    // Walk up to find the enclosing class_declaration
3430    let mut current = method_node.parent()?;
3431    loop {
3432        if current.kind() == "class_declaration" {
3433            break;
3434        }
3435        current = current.parent()?;
3436    }
3437
3438    // Look for modifiers → @RequestMapping annotation on the class
3439    let mut cursor = current.walk();
3440    let modifiers = current
3441        .children(&mut cursor)
3442        .find(|child| child.kind() == "modifiers")?;
3443
3444    let mut mod_cursor = modifiers.walk();
3445    for annotation in modifiers.children(&mut mod_cursor) {
3446        if annotation.kind() != "annotation" {
3447            continue;
3448        }
3449        let Some(name) = extract_annotation_name(annotation, content) else {
3450            continue;
3451        };
3452        if name == "RequestMapping" {
3453            return extract_annotation_path(annotation, content);
3454        }
3455    }
3456
3457    None
3458}
3459
3460/// Extract the content of a string literal node, stripping surrounding quotes.
3461///
3462/// Handles `"path"` -> `path`.
3463fn extract_string_content(string_node: Node, content: &[u8]) -> Option<String> {
3464    let text = string_node.utf8_text(content).ok()?;
3465    let trimmed = text.trim();
3466
3467    // Strip surrounding double quotes
3468    if trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() >= 2 {
3469        Some(trimmed[1..trimmed.len() - 1].to_string())
3470    } else {
3471        None
3472    }
3473}