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