Skip to main content

sqry_lang_java/relations/
graph_builder.rs

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