mod parse_recovery;
use bonsai_common::{FileId, Span};
use bonsai_lang_api::{
decl_index_with_handler, extract_imports_via,
kit::{
c_family_preproc_imports, collect_kinds, first_named_child_of_kind, language_from_pack, node_text,
package_module_segments_with_workspace_prefix, parse_with, span_of,
},
AdapterContext, AdapterError, ArgumentPassingMode, AssignValueKind, DeclIndex, DeclKind,
ExpressionPlaceExtraction, FieldWrite, FlowEvent, GrammarHandler, ImportIndex, ImportSpec,
LanguageAdapter, LanguageCapabilities, LanguageId, ModulePath, SyntaxSpecialForm, TypeAliasBinding,
};
use parse_recovery::{objc_parse_recovery_edits, objc_tree_proves_language};
use tree_sitter::{Language, Node, Tree};
fn objc_foreach_binding(node: Node<'_>) -> Option<(Node<'_>, Node<'_>)> {
if node.kind() == "for_in_statement" {
let binding = node
.child_by_field_name("left")
.or_else(|| node.child_by_field_name("declarator"))?;
let iterable = node
.child_by_field_name("right")
.or_else(|| node.child_by_field_name("value"))?;
return Some((binding, iterable));
}
if node.kind() != "for_statement" {
return None;
}
let child_count = u32::try_from(node.child_count()).ok()?;
let in_index =
(0..child_count).find(|index| node.child(*index).is_some_and(|child| child.kind() == "in"))?;
let binding = (0..in_index)
.rev()
.filter_map(|index| node.child(index))
.find(Node::is_named)?;
let iterable = ((in_index + 1)..child_count)
.filter_map(|index| node.child(index))
.find(Node::is_named)?;
Some((binding, iterable))
}
pub const LANG_ID: LanguageId = LanguageId::new("objc");
const PACK_NAME: &str = "objc";
fn objc_indirect_place_operand(node: Node<'_>) -> Option<Node<'_>> {
if !matches!(node.kind(), "pointer_expression" | "unary_expression") {
return None;
}
let mut cursor = node.walk();
let has_indirection = node
.children(&mut cursor)
.any(|child| matches!(child.kind(), "*" | "&"));
has_indirection
.then(|| {
node.child_by_field_name("argument")
.or_else(|| node.child_by_field_name("operand"))
})
.flatten()
}
const HANDLER: GrammarHandler = GrammarHandler {
literal_value_kinds: &["null", "true", "false"],
string_literal_kinds: &["string_literal", "char_literal", "concatenated_string"],
comment_kinds: &["comment"],
doc_comment_prefixes: &["///", "//!", "/**"],
decorator_kinds: &["attribute"],
parameter_container_kinds: &["parameter_list"],
parameter_kinds: &["parameter", "method_parameter", "parameter_declaration"],
parameter_annotation_kinds: &["attribute"],
parameter_annotation_name_extractor: None,
keyword_parameter_kinds: &["keyword_argument", "keyword_declarator"],
parameter_selector_kinds: &["keyword", "selector_keyword", "identifier"],
last_identifier_parameter_kinds: &["method_parameter"],
binding_identifier_kinds: &["identifier"],
anonymous_variadic_token: Some("..."),
identifier_kinds: &["identifier"],
named_aggregate_kinds: &["initializer_list", "dictionary_literal"],
positional_aggregate_kinds: &["initializer_list", "array_literal"],
aggregate_pair_kinds: &["initializer_pair"],
two_child_aggregate_pair_kinds: &["dictionary_pair"],
aggregate_key_field_names: &["designator"],
aggregate_value_field_names: &["value"],
static_field_name_kinds: &["field_identifier"],
static_subscript_key_extractor: Some(objc_static_string_key),
aggregate_syntax_only_kinds: &["type_identifier"],
transparent_call_wrapper_kinds: &["field_expression", "parenthesized_expression"],
single_expression_group_kinds: &["expression_list"],
assignment_target_wrapper_kinds: &[
"init_declarator",
"declarator",
"function_declarator",
"pointer_declarator",
"parenthesized_declarator",
"block_pointer_declarator",
],
binding_declaration_keyword_spellings: &["auto", "const"],
nested_type_ownership: true,
fn_kinds: &[
"function_definition",
"method_definition",
"class_method_declaration",
"instance_method_declaration",
],
class_kinds: &[
"class_interface",
"class_implementation",
"category_interface",
"category_implementation",
"protocol_declaration",
],
class_decl_kinds: &[
("class_interface", DeclKind::Class),
("class_implementation", DeclKind::Class),
("category_interface", DeclKind::Class),
("category_implementation", DeclKind::Class),
("protocol_declaration", DeclKind::Interface),
],
method_kinds: &["method_definition"],
method_context_kinds: &["class_implementation", "category_implementation"],
method_owner_barrier_kinds: &[],
constructor_method_kinds: &[],
constructor_names: &["init"],
if_kinds: &["if_statement"],
branch_then_field_names: &["consequence", "body"],
branch_else_field_names: &["alternative"],
branch_condition_field_names: &["condition"],
loop_body_field_names: &["body"],
loop_body_kinds: &["compound_statement", "expression_statement"],
branch_arm_kinds: &["compound_statement", "expression_statement"],
for_kinds: &["for_statement"],
foreach_kinds: &[],
foreach_binding_extractor: Some(objc_foreach_binding),
while_kinds: &["while_statement"],
do_kinds: &["do_statement"],
loop_kinds: &[],
call_kinds: &["call_expression", "message_expression"],
call_callee_field_names: &["function"],
call_receiver_field_names: &["receiver"],
call_member_field_names: &["method"],
call_argument_field_names: &["arguments"],
call_argument_container_kinds: &["argument_list"],
direct_call_argument_excluded_fields: &["receiver", "method"],
lambda_body_field_names: &["body"],
argument_passing_mode_extractor: Some(objc_argument_passing_mode),
indirect_place_operand_extractor: Some(objc_indirect_place_operand),
expression_value_kind_extractor: Some(objc_expression_value_kind),
call_ref_kinds: &["call_expression", "message_expression"],
member_expression_kinds: &["field_expression"],
subscript_expression_kinds: &["subscript_expression"],
member_base_field_names: &["argument"],
member_name_field_names: &["field"],
subscript_base_field_names: &["argument"],
subscript_index_field_names: &["index"],
expression_place_extractor: Some(objc_expression_places),
syntax_error_tolerant_call_names: &["va_arg", "__builtin_va_arg"],
assignment_kinds: &["assignment_expression", "init_declarator"],
compound_assignment_operators: &["+=", "-=", "*=", "/=", "%=", "<<=", ">>=", "&=", "^=", "|="],
positional_aggregate_assignment_kinds: &["init_declarator"],
positional_aggregate_value_kinds: &["initializer_list"],
return_kinds: &["return_statement"],
throw_kinds: &["throw_statement"],
lambda_kinds: &["block_literal", "block_literal_expression"],
try_kinds: &["try_statement"],
catch_kinds: &["catch_clause"],
finally_kinds: &["finally_clause"],
break_kinds: &["break_statement"],
continue_kinds: &["continue_statement"],
control_label_field_names: &[],
yield_kinds: &[],
await_kinds: &[],
defer_kinds: &[],
using_kinds: &["synchronized_statement", "autoreleasepool_statement"],
using_body_field_names: &["body"],
try_body_field_names: &["body"],
special_forms: &[SyntaxSpecialForm::DirectCallArguments],
value_free_expression_kinds: &["sizeof_expression", "alignof_expression", "typeof_specifier"],
method_receiver_param_index: None,
implicit_receiver_names: &["self", "super"],
implicit_receiver_prefixes: &[],
tail_expression_returns: false,
void_return_type_names: &[],
..bonsai_lang_api::EMPTY_HANDLER
};
fn objc_expression_value_kind(node: Node<'_>, _src: &[u8]) -> Option<AssignValueKind> {
matches!(node.kind(), "string_literal" | "char_literal" | "number_literal")
.then_some(AssignValueKind::Literal)
}
fn objc_argument_passing_mode(argument: Node<'_>, value: Node<'_>) -> ArgumentPassingMode {
if [argument, value].into_iter().any(|node| {
matches!(node.kind(), "unary_expression" | "pointer_expression") && {
let mut cursor = node.walk();
let has_address_of = node.children(&mut cursor).any(|child| child.kind() == "&");
has_address_of
}
}) {
ArgumentPassingMode::WriteBack
} else {
ArgumentPassingMode::Value
}
}
#[derive(Debug, Default, Copy, Clone)]
pub struct ObjCAdapter;
impl ObjCAdapter {
#[must_use]
pub fn new() -> Self {
Self
}
}
impl LanguageAdapter for ObjCAdapter {
fn language_id(&self) -> LanguageId {
LANG_ID
}
fn display_name(&self) -> &'static str {
"Objective-C"
}
fn file_extensions(&self) -> &'static [&'static str] {
&["m", "mm", "h"]
}
fn tree_sitter_language(&self) -> Result<Language, AdapterError> {
language_from_pack(PACK_NAME)
}
fn source_syntax_proves_language(
&self,
snapshot: &bonsai_lang_api::FileSnapshot,
tree: &Tree,
) -> bonsai_lang_api::LanguageOwnershipEvidence {
if objc_tree_proves_language(snapshot, tree) {
bonsai_lang_api::LanguageOwnershipEvidence::Proven
} else {
bonsai_lang_api::LanguageOwnershipEvidence::Excluded
}
}
fn parse_recovery_edits(
&self,
snapshot: &bonsai_lang_api::FileSnapshot,
vfs: &bonsai_lang_api::Vfs,
tree: &Tree,
) -> Vec<bonsai_lang_api::ParseRecoveryEdit> {
objc_parse_recovery_edits(snapshot, vfs, tree)
}
fn capabilities(&self) -> LanguageCapabilities {
LanguageCapabilities {
module_default_export_names: &[],
universal_type_names: &["id"],
module_path_syntax: bonsai_lang_api::ModulePathSyntax::none(),
macros: bonsai_lang_api::CapabilityLevel::Partial,
receiver_types: bonsai_lang_api::CapabilityLevel::Partial,
constructor_method_names: &["init"],
super_receiver_tokens: &["super"],
implicit_receiver_tokens: &["self"],
same_directory_unqualified_calls: true,
build_target_linkage: true,
..LanguageCapabilities::partial_baseline()
}
}
fn extract_declarations(&self, file: FileId, ctx: &AdapterContext<'_>) -> DeclIndex {
let mut decl_index = decl_index_with_handler(PACK_NAME, file, ctx, &HANDLER);
bonsai_lang_api::apply_file_stem_semantic_identity(&mut decl_index, ctx);
apply_objc_class_semantic_identity(&mut decl_index, ctx);
mark_objc_initializer_methods(&mut decl_index);
let constructor_selectors = decl_index
.defs
.iter()
.filter(|decl| decl.kind == DeclKind::Constructor)
.map(|decl| decl.name.clone())
.collect::<std::collections::HashSet<_>>();
let declared_class_names = decl_index
.defs
.iter()
.filter(|decl| {
matches!(
decl.kind,
DeclKind::Class | DeclKind::Interface | DeclKind::Trait | DeclKind::Struct
)
})
.flat_map(|decl| std::iter::once(decl.name.clone()).chain(decl.qualified_name.clone()))
.map(|name| bonsai_common::short_qualified_tail(&name).to_string())
.collect::<std::collections::HashSet<_>>();
if let Some((snapshot, tree)) = parse_with(PACK_NAME, file, ctx) {
let src = snapshot.text.as_bytes();
bonsai_lang_api::kit::inject_c_family_function_pointer_aliases(&mut decl_index, &tree, src, file);
let aliases_by_span = collect_objc_method_type_aliases(&tree, file, src);
for decl in &mut decl_index.defs {
if let Some(aliases) = aliases_by_span
.iter()
.find_map(|(span, aliases)| (*span == decl.span).then_some(aliases))
{
decl.type_aliases = aliases.clone();
}
}
let bases_by_class = collect_objc_class_bases(&tree, file, src);
for decl in &mut decl_index.defs {
if !matches!(
decl.kind,
DeclKind::Class | DeclKind::Interface | DeclKind::Trait | DeclKind::Struct
) {
continue;
}
if let Some(bases) = bases_by_class.iter().find_map(|(span, name, bases)| {
(*span == decl.span || name == &decl.name).then_some(bases)
}) {
decl.bases = bases.clone();
}
}
let bases_by_name = decl_index
.defs
.iter()
.filter(|decl| {
matches!(
decl.kind,
DeclKind::Class | DeclKind::Interface | DeclKind::Trait | DeclKind::Struct
) && !decl.bases.is_empty()
})
.map(|decl| (decl.name.clone(), decl.bases.clone()))
.collect::<std::collections::HashMap<_, _>>();
for decl in &mut decl_index.defs {
if !matches!(
decl.kind,
DeclKind::Class | DeclKind::Interface | DeclKind::Trait | DeclKind::Struct
) || !decl.bases.is_empty()
{
continue;
}
if let Some(bases) = bases_by_name.get(&decl.name) {
decl.bases = bases.clone();
}
}
for decl in &mut decl_index.defs {
suppress_objc_dynamic_subscript_literal_overwrites(&mut decl.flow_events, &tree, src);
augment_objc_dictionary_flow_events(&mut decl.flow_events, &tree, src);
}
}
for decl in &mut decl_index.defs {
enrich_objc_receiver_field_writes(decl);
tag_objc_alloc_receiver_types(
&mut decl.flow_events,
&constructor_selectors,
&declared_class_names,
);
bonsai_lang_api::normalize_call_result_assignment_sources(&mut decl.flow_events);
}
if let Some((snapshot, tree)) = parse_with(PACK_NAME, file, ctx) {
let src = snapshot.text.as_bytes();
for decl in &mut decl_index.defs {
fix_objc_catch_params(&mut decl.flow_events, &tree, src);
}
}
bonsai_lang_api::apply_constructor_result_type_aliases(&mut decl_index);
bonsai_lang_api::apply_class_field_type_aliases(&mut decl_index);
decl_index
}
fn extract_imports(&self, file: FileId, ctx: &AdapterContext<'_>) -> ImportIndex {
extract_imports_via(PACK_NAME, file, ctx, parse_imports)
}
}
fn apply_objc_class_semantic_identity(decl_index: &mut DeclIndex, ctx: &AdapterContext<'_>) {
for decl in &mut decl_index.defs {
if !matches!(
decl.kind,
DeclKind::Class | DeclKind::Interface | DeclKind::Trait | DeclKind::Struct
) {
continue;
}
let segments =
package_module_segments_with_workspace_prefix(decl_index.file, ctx, [decl.name.clone()], &[]);
let prefix = segments.join("::");
decl.module_path = ModulePath::from_segments(segments);
decl.qualified_name = Some(format!("{prefix}::{}", decl.name));
}
}
fn parse_imports(tree: &Tree, src: &[u8], file: FileId) -> Vec<ImportSpec> {
c_family_preproc_imports(tree, src, file)
}
fn tag_objc_alloc_receiver_types(
events: &mut [bonsai_lang_api::FlowEvent],
constructor_selectors: &std::collections::HashSet<String>,
declared_class_names: &std::collections::HashSet<String>,
) {
for event in events {
match event {
FlowEvent::Call {
name,
receiver,
receiver_types,
call_kind,
..
} => {
let class_name = receiver
.as_deref()
.and_then(objc_alloc_class_name)
.or_else(|| objc_alloc_class_name_from_call_name(name));
if let Some(class_name) = class_name.filter(|name| declared_class_names.contains(name)) {
if !receiver_types.iter().any(|existing| existing == &class_name) {
receiver_types.push(class_name);
}
let selector = name.rsplit('.').next().unwrap_or(name).trim();
if constructor_selectors.contains(selector) {
*call_kind = bonsai_lang_api::CallKind::Constructor;
}
}
}
FlowEvent::Branch {
then_events,
else_events,
..
} => {
tag_objc_alloc_receiver_types(then_events, constructor_selectors, declared_class_names);
tag_objc_alloc_receiver_types(else_events, constructor_selectors, declared_class_names);
}
FlowEvent::Loop { body, .. } | FlowEvent::Defer { body, .. } | FlowEvent::Using { body, .. } => {
tag_objc_alloc_receiver_types(body, constructor_selectors, declared_class_names);
}
FlowEvent::Try {
body,
catch_events,
finally_events,
..
} => {
tag_objc_alloc_receiver_types(body, constructor_selectors, declared_class_names);
tag_objc_alloc_receiver_types(catch_events, constructor_selectors, declared_class_names);
tag_objc_alloc_receiver_types(finally_events, constructor_selectors, declared_class_names);
}
_ => {}
}
}
}
fn mark_objc_initializer_methods(decl_index: &mut DeclIndex) {
for decl in &mut decl_index.defs {
if matches!(decl.kind, DeclKind::Method | DeclKind::Function)
&& (decl.name == "init" || decl.name.starts_with("initWith"))
{
decl.kind = DeclKind::Constructor;
}
}
}
fn suppress_objc_dynamic_subscript_literal_overwrites(events: &mut Vec<FlowEvent>, tree: &Tree, src: &[u8]) {
for event in events.iter_mut() {
match event {
FlowEvent::Branch {
then_events,
else_events,
..
} => {
suppress_objc_dynamic_subscript_literal_overwrites(then_events, tree, src);
suppress_objc_dynamic_subscript_literal_overwrites(else_events, tree, src);
}
FlowEvent::Loop { body, .. } | FlowEvent::Defer { body, .. } | FlowEvent::Using { body, .. } => {
suppress_objc_dynamic_subscript_literal_overwrites(body, tree, src);
}
FlowEvent::Try {
body,
catch_events,
finally_events,
..
} => {
suppress_objc_dynamic_subscript_literal_overwrites(body, tree, src);
suppress_objc_dynamic_subscript_literal_overwrites(catch_events, tree, src);
suppress_objc_dynamic_subscript_literal_overwrites(finally_events, tree, src);
}
_ => {}
}
}
let root = tree.root_node();
events.retain(|event| {
let FlowEvent::Assign {
span,
source_name,
source_call,
source_call_args,
source_names,
value_kind,
..
} = event
else {
return true;
};
let source_free_literal = matches!(value_kind, Some(AssignValueKind::Literal))
|| (value_kind.is_none()
&& source_name.is_none()
&& source_call.is_none()
&& source_call_args.is_empty()
&& source_names.is_empty());
if !source_free_literal {
return true;
}
!objc_assignment_has_dynamic_subscript_lhs(root, *span, src)
});
}
fn objc_assignment_has_dynamic_subscript_lhs(root: Node<'_>, span: Span, src: &[u8]) -> bool {
let Some(node) =
bonsai_lang_api::kit::node_at_span(root, span, &["assignment_expression", "subscript_expression"])
else {
return false;
};
let lhs = node
.child_by_field_name("left")
.or_else(|| node.child_by_field_name("target"))
.unwrap_or(node);
let Some(subscript) = first_descendant_of_kind(lhs, "subscript_expression") else {
return false;
};
let Some(index) = subscript.child_by_field_name("index") else {
return true;
};
objc_static_string_key(index, src).is_none()
}
fn augment_objc_dictionary_flow_events(events: &mut Vec<FlowEvent>, tree: &Tree, src: &[u8]) {
for event in events.iter_mut() {
match event {
FlowEvent::Branch {
then_events,
else_events,
..
} => {
augment_objc_dictionary_flow_events(then_events, tree, src);
augment_objc_dictionary_flow_events(else_events, tree, src);
}
FlowEvent::Loop { body, .. } | FlowEvent::Defer { body, .. } | FlowEvent::Using { body, .. } => {
augment_objc_dictionary_flow_events(body, tree, src);
}
FlowEvent::Try {
body,
catch_events,
finally_events,
..
} => {
augment_objc_dictionary_flow_events(body, tree, src);
augment_objc_dictionary_flow_events(catch_events, tree, src);
augment_objc_dictionary_flow_events(finally_events, tree, src);
}
_ => {}
}
}
let root = tree.root_node();
let mut rewritten = Vec::with_capacity(events.len());
for event in events.drain(..) {
let mut synthetic = Vec::new();
if let FlowEvent::Assign { span, target, .. } = &event {
if let Some(dict) = objc_assignment_dictionary_literal(root, *span) {
synthetic.extend(objc_dictionary_field_assigns(target, *span, dict, src));
}
}
rewritten.push(event);
rewritten.extend(synthetic);
}
*events = rewritten;
}
fn objc_assignment_dictionary_literal<'tree>(root: Node<'tree>, span: Span) -> Option<Node<'tree>> {
let node = bonsai_lang_api::kit::node_at_span(
root,
span,
&["init_declarator", "assignment_expression", "dictionary_literal"],
)?;
if node.kind() == "dictionary_literal" {
return Some(node);
}
let rhs = node
.child_by_field_name("value")
.or_else(|| node.child_by_field_name("right"))
.unwrap_or(node);
first_descendant_of_kind(rhs, "dictionary_literal")
}
fn first_descendant_of_kind<'tree>(node: Node<'tree>, kind: &str) -> Option<Node<'tree>> {
if node.kind() == kind {
return Some(node);
}
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
if let Some(found) = first_descendant_of_kind(child, kind) {
return Some(found);
}
}
None
}
fn objc_dictionary_field_assigns(
target: &str,
span: Span,
dictionary: Node<'_>,
src: &[u8],
) -> Vec<FlowEvent> {
let mut out = Vec::new();
let mut cursor = dictionary.walk();
for pair in dictionary.named_children(&mut cursor) {
if pair.kind() != "dictionary_pair" {
continue;
}
let Some((key_node, value_node)) = objc_dictionary_pair_nodes(pair) else {
continue;
};
let Some(key) = objc_static_string_key(key_node, src) else {
continue;
};
let source_names = objc_value_source_names(value_node, src);
let value_kind = if source_names.is_empty() && objc_value_is_literal(value_node) {
Some(AssignValueKind::Literal)
} else {
Some(AssignValueKind::Compound)
};
out.push(FlowEvent::Assign {
span,
target: format!("{target}.@{key}"),
source_name: None,
source_call: None,
source_call_args: Vec::new(),
source_names,
declares_new_binding: false,
value_kind,
});
}
out
}
fn objc_dictionary_pair_nodes(pair: Node<'_>) -> Option<(Node<'_>, Node<'_>)> {
let mut cursor = pair.walk();
let mut children = pair.named_children(&mut cursor);
let key = children.next()?;
let value = children.next()?;
Some((key, value))
}
fn objc_static_string_key(node: Node<'_>, src: &[u8]) -> Option<String> {
let raw = node_text(&node, src).trim();
let without_at = raw.strip_prefix('@').unwrap_or(raw);
let key = without_at
.strip_prefix('"')
.and_then(|part| part.strip_suffix('"'))
.or_else(|| {
without_at
.strip_prefix('\'')
.and_then(|part| part.strip_suffix('\''))
})?
.trim();
if key.is_empty()
|| !key
.chars()
.next()
.is_some_and(|ch| ch == '_' || ch.is_ascii_alphabetic())
|| !key.chars().all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
{
return None;
}
Some(key.to_string())
}
fn objc_value_source_names(node: Node<'_>, src: &[u8]) -> Vec<String> {
let mut out = Vec::new();
collect_objc_value_source_names(node, src, &mut out);
out
}
fn collect_objc_value_source_names(node: Node<'_>, src: &[u8], out: &mut Vec<String>) {
match node.kind() {
"string_literal" | "number_literal" | "char_literal" | "null" => return,
"identifier" => {
let text = node_text(&node, src).trim();
if objc_identifier_is_value(text) {
push_objc_source_name(out, text.to_string());
}
return;
}
"field_expression" => {
if let Some(place) = objc_place_name(node, src) {
push_objc_source_name(out, place);
return;
}
}
"subscript_expression" => {
if let Some(place) = objc_place_name(node, src) {
push_objc_source_name(out, place);
return;
}
}
"message_expression" => {
let receiver = node.child_by_field_name("receiver");
let method = node.child_by_field_name("method");
if let Some(receiver) = receiver {
collect_objc_value_source_names(receiver, src, out);
}
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
let skip_receiver = receiver.is_some_and(|receiver| receiver.id() == child.id());
let skip_method = method.is_some_and(|method| method.id() == child.id());
if !skip_receiver && !skip_method {
collect_objc_value_source_names(child, src, out);
}
}
return;
}
"call_expression" => {
if let Some(args) = node
.child_by_field_name("arguments")
.or_else(|| node.child_by_field_name("argument_list"))
{
let mut cursor = args.walk();
for arg in args.named_children(&mut cursor) {
collect_objc_value_source_names(arg, src, out);
}
return;
}
}
_ => {}
}
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
collect_objc_value_source_names(child, src, out);
}
}
fn objc_place_name(node: Node<'_>, src: &[u8]) -> Option<String> {
match node.kind() {
"identifier" | "type_identifier" => {
let text = node_text(&node, src).trim();
(!text.is_empty()).then(|| text.to_string())
}
"field_expression" => {
let base = node
.child_by_field_name("argument")
.or_else(|| node.child_by_field_name("object"))
.or_else(|| node.child_by_field_name("value"))?;
let field = node.child_by_field_name("field")?;
let base = objc_place_name(base, src)?;
let field = node_text(&field, src).trim();
(!field.is_empty()).then(|| format!("{base}.{field}"))
}
"message_expression" => {
let receiver = node.child_by_field_name("receiver")?;
let method = node.child_by_field_name("method")?;
let mut cursor = node.walk();
let has_value_arguments = node
.named_children(&mut cursor)
.any(|child| child.id() != receiver.id() && child.id() != method.id());
if has_value_arguments {
return None;
}
let receiver = objc_place_name(receiver, src)?;
let method = node_text(&method, src).trim();
(!method.is_empty()).then(|| format!("{receiver}.{method}"))
}
"subscript_expression" => {
let base = node
.child_by_field_name("argument")
.or_else(|| node.child_by_field_name("object"))
.or_else(|| node.child_by_field_name("value"))?;
let index = node.child_by_field_name("index")?;
let base = objc_place_name(base, src)?;
let key = objc_static_string_key(index, src)?;
Some(format!("{base}.@{key}"))
}
"parenthesized_expression" | "at_expression" => {
let mut cursor = node.walk();
let place = node
.named_children(&mut cursor)
.find_map(|child| objc_place_name(child, src));
place
}
_ => None,
}
}
fn objc_expression_places(node: Node<'_>, src: &[u8]) -> ExpressionPlaceExtraction {
if node.kind() != "field_expression" {
return ExpressionPlaceExtraction::default();
}
objc_place_name(node, src).map_or_else(ExpressionPlaceExtraction::default, |place| {
ExpressionPlaceExtraction {
places: vec![place],
consumed_node_ids: vec![node.id()],
}
})
}
fn objc_identifier_is_value(text: &str) -> bool {
!matches!(text, "" | "nil" | "NULL" | "YES" | "NO" | "true" | "false")
}
fn objc_value_is_literal(node: Node<'_>) -> bool {
match node.kind() {
"string_literal" | "number_literal" | "char_literal" | "null" => true,
"at_expression" | "parenthesized_expression" => {
let mut cursor = node.walk();
let all_literal = node.named_children(&mut cursor).all(objc_value_is_literal);
all_literal
}
"array_literal" => {
let mut cursor = node.walk();
let all_literal = node.named_children(&mut cursor).all(objc_value_is_literal);
all_literal
}
_ => false,
}
}
fn push_objc_source_name(out: &mut Vec<String>, value: String) {
if !value.is_empty() && !out.iter().any(|existing| existing == &value) {
out.push(value);
}
}
fn enrich_objc_receiver_field_writes(decl: &mut bonsai_lang_api::Decl) {
let params = decl.params.clone();
let local_names: std::collections::HashSet<String> =
decl.type_aliases.iter().map(|alias| alias.name.clone()).collect();
enrich_objc_receiver_field_writes_inner(
&mut decl.receiver_field_writes,
&decl.flow_events,
¶ms,
&local_names,
);
decl.receiver_field_writes
.sort_by_key(|write| (write.span.start, write.target.clone()));
decl.receiver_field_writes.dedup_by(|a, b| {
a.span == b.span && a.target == b.target && a.source_param_indices == b.source_param_indices
});
}
fn enrich_objc_receiver_field_writes_inner(
out: &mut Vec<FieldWrite>,
events: &[FlowEvent],
params: &[String],
local_names: &std::collections::HashSet<String>,
) {
for event in events {
match event {
FlowEvent::Assign {
span,
target,
source_name,
source_names,
..
} if objc_target_is_receiver_field(target, local_names) => {
let source_param_indices = params
.iter()
.enumerate()
.filter_map(|(idx, param)| {
let source_matches = source_name.as_deref() == Some(param.as_str())
|| source_names.iter().any(|source| source == param);
source_matches.then_some(idx)
})
.collect::<Vec<_>>();
if source_param_indices.is_empty() {
continue;
}
out.push(FieldWrite {
span: *span,
target: objc_receiver_field_target(target),
source_param_indices,
});
}
FlowEvent::Branch {
then_events,
else_events,
..
} => {
enrich_objc_receiver_field_writes_inner(out, then_events, params, local_names);
enrich_objc_receiver_field_writes_inner(out, else_events, params, local_names);
}
FlowEvent::Loop { body, .. } | FlowEvent::Defer { body, .. } | FlowEvent::Using { body, .. } => {
enrich_objc_receiver_field_writes_inner(out, body, params, local_names);
}
FlowEvent::Try {
body,
catch_events,
finally_events,
..
} => {
enrich_objc_receiver_field_writes_inner(out, body, params, local_names);
enrich_objc_receiver_field_writes_inner(out, catch_events, params, local_names);
enrich_objc_receiver_field_writes_inner(out, finally_events, params, local_names);
}
_ => {}
}
}
}
fn objc_target_is_receiver_field(target: &str, local_names: &std::collections::HashSet<String>) -> bool {
if local_names.contains(target) {
return target.starts_with("self.");
}
target
.strip_prefix('_')
.is_some_and(|tail| !tail.is_empty() && !tail.starts_with('_'))
|| target.starts_with("self.")
}
fn objc_receiver_field_target(target: &str) -> String {
if let Some(field) = target.strip_prefix('_') {
format!("self.{field}")
} else {
target.to_string()
}
}
fn fix_objc_catch_params(events: &mut [bonsai_lang_api::FlowEvent], tree: &Tree, src: &[u8]) {
for event in events {
match event {
FlowEvent::Try {
span,
body,
catch_events,
finally_events,
catch_param,
..
} => {
if let Some(node) =
bonsai_lang_api::kit::node_at_span(tree.root_node(), *span, &["try_statement"])
{
if let Some(name) = objc_catch_param_binding(node, src) {
*catch_param = Some(name);
}
}
fix_objc_catch_params(body, tree, src);
fix_objc_catch_params(catch_events, tree, src);
fix_objc_catch_params(finally_events, tree, src);
}
FlowEvent::Branch {
then_events,
else_events,
..
} => {
fix_objc_catch_params(then_events, tree, src);
fix_objc_catch_params(else_events, tree, src);
}
FlowEvent::Loop { body, .. } | FlowEvent::Defer { body, .. } | FlowEvent::Using { body, .. } => {
fix_objc_catch_params(body, tree, src);
}
_ => {}
}
}
}
fn objc_catch_param_binding(try_node: Node<'_>, src: &[u8]) -> Option<String> {
let mut tcur = try_node.walk();
for child in try_node.named_children(&mut tcur) {
if child.kind() != "catch_clause" {
continue;
}
let mut ccur = child.walk();
for sub in child.named_children(&mut ccur) {
if !matches!(
sub.kind(),
"type_name" | "parameter_list" | "parameter_declaration"
) {
continue;
}
if let Some(text) = last_identifier_text_in_subtree(sub, src) {
return Some(text);
}
}
}
None
}
fn last_identifier_text_in_subtree(node: Node<'_>, src: &[u8]) -> Option<String> {
let mut last: Option<Node<'_>> = None;
let mut stack = vec![node];
while let Some(n) = stack.pop() {
if matches!(n.kind(), "identifier" | "field_identifier") {
match last {
Some(prev) if prev.start_byte() >= n.start_byte() => {}
_ => last = Some(n),
}
}
let mut cursor = n.walk();
for child in n.named_children(&mut cursor) {
stack.push(child);
}
}
last.map(|n| node_text(&n, src).trim().to_string())
}
fn objc_alloc_class_name_from_call_name(name: &str) -> Option<String> {
let trimmed = name.trim();
if !trimmed.starts_with('[') {
return None;
}
let mut depth: i32 = 0;
for (idx, ch) in trimmed.char_indices() {
match ch {
'[' => depth += 1,
']' => {
depth -= 1;
if depth == 0 {
let segment = &trimmed[..=idx];
return objc_alloc_class_name(segment);
}
}
_ => {}
}
}
None
}
fn objc_alloc_class_name(receiver: &str) -> Option<String> {
let trimmed = receiver.trim();
let (class, selector) = if let Some(inner) = trimmed
.strip_prefix('[')
.and_then(|inner| inner.strip_suffix(']'))
{
inner.trim().rsplit_once(char::is_whitespace)?
} else {
trimmed.rsplit_once('.')?
};
let class = class.trim();
let selector = selector.trim();
if !matches!(selector, "alloc" | "new") {
return None;
}
let class = class.split_whitespace().last()?.trim();
if class.is_empty() {
return None;
}
let mut chars = class.chars();
let first = chars.next()?;
if !(first.is_ascii_alphabetic() || first == '_')
|| !chars.all(|character| character.is_alphanumeric() || character == '_')
{
return None;
}
Some(class.to_string())
}
fn collect_objc_class_bases(tree: &Tree, file: FileId, src: &[u8]) -> Vec<(Span, String, Vec<String>)> {
let class_kinds = &[
"class_interface",
"class_implementation",
"category_interface",
"category_implementation",
];
let mut out: Vec<(Span, String, Vec<String>)> = Vec::new();
for class_node in collect_kinds(tree, class_kinds) {
let class_name = objc_class_name(class_node, src).unwrap_or_default();
let mut bases: Vec<String> = Vec::new();
if let Some(superclass) = class_node
.child_by_field_name("superclass")
.or_else(|| class_node.child_by_field_name("superclass_reference"))
.or_else(|| class_node.child_by_field_name("base"))
{
let raw = node_text(&superclass, src).trim().to_string();
if let Some(name) = canonical_objc_base_name(&raw) {
if !bases.iter().any(|existing| existing == &name) {
bases.push(name);
}
}
}
let mut cursor = class_node.walk();
for child in class_node.named_children(&mut cursor) {
if matches!(
child.kind(),
"superclass_reference" | "superclass" | "protocol_reference_list" | "protocol_qualifiers"
) {
let raw = node_text(&child, src).trim().to_string();
for piece in raw.split(',') {
let cleaned = piece
.trim()
.trim_matches(|c: char| matches!(c, '<' | '>' | ':' | '*'));
if let Some(name) = canonical_objc_base_name(cleaned) {
if !bases.iter().any(|existing| existing == &name) {
bases.push(name);
}
}
}
}
}
let class_span = span_of(file, &class_node);
if !bases.is_empty() {
if let Some((_, _, existing)) = out.iter_mut().find(|(span, _, _)| *span == class_span) {
for base in bases {
if !existing.iter().any(|already| already == &base) {
existing.push(base);
}
}
} else {
out.push((class_span, class_name, bases));
}
}
}
out
}
fn objc_class_name(node: Node<'_>, src: &[u8]) -> Option<String> {
let name_node = node
.child_by_field_name("name")
.or_else(|| first_named_child_of_kind(&node, "type_identifier"))
.or_else(|| first_named_child_of_kind(&node, "identifier"))?;
let name = node_text(&name_node, src).trim();
if name.is_empty() {
None
} else {
Some(name.to_string())
}
}
fn canonical_objc_base_name(raw: &str) -> Option<String> {
let trimmed = raw.trim().trim_start_matches(':').trim();
let head = trimmed.split('<').next().unwrap_or(trimmed).trim();
let head = head.split('*').next().unwrap_or(head).trim();
let bare = head.rsplit("::").next().unwrap_or(head).trim();
let bare = bare.trim_start_matches('@').trim();
if bare.is_empty()
|| !bare
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
{
return None;
}
Some(bare.to_string())
}
fn collect_objc_method_type_aliases(
tree: &Tree,
file: FileId,
src: &[u8],
) -> Vec<(bonsai_common::Span, Vec<TypeAliasBinding>)> {
let mut aliases_by_fn = Vec::new();
for fn_node in collect_kinds(
tree,
&[
"function_definition",
"method_definition",
"class_method_declaration",
"instance_method_declaration",
],
) {
let mut aliases: Vec<TypeAliasBinding> = Vec::new();
if let Some(params_list) = find_objc_parameter_list(fn_node) {
collect_objc_c_parameter_aliases(params_list, src, &mut aliases);
}
collect_objc_local_type_aliases(fn_node, src, &mut aliases);
let mut cursor = fn_node.walk();
for child in fn_node.named_children(&mut cursor) {
if child.kind() == "keyword_argument" {
objc_keyword_argument_alias(child, src, &mut aliases);
}
}
dedup_objc_type_aliases(&mut aliases);
if !aliases.is_empty() {
aliases_by_fn.push((span_of(file, &fn_node), aliases));
}
}
aliases_by_fn
}
fn collect_objc_local_type_aliases(node: Node<'_>, src: &[u8], aliases: &mut Vec<TypeAliasBinding>) {
let mut stack = vec![node];
while let Some(current) = stack.pop() {
if current.kind() == "declaration" {
objc_declaration_aliases(current, src, aliases);
}
let mut cursor = current.walk();
for child in current.named_children(&mut cursor) {
stack.push(child);
}
}
}
fn objc_declaration_aliases(node: Node<'_>, src: &[u8], aliases: &mut Vec<TypeAliasBinding>) {
let Some(type_node) = node
.child_by_field_name("type")
.or_else(|| objc_declaration_type_node(node))
else {
return;
};
let Some(canonical_type) = canonical_objc_type_name(node_text(&type_node, src)) else {
return;
};
if let Some(declarator) = node.child_by_field_name("declarator") {
if let Some(name) = objc_declarator_identifier(declarator, src) {
push_objc_type_alias(aliases, &name, &canonical_type);
}
}
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
if child.kind() != "init_declarator" {
continue;
}
if let Some(declarator) = child.child_by_field_name("declarator") {
if let Some(name) = objc_declarator_identifier(declarator, src) {
let effective_type = if canonical_type == "id" {
child
.child_by_field_name("value")
.and_then(|value| objc_cast_type_of_value(&value, src))
.unwrap_or_else(|| canonical_type.clone())
} else {
canonical_type.clone()
};
push_objc_type_alias(aliases, &name, &effective_type);
}
}
}
}
fn objc_cast_type_of_value(value: &Node<'_>, src: &[u8]) -> Option<String> {
if value.kind() != "cast_expression" {
return None;
}
let type_node = value.child_by_field_name("type")?;
let ti = if type_node.kind() == "type_identifier" {
type_node
} else {
objc_first_descendant_of_kind(&type_node, "type_identifier")?
};
canonical_objc_type_name(node_text(&ti, src))
}
fn objc_first_descendant_of_kind<'a>(node: &Node<'a>, kind: &str) -> Option<Node<'a>> {
let mut stack = vec![*node];
while let Some(n) = stack.pop() {
let mut cursor = n.walk();
for child in n.named_children(&mut cursor) {
if child.kind() == kind {
return Some(child);
}
stack.push(child);
}
}
None
}
fn objc_declaration_type_node(node: Node<'_>) -> Option<Node<'_>> {
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
if matches!(
child.kind(),
"type_identifier"
| "primitive_type"
| "sized_type_specifier"
| "qualified_type_identifier"
| "generic_type_specifier"
) {
return Some(child);
}
}
None
}
fn find_objc_parameter_list<'a>(node: Node<'a>) -> Option<Node<'a>> {
if let Some(direct) = node.child_by_field_name("parameters") {
return Some(direct);
}
if let Some(declarator) = node.child_by_field_name("declarator") {
return find_objc_parameter_list(declarator);
}
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
if child.kind() == "parameter_list" {
return Some(child);
}
if let Some(found) = find_objc_parameter_list(child) {
return Some(found);
}
}
None
}
fn collect_objc_c_parameter_aliases(node: Node<'_>, src: &[u8], aliases: &mut Vec<TypeAliasBinding>) {
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
if child.kind() == "parameter_declaration" {
objc_parameter_decl_alias(child, src, aliases);
}
}
}
fn objc_parameter_decl_alias(node: Node<'_>, src: &[u8], aliases: &mut Vec<TypeAliasBinding>) {
let Some(type_node) = node.child_by_field_name("type") else {
return;
};
let Some(canonical_type) = canonical_objc_type_name(node_text(&type_node, src)) else {
return;
};
if let Some(declarator_node) = node.child_by_field_name("declarator") {
if let Some(name) = objc_declarator_identifier(declarator_node, src) {
push_objc_type_alias(aliases, &name, &canonical_type);
}
}
}
fn objc_declarator_identifier(node: Node<'_>, src: &[u8]) -> Option<String> {
if node.kind() == "identifier" {
return Some(node_text(&node, src).trim().to_string());
}
if let Some(inner) = node.child_by_field_name("declarator") {
return objc_declarator_identifier(inner, src);
}
let count = node.named_child_count();
for i in 0..count {
let idx = u32::try_from(i).ok()?;
if let Some(child) = node.named_child(idx) {
if let Some(found) = objc_declarator_identifier(child, src) {
return Some(found);
}
}
}
None
}
fn objc_keyword_argument_alias(node: Node<'_>, src: &[u8], aliases: &mut Vec<TypeAliasBinding>) {
let mut type_text: Option<String> = None;
let mut name_text: Option<String> = None;
let count = node.named_child_count();
for i in 0..count {
let Some(idx) = u32::try_from(i).ok() else {
continue;
};
let Some(child) = node.named_child(idx) else {
continue;
};
match child.kind() {
"type_descriptor" | "type" | "primitive_type" => {
type_text = Some(node_text(&child, src).to_string());
}
"identifier" => {
name_text = Some(node_text(&child, src).trim().to_string());
}
_ => {}
}
}
let Some(raw_type) = type_text else {
return;
};
let Some(name) = name_text else {
return;
};
if let Some(canonical_type) = canonical_objc_type_name(&raw_type) {
push_objc_type_alias(aliases, &name, &canonical_type);
}
}
fn canonical_objc_type_name(raw: &str) -> Option<String> {
let trimmed = raw
.trim()
.trim_end_matches(|c: char| c == '*' || c.is_whitespace())
.trim();
let mut head = trimmed;
for prefix in [
"__autoreleasing",
"__strong",
"__weak",
"__unsafe_unretained",
"nullable",
"nonnull",
"_Nullable",
"_Nonnull",
"const",
] {
if let Some(rest) = head.trim_start().strip_prefix(prefix) {
head = rest.trim_start();
}
}
let without_generics = head.split('<').next().unwrap_or(head).trim();
let bare = without_generics
.trim_start_matches('*')
.trim()
.rsplit(' ')
.next()
.unwrap_or(without_generics)
.trim_end_matches('*')
.trim();
if bare.is_empty() {
return None;
}
Some(bare.to_string())
}
fn push_objc_type_alias(aliases: &mut Vec<TypeAliasBinding>, name: &str, type_name: &str) {
if name.is_empty() || type_name.is_empty() || name == type_name {
return;
}
aliases.push(TypeAliasBinding {
name: name.to_string(),
type_name: type_name.to_string(),
});
}
fn dedup_objc_type_aliases(aliases: &mut Vec<TypeAliasBinding>) {
let mut seen = std::collections::HashSet::new();
aliases.retain(|alias| seen.insert((alias.name.clone(), alias.type_name.clone())));
}