mod parse_recovery;
use bonsai_common::{FileId, Span};
use bonsai_lang_api::{
decl_index_with_handler,
kit::{
collect_kinds, language_from_pack, node_text, package_module_segments_with_workspace_prefix,
parse_with, span_of,
},
AdapterContext, AdapterError, AssignValueKind, CallTargetExtraction, CharacterConstraintDomain,
CharacterConstraintFact, CharacterConstraintOutput, CharacterSubstitutionDomain,
CharacterSubstitutionFact, ConditionEquality, ConditionExpressionFact, ConditionOperandFact, DeclIndex,
DeclKind, FileSnapshot, FiniteLiteralSelectionFact, FlowEvent, GrammarHandler, ImportIndex, ImportScope,
ImportSpec, LanguageAdapter, LanguageCapabilities, LanguageId, ParseRecoveryEdit, PatternBindingSite,
SameOriginPathConstraintFact, StaticScalarValue, StringCompositionFact, StringCompositionPart,
SyntaxTree, TypeAliasBinding, Vfs, Visibility, EMPTY_HANDLER,
};
use parse_recovery::java_parse_recovery_edits;
use tree_sitter::{Language, Node, Tree};
fn java_call_target<'tree>(node: Node<'tree>, src: &[u8]) -> Option<CallTargetExtraction<'tree>> {
match node.kind() {
"method_invocation" => {
let member = node.child_by_field_name("name")?;
let member_text = node_text(&member, src).trim();
if member_text.is_empty() {
return None;
}
let full_text = node.child_by_field_name("object").map_or_else(
|| member_text.to_string(),
|receiver| format!("{}.{}", node_text(&receiver, src).trim(), member_text),
);
Some(CallTargetExtraction {
node: member,
full_text,
})
}
"object_creation_expression" => {
let target = node.child_by_field_name("type")?;
let full_text = node_text(&target, src).trim();
(!full_text.is_empty()).then_some(CallTargetExtraction {
node: target,
full_text: full_text.to_string(),
})
}
"explicit_constructor_invocation" => {
let target = node
.child_by_field_name("constructor")
.or_else(|| node.named_child(0))?;
let full_text = node_text(&target, src).trim();
(!full_text.is_empty()).then_some(CallTargetExtraction {
node: target,
full_text: full_text.to_string(),
})
}
_ => None,
}
}
fn java_foreach_binding(node: Node<'_>) -> Option<(Node<'_>, Node<'_>)> {
(node.kind() == "enhanced_for_statement")
.then(|| {
Some((
node.child_by_field_name("name")?,
node.child_by_field_name("value")?,
))
})
.flatten()
}
fn java_pattern_bindings(node: Node<'_>) -> Vec<PatternBindingSite<'_>> {
let Some(condition) = node.child_by_field_name("condition") else {
return Vec::new();
};
let mut sites = Vec::new();
let mut stack = vec![condition];
while let Some(current) = stack.pop() {
if current.kind() == "instanceof_expression" {
if let Some(source) = current.child_by_field_name("left") {
if let Some(name) = current.child_by_field_name("name") {
sites.push(PatternBindingSite {
span_node: current,
pattern: name,
source,
});
}
if let Some(pattern) = current.child_by_field_name("pattern") {
java_pattern_binding_identifiers(pattern, &mut sites, current, source);
}
}
continue;
}
let mut cursor = current.walk();
stack.extend(current.named_children(&mut cursor));
}
if let Some(body) = node.child_by_field_name("body") {
let mut stack = vec![body];
while let Some(current) = stack.pop() {
if current.kind() == "switch_label" {
let mut cursor = current.walk();
for pattern in current.named_children(&mut cursor) {
java_pattern_binding_identifiers(pattern, &mut sites, current, condition);
}
continue;
}
let mut cursor = current.walk();
stack.extend(current.named_children(&mut cursor));
}
}
sites
}
fn java_pattern_binding_identifiers<'tree>(
pattern: Node<'tree>,
out: &mut Vec<PatternBindingSite<'tree>>,
span_node: Node<'tree>,
source: Node<'tree>,
) {
if matches!(pattern.kind(), "type_pattern" | "record_pattern_component") {
let mut cursor = pattern.walk();
if let Some(name) = pattern
.named_children(&mut cursor)
.filter(|child| child.kind() == "identifier")
.last()
{
out.push(PatternBindingSite {
span_node,
pattern: name,
source,
});
}
return;
}
if !matches!(pattern.kind(), "record_pattern" | "record_pattern_body") {
return;
}
let mut cursor = pattern.walk();
for child in pattern.named_children(&mut cursor) {
java_pattern_binding_identifiers(child, out, span_node, source);
}
}
pub const LANG_ID: LanguageId = LanguageId::new("java");
const PACK_NAME: &str = "java";
const MODULE_SOURCE_ROOTS: &[&[&str]] = &[
&["src", "main", "java"],
&["src", "test", "java"],
&["src", "java"],
];
const HANDLER: GrammarHandler = GrammarHandler {
expression_value_kind_extractor: None,
literal_value_kinds: &[
"null_literal",
"boolean_literal",
"decimal_integer_literal",
"hex_integer_literal",
"octal_integer_literal",
"binary_integer_literal",
"decimal_floating_point_literal",
"hex_floating_point_literal",
"true",
"false",
],
string_literal_kinds: &["string_literal", "character_literal", "template_expression"],
comment_kinds: &["line_comment", "block_comment"],
doc_comment_prefixes: &["/**"],
decorator_kinds: &[
"annotation",
"marker_annotation",
"normal_annotation",
"single_element_annotation",
],
parameter_container_kinds: &["formal_parameters"],
parameter_kinds: &["formal_parameter", "spread_parameter", "receiver_parameter"],
parameter_modifier_kinds: &["modifiers"],
parameter_annotation_kinds: &[
"annotation",
"marker_annotation",
"normal_annotation",
"single_element_annotation",
],
variadic_parameter_kinds: &["spread_parameter"],
binding_identifier_kinds: &["identifier"],
pattern_binding_extractor: Some(java_pattern_bindings),
identifier_kinds: &["identifier"],
positional_aggregate_kinds: &["array_initializer", "array_creation_expression"],
aggregate_value_field_names: &["value"],
aggregate_syntax_only_kinds: &["type_identifier"],
transparent_call_wrapper_kinds: &["field_access", "parenthesized_expression"],
assignment_target_wrapper_kinds: &["variable_declarator"],
binding_declaration_keyword_spellings: &["final"],
fn_kinds: &["method_declaration", "constructor_declaration"],
call_kinds: &[
"method_invocation",
"object_creation_expression",
"explicit_constructor_invocation",
],
constructor_call_kinds: &["object_creation_expression", "explicit_constructor_invocation"],
call_callee_field_names: &["name", "constructor"],
call_receiver_field_names: &["object"],
call_member_field_names: &["name", "constructor"],
constructor_type_field_names: &["type"],
call_target_extractor: Some(java_call_target),
call_argument_field_names: &["arguments"],
call_argument_container_kinds: &["argument_list"],
lambda_body_field_names: &["body"],
argument_passing_mode_extractor: None,
constructor_names: bonsai_lang_api::NO_CONSTRUCTOR_METHOD_NAMES,
runtime_type_guard_operators: &["instanceof"],
runtime_type_wrapper_kinds: &["parenthesized_expression"],
call_ref_kinds: &[
"method_invocation",
"object_creation_expression",
"explicit_constructor_invocation",
"method_reference",
],
callable_reference_kinds: &["method_reference"],
member_expression_kinds: &["field_access"],
subscript_expression_kinds: &["array_access"],
member_base_field_names: &["object"],
member_name_field_names: &["field"],
subscript_base_field_names: &["array"],
subscript_index_field_names: &["index"],
class_kinds: &[
"class_declaration",
"interface_declaration",
"enum_declaration",
"annotation_type_declaration",
"record_declaration",
],
class_decl_kinds: &[
("interface_declaration", DeclKind::Interface),
("annotation_type_declaration", DeclKind::Interface),
("enum_declaration", DeclKind::Enum),
("record_declaration", DeclKind::Class),
("class_declaration", DeclKind::Class),
],
method_owner_barrier_kinds: &["object_creation_expression"],
method_kinds: &["method_declaration"],
method_context_kinds: &[
"class_declaration",
"interface_declaration",
"enum_declaration",
"annotation_type_declaration",
"record_declaration",
],
constructor_method_kinds: &["constructor_declaration"],
if_kinds: &["if_statement", "switch_expression"],
branch_then_field_names: &["consequence", "body"],
branch_else_field_names: &["alternative"],
branch_condition_field_names: &["condition", "value"],
loop_body_field_names: &["body"],
loop_body_kinds: &["block", "expression_statement"],
branch_arm_kinds: &["block", "expression_statement", "switch_block_statement_group"],
for_kinds: &["for_statement"],
foreach_kinds: &["enhanced_for_statement"],
foreach_binding_extractor: Some(java_foreach_binding),
while_kinds: &["while_statement"],
do_kinds: &["do_statement"],
assignment_kinds: &["assignment_expression", "variable_declarator", "resource"],
compound_assignment_operators: &[
"+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", "&=", "^=", "|=",
],
return_kinds: &["return_statement"],
throw_kinds: &["throw_statement"],
lambda_kinds: &["lambda_expression"],
try_kinds: &["try_statement", "try_with_resources_statement"],
catch_kinds: &["catch_clause"],
finally_kinds: &["finally_clause"],
break_kinds: &["break_statement"],
continue_kinds: &["continue_statement"],
control_label_field_names: &["label"],
yield_kinds: &["yield_statement"],
yield_value_field_names: &["value"],
try_body_field_names: &["body"],
implicit_receiver_names: &["this", "super"],
..EMPTY_HANDLER
};
#[derive(Debug, Default, Copy, Clone)]
pub struct JavaAdapter;
impl JavaAdapter {
#[must_use]
pub fn new() -> Self {
Self
}
}
impl LanguageAdapter for JavaAdapter {
fn language_id(&self) -> LanguageId {
LANG_ID
}
fn display_name(&self) -> &'static str {
"Java"
}
fn file_extensions(&self) -> &'static [&'static str] {
&["java"]
}
fn tree_sitter_language(&self) -> Result<Language, AdapterError> {
language_from_pack(PACK_NAME)
}
fn parse_recovery_edits(
&self,
snapshot: &FileSnapshot,
_vfs: &Vfs,
tree: &SyntaxTree,
) -> Vec<ParseRecoveryEdit> {
java_parse_recovery_edits(snapshot, tree)
}
fn capabilities(&self) -> LanguageCapabilities {
LanguageCapabilities {
module_default_export_names: &[],
universal_type_names: &["Object"],
module_path_syntax: bonsai_lang_api::ModulePathSyntax::none(),
exceptions: bonsai_lang_api::CapabilityLevel::Exact,
reflection: bonsai_lang_api::CapabilityLevel::Partial,
receiver_types: bonsai_lang_api::CapabilityLevel::Partial,
field_places_complete: true,
constructor_method_names: bonsai_lang_api::NO_CONSTRUCTOR_METHOD_NAMES,
super_receiver_tokens: &["super"],
implicit_receiver_tokens: &["this"],
receiver_type_syntax: bonsai_lang_api::ReceiverTypeSyntax {
wrapper_calls: &[],
class_object_suffixes: &[".class"],
},
call_text_prefilter: bonsai_lang_api::CallTextPrefilter::Parenthesized,
..LanguageCapabilities::partial_baseline()
}
}
fn extract_declarations(&self, file: FileId, ctx: &AdapterContext<'_>) -> DeclIndex {
let mut index = decl_index_with_handler(PACK_NAME, file, ctx, &HANDLER);
let Some((snapshot, tree)) = parse_with(PACK_NAME, file, ctx) else {
return index;
};
let src = snapshot.text.as_bytes();
populate_java_condition_expressions(&mut index.branch_conditions, &tree, file, src);
populate_java_static_scalar_facts(&mut index, &tree, file, src);
populate_java_immutable_assignment_facts(&mut index, &tree, file, src);
index.string_compositions = java_string_compositions(&tree, file, src);
index.finite_literal_selections = java_finite_literal_selections(&index, &tree, file, src);
index.character_substitutions = java_character_substitutions(&index.defs, &tree, file, src);
index.character_constraints = bonsai_lang_api::character_constraints_from_substitutions(
&index.defs,
&index.character_substitutions,
);
index.same_origin_path_constraints = java_same_origin_path_constraints(&index, &tree, file, src);
index
.character_constraints
.extend(java_compiled_pattern_constraints(&index, &tree, file, src));
index
.character_constraints
.sort_by_key(|fact| (fact.transform_span.start, fact.transform_span.end));
index
.character_constraints
.dedup_by_key(|fact| fact.transform_span);
bonsai_lang_api::populate_decl_return_types(&mut index, &tree, src, &HANDLER);
for decl in &mut index.defs {
populate_java_exception_types(&mut decl.flow_events, &tree, src);
rewrite_java_reflection_chain(&mut decl.flow_events);
}
let field_aliases = collect_java_type_aliases(tree.root_node(), src, &["field_declaration"]);
let method_aliases = collect_java_method_type_aliases(&tree, file, src, &field_aliases);
for decl in &mut index.defs {
if let Some(aliases) = method_aliases
.iter()
.find_map(|(span, aliases)| (*span == decl.span).then_some(aliases))
{
decl.type_aliases = aliases.clone();
}
}
attach_java_nested_callable_type_aliases(&mut index, &tree, file, src);
let class_type_bounds = collect_java_class_type_parameter_bounds(&tree, file, src);
let bounds_by_parent: std::collections::HashMap<_, _> = index
.defs
.iter()
.filter(|decl| is_class_like(decl.kind))
.filter_map(|decl| {
class_type_bounds
.iter()
.find_map(|(span, bounds)| (*span == decl.span).then(|| (decl.symbol, bounds.clone())))
})
.collect();
for decl in &mut index.defs {
if let Some(bounds) = decl.parent.and_then(|parent| bounds_by_parent.get(&parent)) {
expand_java_type_parameter_aliases(&mut decl.type_aliases, bounds);
}
}
let bases_by_span = collect_java_class_bases(&tree, file, src);
for decl in &mut index.defs {
if !is_class_like(decl.kind) {
continue;
}
if let Some(bases) = bases_by_span
.iter()
.find_map(|(span, bases)| (*span == decl.span).then_some(bases))
{
decl.bases = bases.clone();
}
}
qualify_java_instance_field_receivers(&mut index, &tree, src);
rewrite_java_explicit_constructor_invocations(&mut index);
let constants_by_class = collect_java_class_string_constants(&tree, file, src);
attach_java_class_string_constants(&mut index, &constants_by_class);
let visibility_by_span = collect_java_visibility(tree.root_node(), file, src);
for decl in &mut index.defs {
if let Some(vis) = visibility_by_span.get(&decl.span).copied() {
decl.visibility = vis;
}
}
if let Some(segments) = extract_java_package(tree.root_node(), src) {
let segments =
package_module_segments_with_workspace_prefix(file, ctx, segments, MODULE_SOURCE_ROOTS);
bonsai_lang_api::apply_module_path_semantic_identity(&mut index, segments);
} else {
bonsai_lang_api::apply_module_path_semantic_identity(&mut index, Vec::new());
}
for decl in &mut index.defs {
bonsai_lang_api::normalize_call_result_assignment_sources(&mut decl.flow_events);
}
bonsai_lang_api::kit::synthesize_record_members(&mut index, &tree, src, file);
bonsai_lang_api::kit::apply_lexical_member_qualified_names(&mut index, ".");
bonsai_lang_api::kit::qualify_bare_hierarchy_member_calls(&mut index);
bonsai_lang_api::apply_constructor_result_type_aliases(&mut index);
bonsai_lang_api::apply_class_field_type_aliases(&mut index);
index
}
fn extract_imports(&self, file: FileId, ctx: &AdapterContext<'_>) -> ImportIndex {
let Some((snapshot, tree)) = parse_with(PACK_NAME, file, ctx) else {
return ImportIndex {
file,
..Default::default()
};
};
ImportIndex {
file,
imports: collect_java_imports(&tree, file, snapshot.text.as_bytes()),
}
}
}
fn populate_java_immutable_assignment_facts(index: &mut DeclIndex, tree: &Tree, file: FileId, src: &[u8]) {
for field in collect_kinds(tree, &["field_declaration"]) {
if !java_declaration_has_final_modifier(field) {
continue;
}
let field_span = span_of(file, &field);
let owner = index
.defs
.iter()
.filter(|decl| {
is_class_like(decl.kind)
&& decl.span.start <= field_span.start
&& field_span.end <= decl.span.end
})
.min_by_key(|decl| decl.span.len())
.map(|decl| decl.symbol);
for declarator in java_collect_kinds_below(field, &["variable_declarator"]) {
let (Some(name), Some(value)) = (
declarator.child_by_field_name("name"),
declarator.child_by_field_name("value"),
) else {
continue;
};
let name_span = span_of(file, &name);
let name_text = node_text(&name, src);
let value_span = span_of(file, &value);
if let Some(fact) = index.assignment_values.iter_mut().find(|fact| {
fact.target_span == Some(name_span)
|| (fact.value_span == value_span && fact.target.as_deref() == Some(name_text.trim()))
}) {
fact.target_is_immutable = true;
fact.target_owner = owner;
}
}
}
}
fn java_same_origin_path_constraints(
index: &DeclIndex,
tree: &Tree,
file: FileId,
src: &[u8],
) -> Vec<SameOriginPathConstraintFact> {
let mut facts = Vec::new();
for method in collect_kinds(tree, &["method_declaration"]) {
let method_span = span_of(file, &method);
let Some(decl) = index.defs.iter().find(|decl| decl.span == method_span) else {
continue;
};
let Some(body) = method.child_by_field_name("body") else {
continue;
};
let mut cursor = body.walk();
let statements = body.named_children(&mut cursor).collect::<Vec<_>>();
let [guard, final_return] = statements.as_slice() else {
continue;
};
if guard.kind() != "if_statement"
|| guard.child_by_field_name("alternative").is_some()
|| final_return.kind() != "return_statement"
{
continue;
}
let (Some(condition), Some(consequence), Some(return_value)) = (
guard.child_by_field_name("condition"),
guard.child_by_field_name("consequence"),
final_return.named_child(0),
) else {
continue;
};
let fallback_returns = java_collect_kinds_below(consequence, &["return_statement"]);
let [fallback_return] = fallback_returns.as_slice() else {
continue;
};
if fallback_return
.named_child(0)
.and_then(|value| java_static_string_literal(value, src))
.as_deref()
!= Some("/")
{
continue;
}
for (input_param_index, parameter) in decl.params.iter().enumerate() {
if return_value.kind() != "identifier" || node_text(&return_value, src).trim() != parameter {
continue;
}
let mut terms = Vec::new();
java_collect_logical_terms(condition, "||", src, &mut terms);
let requires_absolute_path = terms
.iter()
.any(|term| java_starts_with_literal(*term, parameter, "/", true, src));
let rejects_scheme_relative_path = terms
.iter()
.any(|term| java_starts_with_literal(*term, parameter, "//", false, src));
if requires_absolute_path && rejects_scheme_relative_path {
facts.push(SameOriginPathConstraintFact {
function_span: decl.span,
guard_span: span_of(file, guard),
input_place: parameter.clone(),
input_param_index: Some(input_param_index),
provider_call: None,
rejects_scheme: true,
rejects_authority: true,
requires_absolute_path,
rejects_scheme_relative_path,
});
}
}
}
facts.sort_by_key(|fact| (fact.function_span.start, fact.guard_span.start));
facts.dedup();
facts
}
fn java_collect_kinds_below<'tree>(root: Node<'tree>, kinds: &[&str]) -> Vec<Node<'tree>> {
let mut out = Vec::new();
let mut stack = vec![root];
while let Some(node) = stack.pop() {
if kinds.contains(&node.kind()) {
out.push(node);
continue;
}
let mut cursor = node.walk();
stack.extend(node.named_children(&mut cursor));
}
out
}
fn java_collect_logical_terms<'tree>(
expression: Node<'tree>,
operator: &str,
src: &[u8],
out: &mut Vec<Node<'tree>>,
) {
let expression = java_unwrap_parenthesized(expression);
let operands = (
expression.child_by_field_name("left"),
expression.child_by_field_name("right"),
);
if expression.kind() == "binary_expression"
&& operands.0.zip(operands.1).is_some_and(|(left, right)| {
src.get(left.end_byte()..right.start_byte())
.and_then(|bytes| std::str::from_utf8(bytes).ok())
.is_some_and(|value| value.trim() == operator)
})
{
let (Some(left), Some(right)) = operands else {
return;
};
java_collect_logical_terms(left, operator, src, out);
java_collect_logical_terms(right, operator, src, out);
} else {
out.push(expression);
}
}
fn java_unwrap_parenthesized(mut expression: Node<'_>) -> Node<'_> {
while expression.kind() == "parenthesized_expression" && expression.named_child_count() == 1 {
let Some(inner) = expression.named_child(0) else {
break;
};
expression = inner;
}
expression
}
fn java_starts_with_literal(
expression: Node<'_>,
receiver: &str,
literal: &str,
negated: bool,
src: &[u8],
) -> bool {
let expression = java_unwrap_parenthesized(expression);
let call = if negated {
if expression.kind() != "unary_expression" {
return false;
}
let Some(operand) = expression
.child_by_field_name("operand")
.or_else(|| expression.named_child(0))
else {
return false;
};
if src
.get(expression.start_byte()..operand.start_byte())
.and_then(|bytes| std::str::from_utf8(bytes).ok())
.is_none_or(|prefix| prefix.trim() != "!")
{
return false;
}
java_unwrap_parenthesized(operand)
} else {
expression
};
if call.kind() != "method_invocation"
|| call
.child_by_field_name("object")
.is_none_or(|object| object.kind() != "identifier" || node_text(&object, src).trim() != receiver)
|| call
.child_by_field_name("name")
.is_none_or(|name| node_text(&name, src).trim() != "startsWith")
{
return false;
}
let Some(arguments) = call.child_by_field_name("arguments") else {
return false;
};
let mut cursor = arguments.walk();
let values = arguments.named_children(&mut cursor).collect::<Vec<_>>();
let [value] = values.as_slice() else {
return false;
};
java_static_string_literal(*value, src).as_deref() == Some(literal)
}
fn java_character_substitutions(
defs: &[bonsai_lang_api::Decl],
tree: &Tree,
file: FileId,
src: &[u8],
) -> Vec<CharacterSubstitutionFact> {
let mut facts = Vec::new();
for return_node in collect_kinds(tree, &["return_statement"]) {
let return_span = span_of(file, &return_node);
let Some(decl) = defs
.iter()
.filter(|decl| {
matches!(decl.kind, DeclKind::Method | DeclKind::Constructor)
&& decl.span.start <= return_span.start
&& return_span.end <= decl.span.end
})
.min_by_key(|decl| decl.span.len())
else {
continue;
};
let Some(expression) = return_node.named_child(0) else {
continue;
};
let Some((input_param_index, exact_mappings, characters)) =
java_inline_replace_chain(expression, &decl.params, src)
else {
continue;
};
facts.push(CharacterSubstitutionFact {
function_span: decl.span,
transform_span: return_span,
input_param_index,
exact_mappings,
table: String::new(),
domain: CharacterSubstitutionDomain::ExactCharacters { characters },
});
}
for method in collect_kinds(tree, &["method_declaration"]) {
let method_span = span_of(file, &method);
let Some(decl) = defs.iter().find(|decl| decl.span == method_span) else {
continue;
};
let Some((input_param_index, transform_span, exact_mappings)) =
java_switch_character_substitution(method, decl, file, src)
else {
continue;
};
facts.push(CharacterSubstitutionFact {
function_span: decl.span,
transform_span,
input_param_index,
exact_mappings,
table: String::new(),
domain: CharacterSubstitutionDomain::TableKeysWithIdentityFallback,
});
}
facts.sort_by_key(|fact| (fact.function_span.start, fact.transform_span.start));
facts.dedup();
facts
}
fn java_switch_character_substitution(
method: Node<'_>,
decl: &bonsai_lang_api::Decl,
file: FileId,
src: &[u8],
) -> Option<(
usize,
bonsai_common::Span,
Vec<bonsai_lang_api::StaticStringMapEntry>,
)> {
let body = method.child_by_field_name("body")?;
let statements = body.named_children(&mut body.walk()).collect::<Vec<_>>();
let [builder_decl, loop_node, return_node] = statements.as_slice() else {
return None;
};
if builder_decl.kind() != "local_variable_declaration"
|| loop_node.kind() != "enhanced_for_statement"
|| return_node.kind() != "return_statement"
{
return None;
}
let declarators = builder_decl
.named_children(&mut builder_decl.walk())
.filter(|node| node.kind() == "variable_declarator")
.collect::<Vec<_>>();
let [declarator] = declarators.as_slice() else {
return None;
};
let builder = declarator.child_by_field_name("name")?;
let initializer = declarator.child_by_field_name("value")?;
if builder.kind() != "identifier" || initializer.kind() != "object_creation_expression" {
return None;
}
let builder = node_text(&builder, src).trim();
let builder_type = initializer.child_by_field_name("type")?;
if node_text(&builder_type, src).trim() != "StringBuilder" {
return None;
}
let loop_variable = loop_node.child_by_field_name("name")?;
let iterated = loop_node.child_by_field_name("value")?;
let loop_body = loop_node.child_by_field_name("body")?;
if loop_variable.kind() != "identifier" || iterated.kind() != "method_invocation" {
return None;
}
let loop_variable = node_text(&loop_variable, src).trim();
let iterated_object = iterated.child_by_field_name("object")?;
let iterated_method = iterated.child_by_field_name("name")?;
let input = node_text(&iterated_object, src).trim();
if iterated_object.kind() != "identifier"
|| node_text(&iterated_method, src).trim() != "toCharArray"
|| iterated
.child_by_field_name("arguments")
.is_none_or(|args| args.named_child_count() != 0)
{
return None;
}
let input_param_index = decl.params.iter().position(|parameter| parameter == input)?;
let switches = java_collect_kinds_below(loop_body, &["switch_expression"]);
let [switch_node] = switches.as_slice() else {
return None;
};
let condition = switch_node.child_by_field_name("condition")?;
if node_text(&condition, src)
.trim()
.trim_start_matches('(')
.trim_end_matches(')')
.trim()
!= loop_variable
{
return None;
}
let switch_body = switch_node.child_by_field_name("body")?;
let groups = switch_body
.named_children(&mut switch_body.walk())
.collect::<Vec<_>>();
if groups.is_empty() {
return None;
}
let mut mappings = Vec::new();
let mut saw_identity_default = false;
for group in groups {
if group.kind() != "switch_block_statement_group" {
return None;
}
let children = group.named_children(&mut group.walk()).collect::<Vec<_>>();
let label = children
.first()
.copied()
.filter(|node| node.kind() == "switch_label")?;
let invocations = java_collect_kinds_below(group, &["method_invocation"]);
let [append] = invocations.as_slice() else {
return None;
};
let append_object = append.child_by_field_name("object")?;
let append_name = append.child_by_field_name("name")?;
let append_args = append.child_by_field_name("arguments")?;
let values = append_args
.named_children(&mut append_args.walk())
.collect::<Vec<_>>();
let [value] = values.as_slice() else {
return None;
};
if node_text(&append_object, src).trim() != builder || node_text(&append_name, src).trim() != "append"
{
return None;
}
let label_values = label.named_children(&mut label.walk()).collect::<Vec<_>>();
if label_values.is_empty() {
if saw_identity_default
|| value.kind() != "identifier"
|| node_text(value, src).trim() != loop_variable
|| children.len() != 2
{
return None;
}
saw_identity_default = true;
continue;
}
let [label_value] = label_values.as_slice() else {
return None;
};
let input = java_static_string_or_character(*label_value, src)?;
let output = java_static_string_literal(*value, src)?;
if input.chars().count() != 1
|| children.len() != 3
|| children
.last()
.is_none_or(|node| node.kind() != "break_statement")
|| mappings
.iter()
.any(|entry: &bonsai_lang_api::StaticStringMapEntry| entry.key == input)
{
return None;
}
mappings.push(bonsai_lang_api::StaticStringMapEntry {
key: input,
value: output,
});
}
if !saw_identity_default || mappings.is_empty() {
return None;
}
let returned = return_node.named_child(0)?;
if returned.kind() != "method_invocation"
|| returned
.child_by_field_name("object")
.is_none_or(|object| node_text(&object, src).trim() != builder)
|| returned
.child_by_field_name("name")
.is_none_or(|name| node_text(&name, src).trim() != "toString")
|| returned
.child_by_field_name("arguments")
.is_none_or(|args| args.named_child_count() != 0)
{
return None;
}
mappings.sort_by(|left, right| left.key.cmp(&right.key));
Some((input_param_index, span_of(file, switch_node), mappings))
}
fn java_inline_replace_chain(
expression: Node<'_>,
params: &[String],
src: &[u8],
) -> Option<(usize, Vec<bonsai_lang_api::StaticStringMapEntry>, Vec<String>)> {
let mut current = expression;
let mut mappings = Vec::new();
let mut characters = Vec::new();
while current.kind() == "method_invocation" {
let method = current.child_by_field_name("name")?;
let method = node_text(&method, src).trim();
if !matches!(method, "replace" | "replaceAll") {
break;
}
let arguments = current.child_by_field_name("arguments")?;
let args = arguments
.named_children(&mut arguments.walk())
.collect::<Vec<_>>();
let [pattern, replacement] = args.as_slice() else {
return None;
};
let output = java_static_string_or_character(*replacement, src)?;
let replaced = if method == "replace" {
let value = java_static_string_or_character(*pattern, src)?;
(value.chars().count() == 1).then(|| vec![value])?
} else {
java_exact_regex_character_class(&java_static_string_literal(*pattern, src)?)?
};
for input in replaced {
if mappings
.iter()
.any(|entry: &bonsai_lang_api::StaticStringMapEntry| {
entry.key == input && entry.value != output
})
{
return None;
}
if !mappings.iter().any(|entry| entry.key == input) {
characters.push(input.clone());
mappings.push(bonsai_lang_api::StaticStringMapEntry {
key: input,
value: output.clone(),
});
}
}
current = current.child_by_field_name("object")?;
}
if mappings.is_empty() || current.kind() != "identifier" {
return None;
}
let input = node_text(¤t, src).trim();
let input_param_index = params.iter().position(|param| param == input)?;
characters.sort();
characters.dedup();
mappings.sort_by(|left, right| left.key.cmp(&right.key));
Some((input_param_index, mappings, characters))
}
fn java_static_string_or_character(node: Node<'_>, src: &[u8]) -> Option<String> {
java_static_string_literal(node, src).or_else(|| {
if node.kind() != "character_literal" {
return None;
}
let raw = node_text(&node, src);
let inner = raw.strip_prefix('\'')?.strip_suffix('\'')?;
match inner {
"\\r" => Some("\r".to_string()),
"\\n" => Some("\n".to_string()),
"\\t" => Some("\t".to_string()),
"\\0" => Some("\0".to_string()),
"\\\\" => Some("\\".to_string()),
"\\\"" => Some("\"".to_string()),
_ if inner.chars().count() == 1 => Some(inner.to_string()),
_ => None,
}
})
}
fn java_exact_regex_character_class(pattern: &str) -> Option<Vec<String>> {
let inner = pattern.strip_prefix('[')?.strip_suffix(']')?;
if inner.starts_with('^') || inner.is_empty() {
return None;
}
let mut characters = Vec::new();
let mut chars = inner.chars();
while let Some(character) = chars.next() {
if character == '-' {
return None;
}
let decoded = if character == '\\' {
match chars.next()? {
'r' => '\r',
'n' => '\n',
't' => '\t',
'\\' => '\\',
'"' => '"',
'\'' => '\'',
_ => return None,
}
} else {
character
};
characters.push(decoded.to_string());
}
characters.sort();
characters.dedup();
Some(characters)
}
fn java_compiled_pattern_constraints(
index: &DeclIndex,
tree: &Tree,
file: FileId,
src: &[u8],
) -> Vec<CharacterConstraintFact> {
let bindings = java_bindings(tree, src);
let mut patterns = Vec::new();
for declarator in collect_kinds(tree, &["variable_declarator"]) {
let (Some(name), Some(value)) = (
declarator.child_by_field_name("name"),
declarator.child_by_field_name("value"),
) else {
continue;
};
if name.kind() != "identifier" || value.kind() != "method_invocation" {
continue;
}
let (Some(object), Some(method), Some(arguments)) = (
value.child_by_field_name("object"),
value.child_by_field_name("name"),
value.child_by_field_name("arguments"),
) else {
continue;
};
if node_text(&object, src).trim() != "Pattern" || node_text(&method, src).trim() != "compile" {
continue;
}
let args = arguments
.named_children(&mut arguments.walk())
.collect::<Vec<_>>();
let [pattern] = args.as_slice() else {
continue;
};
let Some(pattern) = java_static_string_literal(*pattern, src) else {
continue;
};
let Some((_, declaration, _)) = java_binding_scope_and_declaration(declarator) else {
continue;
};
if !java_declaration_has_final_modifier(declaration) {
continue;
}
let characters = if pattern == "\\p{Cntrl}" {
vec!["\r".to_string(), "\n".to_string()]
} else if let Some(characters) = java_exact_regex_character_class(&pattern) {
characters
} else {
continue;
};
let name = node_text(&name, src).trim().to_string();
patterns.push((name, span_of(file, &value), characters));
}
let mut facts = Vec::new();
for call in collect_kinds(tree, &["method_invocation"]) {
let (Some(method), Some(receiver), Some(arguments)) = (
call.child_by_field_name("name"),
call.child_by_field_name("object"),
call.child_by_field_name("arguments"),
) else {
continue;
};
if node_text(&method, src).trim() != "replaceAll" || receiver.kind() != "method_invocation" {
continue;
}
let (Some(matcher_name), Some(pattern_receiver), Some(matcher_args)) = (
receiver.child_by_field_name("name"),
receiver.child_by_field_name("object"),
receiver.child_by_field_name("arguments"),
) else {
continue;
};
if node_text(&matcher_name, src).trim() != "matcher" || pattern_receiver.kind() != "identifier" {
continue;
}
let pattern_name = node_text(&pattern_receiver, src).trim();
let Some(binding) = bindings.resolve(
pattern_name,
pattern_receiver.start_byte(),
pattern_receiver.end_byte(),
) else {
continue;
};
let binding_value_span = span_of(file, &binding.initializer);
let Some((_, _, mut characters)) = patterns
.iter()
.find(|(name, value_span, _)| name == pattern_name && *value_span == binding_value_span)
.cloned()
else {
continue;
};
let matcher_args = matcher_args
.named_children(&mut matcher_args.walk())
.collect::<Vec<_>>();
let replace_args = arguments
.named_children(&mut arguments.walk())
.collect::<Vec<_>>();
let ([input], [replacement]) = (matcher_args.as_slice(), replace_args.as_slice()) else {
continue;
};
if input.kind() != "identifier" {
continue;
}
let Some(replacement) = java_static_string_literal(*replacement, src) else {
continue;
};
characters.retain(|character| !replacement.contains(character));
if characters.is_empty() {
continue;
}
let transform_span = span_of(file, &call);
let Some(decl) = index
.defs
.iter()
.filter(|decl| decl.span.start <= transform_span.start && transform_span.end <= decl.span.end)
.min_by_key(|decl| decl.span.len())
else {
continue;
};
let input_place = node_text(input, src).trim().to_string();
let input_param_index = decl.params.iter().position(|param| param == &input_place);
let output = index
.assignment_values
.iter()
.filter(|assignment| {
assignment.target.is_some()
&& assignment.value_span.start <= transform_span.start
&& transform_span.end <= assignment.value_span.end
})
.min_by_key(|assignment| assignment.value_span.len())
.and_then(|assignment| assignment.target.clone())
.map_or(
CharacterConstraintOutput::Expression { span: transform_span },
|target| CharacterConstraintOutput::Assignment { target },
);
facts.push(CharacterConstraintFact {
function_span: decl.span,
transform_span,
input_place,
input_param_index,
output,
domain: CharacterConstraintDomain::ExcludesExact { characters },
});
}
facts
}
fn java_finite_literal_selections(
index: &DeclIndex,
tree: &Tree,
file: FileId,
src: &[u8],
) -> Vec<FiniteLiteralSelectionFact> {
if !java_imports_standard_map(tree, file, src) {
return Vec::new();
}
let bindings = java_bindings(tree, src);
if !bindings.bindings.iter().any(|binding| binding.finite_map) {
return Vec::new();
}
let mut selections = Vec::new();
for call in collect_kinds(tree, &["method_invocation"]) {
let Some(object) = call.child_by_field_name("object") else {
continue;
};
let Some(name) = call.child_by_field_name("name") else {
continue;
};
if object.kind() != "identifier" || !matches!(node_text(&name, src).trim(), "get" | "getOrDefault") {
continue;
}
let map_target = node_text(&object, src).trim();
let Some(binding) = bindings.resolve(map_target, object.start_byte(), object.end_byte()) else {
continue;
};
if !binding.finite_map
|| (!binding.is_field && binding.initializer.end_byte() > call.start_byte())
|| !java_map_selection_has_literal_fallback(call, src)
{
continue;
}
let selection_span = span_of(file, &call);
let Some(fact) = bonsai_lang_api::kit::finite_literal_selection_fact_for_span(
index,
tree,
selection_span,
|value_node| java_expression_is_finite_selection(value_node, call),
) else {
continue;
};
selections.push(fact);
}
bonsai_lang_api::kit::sort_dedup_finite_literal_selections(&mut selections);
selections
}
#[derive(Copy, Clone, Debug)]
struct JavaBinding<'tree> {
name: &'tree str,
initializer: Node<'tree>,
scope: Node<'tree>,
finite_map: bool,
is_field: bool,
is_static: bool,
}
struct JavaBindings<'tree> {
bindings: Vec<JavaBinding<'tree>>,
by_name: std::collections::HashMap<String, Vec<usize>>,
}
impl<'tree> JavaBindings<'tree> {
fn resolve(&self, name: &str, use_start: usize, use_end: usize) -> Option<&JavaBinding<'tree>> {
let candidates = self.by_name.get(name)?;
let smallest_scope = candidates
.iter()
.map(|index| &self.bindings[*index])
.filter(|binding| {
binding.scope.start_byte() <= use_start
&& use_end <= binding.scope.end_byte()
&& (binding.is_field || binding.initializer.end_byte() <= use_start)
})
.map(|binding| binding.scope.end_byte() - binding.scope.start_byte())
.min()?;
let mut candidates = candidates
.iter()
.map(|index| &self.bindings[*index])
.filter(|binding| {
binding.scope.start_byte() <= use_start
&& use_end <= binding.scope.end_byte()
&& binding.scope.end_byte() - binding.scope.start_byte() == smallest_scope
&& (binding.is_field || binding.initializer.end_byte() <= use_start)
});
let binding = candidates.next()?;
candidates.next().is_none().then_some(binding)
}
}
fn java_imports_standard_map(tree: &Tree, file: FileId, src: &[u8]) -> bool {
collect_java_imports(tree, file, src)
.iter()
.any(|import| import.module == "java.util.Map" && !import.is_wildcard)
}
fn java_bindings<'tree>(tree: &'tree Tree, src: &'tree [u8]) -> JavaBindings<'tree> {
let mut bindings = Vec::new();
for declarator in collect_kinds(tree, &["variable_declarator"]) {
let Some(target) = declarator.child_by_field_name("name") else {
continue;
};
if target.kind() != "identifier" {
continue;
}
let Some((scope, declaration, is_field)) = java_binding_scope_and_declaration(declarator) else {
continue;
};
let name = node_text(&target, src).trim();
if name.is_empty() {
continue;
}
let value = declarator.child_by_field_name("value");
bindings.push(JavaBinding {
name,
initializer: value.unwrap_or(target),
scope,
finite_map: java_declaration_has_final_modifier(declaration)
&& value.is_some_and(|value| java_is_finite_literal_map(value, src)),
is_field,
is_static: is_field && java_field_has_modifier(declaration, src, "static"),
});
}
for parameter in collect_kinds(
tree,
&["formal_parameter", "spread_parameter", "catch_formal_parameter"],
) {
let Some(name_node) = parameter
.child_by_field_name("name")
.filter(|name| name.kind() == "identifier")
else {
continue;
};
let mut owner = parameter.parent();
let mut body = None;
while let Some(node) = owner {
if let Some(candidate) = node.child_by_field_name("body") {
body = Some(candidate);
break;
}
if matches!(node.kind(), "class_body" | "program") {
break;
}
owner = node.parent();
}
let Some(scope) = body else {
continue;
};
let name = node_text(&name_node, src).trim();
push_java_blocking_binding(&mut bindings, name_node, scope, name);
}
for lambda in collect_kinds(tree, &["lambda_expression"]) {
let Some(parameters) = lambda.child_by_field_name("parameters") else {
continue;
};
let Some(scope) = lambda.child_by_field_name("body") else {
continue;
};
if parameters.kind() == "identifier" {
let name = node_text(¶meters, src).trim();
push_java_blocking_binding(&mut bindings, parameters, scope, name);
} else if parameters.kind() == "inferred_parameters" {
let mut cursor = parameters.walk();
for name_node in parameters
.named_children(&mut cursor)
.filter(|child| child.kind() == "identifier")
{
let name = node_text(&name_node, src).trim();
push_java_blocking_binding(&mut bindings, name_node, scope, name);
}
}
}
for enhanced_for in collect_kinds(tree, &["enhanced_for_statement"]) {
let Some(name_node) = enhanced_for
.child_by_field_name("name")
.filter(|name| name.kind() == "identifier")
else {
continue;
};
let Some(scope) = enhanced_for.child_by_field_name("body") else {
continue;
};
let name = node_text(&name_node, src).trim();
push_java_blocking_binding(&mut bindings, name_node, scope, name);
}
for pattern in collect_kinds(tree, &["type_pattern", "record_pattern_component"]) {
let mut cursor = pattern.walk();
let Some(name_node) = pattern
.named_children(&mut cursor)
.filter(|child| child.kind() == "identifier")
.last()
else {
continue;
};
let Some(scope) = java_enclosing_block(pattern) else {
continue;
};
let name = node_text(&name_node, src).trim();
push_java_blocking_binding(&mut bindings, name_node, scope, name);
}
let mut by_name: std::collections::HashMap<String, Vec<usize>> = std::collections::HashMap::new();
for (index, binding) in bindings.iter().enumerate() {
by_name.entry(binding.name.to_string()).or_default().push(index);
}
if by_name.contains_key("Map") || java_declares_type_named(tree, src, "Map") {
for binding in &mut bindings {
binding.finite_map = false;
}
}
JavaBindings { bindings, by_name }
}
fn qualify_java_instance_field_receivers(index: &mut DeclIndex, tree: &Tree, src: &[u8]) {
let Some(current_receiver) = HANDLER
.implicit_receiver_names
.first()
.copied()
.filter(|name| !name.is_empty())
else {
return;
};
let bindings = java_bindings(tree, src);
for fact in &mut index.assignment_values {
let (Some(target_span), Some(target)) = (fact.target_span, fact.target.as_deref()) else {
continue;
};
let Some(target_node) =
bonsai_lang_api::kit::node_at_span(tree.root_node(), target_span, &["identifier"])
.filter(|node| node.kind() == "identifier")
else {
continue;
};
let target_name = node_text(&target_node, src).trim();
if target_name.is_empty() || target_name != target {
continue;
}
let Some(binding) = bindings.resolve(target_name, target_node.start_byte(), target_node.end_byte())
else {
continue;
};
if binding.is_field
&& !binding.is_static
&& java_enclosing_class_body(target_node).is_some_and(|body| body.id() == binding.scope.id())
{
fact.target = Some(format!("{current_receiver}.{target_name}"));
}
}
let mut rewrites: std::collections::HashMap<Span, (String, String)> = std::collections::HashMap::new();
for fact in &mut index.call_receivers {
if fact.role != bonsai_lang_api::CallReceiverRole::Value {
continue;
}
let Some(receiver_node) =
bonsai_lang_api::kit::node_at_span(tree.root_node(), fact.receiver_span, &["identifier"])
.filter(|node| node.kind() == "identifier")
else {
continue;
};
let receiver_name = node_text(&receiver_node, src).trim();
if receiver_name.is_empty() {
continue;
}
let Some(binding) = bindings.resolve(
receiver_name,
receiver_node.start_byte(),
receiver_node.end_byte(),
) else {
continue;
};
if !binding.is_field
|| binding.is_static
|| java_enclosing_class_body(receiver_node).is_none_or(|body| body.id() != binding.scope.id())
{
continue;
}
let qualified = format!("{current_receiver}.{receiver_name}");
fact.value_flow = bonsai_lang_api::ExpressionFlow::from_place(qualified.clone());
rewrites.insert(fact.call_span, (receiver_name.to_string(), qualified));
}
if rewrites.is_empty() {
return;
}
for decl in &mut index.defs {
qualify_java_field_receiver_events(&mut decl.flow_events, &rewrites);
decl.receiver_state_sources = bonsai_lang_api::kit::collect_receiver_state_sources(
&decl.flow_events,
&decl.params,
HANDLER.implicit_receiver_names,
);
}
}
fn java_enclosing_class_body(mut node: Node<'_>) -> Option<Node<'_>> {
while let Some(parent) = node.parent() {
if parent.kind() == "class_body" {
return Some(parent);
}
node = parent;
}
None
}
fn qualify_java_field_receiver_events(
events: &mut [FlowEvent],
rewrites: &std::collections::HashMap<Span, (String, String)>,
) {
for event in events {
match event {
FlowEvent::Call {
span, name, receiver, ..
} => {
if let Some((unqualified, qualified)) = rewrites.get(span) {
if receiver.as_deref() == Some(unqualified.as_str()) {
*receiver = Some(qualified.clone());
}
qualify_java_receiver_prefix(name, unqualified, qualified);
}
}
FlowEvent::Assign {
span, source_call, ..
} => {
if let Some(source_call) = source_call {
for (call_span, (unqualified, qualified)) in rewrites {
if span.file == call_span.file
&& span.start <= call_span.start
&& call_span.end <= span.end
{
qualify_java_receiver_prefix(source_call, unqualified, qualified);
}
}
}
}
FlowEvent::Branch {
then_events,
else_events,
..
} => {
qualify_java_field_receiver_events(then_events, rewrites);
qualify_java_field_receiver_events(else_events, rewrites);
}
FlowEvent::Try {
body,
catch_events,
finally_events,
..
} => {
qualify_java_field_receiver_events(body, rewrites);
qualify_java_field_receiver_events(catch_events, rewrites);
qualify_java_field_receiver_events(finally_events, rewrites);
}
FlowEvent::Loop { body, .. } | FlowEvent::Defer { body, .. } | FlowEvent::Using { body, .. } => {
qualify_java_field_receiver_events(body, rewrites);
}
_ => {}
}
}
}
fn qualify_java_receiver_prefix(value: &mut String, unqualified: &str, qualified: &str) {
if value == unqualified {
*value = qualified.to_string();
return;
}
if value
.strip_prefix(unqualified)
.is_some_and(|suffix| suffix.starts_with('.'))
{
value.replace_range(..unqualified.len(), qualified);
}
}
fn push_java_blocking_binding<'tree>(
bindings: &mut Vec<JavaBinding<'tree>>,
name_node: Node<'tree>,
scope: Node<'tree>,
name: &'tree str,
) {
if name.is_empty() {
return;
}
bindings.push(JavaBinding {
name,
initializer: name_node,
scope,
finite_map: false,
is_field: false,
is_static: false,
});
}
fn java_enclosing_block(mut node: Node<'_>) -> Option<Node<'_>> {
while let Some(parent) = node.parent() {
if parent.kind() == "block" {
return Some(parent);
}
node = parent;
}
None
}
fn java_declares_type_named(tree: &Tree, src: &[u8], wanted: &str) -> bool {
collect_kinds(
tree,
&[
"class_declaration",
"interface_declaration",
"enum_declaration",
"record_declaration",
"annotation_type_declaration",
],
)
.into_iter()
.any(|declaration| {
declaration
.child_by_field_name("name")
.is_some_and(|name| node_text(&name, src).trim() == wanted)
})
}
fn java_binding_scope_and_declaration(mut node: Node<'_>) -> Option<(Node<'_>, Node<'_>, bool)> {
let mut declaration = None;
while let Some(parent) = node.parent() {
if matches!(parent.kind(), "field_declaration" | "local_variable_declaration") {
declaration = Some(parent);
}
if matches!(parent.kind(), "block" | "constructor_body") {
return Some((parent, declaration?, false));
}
if parent.kind() == "class_body" {
return Some((parent, declaration?, true));
}
node = parent;
}
None
}
fn java_declaration_has_final_modifier(declaration: Node<'_>) -> bool {
let Some(modifiers) = declaration
.named_children(&mut declaration.walk())
.find(|child| child.kind() == "modifiers")
else {
return false;
};
modifiers
.children(&mut modifiers.walk())
.any(|modifier| modifier.kind() == "final")
}
fn java_map_selection_has_literal_fallback(call: Node<'_>, src: &[u8]) -> bool {
let Some(name) = call.child_by_field_name("name") else {
return false;
};
let Some(arguments) = call.child_by_field_name("arguments") else {
return false;
};
let mut cursor = arguments.walk();
let values: Vec<_> = arguments.named_children(&mut cursor).collect();
match node_text(&name, src).trim() {
"get" => values.len() == 1,
"getOrDefault" => values.len() == 2 && java_is_literal_value(values[1], src),
_ => false,
}
}
fn java_expression_is_finite_selection(mut node: Node<'_>, selection: Node<'_>) -> bool {
while matches!(node.kind(), "parenthesized_expression" | "cast_expression")
&& node.named_child_count() >= 1
{
let Some(inner) = node
.child_by_field_name("value")
.or_else(|| node.named_child(u32::try_from(node.named_child_count() - 1).ok()?))
else {
return false;
};
node = inner;
}
node.id() == selection.id()
}
fn java_is_finite_literal_map(node: Node<'_>, src: &[u8]) -> bool {
if node.kind() != "method_invocation" {
return false;
}
let Some(object) = node.child_by_field_name("object") else {
return false;
};
let Some(name) = node.child_by_field_name("name") else {
return false;
};
if object.kind() != "identifier"
|| node_text(&object, src).trim() != "Map"
|| node_text(&name, src).trim() != "of"
{
return false;
}
let Some(arguments) = node.child_by_field_name("arguments") else {
return false;
};
let mut cursor = arguments.walk();
let values: Vec<_> = arguments.named_children(&mut cursor).collect();
!values.is_empty()
&& values.len() % 2 == 0
&& values.iter().all(|value| java_is_literal_value(*value, src))
}
fn java_is_literal_value(mut node: Node<'_>, src: &[u8]) -> bool {
while matches!(node.kind(), "parenthesized_expression" | "cast_expression")
&& node.named_child_count() >= 1
{
let Some(inner) = node
.child_by_field_name("value")
.or_else(|| node.named_child(u32::try_from(node.named_child_count() - 1).unwrap_or(0)))
else {
return false;
};
node = inner;
}
match node.kind() {
"string_literal" => java_static_string_literal(node, src).is_some(),
"character_literal"
| "decimal_integer_literal"
| "hex_integer_literal"
| "octal_integer_literal"
| "binary_integer_literal"
| "decimal_floating_point_literal"
| "hex_floating_point_literal"
| "true"
| "false"
| "null_literal" => true,
"array_initializer" => {
let mut cursor = node.walk();
let is_literal = node
.named_children(&mut cursor)
.all(|child| java_is_literal_value(child, src));
is_literal
}
_ => false,
}
}
fn populate_java_condition_expressions(
facts: &mut [bonsai_lang_api::BranchConditionFact],
tree: &Tree,
file: FileId,
src: &[u8],
) {
for branch in collect_kinds(tree, &["if_statement"]) {
let branch_span = span_of(file, &branch);
let Some(condition) = branch.child_by_field_name("condition") else {
continue;
};
let Some(fact) = facts.iter_mut().find(|fact| fact.branch_span == branch_span) else {
continue;
};
fact.expression = Some(lower_java_condition_expression(condition, file, src));
}
}
fn lower_java_condition_expression(node: Node<'_>, file: FileId, src: &[u8]) -> ConditionExpressionFact {
if node.kind() == "parenthesized_expression" {
if let Some(inner) = node.named_child(0) {
return lower_java_condition_expression(inner, file, src);
}
}
let span = span_of(file, &node);
if node.kind() == "unary_expression" {
if let Some(operand) = node
.child_by_field_name("operand")
.or_else(|| node.named_child(0))
{
let operator = src
.get(node.start_byte()..operand.start_byte())
.and_then(|bytes| std::str::from_utf8(bytes).ok())
.map(str::trim);
if operator == Some("!") {
return ConditionExpressionFact::Not {
span,
operand: Box::new(lower_java_condition_expression(operand, file, src)),
};
}
}
}
if node.kind() == "binary_expression" {
if let (Some(left), Some(right)) = (
node.child_by_field_name("left"),
node.child_by_field_name("right"),
) {
let operator = src
.get(left.end_byte()..right.start_byte())
.and_then(|bytes| std::str::from_utf8(bytes).ok())
.map(str::trim);
match operator {
Some("||") => {
return merge_java_condition_junction(
span,
lower_java_condition_expression(left, file, src),
lower_java_condition_expression(right, file, src),
false,
);
}
Some("&&") => {
return merge_java_condition_junction(
span,
lower_java_condition_expression(left, file, src),
lower_java_condition_expression(right, file, src),
true,
);
}
Some("==" | "!=") => {
return ConditionExpressionFact::Equality {
span,
relation: if operator == Some("==") {
ConditionEquality::Equal
} else {
ConditionEquality::NotEqual
},
left: java_condition_operand(left, file, src),
right: java_condition_operand(right, file, src),
};
}
_ => {}
}
}
}
if node.kind() == "instanceof_expression" {
if let (Some(subject), Some(type_node)) = (
node.child_by_field_name("left")
.or_else(|| node.child_by_field_name("expression"))
.or_else(|| node.named_child(0)),
node.child_by_field_name("right")
.or_else(|| node.child_by_field_name("type"))
.or_else(|| node.named_child(1)),
) {
let type_name = node_text(&type_node, src).trim().to_string();
if !type_name.is_empty() {
return ConditionExpressionFact::TypeTest {
span,
subject: java_condition_operand(subject, file, src),
type_name,
};
}
}
}
ConditionExpressionFact::Atom { span }
}
fn merge_java_condition_junction(
span: Span,
left: ConditionExpressionFact,
right: ConditionExpressionFact,
all: bool,
) -> ConditionExpressionFact {
let mut operands = Vec::new();
let mut push = |operand: ConditionExpressionFact| match (all, operand) {
(true, ConditionExpressionFact::All { operands: nested, .. })
| (false, ConditionExpressionFact::Any { operands: nested, .. }) => operands.extend(nested),
(_, operand) => operands.push(operand),
};
push(left);
push(right);
if all {
ConditionExpressionFact::All { span, operands }
} else {
ConditionExpressionFact::Any { span, operands }
}
}
fn java_condition_operand(node: Node<'_>, file: FileId, src: &[u8]) -> ConditionOperandFact {
ConditionOperandFact {
span: span_of(file, &node),
value_flow: bonsai_lang_api::kit::expression_flow_from_node_with_handler(node, file, src, &HANDLER),
static_string: java_static_string_literal(node, src),
static_value: java_static_scalar(node, src),
}
}
fn java_static_string_literal(node: Node<'_>, src: &[u8]) -> Option<String> {
if node.kind() != "string_literal" {
return None;
}
let text = node_text(&node, src);
let inner = text.strip_prefix('"')?.strip_suffix('"')?;
decode_java_string_literal(inner)
}
fn decode_java_string_literal(inner: &str) -> Option<String> {
let mut input = inner.chars().peekable();
let mut decoded = String::with_capacity(inner.len());
while let Some(character) = input.next() {
if character != '\\' {
decoded.push(character);
continue;
}
let escape = input.next()?;
match escape {
'b' => decoded.push('\u{0008}'),
't' => decoded.push('\t'),
'n' => decoded.push('\n'),
'f' => decoded.push('\u{000c}'),
'r' => decoded.push('\r'),
's' => decoded.push(' '),
'"' => decoded.push('"'),
'\'' => decoded.push('\''),
'\\' => decoded.push('\\'),
'u' => {
while input.peek() == Some(&'u') {
input.next();
}
let mut value = 0_u32;
for _ in 0..4 {
value = value.checked_mul(16)? + input.next()?.to_digit(16)?;
}
let scalar = char::from_u32(value)?;
if matches!(scalar, '\\' | '"' | '\r' | '\n') {
return None;
}
decoded.push(scalar);
}
first @ '0'..='7' => {
let mut value = first.to_digit(8)?;
let mut digits = 1;
while digits < 3 {
let Some(next) = input.peek().copied() else {
break;
};
let Some(digit) = next.to_digit(8) else {
break;
};
if digits == 2 && first > '3' {
break;
}
input.next();
value = value.checked_mul(8)? + digit;
digits += 1;
}
decoded.push(char::from_u32(value)?);
}
_ => return None,
}
}
Some(decoded)
}
fn java_static_scalar(node: Node<'_>, src: &[u8]) -> Option<StaticScalarValue> {
match node.kind() {
"string_literal" => Some(StaticScalarValue::String(java_static_string_literal(node, src)?)),
"true" => Some(StaticScalarValue::Boolean(true)),
"false" => Some(StaticScalarValue::Boolean(false)),
"null_literal" => Some(StaticScalarValue::Null),
_ => None,
}
}
fn java_string_compositions(tree: &Tree, file: FileId, src: &[u8]) -> Vec<StringCompositionFact> {
let mut facts = Vec::new();
for declarator in collect_kinds(tree, &["variable_declarator"]) {
let (Some(name), Some(value)) = (
declarator.child_by_field_name("name"),
declarator.child_by_field_name("value"),
) else {
continue;
};
if name.kind() != "identifier" {
continue;
}
let mut parts = Vec::new();
if java_lower_string_composition(value, file, src, &mut parts) && parts.len() > 1 {
facts.push(StringCompositionFact {
container_span: span_of(file, &declarator),
value_span: span_of(file, &value),
target: Some(node_text(&name, src).trim().to_string()),
parts,
});
}
}
facts.sort_by_key(|fact| (fact.container_span.start, fact.container_span.end));
facts.dedup();
facts
}
fn java_lower_string_composition(
mut node: Node<'_>,
file: FileId,
src: &[u8],
out: &mut Vec<StringCompositionPart>,
) -> bool {
while node.kind() == "parenthesized_expression" && node.named_child_count() == 1 {
let Some(inner) = node.named_child(0) else {
return false;
};
node = inner;
}
if let Some(value) = java_static_string_literal(node, src) {
out.push(StringCompositionPart::Literal { value });
return true;
}
if node.kind() == "method_invocation" {
let Some(name) = node.child_by_field_name("name") else {
return false;
};
out.push(StringCompositionPart::Call {
span: span_of(file, &name),
});
return true;
}
if node.kind() == "binary_expression" {
let (Some(left), Some(right)) = (
node.child_by_field_name("left"),
node.child_by_field_name("right"),
) else {
return false;
};
let operator = src
.get(left.end_byte()..right.start_byte())
.and_then(|bytes| std::str::from_utf8(bytes).ok())
.map(str::trim);
return operator == Some("+")
&& java_lower_string_composition(left, file, src, out)
&& java_lower_string_composition(right, file, src, out);
}
if node.kind() == "ternary_expression" {
let condition = node
.child_by_field_name("condition")
.or_else(|| node.named_child(0));
let consequence = node
.child_by_field_name("consequence")
.or_else(|| node.named_child(1));
let alternative = node
.child_by_field_name("alternative")
.or_else(|| node.named_child(2));
let (Some(condition), Some(consequence), Some(alternative)) = (condition, consequence, alternative)
else {
return false;
};
let Some(condition_call) = java_null_equality_call(condition, src) else {
return false;
};
let (call, fallback) = if alternative.kind() == "method_invocation" {
(alternative, java_static_string_literal(consequence, src))
} else if consequence.kind() == "method_invocation" {
(consequence, java_static_string_literal(alternative, src))
} else {
return false;
};
let Some(fallback) = fallback else {
return false;
};
if java_method_call_identity(condition_call, src) != java_method_call_identity(call, src) {
return false;
}
out.push(StringCompositionPart::CallOrLiteral {
span: span_of(file, &call.child_by_field_name("name").unwrap_or(call)),
fallback,
});
return true;
}
false
}
fn java_null_equality_call<'tree>(node: Node<'tree>, src: &[u8]) -> Option<Node<'tree>> {
if node.kind() != "binary_expression" {
return None;
}
let left = node.child_by_field_name("left")?;
let right = node.child_by_field_name("right")?;
let operator = src
.get(left.end_byte()..right.start_byte())
.and_then(|bytes| std::str::from_utf8(bytes).ok())
.map(str::trim);
if operator != Some("==") {
return None;
}
if left.kind() == "method_invocation" && right.kind() == "null_literal" {
Some(left)
} else if right.kind() == "method_invocation" && left.kind() == "null_literal" {
Some(right)
} else {
None
}
}
fn java_method_call_identity(node: Node<'_>, src: &[u8]) -> Option<(String, String, usize)> {
if node.kind() != "method_invocation" {
return None;
}
let receiver = node.child_by_field_name("object")?;
let name = node.child_by_field_name("name")?;
let arguments = node.child_by_field_name("arguments")?;
let mut cursor = arguments.walk();
Some((
node_text(&receiver, src).trim().to_string(),
node_text(&name, src).trim().to_string(),
arguments.named_children(&mut cursor).count(),
))
}
fn populate_java_static_scalar_facts(index: &mut DeclIndex, tree: &Tree, file: FileId, src: &[u8]) {
let static_values: std::collections::HashMap<_, _> =
collect_kinds(tree, &["string_literal", "true", "false", "null_literal"])
.into_iter()
.filter_map(|node| {
let value = java_static_scalar(node, src)?;
let span = span_of(file, &node);
Some(((span.start, span.end), value))
})
.collect();
let call_values: std::collections::HashMap<_, _> =
collect_kinds(tree, &["method_invocation", "object_creation_expression"])
.into_iter()
.map(|node| {
let span = span_of(file, &node);
((span.start, span.end), node)
})
.collect();
for fact in &mut index.assignment_values {
if fact.direct_call_name.is_none() {
continue;
}
let Some(call) = call_values.get(&(fact.value_span.start, fact.value_span.end)) else {
continue;
};
let Some(arguments) = call.child_by_field_name("arguments") else {
continue;
};
let mut cursor = arguments.walk();
let argument_nodes: Vec<_> = arguments.named_children(&mut cursor).collect();
if argument_nodes.is_empty() {
continue;
}
let values: Option<Vec<_>> = argument_nodes
.into_iter()
.map(|argument| java_static_scalar(argument, src))
.collect();
fact.exact_static_call_args = values;
}
for fact in &mut index.call_receivers {
fact.static_value = static_values
.get(&(fact.receiver_span.start, fact.receiver_span.end))
.cloned();
}
bonsai_lang_api::kit::populate_call_argument_static_values(
index,
tree,
file,
src,
&HANDLER,
java_static_scalar,
);
populate_java_array_argument_sequences(index, tree, file, src);
}
fn populate_java_array_argument_sequences(index: &mut DeclIndex, tree: &Tree, file: FileId, src: &[u8]) {
let arrays: std::collections::HashMap<_, _> = collect_kinds(tree, &["array_creation_expression"])
.into_iter()
.map(|node| {
let span = span_of(file, &node);
((span.start, span.end), node)
})
.collect();
for fact in &mut index.call_argument_values {
let Some(array) = arrays.get(&(fact.argument_span.start, fact.argument_span.end)) else {
continue;
};
let mut array_cursor = array.walk();
let Some(initializer) = array.child_by_field_name("value").or_else(|| {
array
.named_children(&mut array_cursor)
.find(|child| child.kind() == "array_initializer")
}) else {
continue;
};
let mut cursor = initializer.walk();
let values = initializer
.named_children(&mut cursor)
.map(|value| java_static_scalar(value, src))
.collect::<Vec<_>>();
if !values.is_empty() {
fact.exact_static_sequence_values = Some(values);
}
}
}
fn collect_java_method_type_aliases(
tree: &Tree,
file: FileId,
src: &[u8],
field_aliases: &[TypeAliasBinding],
) -> Vec<(bonsai_common::Span, Vec<TypeAliasBinding>)> {
let mut aliases_by_method = Vec::new();
for method_node in collect_kinds(tree, &["method_declaration", "constructor_declaration"]) {
let mut method_aliases = field_aliases.to_vec();
method_aliases.extend(collect_java_type_aliases(
method_node,
src,
&[
"formal_parameter",
"local_variable_declaration",
"enhanced_for_statement",
"resource",
],
));
let method_type_bounds = java_type_parameter_bounds(method_node, src);
expand_java_type_parameter_aliases(&mut method_aliases, &method_type_bounds);
dedup_type_aliases(&mut method_aliases);
aliases_by_method.push((span_of(file, &method_node), method_aliases));
}
aliases_by_method
}
type JavaTypeParameterBounds = Vec<(String, Vec<String>)>;
fn collect_java_class_type_parameter_bounds(
tree: &Tree,
file: FileId,
src: &[u8],
) -> Vec<(Span, JavaTypeParameterBounds)> {
collect_kinds(
tree,
&[
"class_declaration",
"interface_declaration",
"record_declaration",
"enum_declaration",
"annotation_type_declaration",
],
)
.into_iter()
.filter_map(|node| {
let bounds = java_type_parameter_bounds(node, src);
(!bounds.is_empty()).then(|| (span_of(file, &node), bounds))
})
.collect()
}
fn java_type_parameter_bounds(node: Node<'_>, src: &[u8]) -> JavaTypeParameterBounds {
let Some(parameters) = node.child_by_field_name("type_parameters") else {
return Vec::new();
};
let mut out = Vec::new();
let mut parameter_cursor = parameters.walk();
for parameter in parameters.named_children(&mut parameter_cursor) {
if parameter.kind() != "type_parameter" {
continue;
}
let mut child_cursor = parameter.walk();
let children = parameter.named_children(&mut child_cursor).collect::<Vec<_>>();
let Some(name) = children
.iter()
.find(|child| child.kind() == "type_identifier")
.map(|child| node_text(child, src).trim().to_string())
.filter(|name| !name.is_empty())
else {
continue;
};
let mut bounds = Vec::new();
for bound in children.iter().filter(|child| child.kind() == "type_bound") {
let mut bound_cursor = bound.walk();
for bound_type in bound.named_children(&mut bound_cursor) {
let text = node_text(&bound_type, src).trim();
if !text.is_empty() && !bounds.iter().any(|existing| existing == text) {
bounds.push(text.to_string());
}
}
}
if !bounds.is_empty() {
out.push((name, bounds));
}
}
out
}
fn expand_java_type_parameter_aliases(aliases: &mut Vec<TypeAliasBinding>, bounds: &JavaTypeParameterBounds) {
if aliases.is_empty() || bounds.is_empty() {
return;
}
loop {
let before = aliases.len();
let current = aliases.clone();
for alias in current {
let alias_type = canonical_java_type_name(&alias.type_name)
.unwrap_or_else(|| alias.type_name.trim().to_string());
for (_, upper_bounds) in bounds.iter().filter(|(parameter, _)| parameter == &alias_type) {
for upper_bound in upper_bounds {
if let Some(canonical) = canonical_java_type_name(upper_bound) {
let qualified = qualified_java_type_name(upper_bound);
push_java_type_alias(aliases, &alias.name, &canonical, qualified.as_deref());
}
}
}
}
dedup_type_aliases(aliases);
if aliases.len() == before {
break;
}
}
}
fn attach_java_nested_callable_type_aliases(index: &mut DeclIndex, tree: &Tree, file: FileId, src: &[u8]) {
let mut callable_bindings = Vec::new();
for declaration in collect_kinds(tree, &["local_variable_declaration"]) {
let Some(type_node) = declaration.child_by_field_name("type") else {
continue;
};
let type_text = node_text(&type_node, src);
let Some(canonical_type) = canonical_java_type_name(type_text) else {
continue;
};
let qualified_type = qualified_java_type_name(type_text);
let mut cursor = declaration.walk();
for declarator in declaration
.named_children(&mut cursor)
.filter(|child| child.kind() == "variable_declarator")
{
let (Some(name_node), Some(value)) = (
declarator.child_by_field_name("name"),
declarator.child_by_field_name("value"),
) else {
continue;
};
if value.kind() != "lambda_expression" {
continue;
}
let name = node_text(&name_node, src).trim();
if name.is_empty() {
continue;
}
let mut aliases = Vec::new();
push_java_type_alias(&mut aliases, name, &canonical_type, qualified_type.as_deref());
callable_bindings.push((span_of(file, &value), aliases));
}
}
let inherited = index
.defs
.iter()
.map(|decl| (decl.symbol, decl.type_aliases.clone()))
.collect::<std::collections::HashMap<_, _>>();
for decl in &mut index.defs {
if let Some(parent_aliases) = decl.parent.and_then(|parent| inherited.get(&parent)) {
decl.type_aliases.extend(parent_aliases.iter().cloned());
}
if let Some(aliases) = callable_bindings
.iter()
.find_map(|(span, aliases)| (*span == decl.span).then_some(aliases))
{
decl.type_aliases.extend(aliases.iter().cloned());
}
dedup_type_aliases(&mut decl.type_aliases);
}
}
fn collect_java_type_aliases(root: Node<'_>, src: &[u8], kinds: &[&str]) -> Vec<TypeAliasBinding> {
let mut aliases = Vec::new();
let mut work_stack = vec![root];
while let Some(node) = work_stack.pop() {
if kinds.contains(&node.kind()) {
aliases.extend(java_type_aliases_from_decl(node, src));
}
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
work_stack.push(child);
}
}
expand_java_platform_supertypes(&mut aliases);
dedup_type_aliases(&mut aliases);
aliases
}
fn java_type_aliases_from_decl(node: Node<'_>, src: &[u8]) -> Vec<TypeAliasBinding> {
let Some(type_node) = node.child_by_field_name("type") else {
return Vec::new();
};
let type_text = node_text(&type_node, src);
let mut aliases = Vec::new();
if let Some(canonical_type) = canonical_java_type_name(type_text) {
let qualified_type = qualified_java_type_name(type_text);
if let Some(name_node) = node.child_by_field_name("name") {
push_java_type_alias(
&mut aliases,
node_text(&name_node, src),
&canonical_type,
qualified_type.as_deref(),
);
return aliases;
}
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
if child.kind() != "variable_declarator" {
continue;
}
if let Some(name_node) = child.child_by_field_name("name") {
let name = node_text(&name_node, src);
push_java_type_alias(&mut aliases, name, &canonical_type, qualified_type.as_deref());
}
}
return aliases;
}
if type_text.trim() == "var" {
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
if child.kind() != "variable_declarator" {
continue;
}
let Some(name_node) = child.child_by_field_name("name") else {
continue;
};
let Some(value) = child.child_by_field_name("value") else {
continue;
};
if let Some(cast_raw) = java_cast_type_of_init(value, src) {
if let Some(canonical) = canonical_java_type_name(&cast_raw) {
let qualified = qualified_java_type_name(&cast_raw);
push_java_type_alias(
&mut aliases,
node_text(&name_node, src),
&canonical,
qualified.as_deref(),
);
}
}
}
}
aliases
}
fn java_cast_type_of_init(init: Node<'_>, src: &[u8]) -> Option<String> {
let mut n = init;
while n.kind() == "parenthesized_expression" {
let mut cursor = n.walk();
n = n.named_children(&mut cursor).next()?;
}
if n.kind() == "cast_expression" {
return n
.child_by_field_name("type")
.map(|t| node_text(&t, src).to_string());
}
None
}
fn canonical_java_type_name(raw: &str) -> Option<String> {
let without_generics = raw.split('<').next().unwrap_or(raw);
let without_arrays = without_generics.split('[').next().unwrap_or(without_generics);
let bare_type = without_arrays
.trim()
.rsplit('.')
.next()
.unwrap_or(without_arrays)
.trim();
if bare_type.is_empty() || bare_type == "var" {
return None;
}
Some(bare_type.to_string())
}
fn qualified_java_type_name(raw: &str) -> Option<String> {
let without_generics = raw.split('<').next().unwrap_or(raw);
let without_arrays = without_generics.split('[').next().unwrap_or(without_generics);
let qualified = without_arrays.trim();
let mut parts = qualified.split('.').filter(|part| !part.is_empty()).peekable();
parts.peek()?;
let segments: Vec<&str> = parts.collect();
if segments.len() < 2 {
return None;
}
let tail = segments.last()?.trim();
if tail.is_empty() {
return None;
}
Some(qualified.to_string())
}
fn push_java_type_alias(
aliases: &mut Vec<TypeAliasBinding>,
name: &str,
canonical_type: &str,
qualified_type: Option<&str>,
) {
if let Some(qualified_type) = qualified_type.filter(|qualified| *qualified != canonical_type) {
push_type_alias(aliases, name, qualified_type);
}
push_type_alias(aliases, name, canonical_type);
}
fn push_type_alias(aliases: &mut Vec<TypeAliasBinding>, name: &str, type_name: &str) {
let bare_name = name.trim();
if bare_name.is_empty() || bare_name == type_name {
return;
}
aliases.push(TypeAliasBinding {
name: bare_name.to_string(),
type_name: type_name.to_string(),
});
}
fn expand_java_platform_supertypes(aliases: &mut Vec<TypeAliasBinding>) {
let original = aliases.clone();
for alias in original {
for supertype in java_platform_supertypes(&alias.type_name) {
push_type_alias(aliases, &alias.name, supertype);
}
}
}
fn java_platform_supertypes(type_name: &str) -> &'static [&'static str] {
match type_name {
"CallableStatement" => &["PreparedStatement", "Statement"],
"PreparedStatement" => &["Statement"],
"Statement" => &[],
"ArrayList" | "LinkedList" | "Vector" => &["List", "Collection", "Iterable"],
"HashSet" | "LinkedHashSet" | "TreeSet" => &["Set", "Collection", "Iterable"],
"HashMap" | "LinkedHashMap" | "TreeMap" => &["Map"],
"List" | "Set" => &["Collection", "Iterable"],
"Collection" => &["Iterable"],
_ => &[],
}
}
fn dedup_type_aliases(aliases: &mut Vec<TypeAliasBinding>) {
let mut deduped = Vec::new();
for alias in aliases.drain(..) {
if !deduped.contains(&alias) {
deduped.push(alias);
}
}
*aliases = deduped;
}
fn collect_java_imports(tree: &Tree, file: FileId, src: &[u8]) -> Vec<ImportSpec> {
let mut imports: Vec<_> = collect_kinds(tree, &["import_declaration"])
.into_iter()
.filter_map(|import| java_import_spec(import, file, src))
.collect();
imports.extend(
collect_kinds(tree, &["scoped_type_identifier"])
.into_iter()
.filter(|type_use| !has_ancestor_kind(*type_use, "import_declaration"))
.filter(|type_use| {
type_use
.parent()
.is_none_or(|parent| parent.kind() != "scoped_type_identifier")
})
.filter_map(|type_use| java_qualified_type_package_spec(type_use, file, src)),
);
imports.sort_by_key(|import| import.span.start);
imports.dedup_by(|left, right| left.span == right.span && left.module == right.module);
imports
}
fn java_qualified_type_package_spec(type_use: Node<'_>, file: FileId, src: &[u8]) -> Option<ImportSpec> {
let module = node_text(&type_use, src).trim();
let alias = import_tail_binding(module)?;
Some(ImportSpec {
span: span_of(file, &type_use),
module: module.to_string(),
alias: Some(alias),
is_wildcard: false,
original_name: None,
scope: ImportScope::Local,
})
}
fn has_ancestor_kind(mut node: Node<'_>, kind: &str) -> bool {
while let Some(parent) = node.parent() {
if parent.kind() == kind {
return true;
}
node = parent;
}
false
}
fn java_import_spec(import: Node<'_>, file: FileId, src: &[u8]) -> Option<ImportSpec> {
let is_static = import
.children(&mut import.walk())
.any(|child| child.kind() == "static");
let mut named_cursor = import.walk();
let named_children: Vec<_> = import.named_children(&mut named_cursor).collect();
let is_wildcard = named_children.iter().any(|child| child.kind() == "asterisk");
let path_node = named_children
.iter()
.find(|child| matches!(child.kind(), "identifier" | "scoped_identifier"))?;
let full_path = node_text(path_node, src).trim();
if full_path.is_empty() {
return None;
}
let (module, alias, original_name) = if is_static && !is_wildcard {
let (owner, member) = full_path.rsplit_once('.')?;
(
owner.to_string(),
Some(member.to_string()),
Some(member.to_string()),
)
} else {
(
full_path.to_string(),
(!is_wildcard).then(|| import_tail_binding(full_path)).flatten(),
None,
)
};
Some(ImportSpec {
span: span_of(file, &import),
module,
alias,
is_wildcard,
original_name,
scope: ImportScope::Module,
})
}
fn import_tail_binding(module: &str) -> Option<String> {
let tail = module
.rsplit_once('.')
.map(|(_, tail)| tail)
.unwrap_or(module)
.trim();
(!tail.is_empty() && tail != module).then(|| tail.to_string())
}
fn collect_java_visibility(
root: Node<'_>,
file: FileId,
src: &[u8],
) -> std::collections::HashMap<bonsai_common::Span, Visibility> {
let mut visibility_by_span = std::collections::HashMap::new();
let mut work_stack = vec![root];
while let Some(node) = work_stack.pop() {
let kind = node.kind();
let is_class_or_member_decl = matches!(
kind,
"method_declaration"
| "constructor_declaration"
| "class_declaration"
| "interface_declaration"
| "enum_declaration"
| "annotation_type_declaration"
| "record_declaration"
);
if is_class_or_member_decl {
visibility_by_span.insert(span_of(file, &node), java_node_visibility(&node, src));
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
work_stack.push(child);
}
}
visibility_by_span
}
fn java_node_visibility(node: &Node<'_>, src: &[u8]) -> Visibility {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() != "modifiers" {
continue;
}
let mut modifier_cursor = child.walk();
for modifier in child.children(&mut modifier_cursor) {
match node_text(&modifier, src) {
"public" => return Visibility::Public,
"private" => return Visibility::Private,
"protected" => return Visibility::Protected,
_ => {}
}
}
}
Visibility::Module
}
fn is_class_like(kind: DeclKind) -> bool {
matches!(
kind,
DeclKind::Class | DeclKind::Interface | DeclKind::Trait | DeclKind::Struct | DeclKind::Enum
)
}
fn rewrite_java_explicit_constructor_invocations(index: &mut DeclIndex) {
use std::collections::HashMap;
let class_info: HashMap<bonsai_common::SymbolId, (String, Vec<String>)> = index
.defs
.iter()
.filter(|decl| is_class_like(decl.kind))
.map(|decl| (decl.symbol, (decl.name.clone(), decl.bases.clone())))
.collect();
for decl in &mut index.defs {
if !matches!(decl.kind, DeclKind::Constructor) {
continue;
}
let Some(parent) = decl.parent else {
continue;
};
let Some((class_name, bases)) = class_info.get(&parent) else {
continue;
};
let this_ctor = class_name.as_str();
let super_ctor = bases.first().map(String::as_str);
rewrite_java_explicit_constructor_invocations_in_events(&mut decl.flow_events, this_ctor, super_ctor);
}
}
fn rewrite_java_explicit_constructor_invocations_in_events(
events: &mut [FlowEvent],
this_ctor: &str,
super_ctor: Option<&str>,
) {
for event in events {
match event {
FlowEvent::Call {
name,
receiver,
receiver_types,
call_kind,
..
} => {
let replacement = match name.trim() {
"this" => Some((this_ctor, "this")),
"super" => super_ctor.map(|ctor| (ctor, "super")),
_ => None,
};
if let Some((replacement, replacement_receiver)) =
replacement.filter(|(replacement, _)| !replacement.is_empty())
{
name.clear();
name.push_str(replacement);
*receiver = Some(replacement_receiver.to_string());
receiver_types.clear();
*call_kind = bonsai_lang_api::CallKind::Constructor;
}
}
FlowEvent::Branch {
then_events,
else_events,
..
} => {
rewrite_java_explicit_constructor_invocations_in_events(then_events, this_ctor, super_ctor);
rewrite_java_explicit_constructor_invocations_in_events(else_events, this_ctor, super_ctor);
}
FlowEvent::Loop { body, .. } | FlowEvent::Defer { body, .. } | FlowEvent::Using { body, .. } => {
rewrite_java_explicit_constructor_invocations_in_events(body, this_ctor, super_ctor);
}
FlowEvent::Try {
body,
catch_events,
finally_events,
..
} => {
rewrite_java_explicit_constructor_invocations_in_events(body, this_ctor, super_ctor);
rewrite_java_explicit_constructor_invocations_in_events(catch_events, this_ctor, super_ctor);
rewrite_java_explicit_constructor_invocations_in_events(
finally_events,
this_ctor,
super_ctor,
);
}
FlowEvent::Assign { .. }
| FlowEvent::AggregateAssign { .. }
| FlowEvent::Return { .. }
| FlowEvent::Throw { .. }
| FlowEvent::Break { .. }
| FlowEvent::Continue { .. }
| FlowEvent::Yield { .. }
| FlowEvent::Await { .. }
| FlowEvent::Lifecycle { .. } => {}
}
}
}
fn collect_java_class_bases(
tree: &Tree,
file: FileId,
src: &[u8],
) -> Vec<(bonsai_common::Span, Vec<String>)> {
let mut out = Vec::new();
let class_kinds = &[
"class_declaration",
"interface_declaration",
"record_declaration",
"enum_declaration",
"annotation_type_declaration",
];
for class_node in collect_kinds(tree, class_kinds) {
let mut bases: Vec<String> = Vec::new();
if let Some(sc) = class_node.child_by_field_name("superclass") {
collect_java_base_names(sc, src, &mut bases);
}
if let Some(ifaces) = class_node.child_by_field_name("interfaces") {
collect_java_base_names(ifaces, src, &mut bases);
}
if let Some(extends) = class_node.child_by_field_name("extends_interfaces") {
collect_java_base_names(extends, src, &mut bases);
}
if let Some(permits) = class_node.child_by_field_name("permits") {
collect_java_base_names(permits, src, &mut bases);
}
if !bases.is_empty() {
out.push((span_of(file, &class_node), bases));
}
}
out
}
fn collect_java_class_string_constants(
tree: &Tree,
file: FileId,
src: &[u8],
) -> Vec<(bonsai_common::Span, Vec<FlowEvent>)> {
let class_kinds = &[
"class_declaration",
"interface_declaration",
"record_declaration",
"enum_declaration",
"annotation_type_declaration",
];
let mut out = Vec::new();
for class_node in collect_kinds(tree, class_kinds) {
let Some(body) = class_node.child_by_field_name("body") else {
continue;
};
let mut events = Vec::new();
let mut cursor = body.walk();
for child in body.named_children(&mut cursor) {
if child.kind() == "field_declaration" {
collect_java_final_string_field_assigns(child, file, src, &mut events);
}
}
if !events.is_empty() {
out.push((span_of(file, &class_node), events));
}
}
out
}
fn collect_java_final_string_field_assigns(
field: Node<'_>,
file: FileId,
src: &[u8],
out: &mut Vec<FlowEvent>,
) {
if !java_field_has_modifier(field, src, "final") || !java_field_type_is_string(field, src) {
return;
}
let mut cursor = field.walk();
for child in field.named_children(&mut cursor) {
if child.kind() != "variable_declarator" {
continue;
}
let Some(name_node) = child.child_by_field_name("name") else {
continue;
};
let Some(value_node) = child.child_by_field_name("value") else {
continue;
};
if value_node.kind() != "string_literal" {
continue;
}
out.push(FlowEvent::Assign {
span: span_of(file, &child),
target: node_text(&name_node, src).trim().to_string(),
source_name: None,
source_call: None,
source_call_args: Vec::new(),
source_names: Vec::new(),
declares_new_binding: false,
value_kind: Some(AssignValueKind::Literal),
});
}
}
fn java_field_has_modifier(field: Node<'_>, src: &[u8], wanted: &str) -> bool {
let mut cursor = field.walk();
for child in field.named_children(&mut cursor) {
if child.kind() != "modifiers" {
continue;
}
if node_text(&child, src)
.split_ascii_whitespace()
.any(|modifier| modifier == wanted)
{
return true;
}
}
false
}
fn java_field_type_is_string(field: Node<'_>, src: &[u8]) -> bool {
let Some(type_node) = field.child_by_field_name("type") else {
return false;
};
matches!(
canonical_java_type_name(node_text(&type_node, src)).as_deref(),
Some("String")
)
}
fn attach_java_class_string_constants(
index: &mut DeclIndex,
constants_by_class: &[(bonsai_common::Span, Vec<FlowEvent>)],
) {
if constants_by_class.is_empty() {
return;
}
let parent_by_symbol: std::collections::HashMap<_, _> = index
.defs
.iter()
.filter_map(|decl| Some((decl.symbol, decl.parent?)))
.collect();
let class_symbol_by_span: std::collections::HashMap<_, _> = index
.defs
.iter()
.filter(|decl| is_class_like(decl.kind))
.map(|decl| (decl.span, decl.symbol))
.collect();
let constants_by_symbol: std::collections::HashMap<_, _> = constants_by_class
.iter()
.filter_map(|(span, events)| {
class_symbol_by_span
.get(span)
.copied()
.map(|symbol| (symbol, events))
})
.collect();
for decl in &mut index.defs {
if !matches!(
decl.kind,
DeclKind::Function | DeclKind::Method | DeclKind::Constructor
) {
continue;
}
let mut ancestors = Vec::new();
let mut parent = decl.parent;
while let Some(symbol) = parent {
ancestors.push(symbol);
parent = parent_by_symbol.get(&symbol).copied();
}
if ancestors.is_empty() {
continue;
}
let mut visible_constants = Vec::new();
for symbol in ancestors.into_iter().rev() {
if let Some(events) = constants_by_symbol.get(&symbol) {
visible_constants.extend((*events).iter().cloned());
}
}
visible_constants.retain(|event| {
let FlowEvent::Assign { target, .. } = event else {
return false;
};
!decl.params.iter().any(|param| param == target)
&& !decl
.flow_events
.iter()
.any(|event| flow_event_assigns_target(event, target))
});
if !visible_constants.is_empty() {
visible_constants.extend(std::mem::take(&mut decl.flow_events));
decl.flow_events = visible_constants;
}
}
}
fn flow_event_assigns_target(event: &FlowEvent, wanted: &str) -> bool {
match event {
FlowEvent::Assign { target, .. } => target == wanted,
FlowEvent::Branch {
then_events,
else_events,
..
} => {
then_events
.iter()
.any(|event| flow_event_assigns_target(event, wanted))
|| else_events
.iter()
.any(|event| flow_event_assigns_target(event, wanted))
}
FlowEvent::Loop { body, .. } | FlowEvent::Defer { body, .. } | FlowEvent::Using { body, .. } => {
body.iter().any(|event| flow_event_assigns_target(event, wanted))
}
FlowEvent::Try {
body,
catch_events,
finally_events,
..
} => {
body.iter().any(|event| flow_event_assigns_target(event, wanted))
|| catch_events
.iter()
.any(|event| flow_event_assigns_target(event, wanted))
|| finally_events
.iter()
.any(|event| flow_event_assigns_target(event, wanted))
}
_ => false,
}
}
fn collect_java_base_names(node: Node<'_>, src: &[u8], out: &mut Vec<String>) {
let mut stack = vec![node];
while let Some(n) = stack.pop() {
match n.kind() {
"type_identifier" | "scoped_type_identifier" | "generic_type" => {
if let Some(name) = canonical_java_type_name(node_text(&n, src)) {
if !out.iter().any(|b| b == &name) {
out.push(name);
}
}
}
_ => {}
}
let mut cursor = n.walk();
for child in n.named_children(&mut cursor) {
stack.push(child);
}
}
}
fn populate_java_exception_types(events: &mut [bonsai_lang_api::FlowEvent], tree: &Tree, src: &[u8]) {
use bonsai_lang_api::FlowEvent;
for event in events {
match event {
FlowEvent::Throw {
span, thrown_type, ..
} => {
if thrown_type.is_some() {
continue;
}
if let Some(node) =
bonsai_lang_api::kit::node_at_span(tree.root_node(), *span, &["throw_statement"])
{
if let Some(name) = java_thrown_type_for_node(node, src) {
*thrown_type = Some(name);
}
}
}
FlowEvent::Try {
span,
body,
catch_events,
finally_events,
catch_types,
catch_param,
..
} => {
if let Some(node) =
bonsai_lang_api::kit::node_at_span(tree.root_node(), *span, &["try_statement"])
{
if catch_types.is_empty() {
*catch_types = collect_java_catch_types(node, src);
}
if let Some(name) = collect_java_catch_param_name(node, src) {
*catch_param = Some(name);
}
}
populate_java_exception_types(body, tree, src);
populate_java_exception_types(catch_events, tree, src);
populate_java_exception_types(finally_events, tree, src);
}
FlowEvent::Branch {
then_events,
else_events,
..
} => {
populate_java_exception_types(then_events, tree, src);
populate_java_exception_types(else_events, tree, src);
}
FlowEvent::Loop { body, .. } | FlowEvent::Defer { body, .. } | FlowEvent::Using { body, .. } => {
populate_java_exception_types(body, tree, src);
}
_ => {}
}
}
}
fn java_thrown_type_for_node(throw_node: Node<'_>, src: &[u8]) -> Option<String> {
let mut cursor = throw_node.walk();
for child in throw_node.named_children(&mut cursor) {
if child.kind() == "object_creation_expression" {
if let Some(t) = child.child_by_field_name("type") {
return Some(bonsai_lang_api::kit::canonical_simple_type_name(node_text(
&t, src,
)));
}
let mut tcur = child.walk();
for descendant in child.named_children(&mut tcur) {
if matches!(
descendant.kind(),
"type_identifier" | "generic_type" | "scoped_type_identifier"
) {
return Some(bonsai_lang_api::kit::canonical_simple_type_name(node_text(
&descendant,
src,
)));
}
}
}
}
None
}
fn collect_java_catch_param_name(try_node: Node<'_>, src: &[u8]) -> Option<String> {
let mut cursor = try_node.walk();
for child in try_node.named_children(&mut cursor) {
if child.kind() != "catch_clause" {
continue;
}
let mut ccur = child.walk();
for sub in child.named_children(&mut ccur) {
if sub.kind() != "catch_formal_parameter" {
continue;
}
if let Some(n) = sub.child_by_field_name("name") {
return Some(node_text(&n, src).trim().to_string());
}
let mut pcur = sub.walk();
let mut last_ident: Option<Node<'_>> = None;
for ptype in sub.named_children(&mut pcur) {
if ptype.kind() == "identifier" {
last_ident = Some(ptype);
}
}
if let Some(n) = last_ident {
return Some(node_text(&n, src).trim().to_string());
}
}
}
None
}
fn collect_java_catch_types(try_node: Node<'_>, src: &[u8]) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
let mut cursor = try_node.walk();
for child in try_node.named_children(&mut cursor) {
if child.kind() != "catch_clause" {
continue;
}
let mut ccur = child.walk();
for sub in child.named_children(&mut ccur) {
if sub.kind() != "catch_formal_parameter" {
continue;
}
let mut pcur = sub.walk();
for ptype in sub.named_children(&mut pcur) {
if ptype.kind() == "catch_type" {
let mut tcur = ptype.walk();
for t in ptype.named_children(&mut tcur) {
if matches!(
t.kind(),
"type_identifier" | "generic_type" | "scoped_type_identifier"
) {
let name = bonsai_lang_api::kit::canonical_simple_type_name(node_text(&t, src));
if !name.is_empty() && !out.iter().any(|x| x == &name) {
out.push(name);
}
}
}
}
}
}
}
out
}
fn rewrite_java_reflection_chain(events: &mut [bonsai_lang_api::FlowEvent]) {
use bonsai_lang_api::FlowEvent;
use std::collections::HashMap;
let mut reflective_alias: HashMap<String, String> = HashMap::new();
for event in events.iter_mut() {
match event {
FlowEvent::Assign {
target,
source_call,
source_call_args,
..
} => {
let Some(callee) = source_call else { continue };
let Some(literal_arg) = source_call_args.first() else {
continue;
};
let Some(literal_text) = strip_java_string_quotes(literal_arg) else {
continue;
};
let is_for_name = callee == "Class.forName" || callee.ends_with(".forName");
if is_for_name {
reflective_alias.insert(target.clone(), literal_text);
continue;
}
if let Some(get_method_receiver) = callee.strip_suffix(".getMethod") {
if let Some(class_name) = reflective_alias.get(get_method_receiver) {
let chained = format!("{class_name}.{literal_text}");
reflective_alias.insert(target.clone(), chained);
}
}
}
FlowEvent::Call {
name, receiver, args, ..
} => {
let Some(receiver_name) = receiver.as_deref() else {
continue;
};
if !name.ends_with(".invoke") {
continue;
}
let Some(target_class_method) = reflective_alias.get(receiver_name) else {
continue;
};
name.clone_from(target_class_method);
if !args.is_empty() {
args.remove(0);
}
*receiver = target_class_method
.rsplit_once('.')
.map(|(class_part, _)| class_part.to_string());
}
FlowEvent::Branch {
then_events,
else_events,
..
} => {
rewrite_java_reflection_chain(then_events);
rewrite_java_reflection_chain(else_events);
}
FlowEvent::Loop { body, .. } | FlowEvent::Defer { body, .. } | FlowEvent::Using { body, .. } => {
rewrite_java_reflection_chain(body);
}
FlowEvent::Try {
body,
catch_events,
finally_events,
..
} => {
rewrite_java_reflection_chain(body);
rewrite_java_reflection_chain(catch_events);
rewrite_java_reflection_chain(finally_events);
}
_ => {}
}
}
}
fn strip_java_string_quotes(text: &str) -> Option<String> {
let trimmed = text.trim();
if trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() >= 2 {
return Some(trimmed[1..trimmed.len() - 1].to_string());
}
None
}
fn extract_java_package(root: Node<'_>, src: &[u8]) -> Option<Vec<String>> {
let mut cursor = root.walk();
for child in root.children(&mut cursor) {
if child.kind() != "package_declaration" {
continue;
}
let mut sub = child.walk();
for subchild in child.children(&mut sub) {
if matches!(subchild.kind(), "scoped_identifier" | "identifier") {
let text = node_text(&subchild, src);
let segments: Vec<String> = text
.split('.')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect();
if !segments.is_empty() {
return Some(segments);
}
}
}
}
None
}