Skip to main content

bonsai_lang_java/
lib.rs

1//! Java language adapter.
2mod parse_recovery;
3
4use bonsai_common::{FileId, Span};
5use bonsai_lang_api::{
6    decl_index_with_handler,
7    kit::{
8        collect_kinds, language_from_pack, node_text, package_module_segments_with_workspace_prefix,
9        parse_with, span_of,
10    },
11    AdapterContext, AdapterError, AssignValueKind, CallTargetExtraction, CharacterConstraintDomain,
12    CharacterConstraintFact, CharacterConstraintOutput, CharacterSubstitutionDomain,
13    CharacterSubstitutionFact, ConditionEquality, ConditionExpressionFact, ConditionOperandFact, DeclIndex,
14    DeclKind, FileSnapshot, FiniteLiteralSelectionFact, FlowEvent, GrammarHandler, ImportIndex, ImportScope,
15    ImportSpec, LanguageAdapter, LanguageCapabilities, LanguageId, ParseRecoveryEdit, PatternBindingSite,
16    SameOriginPathConstraintFact, StaticScalarValue, StringCompositionFact, StringCompositionPart,
17    SyntaxTree, TypeAliasBinding, Vfs, Visibility, EMPTY_HANDLER,
18};
19use parse_recovery::java_parse_recovery_edits;
20use tree_sitter::{Language, Node, Tree};
21
22fn java_call_target<'tree>(node: Node<'tree>, src: &[u8]) -> Option<CallTargetExtraction<'tree>> {
23    match node.kind() {
24        "method_invocation" => {
25            let member = node.child_by_field_name("name")?;
26            let member_text = node_text(&member, src).trim();
27            if member_text.is_empty() {
28                return None;
29            }
30            let full_text = node.child_by_field_name("object").map_or_else(
31                || member_text.to_string(),
32                |receiver| format!("{}.{}", node_text(&receiver, src).trim(), member_text),
33            );
34            Some(CallTargetExtraction {
35                node: member,
36                full_text,
37            })
38        }
39        "object_creation_expression" => {
40            let target = node.child_by_field_name("type")?;
41            let full_text = node_text(&target, src).trim();
42            (!full_text.is_empty()).then_some(CallTargetExtraction {
43                node: target,
44                full_text: full_text.to_string(),
45            })
46        }
47        "explicit_constructor_invocation" => {
48            let target = node
49                .child_by_field_name("constructor")
50                .or_else(|| node.named_child(0))?;
51            let full_text = node_text(&target, src).trim();
52            (!full_text.is_empty()).then_some(CallTargetExtraction {
53                node: target,
54                full_text: full_text.to_string(),
55            })
56        }
57        _ => None,
58    }
59}
60
61fn java_foreach_binding(node: Node<'_>) -> Option<(Node<'_>, Node<'_>)> {
62    (node.kind() == "enhanced_for_statement")
63        .then(|| {
64            Some((
65                node.child_by_field_name("name")?,
66                node.child_by_field_name("value")?,
67            ))
68        })
69        .flatten()
70}
71
72fn java_pattern_bindings(node: Node<'_>) -> Vec<PatternBindingSite<'_>> {
73    let Some(condition) = node.child_by_field_name("condition") else {
74        return Vec::new();
75    };
76    let mut sites = Vec::new();
77
78    let mut stack = vec![condition];
79    while let Some(current) = stack.pop() {
80        if current.kind() == "instanceof_expression" {
81            if let Some(source) = current.child_by_field_name("left") {
82                if let Some(name) = current.child_by_field_name("name") {
83                    sites.push(PatternBindingSite {
84                        span_node: current,
85                        pattern: name,
86                        source,
87                    });
88                }
89                if let Some(pattern) = current.child_by_field_name("pattern") {
90                    java_pattern_binding_identifiers(pattern, &mut sites, current, source);
91                }
92            }
93            continue;
94        }
95        let mut cursor = current.walk();
96        stack.extend(current.named_children(&mut cursor));
97    }
98
99    if let Some(body) = node.child_by_field_name("body") {
100        let mut stack = vec![body];
101        while let Some(current) = stack.pop() {
102            if current.kind() == "switch_label" {
103                let mut cursor = current.walk();
104                for pattern in current.named_children(&mut cursor) {
105                    java_pattern_binding_identifiers(pattern, &mut sites, current, condition);
106                }
107                continue;
108            }
109            let mut cursor = current.walk();
110            stack.extend(current.named_children(&mut cursor));
111        }
112    }
113    sites
114}
115
116fn java_pattern_binding_identifiers<'tree>(
117    pattern: Node<'tree>,
118    out: &mut Vec<PatternBindingSite<'tree>>,
119    span_node: Node<'tree>,
120    source: Node<'tree>,
121) {
122    if matches!(pattern.kind(), "type_pattern" | "record_pattern_component") {
123        let mut cursor = pattern.walk();
124        if let Some(name) = pattern
125            .named_children(&mut cursor)
126            .filter(|child| child.kind() == "identifier")
127            .last()
128        {
129            out.push(PatternBindingSite {
130                span_node,
131                pattern: name,
132                source,
133            });
134        }
135        return;
136    }
137    if !matches!(pattern.kind(), "record_pattern" | "record_pattern_body") {
138        return;
139    }
140    let mut cursor = pattern.walk();
141    for child in pattern.named_children(&mut cursor) {
142        java_pattern_binding_identifiers(child, out, span_node, source);
143    }
144}
145
146pub const LANG_ID: LanguageId = LanguageId::new("java");
147const PACK_NAME: &str = "java";
148const MODULE_SOURCE_ROOTS: &[&[&str]] = &[
149    &["src", "main", "java"],
150    &["src", "test", "java"],
151    &["src", "java"],
152];
153
154const HANDLER: GrammarHandler = GrammarHandler {
155    expression_value_kind_extractor: None,
156    literal_value_kinds: &[
157        "null_literal",
158        "boolean_literal",
159        "decimal_integer_literal",
160        "hex_integer_literal",
161        "octal_integer_literal",
162        "binary_integer_literal",
163        "decimal_floating_point_literal",
164        "hex_floating_point_literal",
165        "true",
166        "false",
167    ],
168    string_literal_kinds: &["string_literal", "character_literal", "template_expression"],
169    comment_kinds: &["line_comment", "block_comment"],
170    doc_comment_prefixes: &["/**"],
171    decorator_kinds: &[
172        "annotation",
173        "marker_annotation",
174        "normal_annotation",
175        "single_element_annotation",
176    ],
177    parameter_container_kinds: &["formal_parameters"],
178    parameter_kinds: &["formal_parameter", "spread_parameter", "receiver_parameter"],
179    parameter_modifier_kinds: &["modifiers"],
180    parameter_annotation_kinds: &[
181        "annotation",
182        "marker_annotation",
183        "normal_annotation",
184        "single_element_annotation",
185    ],
186    variadic_parameter_kinds: &["spread_parameter"],
187    binding_identifier_kinds: &["identifier"],
188    pattern_binding_extractor: Some(java_pattern_bindings),
189    identifier_kinds: &["identifier"],
190    positional_aggregate_kinds: &["array_initializer", "array_creation_expression"],
191    aggregate_value_field_names: &["value"],
192    aggregate_syntax_only_kinds: &["type_identifier"],
193    transparent_call_wrapper_kinds: &["field_access", "parenthesized_expression"],
194    assignment_target_wrapper_kinds: &["variable_declarator"],
195    binding_declaration_keyword_spellings: &["final"],
196    fn_kinds: &["method_declaration", "constructor_declaration"],
197    call_kinds: &[
198        "method_invocation",
199        "object_creation_expression",
200        "explicit_constructor_invocation",
201    ],
202    constructor_call_kinds: &["object_creation_expression", "explicit_constructor_invocation"],
203    call_callee_field_names: &["name", "constructor"],
204    call_receiver_field_names: &["object"],
205    call_member_field_names: &["name", "constructor"],
206    constructor_type_field_names: &["type"],
207    call_target_extractor: Some(java_call_target),
208    call_argument_field_names: &["arguments"],
209    call_argument_container_kinds: &["argument_list"],
210    lambda_body_field_names: &["body"],
211    argument_passing_mode_extractor: None,
212    constructor_names: bonsai_lang_api::NO_CONSTRUCTOR_METHOD_NAMES,
213    runtime_type_guard_operators: &["instanceof"],
214    runtime_type_wrapper_kinds: &["parenthesized_expression"],
215    call_ref_kinds: &[
216        "method_invocation",
217        "object_creation_expression",
218        "explicit_constructor_invocation",
219        "method_reference",
220    ],
221    callable_reference_kinds: &["method_reference"],
222    member_expression_kinds: &["field_access"],
223    subscript_expression_kinds: &["array_access"],
224    member_base_field_names: &["object"],
225    member_name_field_names: &["field"],
226    subscript_base_field_names: &["array"],
227    subscript_index_field_names: &["index"],
228    class_kinds: &[
229        "class_declaration",
230        "interface_declaration",
231        "enum_declaration",
232        "annotation_type_declaration",
233        "record_declaration",
234    ],
235    class_decl_kinds: &[
236        ("interface_declaration", DeclKind::Interface),
237        ("annotation_type_declaration", DeclKind::Interface),
238        ("enum_declaration", DeclKind::Enum),
239        ("record_declaration", DeclKind::Class),
240        ("class_declaration", DeclKind::Class),
241    ],
242    method_owner_barrier_kinds: &["object_creation_expression"],
243    method_kinds: &["method_declaration"],
244    method_context_kinds: &[
245        "class_declaration",
246        "interface_declaration",
247        "enum_declaration",
248        "annotation_type_declaration",
249        "record_declaration",
250    ],
251    constructor_method_kinds: &["constructor_declaration"],
252    if_kinds: &["if_statement", "switch_expression"],
253    branch_then_field_names: &["consequence", "body"],
254    branch_else_field_names: &["alternative"],
255    branch_condition_field_names: &["condition", "value"],
256    loop_body_field_names: &["body"],
257    loop_body_kinds: &["block", "expression_statement"],
258    branch_arm_kinds: &["block", "expression_statement", "switch_block_statement_group"],
259    for_kinds: &["for_statement"],
260    foreach_kinds: &["enhanced_for_statement"],
261    foreach_binding_extractor: Some(java_foreach_binding),
262    while_kinds: &["while_statement"],
263    do_kinds: &["do_statement"],
264    // Java try-with-resources binds `try (T r = expr) { .. }` as a
265    // `resource` node, which exposes the same `name`/`value` fields the
266    // assignment branch reads. Marking it an assignment emits the
267    // `r = expr` Assign so the call-RHS summary can carry return-value
268    // taint into `r`. This is the complete Java grammar inventory; shared
269    // lowering does not add a cross-language fallback.
270    assignment_kinds: &["assignment_expression", "variable_declarator", "resource"],
271    compound_assignment_operators: &[
272        "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", "&=", "^=", "|=",
273    ],
274    return_kinds: &["return_statement"],
275    throw_kinds: &["throw_statement"],
276    lambda_kinds: &["lambda_expression"],
277    try_kinds: &["try_statement", "try_with_resources_statement"],
278    catch_kinds: &["catch_clause"],
279    finally_kinds: &["finally_clause"],
280    break_kinds: &["break_statement"],
281    continue_kinds: &["continue_statement"],
282    control_label_field_names: &["label"],
283    yield_kinds: &["yield_statement"],
284    yield_value_field_names: &["value"],
285    try_body_field_names: &["body"],
286    implicit_receiver_names: &["this", "super"],
287    ..EMPTY_HANDLER
288};
289
290#[derive(Debug, Default, Copy, Clone)]
291pub struct JavaAdapter;
292
293impl JavaAdapter {
294    #[must_use]
295    pub fn new() -> Self {
296        Self
297    }
298}
299
300impl LanguageAdapter for JavaAdapter {
301    fn language_id(&self) -> LanguageId {
302        LANG_ID
303    }
304    fn display_name(&self) -> &'static str {
305        "Java"
306    }
307    fn file_extensions(&self) -> &'static [&'static str] {
308        &["java"]
309    }
310    fn tree_sitter_language(&self) -> Result<Language, AdapterError> {
311        language_from_pack(PACK_NAME)
312    }
313    fn parse_recovery_edits(
314        &self,
315        snapshot: &FileSnapshot,
316        _vfs: &Vfs,
317        tree: &SyntaxTree,
318    ) -> Vec<ParseRecoveryEdit> {
319        java_parse_recovery_edits(snapshot, tree)
320    }
321    fn capabilities(&self) -> LanguageCapabilities {
322        // Exceptions: the adapter populates `Throw::thrown_type` from
323        // `throw new IOException(...)` and `Try::catch_types` from
324        // `catch (IOException e)` (including multi-catch
325        // `catch (A | B e)`). The engine seeds the catch param only
326        // when at least one body throw is type-assignable; this lifts
327        // the `Partial` claim to `Exact` for typed-exception flow on
328        // Java code.
329        // Reflection: the adapter rewrites the constant-string
330        // `Class.forName("X").getMethod("Y").invoke(target, args)`
331        // chain into a synthesized direct call `X.Y(args)`. Dynamic
332        // forms remain unrewritten and the rule-load gate still
333        // rejects rules anchored on the reflective shape.
334        LanguageCapabilities {
335            module_default_export_names: &[],
336            universal_type_names: &["Object"],
337            module_path_syntax: bonsai_lang_api::ModulePathSyntax::none(),
338            exceptions: bonsai_lang_api::CapabilityLevel::Exact,
339            reflection: bonsai_lang_api::CapabilityLevel::Partial,
340            receiver_types: bonsai_lang_api::CapabilityLevel::Partial,
341            field_places_complete: true,
342            // Java constructors are class-named, so the kind-based
343            // `DeclKind::Constructor` lookup is authoritative; the
344            // name-list fallback is intentionally empty.
345            constructor_method_names: bonsai_lang_api::NO_CONSTRUCTOR_METHOD_NAMES,
346            super_receiver_tokens: &["super"],
347            implicit_receiver_tokens: &["this"],
348            receiver_type_syntax: bonsai_lang_api::ReceiverTypeSyntax {
349                wrapper_calls: &[],
350                class_object_suffixes: &[".class"],
351            },
352            call_text_prefilter: bonsai_lang_api::CallTextPrefilter::Parenthesized,
353            ..LanguageCapabilities::partial_baseline()
354        }
355    }
356    fn extract_declarations(&self, file: FileId, ctx: &AdapterContext<'_>) -> DeclIndex {
357        let mut index = decl_index_with_handler(PACK_NAME, file, ctx, &HANDLER);
358        let Some((snapshot, tree)) = parse_with(PACK_NAME, file, ctx) else {
359            return index;
360        };
361        let src = snapshot.text.as_bytes();
362        populate_java_condition_expressions(&mut index.branch_conditions, &tree, file, src);
363        populate_java_static_scalar_facts(&mut index, &tree, file, src);
364        populate_java_immutable_assignment_facts(&mut index, &tree, file, src);
365        index.string_compositions = java_string_compositions(&tree, file, src);
366        index.finite_literal_selections = java_finite_literal_selections(&index, &tree, file, src);
367        index.character_substitutions = java_character_substitutions(&index.defs, &tree, file, src);
368        index.character_constraints = bonsai_lang_api::character_constraints_from_substitutions(
369            &index.defs,
370            &index.character_substitutions,
371        );
372        index.same_origin_path_constraints = java_same_origin_path_constraints(&index, &tree, file, src);
373        index
374            .character_constraints
375            .extend(java_compiled_pattern_constraints(&index, &tree, file, src));
376        index
377            .character_constraints
378            .sort_by_key(|fact| (fact.transform_span.start, fact.transform_span.end));
379        index
380            .character_constraints
381            .dedup_by_key(|fact| fact.transform_span);
382        // Phase-6 return-type extraction: `T method() {}` populates
383        // `Decl.return_type` for `apply_assign_call_result_types`.
384        bonsai_lang_api::populate_decl_return_types(&mut index, &tree, src, &HANDLER);
385        // Populate Throw::thrown_type and Try::catch_types from the
386        // parse tree before downstream resolution. Done first so the
387        // type info propagates through every later mutation.
388        for decl in &mut index.defs {
389            populate_java_exception_types(&mut decl.flow_events, &tree, src);
390            rewrite_java_reflection_chain(&mut decl.flow_events);
391        }
392        let field_aliases = collect_java_type_aliases(tree.root_node(), src, &["field_declaration"]);
393        let method_aliases = collect_java_method_type_aliases(&tree, file, src, &field_aliases);
394        for decl in &mut index.defs {
395            if let Some(aliases) = method_aliases
396                .iter()
397                .find_map(|(span, aliases)| (*span == decl.span).then_some(aliases))
398            {
399                decl.type_aliases = aliases.clone();
400            }
401        }
402        attach_java_nested_callable_type_aliases(&mut index, &tree, file, src);
403        // Resolve generic type variables from their Tree-sitter type bounds.
404        // `T data` in `class Box<T extends App.Envelope>` carries both the
405        // declared `T` identity and the compiler-proven `App.Envelope` upper
406        // bound, so receiver dispatch on `data.method()` can resolve against
407        // the bound without any method-name inventory.
408        let class_type_bounds = collect_java_class_type_parameter_bounds(&tree, file, src);
409        let bounds_by_parent: std::collections::HashMap<_, _> = index
410            .defs
411            .iter()
412            .filter(|decl| is_class_like(decl.kind))
413            .filter_map(|decl| {
414                class_type_bounds
415                    .iter()
416                    .find_map(|(span, bounds)| (*span == decl.span).then(|| (decl.symbol, bounds.clone())))
417            })
418            .collect();
419        for decl in &mut index.defs {
420            if let Some(bounds) = decl.parent.and_then(|parent| bounds_by_parent.get(&parent)) {
421                expand_java_type_parameter_aliases(&mut decl.type_aliases, bounds);
422            }
423        }
424        // Per-class `bases`: `class C extends B implements I, J` →
425        // ["B", "I", "J"]. Lets `kind: param` rules require an
426        // ancestor type (`in_class: [WebSocketHandler]` matching a
427        // user `class Echo extends WebSocketHandler { ... }`).
428        let bases_by_span = collect_java_class_bases(&tree, file, src);
429        for decl in &mut index.defs {
430            if !is_class_like(decl.kind) {
431                continue;
432            }
433            if let Some(bases) = bases_by_span
434                .iter()
435                .find_map(|(span, bases)| (*span == decl.span).then_some(bases))
436            {
437                decl.bases = bases.clone();
438            }
439        }
440        qualify_java_instance_field_receivers(&mut index, &tree, src);
441        rewrite_java_explicit_constructor_invocations(&mut index);
442        let constants_by_class = collect_java_class_string_constants(&tree, file, src);
443        attach_java_class_string_constants(&mut index, &constants_by_class);
444        // Java visibility from real syntax — `public`/`private`/
445        // `protected` modifiers, and absence-of-modifier = package-private.
446        let visibility_by_span = collect_java_visibility(tree.root_node(), file, src);
447        for decl in &mut index.defs {
448            if let Some(vis) = visibility_by_span.get(&decl.span).copied() {
449                decl.visibility = vis;
450            }
451        }
452        // Module path from `package com.foo.bar;` declaration. A compilation
453        // unit without a package declaration belongs to Java's unnamed
454        // package; its filename is not a namespace. Keeping the module path
455        // empty lets exact receiver-type resolution link peer types in that
456        // package while still rejecting duplicate type identities as invalid
457        // Java source.
458        if let Some(segments) = extract_java_package(tree.root_node(), src) {
459            let segments =
460                package_module_segments_with_workspace_prefix(file, ctx, segments, MODULE_SOURCE_ROOTS);
461            bonsai_lang_api::apply_module_path_semantic_identity(&mut index, segments);
462        } else {
463            bonsai_lang_api::apply_module_path_semantic_identity(&mut index, Vec::new());
464        }
465        for decl in &mut index.defs {
466            bonsai_lang_api::normalize_call_result_assignment_sources(&mut decl.flow_events);
467        }
468        // Synthesize the implicit members of `record` declarations —
469        // Java auto-generates a canonical constructor (`this.<comp> =
470        // <comp>` for each component) and a zero-arg accessor per
471        // component (`<comp>()` returns `this.<comp>`). The grammar has
472        // no nodes for these, so without synthesis `new R(..)` and
473        // `r.comp()` are opaque and taint can't thread through a record.
474        bonsai_lang_api::kit::synthesize_record_members(&mut index, &tree, src, file);
475        bonsai_lang_api::kit::apply_lexical_member_qualified_names(&mut index, ".");
476        bonsai_lang_api::kit::qualify_bare_hierarchy_member_calls(&mut index);
477        // Precompute `self.<field> → Type` bindings from each
478        // class's constructor `receiver_field_writes` so receiver-
479        // typed dispatch through stable instance state is an O(1)
480        // lookup against the method's `type_aliases` instead of a
481        // per-call walk over sibling decls.
482        // Local constructor-result receiver typing (`Foo c = new Foo()`
483        // → `c: Foo`) is driven by Java's object-creation CST node or an
484        // exactly resolved declaration. Capitalization is a convention, not
485        // part of Java's type system, and is never used as proof.
486        bonsai_lang_api::apply_constructor_result_type_aliases(&mut index);
487        bonsai_lang_api::apply_class_field_type_aliases(&mut index);
488        index
489    }
490    fn extract_imports(&self, file: FileId, ctx: &AdapterContext<'_>) -> ImportIndex {
491        let Some((snapshot, tree)) = parse_with(PACK_NAME, file, ctx) else {
492            return ImportIndex {
493                file,
494                ..Default::default()
495            };
496        };
497        ImportIndex {
498            file,
499            imports: collect_java_imports(&tree, file, snapshot.text.as_bytes()),
500        }
501    }
502}
503
504fn populate_java_immutable_assignment_facts(index: &mut DeclIndex, tree: &Tree, file: FileId, src: &[u8]) {
505    for field in collect_kinds(tree, &["field_declaration"]) {
506        if !java_declaration_has_final_modifier(field) {
507            continue;
508        }
509        let field_span = span_of(file, &field);
510        let owner = index
511            .defs
512            .iter()
513            .filter(|decl| {
514                is_class_like(decl.kind)
515                    && decl.span.start <= field_span.start
516                    && field_span.end <= decl.span.end
517            })
518            .min_by_key(|decl| decl.span.len())
519            .map(|decl| decl.symbol);
520        for declarator in java_collect_kinds_below(field, &["variable_declarator"]) {
521            let (Some(name), Some(value)) = (
522                declarator.child_by_field_name("name"),
523                declarator.child_by_field_name("value"),
524            ) else {
525                continue;
526            };
527            let name_span = span_of(file, &name);
528            let name_text = node_text(&name, src);
529            let value_span = span_of(file, &value);
530            if let Some(fact) = index.assignment_values.iter_mut().find(|fact| {
531                fact.target_span == Some(name_span)
532                    || (fact.value_span == value_span && fact.target.as_deref() == Some(name_text.trim()))
533            }) {
534                fact.target_is_immutable = true;
535                fact.target_owner = owner;
536            }
537        }
538    }
539}
540
541fn java_same_origin_path_constraints(
542    index: &DeclIndex,
543    tree: &Tree,
544    file: FileId,
545    src: &[u8],
546) -> Vec<SameOriginPathConstraintFact> {
547    let mut facts = Vec::new();
548    for method in collect_kinds(tree, &["method_declaration"]) {
549        let method_span = span_of(file, &method);
550        let Some(decl) = index.defs.iter().find(|decl| decl.span == method_span) else {
551            continue;
552        };
553        let Some(body) = method.child_by_field_name("body") else {
554            continue;
555        };
556        let mut cursor = body.walk();
557        let statements = body.named_children(&mut cursor).collect::<Vec<_>>();
558        let [guard, final_return] = statements.as_slice() else {
559            continue;
560        };
561        if guard.kind() != "if_statement"
562            || guard.child_by_field_name("alternative").is_some()
563            || final_return.kind() != "return_statement"
564        {
565            continue;
566        }
567        let (Some(condition), Some(consequence), Some(return_value)) = (
568            guard.child_by_field_name("condition"),
569            guard.child_by_field_name("consequence"),
570            final_return.named_child(0),
571        ) else {
572            continue;
573        };
574        let fallback_returns = java_collect_kinds_below(consequence, &["return_statement"]);
575        let [fallback_return] = fallback_returns.as_slice() else {
576            continue;
577        };
578        if fallback_return
579            .named_child(0)
580            .and_then(|value| java_static_string_literal(value, src))
581            .as_deref()
582            != Some("/")
583        {
584            continue;
585        }
586        for (input_param_index, parameter) in decl.params.iter().enumerate() {
587            if return_value.kind() != "identifier" || node_text(&return_value, src).trim() != parameter {
588                continue;
589            }
590            let mut terms = Vec::new();
591            java_collect_logical_terms(condition, "||", src, &mut terms);
592            let requires_absolute_path = terms
593                .iter()
594                .any(|term| java_starts_with_literal(*term, parameter, "/", true, src));
595            let rejects_scheme_relative_path = terms
596                .iter()
597                .any(|term| java_starts_with_literal(*term, parameter, "//", false, src));
598            if requires_absolute_path && rejects_scheme_relative_path {
599                facts.push(SameOriginPathConstraintFact {
600                    function_span: decl.span,
601                    guard_span: span_of(file, guard),
602                    input_place: parameter.clone(),
603                    input_param_index: Some(input_param_index),
604                    provider_call: None,
605                    rejects_scheme: true,
606                    rejects_authority: true,
607                    requires_absolute_path,
608                    rejects_scheme_relative_path,
609                });
610            }
611        }
612    }
613    facts.sort_by_key(|fact| (fact.function_span.start, fact.guard_span.start));
614    facts.dedup();
615    facts
616}
617
618fn java_collect_kinds_below<'tree>(root: Node<'tree>, kinds: &[&str]) -> Vec<Node<'tree>> {
619    let mut out = Vec::new();
620    let mut stack = vec![root];
621    while let Some(node) = stack.pop() {
622        if kinds.contains(&node.kind()) {
623            out.push(node);
624            continue;
625        }
626        let mut cursor = node.walk();
627        stack.extend(node.named_children(&mut cursor));
628    }
629    out
630}
631
632fn java_collect_logical_terms<'tree>(
633    expression: Node<'tree>,
634    operator: &str,
635    src: &[u8],
636    out: &mut Vec<Node<'tree>>,
637) {
638    let expression = java_unwrap_parenthesized(expression);
639    let operands = (
640        expression.child_by_field_name("left"),
641        expression.child_by_field_name("right"),
642    );
643    if expression.kind() == "binary_expression"
644        && operands.0.zip(operands.1).is_some_and(|(left, right)| {
645            src.get(left.end_byte()..right.start_byte())
646                .and_then(|bytes| std::str::from_utf8(bytes).ok())
647                .is_some_and(|value| value.trim() == operator)
648        })
649    {
650        let (Some(left), Some(right)) = operands else {
651            return;
652        };
653        java_collect_logical_terms(left, operator, src, out);
654        java_collect_logical_terms(right, operator, src, out);
655    } else {
656        out.push(expression);
657    }
658}
659
660fn java_unwrap_parenthesized(mut expression: Node<'_>) -> Node<'_> {
661    while expression.kind() == "parenthesized_expression" && expression.named_child_count() == 1 {
662        let Some(inner) = expression.named_child(0) else {
663            break;
664        };
665        expression = inner;
666    }
667    expression
668}
669
670fn java_starts_with_literal(
671    expression: Node<'_>,
672    receiver: &str,
673    literal: &str,
674    negated: bool,
675    src: &[u8],
676) -> bool {
677    let expression = java_unwrap_parenthesized(expression);
678    let call = if negated {
679        if expression.kind() != "unary_expression" {
680            return false;
681        }
682        let Some(operand) = expression
683            .child_by_field_name("operand")
684            .or_else(|| expression.named_child(0))
685        else {
686            return false;
687        };
688        if src
689            .get(expression.start_byte()..operand.start_byte())
690            .and_then(|bytes| std::str::from_utf8(bytes).ok())
691            .is_none_or(|prefix| prefix.trim() != "!")
692        {
693            return false;
694        }
695        java_unwrap_parenthesized(operand)
696    } else {
697        expression
698    };
699    if call.kind() != "method_invocation"
700        || call
701            .child_by_field_name("object")
702            .is_none_or(|object| object.kind() != "identifier" || node_text(&object, src).trim() != receiver)
703        || call
704            .child_by_field_name("name")
705            .is_none_or(|name| node_text(&name, src).trim() != "startsWith")
706    {
707        return false;
708    }
709    let Some(arguments) = call.child_by_field_name("arguments") else {
710        return false;
711    };
712    let mut cursor = arguments.walk();
713    let values = arguments.named_children(&mut cursor).collect::<Vec<_>>();
714    let [value] = values.as_slice() else {
715        return false;
716    };
717    java_static_string_literal(*value, src).as_deref() == Some(literal)
718}
719
720fn java_character_substitutions(
721    defs: &[bonsai_lang_api::Decl],
722    tree: &Tree,
723    file: FileId,
724    src: &[u8],
725) -> Vec<CharacterSubstitutionFact> {
726    let mut facts = Vec::new();
727    for return_node in collect_kinds(tree, &["return_statement"]) {
728        let return_span = span_of(file, &return_node);
729        let Some(decl) = defs
730            .iter()
731            .filter(|decl| {
732                matches!(decl.kind, DeclKind::Method | DeclKind::Constructor)
733                    && decl.span.start <= return_span.start
734                    && return_span.end <= decl.span.end
735            })
736            .min_by_key(|decl| decl.span.len())
737        else {
738            continue;
739        };
740        let Some(expression) = return_node.named_child(0) else {
741            continue;
742        };
743        let Some((input_param_index, exact_mappings, characters)) =
744            java_inline_replace_chain(expression, &decl.params, src)
745        else {
746            continue;
747        };
748        facts.push(CharacterSubstitutionFact {
749            function_span: decl.span,
750            transform_span: return_span,
751            input_param_index,
752            exact_mappings,
753            table: String::new(),
754            domain: CharacterSubstitutionDomain::ExactCharacters { characters },
755        });
756    }
757    for method in collect_kinds(tree, &["method_declaration"]) {
758        let method_span = span_of(file, &method);
759        let Some(decl) = defs.iter().find(|decl| decl.span == method_span) else {
760            continue;
761        };
762        let Some((input_param_index, transform_span, exact_mappings)) =
763            java_switch_character_substitution(method, decl, file, src)
764        else {
765            continue;
766        };
767        facts.push(CharacterSubstitutionFact {
768            function_span: decl.span,
769            transform_span,
770            input_param_index,
771            exact_mappings,
772            table: String::new(),
773            domain: CharacterSubstitutionDomain::TableKeysWithIdentityFallback,
774        });
775    }
776    facts.sort_by_key(|fact| (fact.function_span.start, fact.transform_span.start));
777    facts.dedup();
778    facts
779}
780
781/// Prove a total character-wise `StringBuilder` transform implemented as an
782/// enhanced-for loop and switch. Every explicit case must append one static
783/// replacement and terminate; the default must append the original loop
784/// variable. The exact AST contract prevents partial switch bodies or hidden
785/// side effects from being summarized as sanitizers.
786fn java_switch_character_substitution(
787    method: Node<'_>,
788    decl: &bonsai_lang_api::Decl,
789    file: FileId,
790    src: &[u8],
791) -> Option<(
792    usize,
793    bonsai_common::Span,
794    Vec<bonsai_lang_api::StaticStringMapEntry>,
795)> {
796    let body = method.child_by_field_name("body")?;
797    let statements = body.named_children(&mut body.walk()).collect::<Vec<_>>();
798    let [builder_decl, loop_node, return_node] = statements.as_slice() else {
799        return None;
800    };
801    if builder_decl.kind() != "local_variable_declaration"
802        || loop_node.kind() != "enhanced_for_statement"
803        || return_node.kind() != "return_statement"
804    {
805        return None;
806    }
807    let declarators = builder_decl
808        .named_children(&mut builder_decl.walk())
809        .filter(|node| node.kind() == "variable_declarator")
810        .collect::<Vec<_>>();
811    let [declarator] = declarators.as_slice() else {
812        return None;
813    };
814    let builder = declarator.child_by_field_name("name")?;
815    let initializer = declarator.child_by_field_name("value")?;
816    if builder.kind() != "identifier" || initializer.kind() != "object_creation_expression" {
817        return None;
818    }
819    let builder = node_text(&builder, src).trim();
820    let builder_type = initializer.child_by_field_name("type")?;
821    if node_text(&builder_type, src).trim() != "StringBuilder" {
822        return None;
823    }
824
825    let loop_variable = loop_node.child_by_field_name("name")?;
826    let iterated = loop_node.child_by_field_name("value")?;
827    let loop_body = loop_node.child_by_field_name("body")?;
828    if loop_variable.kind() != "identifier" || iterated.kind() != "method_invocation" {
829        return None;
830    }
831    let loop_variable = node_text(&loop_variable, src).trim();
832    let iterated_object = iterated.child_by_field_name("object")?;
833    let iterated_method = iterated.child_by_field_name("name")?;
834    let input = node_text(&iterated_object, src).trim();
835    if iterated_object.kind() != "identifier"
836        || node_text(&iterated_method, src).trim() != "toCharArray"
837        || iterated
838            .child_by_field_name("arguments")
839            .is_none_or(|args| args.named_child_count() != 0)
840    {
841        return None;
842    }
843    let input_param_index = decl.params.iter().position(|parameter| parameter == input)?;
844    let switches = java_collect_kinds_below(loop_body, &["switch_expression"]);
845    let [switch_node] = switches.as_slice() else {
846        return None;
847    };
848    let condition = switch_node.child_by_field_name("condition")?;
849    if node_text(&condition, src)
850        .trim()
851        .trim_start_matches('(')
852        .trim_end_matches(')')
853        .trim()
854        != loop_variable
855    {
856        return None;
857    }
858    let switch_body = switch_node.child_by_field_name("body")?;
859    let groups = switch_body
860        .named_children(&mut switch_body.walk())
861        .collect::<Vec<_>>();
862    if groups.is_empty() {
863        return None;
864    }
865    let mut mappings = Vec::new();
866    let mut saw_identity_default = false;
867    for group in groups {
868        if group.kind() != "switch_block_statement_group" {
869            return None;
870        }
871        let children = group.named_children(&mut group.walk()).collect::<Vec<_>>();
872        let label = children
873            .first()
874            .copied()
875            .filter(|node| node.kind() == "switch_label")?;
876        let invocations = java_collect_kinds_below(group, &["method_invocation"]);
877        let [append] = invocations.as_slice() else {
878            return None;
879        };
880        let append_object = append.child_by_field_name("object")?;
881        let append_name = append.child_by_field_name("name")?;
882        let append_args = append.child_by_field_name("arguments")?;
883        let values = append_args
884            .named_children(&mut append_args.walk())
885            .collect::<Vec<_>>();
886        let [value] = values.as_slice() else {
887            return None;
888        };
889        if node_text(&append_object, src).trim() != builder || node_text(&append_name, src).trim() != "append"
890        {
891            return None;
892        }
893        let label_values = label.named_children(&mut label.walk()).collect::<Vec<_>>();
894        if label_values.is_empty() {
895            if saw_identity_default
896                || value.kind() != "identifier"
897                || node_text(value, src).trim() != loop_variable
898                || children.len() != 2
899            {
900                return None;
901            }
902            saw_identity_default = true;
903            continue;
904        }
905        let [label_value] = label_values.as_slice() else {
906            return None;
907        };
908        let input = java_static_string_or_character(*label_value, src)?;
909        let output = java_static_string_literal(*value, src)?;
910        if input.chars().count() != 1
911            || children.len() != 3
912            || children
913                .last()
914                .is_none_or(|node| node.kind() != "break_statement")
915            || mappings
916                .iter()
917                .any(|entry: &bonsai_lang_api::StaticStringMapEntry| entry.key == input)
918        {
919            return None;
920        }
921        mappings.push(bonsai_lang_api::StaticStringMapEntry {
922            key: input,
923            value: output,
924        });
925    }
926    if !saw_identity_default || mappings.is_empty() {
927        return None;
928    }
929    let returned = return_node.named_child(0)?;
930    if returned.kind() != "method_invocation"
931        || returned
932            .child_by_field_name("object")
933            .is_none_or(|object| node_text(&object, src).trim() != builder)
934        || returned
935            .child_by_field_name("name")
936            .is_none_or(|name| node_text(&name, src).trim() != "toString")
937        || returned
938            .child_by_field_name("arguments")
939            .is_none_or(|args| args.named_child_count() != 0)
940    {
941        return None;
942    }
943    mappings.sort_by(|left, right| left.key.cmp(&right.key));
944    Some((input_param_index, span_of(file, switch_node), mappings))
945}
946
947fn java_inline_replace_chain(
948    expression: Node<'_>,
949    params: &[String],
950    src: &[u8],
951) -> Option<(usize, Vec<bonsai_lang_api::StaticStringMapEntry>, Vec<String>)> {
952    let mut current = expression;
953    let mut mappings = Vec::new();
954    let mut characters = Vec::new();
955    while current.kind() == "method_invocation" {
956        let method = current.child_by_field_name("name")?;
957        let method = node_text(&method, src).trim();
958        if !matches!(method, "replace" | "replaceAll") {
959            break;
960        }
961        let arguments = current.child_by_field_name("arguments")?;
962        let args = arguments
963            .named_children(&mut arguments.walk())
964            .collect::<Vec<_>>();
965        let [pattern, replacement] = args.as_slice() else {
966            return None;
967        };
968        let output = java_static_string_or_character(*replacement, src)?;
969        let replaced = if method == "replace" {
970            let value = java_static_string_or_character(*pattern, src)?;
971            (value.chars().count() == 1).then(|| vec![value])?
972        } else {
973            java_exact_regex_character_class(&java_static_string_literal(*pattern, src)?)?
974        };
975        for input in replaced {
976            if mappings
977                .iter()
978                .any(|entry: &bonsai_lang_api::StaticStringMapEntry| {
979                    entry.key == input && entry.value != output
980                })
981            {
982                return None;
983            }
984            if !mappings.iter().any(|entry| entry.key == input) {
985                characters.push(input.clone());
986                mappings.push(bonsai_lang_api::StaticStringMapEntry {
987                    key: input,
988                    value: output.clone(),
989                });
990            }
991        }
992        current = current.child_by_field_name("object")?;
993    }
994    if mappings.is_empty() || current.kind() != "identifier" {
995        return None;
996    }
997    let input = node_text(&current, src).trim();
998    let input_param_index = params.iter().position(|param| param == input)?;
999    characters.sort();
1000    characters.dedup();
1001    mappings.sort_by(|left, right| left.key.cmp(&right.key));
1002    Some((input_param_index, mappings, characters))
1003}
1004
1005fn java_static_string_or_character(node: Node<'_>, src: &[u8]) -> Option<String> {
1006    java_static_string_literal(node, src).or_else(|| {
1007        if node.kind() != "character_literal" {
1008            return None;
1009        }
1010        let raw = node_text(&node, src);
1011        let inner = raw.strip_prefix('\'')?.strip_suffix('\'')?;
1012        match inner {
1013            "\\r" => Some("\r".to_string()),
1014            "\\n" => Some("\n".to_string()),
1015            "\\t" => Some("\t".to_string()),
1016            "\\0" => Some("\0".to_string()),
1017            "\\\\" => Some("\\".to_string()),
1018            "\\\"" => Some("\"".to_string()),
1019            _ if inner.chars().count() == 1 => Some(inner.to_string()),
1020            _ => None,
1021        }
1022    })
1023}
1024
1025fn java_exact_regex_character_class(pattern: &str) -> Option<Vec<String>> {
1026    let inner = pattern.strip_prefix('[')?.strip_suffix(']')?;
1027    if inner.starts_with('^') || inner.is_empty() {
1028        return None;
1029    }
1030    let mut characters = Vec::new();
1031    let mut chars = inner.chars();
1032    while let Some(character) = chars.next() {
1033        if character == '-' {
1034            return None;
1035        }
1036        let decoded = if character == '\\' {
1037            match chars.next()? {
1038                'r' => '\r',
1039                'n' => '\n',
1040                't' => '\t',
1041                '\\' => '\\',
1042                '"' => '"',
1043                '\'' => '\'',
1044                _ => return None,
1045            }
1046        } else {
1047            character
1048        };
1049        characters.push(decoded.to_string());
1050    }
1051    characters.sort();
1052    characters.dedup();
1053    Some(characters)
1054}
1055
1056fn java_compiled_pattern_constraints(
1057    index: &DeclIndex,
1058    tree: &Tree,
1059    file: FileId,
1060    src: &[u8],
1061) -> Vec<CharacterConstraintFact> {
1062    let bindings = java_bindings(tree, src);
1063    let mut patterns = Vec::new();
1064    for declarator in collect_kinds(tree, &["variable_declarator"]) {
1065        let (Some(name), Some(value)) = (
1066            declarator.child_by_field_name("name"),
1067            declarator.child_by_field_name("value"),
1068        ) else {
1069            continue;
1070        };
1071        if name.kind() != "identifier" || value.kind() != "method_invocation" {
1072            continue;
1073        }
1074        let (Some(object), Some(method), Some(arguments)) = (
1075            value.child_by_field_name("object"),
1076            value.child_by_field_name("name"),
1077            value.child_by_field_name("arguments"),
1078        ) else {
1079            continue;
1080        };
1081        if node_text(&object, src).trim() != "Pattern" || node_text(&method, src).trim() != "compile" {
1082            continue;
1083        }
1084        let args = arguments
1085            .named_children(&mut arguments.walk())
1086            .collect::<Vec<_>>();
1087        let [pattern] = args.as_slice() else {
1088            continue;
1089        };
1090        let Some(pattern) = java_static_string_literal(*pattern, src) else {
1091            continue;
1092        };
1093        let Some((_, declaration, _)) = java_binding_scope_and_declaration(declarator) else {
1094            continue;
1095        };
1096        if !java_declaration_has_final_modifier(declaration) {
1097            continue;
1098        }
1099        let characters = if pattern == "\\p{Cntrl}" {
1100            vec!["\r".to_string(), "\n".to_string()]
1101        } else if let Some(characters) = java_exact_regex_character_class(&pattern) {
1102            characters
1103        } else {
1104            continue;
1105        };
1106        let name = node_text(&name, src).trim().to_string();
1107        patterns.push((name, span_of(file, &value), characters));
1108    }
1109
1110    let mut facts = Vec::new();
1111    for call in collect_kinds(tree, &["method_invocation"]) {
1112        let (Some(method), Some(receiver), Some(arguments)) = (
1113            call.child_by_field_name("name"),
1114            call.child_by_field_name("object"),
1115            call.child_by_field_name("arguments"),
1116        ) else {
1117            continue;
1118        };
1119        if node_text(&method, src).trim() != "replaceAll" || receiver.kind() != "method_invocation" {
1120            continue;
1121        }
1122        let (Some(matcher_name), Some(pattern_receiver), Some(matcher_args)) = (
1123            receiver.child_by_field_name("name"),
1124            receiver.child_by_field_name("object"),
1125            receiver.child_by_field_name("arguments"),
1126        ) else {
1127            continue;
1128        };
1129        if node_text(&matcher_name, src).trim() != "matcher" || pattern_receiver.kind() != "identifier" {
1130            continue;
1131        }
1132        let pattern_name = node_text(&pattern_receiver, src).trim();
1133        let Some(binding) = bindings.resolve(
1134            pattern_name,
1135            pattern_receiver.start_byte(),
1136            pattern_receiver.end_byte(),
1137        ) else {
1138            continue;
1139        };
1140        let binding_value_span = span_of(file, &binding.initializer);
1141        let Some((_, _, mut characters)) = patterns
1142            .iter()
1143            .find(|(name, value_span, _)| name == pattern_name && *value_span == binding_value_span)
1144            .cloned()
1145        else {
1146            continue;
1147        };
1148        let matcher_args = matcher_args
1149            .named_children(&mut matcher_args.walk())
1150            .collect::<Vec<_>>();
1151        let replace_args = arguments
1152            .named_children(&mut arguments.walk())
1153            .collect::<Vec<_>>();
1154        let ([input], [replacement]) = (matcher_args.as_slice(), replace_args.as_slice()) else {
1155            continue;
1156        };
1157        if input.kind() != "identifier" {
1158            continue;
1159        }
1160        let Some(replacement) = java_static_string_literal(*replacement, src) else {
1161            continue;
1162        };
1163        characters.retain(|character| !replacement.contains(character));
1164        if characters.is_empty() {
1165            continue;
1166        }
1167        let transform_span = span_of(file, &call);
1168        let Some(decl) = index
1169            .defs
1170            .iter()
1171            .filter(|decl| decl.span.start <= transform_span.start && transform_span.end <= decl.span.end)
1172            .min_by_key(|decl| decl.span.len())
1173        else {
1174            continue;
1175        };
1176        let input_place = node_text(input, src).trim().to_string();
1177        let input_param_index = decl.params.iter().position(|param| param == &input_place);
1178        let output = index
1179            .assignment_values
1180            .iter()
1181            .filter(|assignment| {
1182                assignment.target.is_some()
1183                    && assignment.value_span.start <= transform_span.start
1184                    && transform_span.end <= assignment.value_span.end
1185            })
1186            .min_by_key(|assignment| assignment.value_span.len())
1187            .and_then(|assignment| assignment.target.clone())
1188            .map_or(
1189                CharacterConstraintOutput::Expression { span: transform_span },
1190                |target| CharacterConstraintOutput::Assignment { target },
1191            );
1192        facts.push(CharacterConstraintFact {
1193            function_span: decl.span,
1194            transform_span,
1195            input_place,
1196            input_param_index,
1197            output,
1198            domain: CharacterConstraintDomain::ExcludesExact { characters },
1199        });
1200    }
1201    facts
1202}
1203
1204fn java_finite_literal_selections(
1205    index: &DeclIndex,
1206    tree: &Tree,
1207    file: FileId,
1208    src: &[u8],
1209) -> Vec<FiniteLiteralSelectionFact> {
1210    if !java_imports_standard_map(tree, file, src) {
1211        return Vec::new();
1212    }
1213    let bindings = java_bindings(tree, src);
1214    if !bindings.bindings.iter().any(|binding| binding.finite_map) {
1215        return Vec::new();
1216    }
1217    let mut selections = Vec::new();
1218    for call in collect_kinds(tree, &["method_invocation"]) {
1219        let Some(object) = call.child_by_field_name("object") else {
1220            continue;
1221        };
1222        let Some(name) = call.child_by_field_name("name") else {
1223            continue;
1224        };
1225        if object.kind() != "identifier" || !matches!(node_text(&name, src).trim(), "get" | "getOrDefault") {
1226            continue;
1227        }
1228        let map_target = node_text(&object, src).trim();
1229        let Some(binding) = bindings.resolve(map_target, object.start_byte(), object.end_byte()) else {
1230            continue;
1231        };
1232        if !binding.finite_map
1233            || (!binding.is_field && binding.initializer.end_byte() > call.start_byte())
1234            || !java_map_selection_has_literal_fallback(call, src)
1235        {
1236            continue;
1237        }
1238        let selection_span = span_of(file, &call);
1239        let Some(fact) = bonsai_lang_api::kit::finite_literal_selection_fact_for_span(
1240            index,
1241            tree,
1242            selection_span,
1243            |value_node| java_expression_is_finite_selection(value_node, call),
1244        ) else {
1245            continue;
1246        };
1247        selections.push(fact);
1248    }
1249    bonsai_lang_api::kit::sort_dedup_finite_literal_selections(&mut selections);
1250    selections
1251}
1252
1253#[derive(Copy, Clone, Debug)]
1254struct JavaBinding<'tree> {
1255    name: &'tree str,
1256    initializer: Node<'tree>,
1257    scope: Node<'tree>,
1258    finite_map: bool,
1259    is_field: bool,
1260    is_static: bool,
1261}
1262
1263struct JavaBindings<'tree> {
1264    bindings: Vec<JavaBinding<'tree>>,
1265    by_name: std::collections::HashMap<String, Vec<usize>>,
1266}
1267
1268impl<'tree> JavaBindings<'tree> {
1269    fn resolve(&self, name: &str, use_start: usize, use_end: usize) -> Option<&JavaBinding<'tree>> {
1270        let candidates = self.by_name.get(name)?;
1271        let smallest_scope = candidates
1272            .iter()
1273            .map(|index| &self.bindings[*index])
1274            .filter(|binding| {
1275                binding.scope.start_byte() <= use_start
1276                    && use_end <= binding.scope.end_byte()
1277                    && (binding.is_field || binding.initializer.end_byte() <= use_start)
1278            })
1279            .map(|binding| binding.scope.end_byte() - binding.scope.start_byte())
1280            .min()?;
1281        let mut candidates = candidates
1282            .iter()
1283            .map(|index| &self.bindings[*index])
1284            .filter(|binding| {
1285                binding.scope.start_byte() <= use_start
1286                    && use_end <= binding.scope.end_byte()
1287                    && binding.scope.end_byte() - binding.scope.start_byte() == smallest_scope
1288                    && (binding.is_field || binding.initializer.end_byte() <= use_start)
1289            });
1290        let binding = candidates.next()?;
1291        candidates.next().is_none().then_some(binding)
1292    }
1293}
1294
1295fn java_imports_standard_map(tree: &Tree, file: FileId, src: &[u8]) -> bool {
1296    collect_java_imports(tree, file, src)
1297        .iter()
1298        .any(|import| import.module == "java.util.Map" && !import.is_wildcard)
1299}
1300
1301fn java_bindings<'tree>(tree: &'tree Tree, src: &'tree [u8]) -> JavaBindings<'tree> {
1302    let mut bindings = Vec::new();
1303    for declarator in collect_kinds(tree, &["variable_declarator"]) {
1304        let Some(target) = declarator.child_by_field_name("name") else {
1305            continue;
1306        };
1307        if target.kind() != "identifier" {
1308            continue;
1309        }
1310        let Some((scope, declaration, is_field)) = java_binding_scope_and_declaration(declarator) else {
1311            continue;
1312        };
1313        let name = node_text(&target, src).trim();
1314        if name.is_empty() {
1315            continue;
1316        }
1317        let value = declarator.child_by_field_name("value");
1318        bindings.push(JavaBinding {
1319            name,
1320            initializer: value.unwrap_or(target),
1321            scope,
1322            finite_map: java_declaration_has_final_modifier(declaration)
1323                && value.is_some_and(|value| java_is_finite_literal_map(value, src)),
1324            is_field,
1325            is_static: is_field && java_field_has_modifier(declaration, src, "static"),
1326        });
1327    }
1328
1329    for parameter in collect_kinds(
1330        tree,
1331        &["formal_parameter", "spread_parameter", "catch_formal_parameter"],
1332    ) {
1333        let Some(name_node) = parameter
1334            .child_by_field_name("name")
1335            .filter(|name| name.kind() == "identifier")
1336        else {
1337            continue;
1338        };
1339        let mut owner = parameter.parent();
1340        let mut body = None;
1341        while let Some(node) = owner {
1342            if let Some(candidate) = node.child_by_field_name("body") {
1343                body = Some(candidate);
1344                break;
1345            }
1346            if matches!(node.kind(), "class_body" | "program") {
1347                break;
1348            }
1349            owner = node.parent();
1350        }
1351        let Some(scope) = body else {
1352            continue;
1353        };
1354        let name = node_text(&name_node, src).trim();
1355        push_java_blocking_binding(&mut bindings, name_node, scope, name);
1356    }
1357
1358    for lambda in collect_kinds(tree, &["lambda_expression"]) {
1359        let Some(parameters) = lambda.child_by_field_name("parameters") else {
1360            continue;
1361        };
1362        let Some(scope) = lambda.child_by_field_name("body") else {
1363            continue;
1364        };
1365        if parameters.kind() == "identifier" {
1366            let name = node_text(&parameters, src).trim();
1367            push_java_blocking_binding(&mut bindings, parameters, scope, name);
1368        } else if parameters.kind() == "inferred_parameters" {
1369            let mut cursor = parameters.walk();
1370            for name_node in parameters
1371                .named_children(&mut cursor)
1372                .filter(|child| child.kind() == "identifier")
1373            {
1374                let name = node_text(&name_node, src).trim();
1375                push_java_blocking_binding(&mut bindings, name_node, scope, name);
1376            }
1377        }
1378    }
1379
1380    for enhanced_for in collect_kinds(tree, &["enhanced_for_statement"]) {
1381        let Some(name_node) = enhanced_for
1382            .child_by_field_name("name")
1383            .filter(|name| name.kind() == "identifier")
1384        else {
1385            continue;
1386        };
1387        let Some(scope) = enhanced_for.child_by_field_name("body") else {
1388            continue;
1389        };
1390        let name = node_text(&name_node, src).trim();
1391        push_java_blocking_binding(&mut bindings, name_node, scope, name);
1392    }
1393
1394    for pattern in collect_kinds(tree, &["type_pattern", "record_pattern_component"]) {
1395        let mut cursor = pattern.walk();
1396        let Some(name_node) = pattern
1397            .named_children(&mut cursor)
1398            .filter(|child| child.kind() == "identifier")
1399            .last()
1400        else {
1401            continue;
1402        };
1403        let Some(scope) = java_enclosing_block(pattern) else {
1404            continue;
1405        };
1406        let name = node_text(&name_node, src).trim();
1407        push_java_blocking_binding(&mut bindings, name_node, scope, name);
1408    }
1409    let mut by_name: std::collections::HashMap<String, Vec<usize>> = std::collections::HashMap::new();
1410    for (index, binding) in bindings.iter().enumerate() {
1411        by_name.entry(binding.name.to_string()).or_default().push(index);
1412    }
1413    if by_name.contains_key("Map") || java_declares_type_named(tree, src, "Map") {
1414        for binding in &mut bindings {
1415            binding.finite_map = false;
1416        }
1417    }
1418    JavaBindings { bindings, by_name }
1419}
1420
1421/// Qualify a method-call receiver that Tree-sitter parsed as a bare
1422/// identifier when Java's lexical binding rules prove that identifier is an
1423/// instance field of the current class (`data.cmd()` ->
1424/// `this.data.cmd()`). The shared IDG consumes the resulting place directly;
1425/// it must not guess whether an arbitrary bare receiver is a local, field,
1426/// type, or package.
1427fn qualify_java_instance_field_receivers(index: &mut DeclIndex, tree: &Tree, src: &[u8]) {
1428    let Some(current_receiver) = HANDLER
1429        .implicit_receiver_names
1430        .first()
1431        .copied()
1432        .filter(|name| !name.is_empty())
1433    else {
1434        return;
1435    };
1436    let bindings = java_bindings(tree, src);
1437
1438    // Field initializers and later receiver calls must name the same storage
1439    // place. Tree-sitter exposes the declarator target as a bare identifier,
1440    // but Java resolves a non-static field target through the current
1441    // instance. Canonicalize the compiler fact here so shared guard/IDG code
1442    // never has to treat `field` and `this.field` as language-specific aliases.
1443    for fact in &mut index.assignment_values {
1444        let (Some(target_span), Some(target)) = (fact.target_span, fact.target.as_deref()) else {
1445            continue;
1446        };
1447        let Some(target_node) =
1448            bonsai_lang_api::kit::node_at_span(tree.root_node(), target_span, &["identifier"])
1449                .filter(|node| node.kind() == "identifier")
1450        else {
1451            continue;
1452        };
1453        let target_name = node_text(&target_node, src).trim();
1454        if target_name.is_empty() || target_name != target {
1455            continue;
1456        }
1457        let Some(binding) = bindings.resolve(target_name, target_node.start_byte(), target_node.end_byte())
1458        else {
1459            continue;
1460        };
1461        if binding.is_field
1462            && !binding.is_static
1463            && java_enclosing_class_body(target_node).is_some_and(|body| body.id() == binding.scope.id())
1464        {
1465            fact.target = Some(format!("{current_receiver}.{target_name}"));
1466        }
1467    }
1468
1469    let mut rewrites: std::collections::HashMap<Span, (String, String)> = std::collections::HashMap::new();
1470
1471    for fact in &mut index.call_receivers {
1472        if fact.role != bonsai_lang_api::CallReceiverRole::Value {
1473            continue;
1474        }
1475        let Some(receiver_node) =
1476            bonsai_lang_api::kit::node_at_span(tree.root_node(), fact.receiver_span, &["identifier"])
1477                .filter(|node| node.kind() == "identifier")
1478        else {
1479            continue;
1480        };
1481        let receiver_name = node_text(&receiver_node, src).trim();
1482        if receiver_name.is_empty() {
1483            continue;
1484        }
1485        let Some(binding) = bindings.resolve(
1486            receiver_name,
1487            receiver_node.start_byte(),
1488            receiver_node.end_byte(),
1489        ) else {
1490            continue;
1491        };
1492        // A field of a lexically enclosing outer class is not `this.field`
1493        // in a nested class. Only the nearest class body's non-static field
1494        // is the current receiver's instance state.
1495        if !binding.is_field
1496            || binding.is_static
1497            || java_enclosing_class_body(receiver_node).is_none_or(|body| body.id() != binding.scope.id())
1498        {
1499            continue;
1500        }
1501        let qualified = format!("{current_receiver}.{receiver_name}");
1502        fact.value_flow = bonsai_lang_api::ExpressionFlow::from_place(qualified.clone());
1503        rewrites.insert(fact.call_span, (receiver_name.to_string(), qualified));
1504    }
1505
1506    if rewrites.is_empty() {
1507        return;
1508    }
1509    for decl in &mut index.defs {
1510        qualify_java_field_receiver_events(&mut decl.flow_events, &rewrites);
1511        decl.receiver_state_sources = bonsai_lang_api::kit::collect_receiver_state_sources(
1512            &decl.flow_events,
1513            &decl.params,
1514            HANDLER.implicit_receiver_names,
1515        );
1516    }
1517}
1518
1519fn java_enclosing_class_body(mut node: Node<'_>) -> Option<Node<'_>> {
1520    while let Some(parent) = node.parent() {
1521        if parent.kind() == "class_body" {
1522            return Some(parent);
1523        }
1524        node = parent;
1525    }
1526    None
1527}
1528
1529fn qualify_java_field_receiver_events(
1530    events: &mut [FlowEvent],
1531    rewrites: &std::collections::HashMap<Span, (String, String)>,
1532) {
1533    for event in events {
1534        match event {
1535            FlowEvent::Call {
1536                span, name, receiver, ..
1537            } => {
1538                if let Some((unqualified, qualified)) = rewrites.get(span) {
1539                    if receiver.as_deref() == Some(unqualified.as_str()) {
1540                        *receiver = Some(qualified.clone());
1541                    }
1542                    qualify_java_receiver_prefix(name, unqualified, qualified);
1543                }
1544            }
1545            FlowEvent::Assign {
1546                span, source_call, ..
1547            } => {
1548                if let Some(source_call) = source_call {
1549                    for (call_span, (unqualified, qualified)) in rewrites {
1550                        if span.file == call_span.file
1551                            && span.start <= call_span.start
1552                            && call_span.end <= span.end
1553                        {
1554                            qualify_java_receiver_prefix(source_call, unqualified, qualified);
1555                        }
1556                    }
1557                }
1558            }
1559            FlowEvent::Branch {
1560                then_events,
1561                else_events,
1562                ..
1563            } => {
1564                qualify_java_field_receiver_events(then_events, rewrites);
1565                qualify_java_field_receiver_events(else_events, rewrites);
1566            }
1567            FlowEvent::Try {
1568                body,
1569                catch_events,
1570                finally_events,
1571                ..
1572            } => {
1573                qualify_java_field_receiver_events(body, rewrites);
1574                qualify_java_field_receiver_events(catch_events, rewrites);
1575                qualify_java_field_receiver_events(finally_events, rewrites);
1576            }
1577            FlowEvent::Loop { body, .. } | FlowEvent::Defer { body, .. } | FlowEvent::Using { body, .. } => {
1578                qualify_java_field_receiver_events(body, rewrites);
1579            }
1580            _ => {}
1581        }
1582    }
1583}
1584
1585fn qualify_java_receiver_prefix(value: &mut String, unqualified: &str, qualified: &str) {
1586    if value == unqualified {
1587        *value = qualified.to_string();
1588        return;
1589    }
1590    if value
1591        .strip_prefix(unqualified)
1592        .is_some_and(|suffix| suffix.starts_with('.'))
1593    {
1594        value.replace_range(..unqualified.len(), qualified);
1595    }
1596}
1597
1598fn push_java_blocking_binding<'tree>(
1599    bindings: &mut Vec<JavaBinding<'tree>>,
1600    name_node: Node<'tree>,
1601    scope: Node<'tree>,
1602    name: &'tree str,
1603) {
1604    if name.is_empty() {
1605        return;
1606    }
1607    bindings.push(JavaBinding {
1608        name,
1609        initializer: name_node,
1610        scope,
1611        finite_map: false,
1612        is_field: false,
1613        is_static: false,
1614    });
1615}
1616
1617fn java_enclosing_block(mut node: Node<'_>) -> Option<Node<'_>> {
1618    while let Some(parent) = node.parent() {
1619        if parent.kind() == "block" {
1620            return Some(parent);
1621        }
1622        node = parent;
1623    }
1624    None
1625}
1626
1627fn java_declares_type_named(tree: &Tree, src: &[u8], wanted: &str) -> bool {
1628    collect_kinds(
1629        tree,
1630        &[
1631            "class_declaration",
1632            "interface_declaration",
1633            "enum_declaration",
1634            "record_declaration",
1635            "annotation_type_declaration",
1636        ],
1637    )
1638    .into_iter()
1639    .any(|declaration| {
1640        declaration
1641            .child_by_field_name("name")
1642            .is_some_and(|name| node_text(&name, src).trim() == wanted)
1643    })
1644}
1645
1646fn java_binding_scope_and_declaration(mut node: Node<'_>) -> Option<(Node<'_>, Node<'_>, bool)> {
1647    let mut declaration = None;
1648    while let Some(parent) = node.parent() {
1649        if matches!(parent.kind(), "field_declaration" | "local_variable_declaration") {
1650            declaration = Some(parent);
1651        }
1652        // Tree-sitter Java gives constructor bodies their own
1653        // `constructor_body` node rather than the `block` used by ordinary
1654        // methods. Both are lexical local scopes. Treating a constructor
1655        // body as a class boundary turns constructor locals into fields and
1656        // incorrectly canonicalizes `local.method()` as
1657        // `this.local.method()`.
1658        if matches!(parent.kind(), "block" | "constructor_body") {
1659            return Some((parent, declaration?, false));
1660        }
1661        if parent.kind() == "class_body" {
1662            return Some((parent, declaration?, true));
1663        }
1664        node = parent;
1665    }
1666    None
1667}
1668
1669fn java_declaration_has_final_modifier(declaration: Node<'_>) -> bool {
1670    let Some(modifiers) = declaration
1671        .named_children(&mut declaration.walk())
1672        .find(|child| child.kind() == "modifiers")
1673    else {
1674        return false;
1675    };
1676    modifiers
1677        .children(&mut modifiers.walk())
1678        .any(|modifier| modifier.kind() == "final")
1679}
1680
1681fn java_map_selection_has_literal_fallback(call: Node<'_>, src: &[u8]) -> bool {
1682    let Some(name) = call.child_by_field_name("name") else {
1683        return false;
1684    };
1685    let Some(arguments) = call.child_by_field_name("arguments") else {
1686        return false;
1687    };
1688    let mut cursor = arguments.walk();
1689    let values: Vec<_> = arguments.named_children(&mut cursor).collect();
1690    match node_text(&name, src).trim() {
1691        "get" => values.len() == 1,
1692        "getOrDefault" => values.len() == 2 && java_is_literal_value(values[1], src),
1693        _ => false,
1694    }
1695}
1696
1697fn java_expression_is_finite_selection(mut node: Node<'_>, selection: Node<'_>) -> bool {
1698    while matches!(node.kind(), "parenthesized_expression" | "cast_expression")
1699        && node.named_child_count() >= 1
1700    {
1701        let Some(inner) = node
1702            .child_by_field_name("value")
1703            .or_else(|| node.named_child(u32::try_from(node.named_child_count() - 1).ok()?))
1704        else {
1705            return false;
1706        };
1707        node = inner;
1708    }
1709    node.id() == selection.id()
1710}
1711
1712fn java_is_finite_literal_map(node: Node<'_>, src: &[u8]) -> bool {
1713    if node.kind() != "method_invocation" {
1714        return false;
1715    }
1716    let Some(object) = node.child_by_field_name("object") else {
1717        return false;
1718    };
1719    let Some(name) = node.child_by_field_name("name") else {
1720        return false;
1721    };
1722    if object.kind() != "identifier"
1723        || node_text(&object, src).trim() != "Map"
1724        || node_text(&name, src).trim() != "of"
1725    {
1726        return false;
1727    }
1728    let Some(arguments) = node.child_by_field_name("arguments") else {
1729        return false;
1730    };
1731    let mut cursor = arguments.walk();
1732    let values: Vec<_> = arguments.named_children(&mut cursor).collect();
1733    !values.is_empty()
1734        && values.len() % 2 == 0
1735        && values.iter().all(|value| java_is_literal_value(*value, src))
1736}
1737
1738fn java_is_literal_value(mut node: Node<'_>, src: &[u8]) -> bool {
1739    while matches!(node.kind(), "parenthesized_expression" | "cast_expression")
1740        && node.named_child_count() >= 1
1741    {
1742        let Some(inner) = node
1743            .child_by_field_name("value")
1744            .or_else(|| node.named_child(u32::try_from(node.named_child_count() - 1).unwrap_or(0)))
1745        else {
1746            return false;
1747        };
1748        node = inner;
1749    }
1750    match node.kind() {
1751        "string_literal" => java_static_string_literal(node, src).is_some(),
1752        "character_literal"
1753        | "decimal_integer_literal"
1754        | "hex_integer_literal"
1755        | "octal_integer_literal"
1756        | "binary_integer_literal"
1757        | "decimal_floating_point_literal"
1758        | "hex_floating_point_literal"
1759        | "true"
1760        | "false"
1761        | "null_literal" => true,
1762        "array_initializer" => {
1763            let mut cursor = node.walk();
1764            let is_literal = node
1765                .named_children(&mut cursor)
1766                .all(|child| java_is_literal_value(child, src));
1767            is_literal
1768        }
1769        _ => false,
1770    }
1771}
1772
1773fn populate_java_condition_expressions(
1774    facts: &mut [bonsai_lang_api::BranchConditionFact],
1775    tree: &Tree,
1776    file: FileId,
1777    src: &[u8],
1778) {
1779    for branch in collect_kinds(tree, &["if_statement"]) {
1780        let branch_span = span_of(file, &branch);
1781        let Some(condition) = branch.child_by_field_name("condition") else {
1782            continue;
1783        };
1784        let Some(fact) = facts.iter_mut().find(|fact| fact.branch_span == branch_span) else {
1785            continue;
1786        };
1787        fact.expression = Some(lower_java_condition_expression(condition, file, src));
1788    }
1789}
1790
1791fn lower_java_condition_expression(node: Node<'_>, file: FileId, src: &[u8]) -> ConditionExpressionFact {
1792    if node.kind() == "parenthesized_expression" {
1793        if let Some(inner) = node.named_child(0) {
1794            return lower_java_condition_expression(inner, file, src);
1795        }
1796    }
1797    let span = span_of(file, &node);
1798    if node.kind() == "unary_expression" {
1799        if let Some(operand) = node
1800            .child_by_field_name("operand")
1801            .or_else(|| node.named_child(0))
1802        {
1803            let operator = src
1804                .get(node.start_byte()..operand.start_byte())
1805                .and_then(|bytes| std::str::from_utf8(bytes).ok())
1806                .map(str::trim);
1807            if operator == Some("!") {
1808                return ConditionExpressionFact::Not {
1809                    span,
1810                    operand: Box::new(lower_java_condition_expression(operand, file, src)),
1811                };
1812            }
1813        }
1814    }
1815    if node.kind() == "binary_expression" {
1816        if let (Some(left), Some(right)) = (
1817            node.child_by_field_name("left"),
1818            node.child_by_field_name("right"),
1819        ) {
1820            let operator = src
1821                .get(left.end_byte()..right.start_byte())
1822                .and_then(|bytes| std::str::from_utf8(bytes).ok())
1823                .map(str::trim);
1824            match operator {
1825                Some("||") => {
1826                    return merge_java_condition_junction(
1827                        span,
1828                        lower_java_condition_expression(left, file, src),
1829                        lower_java_condition_expression(right, file, src),
1830                        false,
1831                    );
1832                }
1833                Some("&&") => {
1834                    return merge_java_condition_junction(
1835                        span,
1836                        lower_java_condition_expression(left, file, src),
1837                        lower_java_condition_expression(right, file, src),
1838                        true,
1839                    );
1840                }
1841                Some("==" | "!=") => {
1842                    return ConditionExpressionFact::Equality {
1843                        span,
1844                        relation: if operator == Some("==") {
1845                            ConditionEquality::Equal
1846                        } else {
1847                            ConditionEquality::NotEqual
1848                        },
1849                        left: java_condition_operand(left, file, src),
1850                        right: java_condition_operand(right, file, src),
1851                    };
1852                }
1853                _ => {}
1854            }
1855        }
1856    }
1857    if node.kind() == "instanceof_expression" {
1858        if let (Some(subject), Some(type_node)) = (
1859            node.child_by_field_name("left")
1860                .or_else(|| node.child_by_field_name("expression"))
1861                .or_else(|| node.named_child(0)),
1862            node.child_by_field_name("right")
1863                .or_else(|| node.child_by_field_name("type"))
1864                .or_else(|| node.named_child(1)),
1865        ) {
1866            let type_name = node_text(&type_node, src).trim().to_string();
1867            if !type_name.is_empty() {
1868                return ConditionExpressionFact::TypeTest {
1869                    span,
1870                    subject: java_condition_operand(subject, file, src),
1871                    type_name,
1872                };
1873            }
1874        }
1875    }
1876    ConditionExpressionFact::Atom { span }
1877}
1878
1879fn merge_java_condition_junction(
1880    span: Span,
1881    left: ConditionExpressionFact,
1882    right: ConditionExpressionFact,
1883    all: bool,
1884) -> ConditionExpressionFact {
1885    let mut operands = Vec::new();
1886    let mut push = |operand: ConditionExpressionFact| match (all, operand) {
1887        (true, ConditionExpressionFact::All { operands: nested, .. })
1888        | (false, ConditionExpressionFact::Any { operands: nested, .. }) => operands.extend(nested),
1889        (_, operand) => operands.push(operand),
1890    };
1891    push(left);
1892    push(right);
1893    if all {
1894        ConditionExpressionFact::All { span, operands }
1895    } else {
1896        ConditionExpressionFact::Any { span, operands }
1897    }
1898}
1899
1900fn java_condition_operand(node: Node<'_>, file: FileId, src: &[u8]) -> ConditionOperandFact {
1901    ConditionOperandFact {
1902        span: span_of(file, &node),
1903        value_flow: bonsai_lang_api::kit::expression_flow_from_node_with_handler(node, file, src, &HANDLER),
1904        static_string: java_static_string_literal(node, src),
1905        static_value: java_static_scalar(node, src),
1906    }
1907}
1908
1909fn java_static_string_literal(node: Node<'_>, src: &[u8]) -> Option<String> {
1910    if node.kind() != "string_literal" {
1911        return None;
1912    }
1913    let text = node_text(&node, src);
1914    let inner = text.strip_prefix('"')?.strip_suffix('"')?;
1915    decode_java_string_literal(inner)
1916}
1917
1918/// Decode the Java runtime value of a regular string literal.
1919///
1920/// Regex and replacement semantics operate on decoded strings, not source
1921/// spellings. Keeping this in the Java frontend prevents shared analyses from
1922/// interpreting Java escapes and lets exact compiler facts represent values
1923/// such as `"[\\\\r\\\\n]"` correctly.
1924fn decode_java_string_literal(inner: &str) -> Option<String> {
1925    let mut input = inner.chars().peekable();
1926    let mut decoded = String::with_capacity(inner.len());
1927    while let Some(character) = input.next() {
1928        if character != '\\' {
1929            decoded.push(character);
1930            continue;
1931        }
1932        let escape = input.next()?;
1933        match escape {
1934            'b' => decoded.push('\u{0008}'),
1935            't' => decoded.push('\t'),
1936            'n' => decoded.push('\n'),
1937            'f' => decoded.push('\u{000c}'),
1938            'r' => decoded.push('\r'),
1939            's' => decoded.push(' '),
1940            '"' => decoded.push('"'),
1941            '\'' => decoded.push('\''),
1942            '\\' => decoded.push('\\'),
1943            'u' => {
1944                while input.peek() == Some(&'u') {
1945                    input.next();
1946                }
1947                let mut value = 0_u32;
1948                for _ in 0..4 {
1949                    value = value.checked_mul(16)? + input.next()?.to_digit(16)?;
1950                }
1951                let scalar = char::from_u32(value)?;
1952                // A Unicode escape which produces a lexical delimiter or a
1953                // second escape prefix requires Java's pre-tokenization
1954                // Unicode translation. Refuse that uncommon shape rather
1955                // than claim an inexact runtime value.
1956                if matches!(scalar, '\\' | '"' | '\r' | '\n') {
1957                    return None;
1958                }
1959                decoded.push(scalar);
1960            }
1961            first @ '0'..='7' => {
1962                let mut value = first.to_digit(8)?;
1963                let mut digits = 1;
1964                while digits < 3 {
1965                    let Some(next) = input.peek().copied() else {
1966                        break;
1967                    };
1968                    let Some(digit) = next.to_digit(8) else {
1969                        break;
1970                    };
1971                    if digits == 2 && first > '3' {
1972                        break;
1973                    }
1974                    input.next();
1975                    value = value.checked_mul(8)? + digit;
1976                    digits += 1;
1977                }
1978                decoded.push(char::from_u32(value)?);
1979            }
1980            _ => return None,
1981        }
1982    }
1983    Some(decoded)
1984}
1985
1986fn java_static_scalar(node: Node<'_>, src: &[u8]) -> Option<StaticScalarValue> {
1987    match node.kind() {
1988        "string_literal" => Some(StaticScalarValue::String(java_static_string_literal(node, src)?)),
1989        "true" => Some(StaticScalarValue::Boolean(true)),
1990        "false" => Some(StaticScalarValue::Boolean(false)),
1991        "null_literal" => Some(StaticScalarValue::Null),
1992        _ => None,
1993    }
1994}
1995
1996fn java_string_compositions(tree: &Tree, file: FileId, src: &[u8]) -> Vec<StringCompositionFact> {
1997    let mut facts = Vec::new();
1998    for declarator in collect_kinds(tree, &["variable_declarator"]) {
1999        let (Some(name), Some(value)) = (
2000            declarator.child_by_field_name("name"),
2001            declarator.child_by_field_name("value"),
2002        ) else {
2003            continue;
2004        };
2005        if name.kind() != "identifier" {
2006            continue;
2007        }
2008        let mut parts = Vec::new();
2009        if java_lower_string_composition(value, file, src, &mut parts) && parts.len() > 1 {
2010            facts.push(StringCompositionFact {
2011                container_span: span_of(file, &declarator),
2012                value_span: span_of(file, &value),
2013                target: Some(node_text(&name, src).trim().to_string()),
2014                parts,
2015            });
2016        }
2017    }
2018    facts.sort_by_key(|fact| (fact.container_span.start, fact.container_span.end));
2019    facts.dedup();
2020    facts
2021}
2022
2023fn java_lower_string_composition(
2024    mut node: Node<'_>,
2025    file: FileId,
2026    src: &[u8],
2027    out: &mut Vec<StringCompositionPart>,
2028) -> bool {
2029    while node.kind() == "parenthesized_expression" && node.named_child_count() == 1 {
2030        let Some(inner) = node.named_child(0) else {
2031            return false;
2032        };
2033        node = inner;
2034    }
2035    if let Some(value) = java_static_string_literal(node, src) {
2036        out.push(StringCompositionPart::Literal { value });
2037        return true;
2038    }
2039    if node.kind() == "method_invocation" {
2040        let Some(name) = node.child_by_field_name("name") else {
2041            return false;
2042        };
2043        out.push(StringCompositionPart::Call {
2044            span: span_of(file, &name),
2045        });
2046        return true;
2047    }
2048    if node.kind() == "binary_expression" {
2049        let (Some(left), Some(right)) = (
2050            node.child_by_field_name("left"),
2051            node.child_by_field_name("right"),
2052        ) else {
2053            return false;
2054        };
2055        let operator = src
2056            .get(left.end_byte()..right.start_byte())
2057            .and_then(|bytes| std::str::from_utf8(bytes).ok())
2058            .map(str::trim);
2059        return operator == Some("+")
2060            && java_lower_string_composition(left, file, src, out)
2061            && java_lower_string_composition(right, file, src, out);
2062    }
2063    if node.kind() == "ternary_expression" {
2064        let condition = node
2065            .child_by_field_name("condition")
2066            .or_else(|| node.named_child(0));
2067        let consequence = node
2068            .child_by_field_name("consequence")
2069            .or_else(|| node.named_child(1));
2070        let alternative = node
2071            .child_by_field_name("alternative")
2072            .or_else(|| node.named_child(2));
2073        let (Some(condition), Some(consequence), Some(alternative)) = (condition, consequence, alternative)
2074        else {
2075            return false;
2076        };
2077        let Some(condition_call) = java_null_equality_call(condition, src) else {
2078            return false;
2079        };
2080        let (call, fallback) = if alternative.kind() == "method_invocation" {
2081            (alternative, java_static_string_literal(consequence, src))
2082        } else if consequence.kind() == "method_invocation" {
2083            (consequence, java_static_string_literal(alternative, src))
2084        } else {
2085            return false;
2086        };
2087        let Some(fallback) = fallback else {
2088            return false;
2089        };
2090        if java_method_call_identity(condition_call, src) != java_method_call_identity(call, src) {
2091            return false;
2092        }
2093        out.push(StringCompositionPart::CallOrLiteral {
2094            span: span_of(file, &call.child_by_field_name("name").unwrap_or(call)),
2095            fallback,
2096        });
2097        return true;
2098    }
2099    false
2100}
2101
2102fn java_null_equality_call<'tree>(node: Node<'tree>, src: &[u8]) -> Option<Node<'tree>> {
2103    if node.kind() != "binary_expression" {
2104        return None;
2105    }
2106    let left = node.child_by_field_name("left")?;
2107    let right = node.child_by_field_name("right")?;
2108    let operator = src
2109        .get(left.end_byte()..right.start_byte())
2110        .and_then(|bytes| std::str::from_utf8(bytes).ok())
2111        .map(str::trim);
2112    if operator != Some("==") {
2113        return None;
2114    }
2115    if left.kind() == "method_invocation" && right.kind() == "null_literal" {
2116        Some(left)
2117    } else if right.kind() == "method_invocation" && left.kind() == "null_literal" {
2118        Some(right)
2119    } else {
2120        None
2121    }
2122}
2123
2124fn java_method_call_identity(node: Node<'_>, src: &[u8]) -> Option<(String, String, usize)> {
2125    if node.kind() != "method_invocation" {
2126        return None;
2127    }
2128    let receiver = node.child_by_field_name("object")?;
2129    let name = node.child_by_field_name("name")?;
2130    let arguments = node.child_by_field_name("arguments")?;
2131    let mut cursor = arguments.walk();
2132    Some((
2133        node_text(&receiver, src).trim().to_string(),
2134        node_text(&name, src).trim().to_string(),
2135        arguments.named_children(&mut cursor).count(),
2136    ))
2137}
2138
2139fn populate_java_static_scalar_facts(index: &mut DeclIndex, tree: &Tree, file: FileId, src: &[u8]) {
2140    let static_values: std::collections::HashMap<_, _> =
2141        collect_kinds(tree, &["string_literal", "true", "false", "null_literal"])
2142            .into_iter()
2143            .filter_map(|node| {
2144                let value = java_static_scalar(node, src)?;
2145                let span = span_of(file, &node);
2146                Some(((span.start, span.end), value))
2147            })
2148            .collect();
2149    let call_values: std::collections::HashMap<_, _> =
2150        collect_kinds(tree, &["method_invocation", "object_creation_expression"])
2151            .into_iter()
2152            .map(|node| {
2153                let span = span_of(file, &node);
2154                ((span.start, span.end), node)
2155            })
2156            .collect();
2157    for fact in &mut index.assignment_values {
2158        if fact.direct_call_name.is_none() {
2159            continue;
2160        }
2161        let Some(call) = call_values.get(&(fact.value_span.start, fact.value_span.end)) else {
2162            continue;
2163        };
2164        let Some(arguments) = call.child_by_field_name("arguments") else {
2165            continue;
2166        };
2167        let mut cursor = arguments.walk();
2168        let argument_nodes: Vec<_> = arguments.named_children(&mut cursor).collect();
2169        if argument_nodes.is_empty() {
2170            continue;
2171        }
2172        let values: Option<Vec<_>> = argument_nodes
2173            .into_iter()
2174            .map(|argument| java_static_scalar(argument, src))
2175            .collect();
2176        fact.exact_static_call_args = values;
2177    }
2178    for fact in &mut index.call_receivers {
2179        fact.static_value = static_values
2180            .get(&(fact.receiver_span.start, fact.receiver_span.end))
2181            .cloned();
2182    }
2183    bonsai_lang_api::kit::populate_call_argument_static_values(
2184        index,
2185        tree,
2186        file,
2187        src,
2188        &HANDLER,
2189        java_static_scalar,
2190    );
2191    populate_java_array_argument_sequences(index, tree, file, src);
2192}
2193
2194/// Lower Java's `new T[] { ... }` wrapper into the shared ordered-sequence
2195/// fact. The wrapper and `array_initializer` field are Java grammar details;
2196/// downstream matching sees only exact scalar values (and `None` for dynamic
2197/// elements).
2198fn populate_java_array_argument_sequences(index: &mut DeclIndex, tree: &Tree, file: FileId, src: &[u8]) {
2199    let arrays: std::collections::HashMap<_, _> = collect_kinds(tree, &["array_creation_expression"])
2200        .into_iter()
2201        .map(|node| {
2202            let span = span_of(file, &node);
2203            ((span.start, span.end), node)
2204        })
2205        .collect();
2206    for fact in &mut index.call_argument_values {
2207        let Some(array) = arrays.get(&(fact.argument_span.start, fact.argument_span.end)) else {
2208            continue;
2209        };
2210        let mut array_cursor = array.walk();
2211        let Some(initializer) = array.child_by_field_name("value").or_else(|| {
2212            array
2213                .named_children(&mut array_cursor)
2214                .find(|child| child.kind() == "array_initializer")
2215        }) else {
2216            continue;
2217        };
2218        let mut cursor = initializer.walk();
2219        let values = initializer
2220            .named_children(&mut cursor)
2221            .map(|value| java_static_scalar(value, src))
2222            .collect::<Vec<_>>();
2223        if !values.is_empty() {
2224            fact.exact_static_sequence_values = Some(values);
2225        }
2226    }
2227}
2228
2229/// Build per-method type-alias bindings (`Foo bar` → `bar : Foo`) by
2230/// merging file-level field aliases with each method's local declarations.
2231fn collect_java_method_type_aliases(
2232    tree: &Tree,
2233    file: FileId,
2234    src: &[u8],
2235    field_aliases: &[TypeAliasBinding],
2236) -> Vec<(bonsai_common::Span, Vec<TypeAliasBinding>)> {
2237    let mut aliases_by_method = Vec::new();
2238    for method_node in collect_kinds(tree, &["method_declaration", "constructor_declaration"]) {
2239        // Start every method with the file's field aliases — fields are
2240        // visible throughout the method body.
2241        let mut method_aliases = field_aliases.to_vec();
2242        method_aliases.extend(collect_java_type_aliases(
2243            method_node,
2244            src,
2245            &[
2246                "formal_parameter",
2247                "local_variable_declaration",
2248                "enhanced_for_statement",
2249                // Try-with-resources binding `try (T r = expr)` — the
2250                // `resource` node exposes the same `type`/`name` fields,
2251                // so a JDBC `try (Statement s = ...)` yields `s: Statement`
2252                // and the receiver-type SQLi rule resolves.
2253                "resource",
2254            ],
2255        ));
2256        let method_type_bounds = java_type_parameter_bounds(method_node, src);
2257        expand_java_type_parameter_aliases(&mut method_aliases, &method_type_bounds);
2258        dedup_type_aliases(&mut method_aliases);
2259        aliases_by_method.push((span_of(file, &method_node), method_aliases));
2260    }
2261    aliases_by_method
2262}
2263
2264type JavaTypeParameterBounds = Vec<(String, Vec<String>)>;
2265
2266fn collect_java_class_type_parameter_bounds(
2267    tree: &Tree,
2268    file: FileId,
2269    src: &[u8],
2270) -> Vec<(Span, JavaTypeParameterBounds)> {
2271    collect_kinds(
2272        tree,
2273        &[
2274            "class_declaration",
2275            "interface_declaration",
2276            "record_declaration",
2277            "enum_declaration",
2278            "annotation_type_declaration",
2279        ],
2280    )
2281    .into_iter()
2282    .filter_map(|node| {
2283        let bounds = java_type_parameter_bounds(node, src);
2284        (!bounds.is_empty()).then(|| (span_of(file, &node), bounds))
2285    })
2286    .collect()
2287}
2288
2289fn java_type_parameter_bounds(node: Node<'_>, src: &[u8]) -> JavaTypeParameterBounds {
2290    let Some(parameters) = node.child_by_field_name("type_parameters") else {
2291        return Vec::new();
2292    };
2293    let mut out = Vec::new();
2294    let mut parameter_cursor = parameters.walk();
2295    for parameter in parameters.named_children(&mut parameter_cursor) {
2296        if parameter.kind() != "type_parameter" {
2297            continue;
2298        }
2299        let mut child_cursor = parameter.walk();
2300        let children = parameter.named_children(&mut child_cursor).collect::<Vec<_>>();
2301        let Some(name) = children
2302            .iter()
2303            .find(|child| child.kind() == "type_identifier")
2304            .map(|child| node_text(child, src).trim().to_string())
2305            .filter(|name| !name.is_empty())
2306        else {
2307            continue;
2308        };
2309        let mut bounds = Vec::new();
2310        for bound in children.iter().filter(|child| child.kind() == "type_bound") {
2311            let mut bound_cursor = bound.walk();
2312            for bound_type in bound.named_children(&mut bound_cursor) {
2313                let text = node_text(&bound_type, src).trim();
2314                if !text.is_empty() && !bounds.iter().any(|existing| existing == text) {
2315                    bounds.push(text.to_string());
2316                }
2317            }
2318        }
2319        if !bounds.is_empty() {
2320            out.push((name, bounds));
2321        }
2322    }
2323    out
2324}
2325
2326fn expand_java_type_parameter_aliases(aliases: &mut Vec<TypeAliasBinding>, bounds: &JavaTypeParameterBounds) {
2327    if aliases.is_empty() || bounds.is_empty() {
2328        return;
2329    }
2330    loop {
2331        let before = aliases.len();
2332        let current = aliases.clone();
2333        for alias in current {
2334            let alias_type = canonical_java_type_name(&alias.type_name)
2335                .unwrap_or_else(|| alias.type_name.trim().to_string());
2336            for (_, upper_bounds) in bounds.iter().filter(|(parameter, _)| parameter == &alias_type) {
2337                for upper_bound in upper_bounds {
2338                    if let Some(canonical) = canonical_java_type_name(upper_bound) {
2339                        let qualified = qualified_java_type_name(upper_bound);
2340                        push_java_type_alias(aliases, &alias.name, &canonical, qualified.as_deref());
2341                    }
2342                }
2343            }
2344        }
2345        dedup_type_aliases(aliases);
2346        if aliases.len() == before {
2347            break;
2348        }
2349    }
2350}
2351
2352/// Attach only compiler-provable types to nested callable declarations.
2353/// Java's source syntax tells us that `Interface<T> f = value -> ...` binds
2354/// the lambda value `f` to `Interface`; it does not tell us the external
2355/// interface method's parameter types. Those external signatures are
2356/// rulepack typing data and are applied later by the matcher.
2357fn attach_java_nested_callable_type_aliases(index: &mut DeclIndex, tree: &Tree, file: FileId, src: &[u8]) {
2358    let mut callable_bindings = Vec::new();
2359    for declaration in collect_kinds(tree, &["local_variable_declaration"]) {
2360        let Some(type_node) = declaration.child_by_field_name("type") else {
2361            continue;
2362        };
2363        let type_text = node_text(&type_node, src);
2364        let Some(canonical_type) = canonical_java_type_name(type_text) else {
2365            continue;
2366        };
2367        let qualified_type = qualified_java_type_name(type_text);
2368        let mut cursor = declaration.walk();
2369        for declarator in declaration
2370            .named_children(&mut cursor)
2371            .filter(|child| child.kind() == "variable_declarator")
2372        {
2373            let (Some(name_node), Some(value)) = (
2374                declarator.child_by_field_name("name"),
2375                declarator.child_by_field_name("value"),
2376            ) else {
2377                continue;
2378            };
2379            if value.kind() != "lambda_expression" {
2380                continue;
2381            }
2382            let name = node_text(&name_node, src).trim();
2383            if name.is_empty() {
2384                continue;
2385            }
2386            let mut aliases = Vec::new();
2387            push_java_type_alias(&mut aliases, name, &canonical_type, qualified_type.as_deref());
2388            callable_bindings.push((span_of(file, &value), aliases));
2389        }
2390    }
2391
2392    let inherited = index
2393        .defs
2394        .iter()
2395        .map(|decl| (decl.symbol, decl.type_aliases.clone()))
2396        .collect::<std::collections::HashMap<_, _>>();
2397    for decl in &mut index.defs {
2398        if let Some(parent_aliases) = decl.parent.and_then(|parent| inherited.get(&parent)) {
2399            decl.type_aliases.extend(parent_aliases.iter().cloned());
2400        }
2401        if let Some(aliases) = callable_bindings
2402            .iter()
2403            .find_map(|(span, aliases)| (*span == decl.span).then_some(aliases))
2404        {
2405            decl.type_aliases.extend(aliases.iter().cloned());
2406        }
2407        dedup_type_aliases(&mut decl.type_aliases);
2408    }
2409}
2410
2411/// Walk `root` collecting `(name, type)` aliases from every declaration
2412/// node whose kind matches `kinds`. The result is deduplicated.
2413fn collect_java_type_aliases(root: Node<'_>, src: &[u8], kinds: &[&str]) -> Vec<TypeAliasBinding> {
2414    let mut aliases = Vec::new();
2415    let mut work_stack = vec![root];
2416    while let Some(node) = work_stack.pop() {
2417        if kinds.contains(&node.kind()) {
2418            aliases.extend(java_type_aliases_from_decl(node, src));
2419        }
2420        let mut cursor = node.walk();
2421        for child in node.named_children(&mut cursor) {
2422            work_stack.push(child);
2423        }
2424    }
2425    expand_java_platform_supertypes(&mut aliases);
2426    dedup_type_aliases(&mut aliases);
2427    aliases
2428}
2429
2430/// Pull every `(name, type)` binding out of a single declaration node —
2431/// handles both `Foo bar` (single name field) and `Foo a, b, c`
2432/// (multiple `variable_declarator` children).
2433fn java_type_aliases_from_decl(node: Node<'_>, src: &[u8]) -> Vec<TypeAliasBinding> {
2434    let Some(type_node) = node.child_by_field_name("type") else {
2435        return Vec::new();
2436    };
2437    let type_text = node_text(&type_node, src);
2438    let mut aliases = Vec::new();
2439    if let Some(canonical_type) = canonical_java_type_name(type_text) {
2440        let qualified_type = qualified_java_type_name(type_text);
2441        // Single-name declarations (most parameter shapes).
2442        if let Some(name_node) = node.child_by_field_name("name") {
2443            push_java_type_alias(
2444                &mut aliases,
2445                node_text(&name_node, src),
2446                &canonical_type,
2447                qualified_type.as_deref(),
2448            );
2449            return aliases;
2450        }
2451        // Multi-name `Foo a, b, c;` — one `variable_declarator` per name.
2452        let mut cursor = node.walk();
2453        for child in node.named_children(&mut cursor) {
2454            if child.kind() != "variable_declarator" {
2455                continue;
2456            }
2457            if let Some(name_node) = child.child_by_field_name("name") {
2458                let name = node_text(&name_node, src);
2459                push_java_type_alias(&mut aliases, name, &canonical_type, qualified_type.as_deref());
2460            }
2461        }
2462        return aliases;
2463    }
2464    // WS2: `var c = (Foo) make()` — the inferred (`var`) LHS carries no
2465    // class, so the type lives only on the cast initializer. Read the
2466    // declarator's `value` field directly (a cast nested in a call
2467    // argument must NOT mistype the local) and type the binding by it.
2468    if type_text.trim() == "var" {
2469        let mut cursor = node.walk();
2470        for child in node.named_children(&mut cursor) {
2471            if child.kind() != "variable_declarator" {
2472                continue;
2473            }
2474            let Some(name_node) = child.child_by_field_name("name") else {
2475                continue;
2476            };
2477            let Some(value) = child.child_by_field_name("value") else {
2478                continue;
2479            };
2480            if let Some(cast_raw) = java_cast_type_of_init(value, src) {
2481                if let Some(canonical) = canonical_java_type_name(&cast_raw) {
2482                    let qualified = qualified_java_type_name(&cast_raw);
2483                    push_java_type_alias(
2484                        &mut aliases,
2485                        node_text(&name_node, src),
2486                        &canonical,
2487                        qualified.as_deref(),
2488                    );
2489                }
2490            }
2491        }
2492    }
2493    aliases
2494}
2495
2496/// The cast type of a direct initializer (`(Foo) x` → `Foo`), unwrapping
2497/// redundant parentheses. Java has no `as`-cast, so only `cast_expression`
2498/// counts. Returns `None` for any other initializer shape so only a cast
2499/// that IS the initializer types the local.
2500fn java_cast_type_of_init(init: Node<'_>, src: &[u8]) -> Option<String> {
2501    let mut n = init;
2502    while n.kind() == "parenthesized_expression" {
2503        let mut cursor = n.walk();
2504        n = n.named_children(&mut cursor).next()?;
2505    }
2506    if n.kind() == "cast_expression" {
2507        return n
2508            .child_by_field_name("type")
2509            .map(|t| node_text(&t, src).to_string());
2510    }
2511    None
2512}
2513
2514/// Canonicalize a Java type expression to its short, generics/array-free
2515/// form: `List<String>` → `List`, `int[]` → `int`, `java.util.Map` →
2516/// `Map`. `var` is excluded because it is an inference marker; primitive
2517/// types and lowercase user-defined types remain valid compiler facts.
2518fn canonical_java_type_name(raw: &str) -> Option<String> {
2519    // Strip generics and array brackets — they don't change the receiver type.
2520    let without_generics = raw.split('<').next().unwrap_or(raw);
2521    let without_arrays = without_generics.split('[').next().unwrap_or(without_generics);
2522    // Take the rightmost path segment as the bare type name.
2523    let bare_type = without_arrays
2524        .trim()
2525        .rsplit('.')
2526        .next()
2527        .unwrap_or(without_arrays)
2528        .trim();
2529    if bare_type.is_empty() || bare_type == "var" {
2530        return None;
2531    }
2532    Some(bare_type.to_string())
2533}
2534
2535/// Preserve Java source-level qualified type names as additional receiver
2536/// evidence. Package-qualified (`javax.naming.Foo`) and nested
2537/// (`Outer.Inner`) types both carry semantic owner information that the
2538/// resolver needs; reducing either to the final identifier can make distinct
2539/// declarations indistinguishable or make a nested member unreachable.
2540fn qualified_java_type_name(raw: &str) -> Option<String> {
2541    let without_generics = raw.split('<').next().unwrap_or(raw);
2542    let without_arrays = without_generics.split('[').next().unwrap_or(without_generics);
2543    let qualified = without_arrays.trim();
2544    let mut parts = qualified.split('.').filter(|part| !part.is_empty()).peekable();
2545    parts.peek()?;
2546    let segments: Vec<&str> = parts.collect();
2547    if segments.len() < 2 {
2548        return None;
2549    }
2550    let tail = segments.last()?.trim();
2551    if tail.is_empty() {
2552        return None;
2553    }
2554    Some(qualified.to_string())
2555}
2556
2557fn push_java_type_alias(
2558    aliases: &mut Vec<TypeAliasBinding>,
2559    name: &str,
2560    canonical_type: &str,
2561    qualified_type: Option<&str>,
2562) {
2563    if let Some(qualified_type) = qualified_type.filter(|qualified| *qualified != canonical_type) {
2564        push_type_alias(aliases, name, qualified_type);
2565    }
2566    push_type_alias(aliases, name, canonical_type);
2567}
2568
2569/// Append a type-alias binding to `aliases`, skipping empty names and
2570/// self-aliases (`Foo Foo`).
2571fn push_type_alias(aliases: &mut Vec<TypeAliasBinding>, name: &str, type_name: &str) {
2572    let bare_name = name.trim();
2573    if bare_name.is_empty() || bare_name == type_name {
2574        return;
2575    }
2576    aliases.push(TypeAliasBinding {
2577        name: bare_name.to_string(),
2578        type_name: type_name.to_string(),
2579    });
2580}
2581
2582fn expand_java_platform_supertypes(aliases: &mut Vec<TypeAliasBinding>) {
2583    let original = aliases.clone();
2584    for alias in original {
2585        for supertype in java_platform_supertypes(&alias.type_name) {
2586            push_type_alias(aliases, &alias.name, supertype);
2587        }
2588    }
2589}
2590
2591fn java_platform_supertypes(type_name: &str) -> &'static [&'static str] {
2592    match type_name {
2593        "CallableStatement" => &["PreparedStatement", "Statement"],
2594        "PreparedStatement" => &["Statement"],
2595        "Statement" => &[],
2596        "ArrayList" | "LinkedList" | "Vector" => &["List", "Collection", "Iterable"],
2597        "HashSet" | "LinkedHashSet" | "TreeSet" => &["Set", "Collection", "Iterable"],
2598        "HashMap" | "LinkedHashMap" | "TreeMap" => &["Map"],
2599        "List" | "Set" => &["Collection", "Iterable"],
2600        "Collection" => &["Iterable"],
2601        _ => &[],
2602    }
2603}
2604
2605/// Drop duplicate `TypeAliasBinding` entries while preserving source-order.
2606fn dedup_type_aliases(aliases: &mut Vec<TypeAliasBinding>) {
2607    let mut deduped = Vec::new();
2608    for alias in aliases.drain(..) {
2609        if !deduped.contains(&alias) {
2610            deduped.push(alias);
2611        }
2612    }
2613    *aliases = deduped;
2614}
2615
2616fn collect_java_imports(tree: &Tree, file: FileId, src: &[u8]) -> Vec<ImportSpec> {
2617    let mut imports: Vec<_> = collect_kinds(tree, &["import_declaration"])
2618        .into_iter()
2619        .filter_map(|import| java_import_spec(import, file, src))
2620        .collect();
2621    imports.extend(
2622        collect_kinds(tree, &["scoped_type_identifier"])
2623            .into_iter()
2624            .filter(|type_use| !has_ancestor_kind(*type_use, "import_declaration"))
2625            .filter(|type_use| {
2626                type_use
2627                    .parent()
2628                    .is_none_or(|parent| parent.kind() != "scoped_type_identifier")
2629            })
2630            .filter_map(|type_use| java_qualified_type_package_spec(type_use, file, src)),
2631    );
2632    imports.sort_by_key(|import| import.span.start);
2633    imports.dedup_by(|left, right| left.span == right.span && left.module == right.module);
2634    imports
2635}
2636
2637/// Lower a fully-qualified Java type use into file-local package evidence.
2638/// This is not a source import and therefore stays hidden from the public
2639/// import inventory, but the exact qualifier is available to resolver and
2640/// security package gates. Nested class names are harmless here: consumers
2641/// still require an exact rule-declared package prefix.
2642fn java_qualified_type_package_spec(type_use: Node<'_>, file: FileId, src: &[u8]) -> Option<ImportSpec> {
2643    let module = node_text(&type_use, src).trim();
2644    let alias = import_tail_binding(module)?;
2645    Some(ImportSpec {
2646        span: span_of(file, &type_use),
2647        module: module.to_string(),
2648        alias: Some(alias),
2649        is_wildcard: false,
2650        original_name: None,
2651        scope: ImportScope::Local,
2652    })
2653}
2654
2655fn has_ancestor_kind(mut node: Node<'_>, kind: &str) -> bool {
2656    while let Some(parent) = node.parent() {
2657        if parent.kind() == kind {
2658            return true;
2659        }
2660        node = parent;
2661    }
2662    false
2663}
2664
2665fn java_import_spec(import: Node<'_>, file: FileId, src: &[u8]) -> Option<ImportSpec> {
2666    let is_static = import
2667        .children(&mut import.walk())
2668        .any(|child| child.kind() == "static");
2669    let mut named_cursor = import.walk();
2670    let named_children: Vec<_> = import.named_children(&mut named_cursor).collect();
2671    let is_wildcard = named_children.iter().any(|child| child.kind() == "asterisk");
2672    let path_node = named_children
2673        .iter()
2674        .find(|child| matches!(child.kind(), "identifier" | "scoped_identifier"))?;
2675    let full_path = node_text(path_node, src).trim();
2676    if full_path.is_empty() {
2677        return None;
2678    }
2679    let (module, alias, original_name) = if is_static && !is_wildcard {
2680        let (owner, member) = full_path.rsplit_once('.')?;
2681        (
2682            owner.to_string(),
2683            Some(member.to_string()),
2684            Some(member.to_string()),
2685        )
2686    } else {
2687        (
2688            full_path.to_string(),
2689            (!is_wildcard).then(|| import_tail_binding(full_path)).flatten(),
2690            None,
2691        )
2692    };
2693    Some(ImportSpec {
2694        span: span_of(file, &import),
2695        module,
2696        alias,
2697        is_wildcard,
2698        original_name,
2699        scope: ImportScope::Module,
2700    })
2701}
2702
2703fn import_tail_binding(module: &str) -> Option<String> {
2704    let tail = module
2705        .rsplit_once('.')
2706        .map(|(_, tail)| tail)
2707        .unwrap_or(module)
2708        .trim();
2709    (!tail.is_empty() && tail != module).then(|| tail.to_string())
2710}
2711
2712/// Walk the Java tree and map each function/class/method/constructor
2713/// span to its real Visibility from `modifiers` siblings. Java
2714/// privacy rules:
2715///   - `public` → Public
2716///   - `private` → Private
2717///   - `protected` → Protected
2718///   - no modifier → package-private (Visibility::Module)
2719fn collect_java_visibility(
2720    root: Node<'_>,
2721    file: FileId,
2722    src: &[u8],
2723) -> std::collections::HashMap<bonsai_common::Span, Visibility> {
2724    let mut visibility_by_span = std::collections::HashMap::new();
2725    let mut work_stack = vec![root];
2726    while let Some(node) = work_stack.pop() {
2727        let kind = node.kind();
2728        let is_class_or_member_decl = matches!(
2729            kind,
2730            "method_declaration"
2731                | "constructor_declaration"
2732                | "class_declaration"
2733                | "interface_declaration"
2734                | "enum_declaration"
2735                | "annotation_type_declaration"
2736                | "record_declaration"
2737        );
2738        if is_class_or_member_decl {
2739            visibility_by_span.insert(span_of(file, &node), java_node_visibility(&node, src));
2740        }
2741        // Walk every child, named or not — modifiers are usually named but
2742        // `public`/`private` keyword tokens are anonymous.
2743        let mut cursor = node.walk();
2744        for child in node.children(&mut cursor) {
2745            work_stack.push(child);
2746        }
2747    }
2748    visibility_by_span
2749}
2750
2751/// Read the `modifiers` child of a Java declaration to determine its
2752/// declared `Visibility`. No modifier means package-private (Module).
2753fn java_node_visibility(node: &Node<'_>, src: &[u8]) -> Visibility {
2754    let mut cursor = node.walk();
2755    for child in node.children(&mut cursor) {
2756        if child.kind() != "modifiers" {
2757            continue;
2758        }
2759        let mut modifier_cursor = child.walk();
2760        for modifier in child.children(&mut modifier_cursor) {
2761            match node_text(&modifier, src) {
2762                "public" => return Visibility::Public,
2763                "private" => return Visibility::Private,
2764                "protected" => return Visibility::Protected,
2765                _ => {}
2766            }
2767        }
2768    }
2769    // Java's default access is package-private — represented as Module
2770    // in the bonsai visibility lattice.
2771    Visibility::Module
2772}
2773
2774/// Find the `package com.foo.bar;` declaration at the top of the
2775/// file and return its segments. Returns None for files in the
2776/// default (unnamed) package.
2777/// True for class-like decls whose `bases:` we should populate.
2778/// Java emits `interface_declaration`, `enum_declaration`,
2779/// `record_declaration`, `class_declaration`, and
2780/// `annotation_type_declaration`. The adapter maps those Tree-sitter node
2781/// kinds to the corresponding compiler declaration kind in `HANDLER`.
2782fn is_class_like(kind: DeclKind) -> bool {
2783    matches!(
2784        kind,
2785        DeclKind::Class | DeclKind::Interface | DeclKind::Trait | DeclKind::Struct | DeclKind::Enum
2786    )
2787}
2788
2789fn rewrite_java_explicit_constructor_invocations(index: &mut DeclIndex) {
2790    use std::collections::HashMap;
2791
2792    let class_info: HashMap<bonsai_common::SymbolId, (String, Vec<String>)> = index
2793        .defs
2794        .iter()
2795        .filter(|decl| is_class_like(decl.kind))
2796        .map(|decl| (decl.symbol, (decl.name.clone(), decl.bases.clone())))
2797        .collect();
2798
2799    for decl in &mut index.defs {
2800        if !matches!(decl.kind, DeclKind::Constructor) {
2801            continue;
2802        }
2803        let Some(parent) = decl.parent else {
2804            continue;
2805        };
2806        let Some((class_name, bases)) = class_info.get(&parent) else {
2807            continue;
2808        };
2809        let this_ctor = class_name.as_str();
2810        let super_ctor = bases.first().map(String::as_str);
2811        rewrite_java_explicit_constructor_invocations_in_events(&mut decl.flow_events, this_ctor, super_ctor);
2812    }
2813}
2814
2815fn rewrite_java_explicit_constructor_invocations_in_events(
2816    events: &mut [FlowEvent],
2817    this_ctor: &str,
2818    super_ctor: Option<&str>,
2819) {
2820    for event in events {
2821        match event {
2822            FlowEvent::Call {
2823                name,
2824                receiver,
2825                receiver_types,
2826                call_kind,
2827                ..
2828            } => {
2829                let replacement = match name.trim() {
2830                    "this" => Some((this_ctor, "this")),
2831                    "super" => super_ctor.map(|ctor| (ctor, "super")),
2832                    _ => None,
2833                };
2834                if let Some((replacement, replacement_receiver)) =
2835                    replacement.filter(|(replacement, _)| !replacement.is_empty())
2836                {
2837                    name.clear();
2838                    name.push_str(replacement);
2839                    *receiver = Some(replacement_receiver.to_string());
2840                    receiver_types.clear();
2841                    // Tree-sitter classifies `this(...)` / `super(...)` as
2842                    // `explicit_constructor_invocation`. Preserve that AST
2843                    // fact through HIR instead of disguising the edge as an
2844                    // ordinary receiver method call. Constructor identity is
2845                    // what lets the semantic resolver select the class-named
2846                    // declaration and lets the IDG carry receiver state
2847                    // through an inheritance chain.
2848                    *call_kind = bonsai_lang_api::CallKind::Constructor;
2849                }
2850            }
2851            FlowEvent::Branch {
2852                then_events,
2853                else_events,
2854                ..
2855            } => {
2856                rewrite_java_explicit_constructor_invocations_in_events(then_events, this_ctor, super_ctor);
2857                rewrite_java_explicit_constructor_invocations_in_events(else_events, this_ctor, super_ctor);
2858            }
2859            FlowEvent::Loop { body, .. } | FlowEvent::Defer { body, .. } | FlowEvent::Using { body, .. } => {
2860                rewrite_java_explicit_constructor_invocations_in_events(body, this_ctor, super_ctor);
2861            }
2862            FlowEvent::Try {
2863                body,
2864                catch_events,
2865                finally_events,
2866                ..
2867            } => {
2868                rewrite_java_explicit_constructor_invocations_in_events(body, this_ctor, super_ctor);
2869                rewrite_java_explicit_constructor_invocations_in_events(catch_events, this_ctor, super_ctor);
2870                rewrite_java_explicit_constructor_invocations_in_events(
2871                    finally_events,
2872                    this_ctor,
2873                    super_ctor,
2874                );
2875            }
2876            FlowEvent::Assign { .. }
2877            | FlowEvent::AggregateAssign { .. }
2878            | FlowEvent::Return { .. }
2879            | FlowEvent::Throw { .. }
2880            | FlowEvent::Break { .. }
2881            | FlowEvent::Continue { .. }
2882            | FlowEvent::Yield { .. }
2883            | FlowEvent::Await { .. }
2884            | FlowEvent::Lifecycle { .. } => {}
2885        }
2886    }
2887}
2888
2889/// Walk every Java class-like declaration and collect the
2890/// superclass + super_interfaces names. Java grammar:
2891///
2892///   `class C extends B implements I, J { ... }` →
2893///     superclass: (superclass (type_identifier))
2894///     interfaces: (super_interfaces (type_list (type_identifier) (type_identifier)))
2895///
2896/// `interface_declaration` uses `extends_interfaces` instead of
2897/// `superclass`. `record_declaration` only has `interfaces`.
2898fn collect_java_class_bases(
2899    tree: &Tree,
2900    file: FileId,
2901    src: &[u8],
2902) -> Vec<(bonsai_common::Span, Vec<String>)> {
2903    let mut out = Vec::new();
2904    let class_kinds = &[
2905        "class_declaration",
2906        "interface_declaration",
2907        "record_declaration",
2908        "enum_declaration",
2909        "annotation_type_declaration",
2910    ];
2911    for class_node in collect_kinds(tree, class_kinds) {
2912        let mut bases: Vec<String> = Vec::new();
2913        // `superclass` field — single parent.
2914        if let Some(sc) = class_node.child_by_field_name("superclass") {
2915            collect_java_base_names(sc, src, &mut bases);
2916        }
2917        // `interfaces` field — `super_interfaces` wrapping `type_list`.
2918        if let Some(ifaces) = class_node.child_by_field_name("interfaces") {
2919            collect_java_base_names(ifaces, src, &mut bases);
2920        }
2921        // `interface_declaration` carries `extends_interfaces`.
2922        if let Some(extends) = class_node.child_by_field_name("extends_interfaces") {
2923            collect_java_base_names(extends, src, &mut bases);
2924        }
2925        // `permits` clause from sealed classes. The matcher consults
2926        // Decl.bases for hierarchy resolution (e.g. `kind: param` rules
2927        // with `in_class:` constraints). For sealed types both
2928        // directions of the relationship are useful: the parent
2929        // declares which subtypes inherit, so emitting the permits
2930        // members lets cross-file rules that key on the ancestor
2931        // type still resolve through the sealed parent's permitted
2932        // subclass list.
2933        if let Some(permits) = class_node.child_by_field_name("permits") {
2934            collect_java_base_names(permits, src, &mut bases);
2935        }
2936        if !bases.is_empty() {
2937            out.push((span_of(file, &class_node), bases));
2938        }
2939    }
2940    out
2941}
2942
2943fn collect_java_class_string_constants(
2944    tree: &Tree,
2945    file: FileId,
2946    src: &[u8],
2947) -> Vec<(bonsai_common::Span, Vec<FlowEvent>)> {
2948    let class_kinds = &[
2949        "class_declaration",
2950        "interface_declaration",
2951        "record_declaration",
2952        "enum_declaration",
2953        "annotation_type_declaration",
2954    ];
2955    let mut out = Vec::new();
2956    for class_node in collect_kinds(tree, class_kinds) {
2957        let Some(body) = class_node.child_by_field_name("body") else {
2958            continue;
2959        };
2960        let mut events = Vec::new();
2961        let mut cursor = body.walk();
2962        for child in body.named_children(&mut cursor) {
2963            if child.kind() == "field_declaration" {
2964                collect_java_final_string_field_assigns(child, file, src, &mut events);
2965            }
2966        }
2967        if !events.is_empty() {
2968            out.push((span_of(file, &class_node), events));
2969        }
2970    }
2971    out
2972}
2973
2974fn collect_java_final_string_field_assigns(
2975    field: Node<'_>,
2976    file: FileId,
2977    src: &[u8],
2978    out: &mut Vec<FlowEvent>,
2979) {
2980    if !java_field_has_modifier(field, src, "final") || !java_field_type_is_string(field, src) {
2981        return;
2982    }
2983    let mut cursor = field.walk();
2984    for child in field.named_children(&mut cursor) {
2985        if child.kind() != "variable_declarator" {
2986            continue;
2987        }
2988        let Some(name_node) = child.child_by_field_name("name") else {
2989            continue;
2990        };
2991        let Some(value_node) = child.child_by_field_name("value") else {
2992            continue;
2993        };
2994        if value_node.kind() != "string_literal" {
2995            continue;
2996        }
2997        out.push(FlowEvent::Assign {
2998            span: span_of(file, &child),
2999            target: node_text(&name_node, src).trim().to_string(),
3000            source_name: None,
3001            source_call: None,
3002            source_call_args: Vec::new(),
3003            source_names: Vec::new(),
3004            declares_new_binding: false,
3005            value_kind: Some(AssignValueKind::Literal),
3006        });
3007    }
3008}
3009
3010fn java_field_has_modifier(field: Node<'_>, src: &[u8], wanted: &str) -> bool {
3011    let mut cursor = field.walk();
3012    for child in field.named_children(&mut cursor) {
3013        if child.kind() != "modifiers" {
3014            continue;
3015        }
3016        if node_text(&child, src)
3017            .split_ascii_whitespace()
3018            .any(|modifier| modifier == wanted)
3019        {
3020            return true;
3021        }
3022    }
3023    false
3024}
3025
3026fn java_field_type_is_string(field: Node<'_>, src: &[u8]) -> bool {
3027    let Some(type_node) = field.child_by_field_name("type") else {
3028        return false;
3029    };
3030    matches!(
3031        canonical_java_type_name(node_text(&type_node, src)).as_deref(),
3032        Some("String")
3033    )
3034}
3035
3036fn attach_java_class_string_constants(
3037    index: &mut DeclIndex,
3038    constants_by_class: &[(bonsai_common::Span, Vec<FlowEvent>)],
3039) {
3040    if constants_by_class.is_empty() {
3041        return;
3042    }
3043    let parent_by_symbol: std::collections::HashMap<_, _> = index
3044        .defs
3045        .iter()
3046        .filter_map(|decl| Some((decl.symbol, decl.parent?)))
3047        .collect();
3048    let class_symbol_by_span: std::collections::HashMap<_, _> = index
3049        .defs
3050        .iter()
3051        .filter(|decl| is_class_like(decl.kind))
3052        .map(|decl| (decl.span, decl.symbol))
3053        .collect();
3054    let constants_by_symbol: std::collections::HashMap<_, _> = constants_by_class
3055        .iter()
3056        .filter_map(|(span, events)| {
3057            class_symbol_by_span
3058                .get(span)
3059                .copied()
3060                .map(|symbol| (symbol, events))
3061        })
3062        .collect();
3063
3064    for decl in &mut index.defs {
3065        if !matches!(
3066            decl.kind,
3067            DeclKind::Function | DeclKind::Method | DeclKind::Constructor
3068        ) {
3069            continue;
3070        }
3071        let mut ancestors = Vec::new();
3072        let mut parent = decl.parent;
3073        while let Some(symbol) = parent {
3074            ancestors.push(symbol);
3075            parent = parent_by_symbol.get(&symbol).copied();
3076        }
3077        if ancestors.is_empty() {
3078            continue;
3079        }
3080
3081        let mut visible_constants = Vec::new();
3082        for symbol in ancestors.into_iter().rev() {
3083            if let Some(events) = constants_by_symbol.get(&symbol) {
3084                visible_constants.extend((*events).iter().cloned());
3085            }
3086        }
3087        visible_constants.retain(|event| {
3088            let FlowEvent::Assign { target, .. } = event else {
3089                return false;
3090            };
3091            !decl.params.iter().any(|param| param == target)
3092                && !decl
3093                    .flow_events
3094                    .iter()
3095                    .any(|event| flow_event_assigns_target(event, target))
3096        });
3097        if !visible_constants.is_empty() {
3098            visible_constants.extend(std::mem::take(&mut decl.flow_events));
3099            decl.flow_events = visible_constants;
3100        }
3101    }
3102}
3103
3104fn flow_event_assigns_target(event: &FlowEvent, wanted: &str) -> bool {
3105    match event {
3106        FlowEvent::Assign { target, .. } => target == wanted,
3107        FlowEvent::Branch {
3108            then_events,
3109            else_events,
3110            ..
3111        } => {
3112            then_events
3113                .iter()
3114                .any(|event| flow_event_assigns_target(event, wanted))
3115                || else_events
3116                    .iter()
3117                    .any(|event| flow_event_assigns_target(event, wanted))
3118        }
3119        FlowEvent::Loop { body, .. } | FlowEvent::Defer { body, .. } | FlowEvent::Using { body, .. } => {
3120            body.iter().any(|event| flow_event_assigns_target(event, wanted))
3121        }
3122        FlowEvent::Try {
3123            body,
3124            catch_events,
3125            finally_events,
3126            ..
3127        } => {
3128            body.iter().any(|event| flow_event_assigns_target(event, wanted))
3129                || catch_events
3130                    .iter()
3131                    .any(|event| flow_event_assigns_target(event, wanted))
3132                || finally_events
3133                    .iter()
3134                    .any(|event| flow_event_assigns_target(event, wanted))
3135        }
3136        _ => false,
3137    }
3138}
3139
3140/// Pull every `type_identifier` / `scoped_type_identifier` /
3141/// `generic_type` descendant of a Java parent-clause node and push
3142/// the canonical short name into `out`. Skips internal punctuation
3143/// nodes.
3144fn collect_java_base_names(node: Node<'_>, src: &[u8], out: &mut Vec<String>) {
3145    let mut stack = vec![node];
3146    while let Some(n) = stack.pop() {
3147        match n.kind() {
3148            "type_identifier" | "scoped_type_identifier" | "generic_type" => {
3149                if let Some(name) = canonical_java_type_name(node_text(&n, src)) {
3150                    if !out.iter().any(|b| b == &name) {
3151                        out.push(name);
3152                    }
3153                }
3154            }
3155            _ => {}
3156        }
3157        let mut cursor = n.walk();
3158        for child in n.named_children(&mut cursor) {
3159            stack.push(child);
3160        }
3161    }
3162}
3163
3164/// Walk `decl.flow_events` recursively and populate
3165/// `Throw::thrown_type` / `Try::catch_types` from the Java parse
3166/// tree by span lookup. Java syntax:
3167///   throw new IOException("...")  → thrown_type: "IOException"
3168///   throw err                     → thrown_type: None (need data-flow)
3169///   `try { } catch (IOException e) { } catch (A | B e) { }`
3170///                                 → `catch_types = vec!["IOException", "A", "B"]`
3171fn populate_java_exception_types(events: &mut [bonsai_lang_api::FlowEvent], tree: &Tree, src: &[u8]) {
3172    use bonsai_lang_api::FlowEvent;
3173    for event in events {
3174        match event {
3175            FlowEvent::Throw {
3176                span, thrown_type, ..
3177            } => {
3178                if thrown_type.is_some() {
3179                    continue;
3180                }
3181                if let Some(node) =
3182                    bonsai_lang_api::kit::node_at_span(tree.root_node(), *span, &["throw_statement"])
3183                {
3184                    if let Some(name) = java_thrown_type_for_node(node, src) {
3185                        *thrown_type = Some(name);
3186                    }
3187                }
3188            }
3189            FlowEvent::Try {
3190                span,
3191                body,
3192                catch_events,
3193                finally_events,
3194                catch_types,
3195                catch_param,
3196                ..
3197            } => {
3198                if let Some(node) =
3199                    bonsai_lang_api::kit::node_at_span(tree.root_node(), *span, &["try_statement"])
3200                {
3201                    if catch_types.is_empty() {
3202                        *catch_types = collect_java_catch_types(node, src);
3203                    }
3204                    // The kit's generic catch_param extractor sometimes
3205                    // picks the type identifier instead of the variable
3206                    // name on Java's `catch (T name)` shape. Fix in the
3207                    // adapter where we have the structural context.
3208                    if let Some(name) = collect_java_catch_param_name(node, src) {
3209                        *catch_param = Some(name);
3210                    }
3211                }
3212                populate_java_exception_types(body, tree, src);
3213                populate_java_exception_types(catch_events, tree, src);
3214                populate_java_exception_types(finally_events, tree, src);
3215            }
3216            FlowEvent::Branch {
3217                then_events,
3218                else_events,
3219                ..
3220            } => {
3221                populate_java_exception_types(then_events, tree, src);
3222                populate_java_exception_types(else_events, tree, src);
3223            }
3224            FlowEvent::Loop { body, .. } | FlowEvent::Defer { body, .. } | FlowEvent::Using { body, .. } => {
3225                populate_java_exception_types(body, tree, src);
3226            }
3227            _ => {}
3228        }
3229    }
3230}
3231
3232fn java_thrown_type_for_node(throw_node: Node<'_>, src: &[u8]) -> Option<String> {
3233    // throw_statement > object_creation_expression > type_identifier (or generic_type)
3234    let mut cursor = throw_node.walk();
3235    for child in throw_node.named_children(&mut cursor) {
3236        if child.kind() == "object_creation_expression" {
3237            if let Some(t) = child.child_by_field_name("type") {
3238                return Some(bonsai_lang_api::kit::canonical_simple_type_name(node_text(
3239                    &t, src,
3240                )));
3241            }
3242            // Fallback: walk for first type_identifier descendant.
3243            let mut tcur = child.walk();
3244            for descendant in child.named_children(&mut tcur) {
3245                if matches!(
3246                    descendant.kind(),
3247                    "type_identifier" | "generic_type" | "scoped_type_identifier"
3248                ) {
3249                    return Some(bonsai_lang_api::kit::canonical_simple_type_name(node_text(
3250                        &descendant,
3251                        src,
3252                    )));
3253                }
3254            }
3255        }
3256    }
3257    None
3258}
3259
3260fn collect_java_catch_param_name(try_node: Node<'_>, src: &[u8]) -> Option<String> {
3261    // Java's `catch (T name)` parses as:
3262    //   catch_clause
3263    //     catch_formal_parameter
3264    //       <modifiers>?
3265    //       catch_type
3266    //         type_identifier
3267    //       name: identifier
3268    // We want the `name` field of the catch_formal_parameter.
3269    let mut cursor = try_node.walk();
3270    for child in try_node.named_children(&mut cursor) {
3271        if child.kind() != "catch_clause" {
3272            continue;
3273        }
3274        let mut ccur = child.walk();
3275        for sub in child.named_children(&mut ccur) {
3276            if sub.kind() != "catch_formal_parameter" {
3277                continue;
3278            }
3279            if let Some(n) = sub.child_by_field_name("name") {
3280                return Some(node_text(&n, src).trim().to_string());
3281            }
3282            // Fallback: rightmost `identifier` (after the type).
3283            let mut pcur = sub.walk();
3284            let mut last_ident: Option<Node<'_>> = None;
3285            for ptype in sub.named_children(&mut pcur) {
3286                if ptype.kind() == "identifier" {
3287                    last_ident = Some(ptype);
3288                }
3289            }
3290            if let Some(n) = last_ident {
3291                return Some(node_text(&n, src).trim().to_string());
3292            }
3293        }
3294    }
3295    None
3296}
3297
3298fn collect_java_catch_types(try_node: Node<'_>, src: &[u8]) -> Vec<String> {
3299    let mut out: Vec<String> = Vec::new();
3300    let mut cursor = try_node.walk();
3301    for child in try_node.named_children(&mut cursor) {
3302        if child.kind() != "catch_clause" {
3303            continue;
3304        }
3305        // catch_clause > catch_formal_parameter > catch_type > type_identifier (one or more)
3306        let mut ccur = child.walk();
3307        for sub in child.named_children(&mut ccur) {
3308            if sub.kind() != "catch_formal_parameter" {
3309                continue;
3310            }
3311            let mut pcur = sub.walk();
3312            for ptype in sub.named_children(&mut pcur) {
3313                if ptype.kind() == "catch_type" {
3314                    let mut tcur = ptype.walk();
3315                    for t in ptype.named_children(&mut tcur) {
3316                        if matches!(
3317                            t.kind(),
3318                            "type_identifier" | "generic_type" | "scoped_type_identifier"
3319                        ) {
3320                            let name = bonsai_lang_api::kit::canonical_simple_type_name(node_text(&t, src));
3321                            if !name.is_empty() && !out.iter().any(|x| x == &name) {
3322                                out.push(name);
3323                            }
3324                        }
3325                    }
3326                }
3327            }
3328        }
3329    }
3330    out
3331}
3332
3333/// Rewrite Java's reflection chain `Class.forName("X").getMethod("Y")
3334/// .invoke(target, args...)` into a synthesized direct call to
3335/// `X.Y(args...)` so the resolver narrows it like a normal method
3336/// dispatch. Walks `flow_events` in source order, building an alias
3337/// map of `var → "Class"` and `var → "Class.Method"` entries from
3338/// constant-string `forName` / `getMethod` calls. When it sees
3339/// `m.invoke(target, ...args)` where `m` resolves to a known
3340/// "Class.Method", rewrites the Call's `name` to that target and
3341/// drops the leading `target` (or `null`) arg so the remaining args
3342/// align with the real method's parameters.
3343///
3344/// Dynamic forms (computed string args) stay unrewritten and the
3345/// `reflection: Unsupported` rule continues to gate them at rulepack
3346/// load time. This is the Java analog of P2.1's Python rewrite.
3347fn rewrite_java_reflection_chain(events: &mut [bonsai_lang_api::FlowEvent]) {
3348    use bonsai_lang_api::FlowEvent;
3349    use std::collections::HashMap;
3350    // var name -> what it points at, accumulated across the walk:
3351    //   "c" -> "Sink"        (after `Class<?> c = Class.forName("Sink")`)
3352    //   "m" -> "Sink.run"    (after `Method m = c.getMethod("run", ...)`)
3353    let mut reflective_alias: HashMap<String, String> = HashMap::new();
3354    for event in events.iter_mut() {
3355        match event {
3356            FlowEvent::Assign {
3357                target,
3358                source_call,
3359                source_call_args,
3360                ..
3361            } => {
3362                // Only the constant-string forms are usable; the second
3363                // arg / dynamic forms stay unrewritten.
3364                let Some(callee) = source_call else { continue };
3365                let Some(literal_arg) = source_call_args.first() else {
3366                    continue;
3367                };
3368                let Some(literal_text) = strip_java_string_quotes(literal_arg) else {
3369                    continue;
3370                };
3371                // `Class.forName("X")` — record `target -> "X"`.
3372                let is_for_name = callee == "Class.forName" || callee.ends_with(".forName");
3373                if is_for_name {
3374                    reflective_alias.insert(target.clone(), literal_text);
3375                    continue;
3376                }
3377                // `<receiver>.getMethod("Y")` — chain only if receiver
3378                // is itself a Class<?> we tracked. Result: `target ->
3379                // "<receiver-class>.Y"`.
3380                if let Some(get_method_receiver) = callee.strip_suffix(".getMethod") {
3381                    if let Some(class_name) = reflective_alias.get(get_method_receiver) {
3382                        let chained = format!("{class_name}.{literal_text}");
3383                        reflective_alias.insert(target.clone(), chained);
3384                    }
3385                }
3386            }
3387            FlowEvent::Call {
3388                name, receiver, args, ..
3389            } => {
3390                // `<receiver>.invoke(target_or_null, arg1, ..)` is the
3391                // Java reflection escape hatch. Rewrite when the
3392                // receiver was bound to a known Method handle.
3393                let Some(receiver_name) = receiver.as_deref() else {
3394                    continue;
3395                };
3396                if !name.ends_with(".invoke") {
3397                    continue;
3398                }
3399                let Some(target_class_method) = reflective_alias.get(receiver_name) else {
3400                    continue;
3401                };
3402                // Rewrite the call to point at the underlying method.
3403                name.clone_from(target_class_method);
3404                // `m.invoke(target, a, b)` ⇒ `Class.Method(a, b)`:
3405                // drop the first arg (the receiver / `null` static
3406                // target) so the remaining args line up with the real
3407                // method's parameter list.
3408                if !args.is_empty() {
3409                    args.remove(0);
3410                }
3411                // Update `receiver` to the class part of the qualified
3412                // name, e.g. "Sink.run" -> Some("Sink"). The resolver
3413                // uses this to anchor the dispatch.
3414                *receiver = target_class_method
3415                    .rsplit_once('.')
3416                    .map(|(class_part, _)| class_part.to_string());
3417            }
3418            // Reflection chains can hide inside any control-flow
3419            // container — keep walking.
3420            FlowEvent::Branch {
3421                then_events,
3422                else_events,
3423                ..
3424            } => {
3425                rewrite_java_reflection_chain(then_events);
3426                rewrite_java_reflection_chain(else_events);
3427            }
3428            FlowEvent::Loop { body, .. } | FlowEvent::Defer { body, .. } | FlowEvent::Using { body, .. } => {
3429                rewrite_java_reflection_chain(body);
3430            }
3431            FlowEvent::Try {
3432                body,
3433                catch_events,
3434                finally_events,
3435                ..
3436            } => {
3437                rewrite_java_reflection_chain(body);
3438                rewrite_java_reflection_chain(catch_events);
3439                rewrite_java_reflection_chain(finally_events);
3440            }
3441            _ => {}
3442        }
3443    }
3444}
3445
3446/// Strip surrounding `"..."` quotes from a Java string-literal arg-text
3447/// representation, returning the inner content. Returns `None` for any
3448/// non-literal form (variable, expression, single-quoted char, etc.).
3449fn strip_java_string_quotes(text: &str) -> Option<String> {
3450    let trimmed = text.trim();
3451    if trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() >= 2 {
3452        return Some(trimmed[1..trimmed.len() - 1].to_string());
3453    }
3454    None
3455}
3456
3457fn extract_java_package(root: Node<'_>, src: &[u8]) -> Option<Vec<String>> {
3458    let mut cursor = root.walk();
3459    for child in root.children(&mut cursor) {
3460        if child.kind() != "package_declaration" {
3461            continue;
3462        }
3463        // package_declaration's `name` child is a `scoped_identifier`
3464        // (or bare `identifier` for single-segment packages).
3465        let mut sub = child.walk();
3466        for subchild in child.children(&mut sub) {
3467            if matches!(subchild.kind(), "scoped_identifier" | "identifier") {
3468                let text = node_text(&subchild, src);
3469                let segments: Vec<String> = text
3470                    .split('.')
3471                    .map(str::trim)
3472                    .filter(|s| !s.is_empty())
3473                    .map(str::to_string)
3474                    .collect();
3475                if !segments.is_empty() {
3476                    return Some(segments);
3477                }
3478            }
3479        }
3480    }
3481    None
3482}