Skip to main content

sqry_lang_java/relations/
graph_builder.rs

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