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