Skip to main content

sqry_lang_java/relations/
graph_builder.rs

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