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