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