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