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