mod parse_recovery;
use bonsai_common::{FileId, Span};
use bonsai_lang_api::{
collect_assign_targets, collect_param_type_aliases, decl_index_with_handler, extract_imports_via,
kit::{
call_arg_from_node_with_handler, canonical_simple_type_name, collect_kinds,
collect_receiver_field_writes, language_from_pack, node_text,
package_module_segments_with_workspace_prefix, parse_with, span_of,
},
AdapterContext, AdapterError, ArgumentPassingMode, CallArg, CallKind, CallTargetExtraction, DeclIndex,
DeclKind, FieldWrite, FlowEvent, GrammarHandler, ImportIndex, ImportScope, ImportSpec, LanguageAdapter,
LanguageCapabilities, LanguageId, PatternBindingSite, TypeAliasBinding, TypeAliasVocabulary, Visibility,
EMPTY_HANDLER,
};
use parse_recovery::csharp_parse_recovery_edits;
use tree_sitter::Node;
fn csharp_call_target<'tree>(node: Node<'tree>, src: &[u8]) -> Option<CallTargetExtraction<'tree>> {
let target = match node.kind() {
"invocation_expression" => node.child_by_field_name("function")?,
"object_creation_expression" => node.child_by_field_name("type")?,
_ => return None,
};
let full_text = node_text(&target, src).trim();
(!full_text.is_empty()).then_some(CallTargetExtraction {
node: target,
full_text: full_text.to_string(),
})
}
fn csharp_pattern_bindings(node: Node<'_>) -> Vec<PatternBindingSite<'_>> {
let mut sites = Vec::new();
if let Some(condition) = node.child_by_field_name("condition") {
let mut stack = vec![condition];
while let Some(current) = stack.pop() {
if current.kind() == "is_pattern_expression" {
if let (Some(source), Some(pattern)) = (
current.child_by_field_name("expression"),
current.child_by_field_name("pattern"),
) {
csharp_pattern_binding_names(pattern, current, source, &mut sites);
}
continue;
}
let mut cursor = current.walk();
stack.extend(current.named_children(&mut cursor));
}
}
if let (Some(source), Some(body)) = (
node.child_by_field_name("value"),
node.child_by_field_name("body"),
) {
let mut stack = vec![body];
while let Some(current) = stack.pop() {
if current.kind() == "switch_section" {
let mut cursor = current.walk();
for child in current.named_children(&mut cursor) {
csharp_pattern_binding_names(child, current, source, &mut sites);
}
continue;
}
let mut cursor = current.walk();
stack.extend(current.named_children(&mut cursor));
}
}
sites
}
fn csharp_pattern_binding_names<'tree>(
pattern: Node<'tree>,
span_node: Node<'tree>,
source: Node<'tree>,
out: &mut Vec<PatternBindingSite<'tree>>,
) {
if matches!(
pattern.kind(),
"declaration_pattern" | "var_pattern" | "recursive_pattern"
) {
if let Some(name) = pattern.child_by_field_name("name") {
out.push(PatternBindingSite {
span_node,
pattern: name,
source,
});
}
}
if pattern.kind() == "parenthesized_variable_designation" {
let mut cursor = pattern.walk();
if cursor.goto_first_child() {
loop {
let child = cursor.node();
if child.is_named() && cursor.field_name() == Some("name") {
out.push(PatternBindingSite {
span_node,
pattern: child,
source,
});
}
if !cursor.goto_next_sibling() {
break;
}
}
}
}
if !matches!(
pattern.kind(),
"pattern"
| "declaration_pattern"
| "var_pattern"
| "recursive_pattern"
| "parenthesized_pattern"
| "and_pattern"
| "or_pattern"
| "negated_pattern"
| "list_pattern"
| "tuple_pattern"
| "subpattern"
| "positional_pattern_clause"
| "property_pattern_clause"
| "parenthesized_variable_designation"
) {
return;
}
let mut cursor = pattern.walk();
for child in pattern.named_children(&mut cursor) {
if pattern
.child_by_field_name("type")
.is_some_and(|ty| ty.id() == child.id())
{
continue;
}
csharp_pattern_binding_names(child, span_node, source, out);
}
}
const CSHARP_TYPE_ALIASES: TypeAliasVocabulary = TypeAliasVocabulary {
fn_kinds: &[
"method_declaration",
"constructor_declaration",
"local_function_statement",
],
param_kinds: &["parameter"],
name_field: "name",
type_field: "type",
};
const CSHARP_DECL_KINDS: &[&str] = &[
"method_declaration",
"constructor_declaration",
"destructor_declaration",
"class_declaration",
"struct_declaration",
"interface_declaration",
"record_declaration",
"enum_declaration",
"delegate_declaration",
"property_declaration",
"event_declaration",
"field_declaration",
"local_function_statement",
];
const CSHARP_DEFAULT_VISIBILITY: Visibility = Visibility::Public;
use tree_sitter::{Language, Tree};
fn csharp_foreach_binding(node: Node<'_>) -> Option<(Node<'_>, Node<'_>)> {
(node.kind() == "foreach_statement")
.then(|| {
Some((
node.child_by_field_name("left")?,
node.child_by_field_name("right")?,
))
})
.flatten()
}
pub const LANG_ID: LanguageId = LanguageId::new("csharp");
const PACK_NAME: &str = "csharp";
const HANDLER: GrammarHandler = GrammarHandler {
expression_value_kind_extractor: None,
literal_value_kinds: &[
"null_literal",
"boolean_literal",
"integer_literal",
"real_literal",
"true",
"false",
],
string_literal_kinds: &[
"string_literal",
"verbatim_string_literal",
"raw_string_literal",
"interpolated_string_expression",
"character_literal",
],
comment_kinds: &["comment"],
doc_comment_prefixes: &["///", "/**"],
decorator_kinds: &["attribute"],
parameter_container_kinds: &["parameter_list"],
parameter_kinds: &["parameter", "implicit_parameter"],
parameter_modifier_kinds: &["attribute_list"],
parameter_annotation_kinds: &["attribute"],
implicit_parameter_kinds: &["implicit_parameter"],
binding_identifier_kinds: &["identifier"],
pattern_binding_extractor: Some(csharp_pattern_bindings),
identifier_kinds: &["identifier"],
aggregate_pattern_kinds: &["tuple_pattern"],
positional_aggregate_kinds: &[
"tuple_expression",
"array_initializer",
"array_creation_expression",
],
aggregate_value_field_names: &["value", "expression"],
spread_kinds: &["spread_element"],
spread_value_field_names: &["expression"],
aggregate_syntax_only_kinds: &["type"],
transparent_call_wrapper_kinds: &[
"member_access_expression",
"parenthesized_expression",
"await_expression",
"as_expression",
"non_null_expression",
],
assignment_target_wrapper_kinds: &["variable_declarator", "variable_declaration"],
binding_declaration_keyword_spellings: &["const"],
fn_kinds: &[
"method_declaration",
"local_function_statement",
"accessor_declaration",
"constructor_declaration",
"destructor_declaration",
],
call_kinds: &["invocation_expression", "object_creation_expression"],
constructor_call_kinds: &["object_creation_expression"],
call_callee_field_names: &["function"],
constructor_type_field_names: &["type"],
call_target_extractor: Some(csharp_call_target),
call_argument_field_names: &["arguments"],
call_argument_container_kinds: &["argument_list"],
argument_wrapper_kinds: &["argument"],
argument_name_field_names: &["name"],
argument_value_field_names: &["expression"],
writeback_operand_field_names: &["expression"],
transparent_expression_wrapper_kinds: &["expression"],
lambda_body_field_names: &["body", "expression_body"],
argument_passing_mode_extractor: Some(csharp_argument_passing_mode),
constructor_names: bonsai_lang_api::NO_CONSTRUCTOR_METHOD_NAMES,
runtime_type_guard_operators: &["is"],
runtime_type_wrapper_kinds: &["parenthesized_expression"],
value_free_expression_kinds: &["sizeof_expression", "typeof_expression"],
value_free_call_names: &["nameof"],
call_ref_kinds: &["invocation_expression", "object_creation_expression"],
member_expression_kinds: &["member_access_expression", "property_access_expression"],
subscript_expression_kinds: &["element_access_expression"],
member_base_field_names: &["expression", "object"],
member_name_field_names: &["name"],
subscript_base_field_names: &["expression", "object"],
subscript_index_field_names: &["argument", "index"],
class_kinds: &[
"class_declaration",
"struct_declaration",
"interface_declaration",
"enum_declaration",
"record_declaration",
],
class_decl_kinds: &[
("class_declaration", DeclKind::Class),
("record_declaration", DeclKind::Class),
("struct_declaration", DeclKind::Struct),
("interface_declaration", DeclKind::Interface),
("enum_declaration", DeclKind::Enum),
],
method_kinds: &["method_declaration", "accessor_declaration"],
method_context_kinds: &[
"class_declaration",
"struct_declaration",
"interface_declaration",
"record_declaration",
],
constructor_method_kinds: &["constructor_declaration"],
if_kinds: &[
"if_statement",
"conditional_expression",
"switch_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_section"],
for_kinds: &["for_statement"],
foreach_kinds: &["foreach_statement"],
foreach_binding_extractor: Some(csharp_foreach_binding),
while_kinds: &["while_statement"],
do_kinds: &["do_statement"],
assignment_kinds: &[
"assignment_expression",
"variable_declarator",
"property_declaration",
"variable_declaration",
"local_declaration_statement",
],
compound_assignment_operators: &[
"+=", "-=", "*=", "/=", "%=", "<<=", ">>=", "&=", "^=", "|=", "??=",
],
type_only_declaration_kinds: &[
"property_declaration",
"variable_declaration",
"local_declaration_statement",
],
return_kinds: &["return_statement"],
throw_kinds: &["throw_statement", "throw_expression"],
lambda_kinds: &["lambda_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: &["yield_statement"],
yield_value_field_names: &["expression"],
await_kinds: &["await_expression"],
using_kinds: &["using_statement"],
using_body_field_names: &["body"],
try_body_field_names: &["body"],
implicit_receiver_names: &["this", "base"],
..EMPTY_HANDLER
};
fn csharp_argument_passing_mode(argument: Node<'_>, value: Node<'_>) -> ArgumentPassingMode {
if [argument, value].into_iter().any(|node| {
matches!(node.kind(), "argument" | "ref_expression") && {
let mut cursor = node.walk();
let has_writeback_marker = node
.children(&mut cursor)
.any(|child| matches!(child.kind(), "ref" | "out" | "in" | "ref_kind_keyword"));
has_writeback_marker
}
}) {
ArgumentPassingMode::WriteBack
} else {
ArgumentPassingMode::Value
}
}
#[derive(Debug, Default, Copy, Clone)]
pub struct CSharpAdapter;
impl CSharpAdapter {
#[must_use]
pub fn new() -> Self {
Self
}
}
impl LanguageAdapter for CSharpAdapter {
fn language_id(&self) -> LanguageId {
LANG_ID
}
fn display_name(&self) -> &'static str {
"C#"
}
fn file_extensions(&self) -> &'static [&'static str] {
&["cs", "csx"]
}
fn tree_sitter_language(&self) -> Result<Language, AdapterError> {
language_from_pack(PACK_NAME)
}
fn parse_recovery_edits(
&self,
snapshot: &bonsai_lang_api::FileSnapshot,
_vfs: &bonsai_lang_api::Vfs,
tree: &bonsai_lang_api::SyntaxTree,
) -> Vec<bonsai_lang_api::ParseRecoveryEdit> {
csharp_parse_recovery_edits(snapshot, tree)
}
fn capabilities(&self) -> LanguageCapabilities {
LanguageCapabilities {
module_default_export_names: &[],
universal_type_names: &["object", "Object", "dynamic"],
module_path_syntax: bonsai_lang_api::ModulePathSyntax::none(),
exceptions: bonsai_lang_api::CapabilityLevel::Exact,
receiver_types: bonsai_lang_api::CapabilityLevel::Partial,
constructor_method_names: bonsai_lang_api::NO_CONSTRUCTOR_METHOD_NAMES,
super_receiver_tokens: &["base"],
implicit_receiver_tokens: &["this"],
..LanguageCapabilities::partial_baseline()
}
}
fn extract_declarations(&self, file: FileId, ctx: &AdapterContext<'_>) -> DeclIndex {
let mut idx = decl_index_with_handler(PACK_NAME, file, ctx, &HANDLER);
let mut class_member_names_by_symbol: std::collections::HashMap<
bonsai_common::SymbolId,
std::collections::HashSet<String>,
> = std::collections::HashMap::new();
if let Some((snapshot, tree)) = parse_with(PACK_NAME, file, ctx) {
let src = snapshot.text.as_bytes();
bonsai_lang_api::populate_decl_return_types(&mut idx, &tree, src, &HANDLER);
for decl in &mut idx.defs {
populate_csharp_exception_types(&mut decl.flow_events, &tree, src);
}
}
let pkg = parse_with(PACK_NAME, file, ctx).and_then(|(snapshot, tree)| {
extract_csharp_namespace(tree.root_node(), snapshot.text.as_bytes())
});
if let Some(segments) = pkg {
let segments = package_module_segments_with_workspace_prefix(file, ctx, segments, &[]);
bonsai_lang_api::apply_module_path_semantic_identity(&mut idx, segments);
} else {
bonsai_lang_api::apply_file_stem_semantic_identity(&mut idx, ctx);
}
if let Some((snapshot, tree)) = parse_with(PACK_NAME, file, ctx) {
let src = snapshot.text.as_bytes();
let vis_map = collect_csharp_visibility(tree.root_node(), file, src);
let alias_map = collect_param_type_aliases(&tree, file, src, &CSHARP_TYPE_ALIASES);
let local_alias_map = collect_csharp_local_type_aliases(&tree, file, src);
let class_field_aliases = collect_csharp_class_field_aliases(&tree, file, src);
let class_span_for_parent: std::collections::HashMap<bonsai_common::SymbolId, Span> = idx
.defs
.iter()
.filter(|candidate| is_class_like(candidate.kind))
.map(|candidate| (candidate.symbol, candidate.span))
.collect();
for (class_symbol, class_span) in &class_span_for_parent {
let Some(field_aliases) = class_field_aliases
.iter()
.find_map(|(span, aliases)| (*span == *class_span).then_some(aliases))
else {
continue;
};
let names = class_member_names_by_symbol.entry(*class_symbol).or_default();
names.extend(
field_aliases
.iter()
.map(|alias| alias.name.trim())
.filter(|name| !name.is_empty())
.map(str::to_string),
);
}
for decl in &mut idx.defs {
if let Some(vis) = vis_map.get(&decl.span).copied() {
decl.visibility = vis;
}
let mut aliases = alias_map.get(&decl.span).cloned().unwrap_or_default();
if let Some(locals) = local_alias_map.get(&decl.span) {
for alias in locals {
if !aliases.iter().any(|existing| existing.name == alias.name) {
aliases.push(alias.clone());
}
}
}
if matches!(
decl.kind,
DeclKind::Function | DeclKind::Method | DeclKind::Constructor
) {
if let Some(class_span) = decl
.parent
.and_then(|parent_sym| class_span_for_parent.get(&parent_sym).copied())
{
if let Some(field_aliases) = class_field_aliases
.iter()
.find_map(|(span, list)| (*span == class_span).then_some(list))
{
for alias in field_aliases {
if !aliases.contains(alias) {
aliases.push(alias.clone());
}
}
}
}
}
if !aliases.is_empty() {
decl.type_aliases = aliases;
}
}
let bases_by_span = collect_csharp_class_bases(&tree, file, src);
for decl in &mut idx.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();
}
}
}
for decl in &mut idx.defs {
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();
bonsai_lang_api::kit::synthesize_record_members(&mut idx, &tree, src, file);
synthesize_csharp_expression_bodied_properties(&mut idx, &tree, src, file);
synthesize_csharp_constructor_implicit_returns(&mut idx, &tree, src, file);
}
qualify_csharp_implicit_member_accesses(&mut idx, &class_member_names_by_symbol);
for decl in &mut idx.defs {
enrich_csharp_receiver_field_writes(decl);
}
propagate_csharp_base_constructor_field_writes(&mut idx);
bonsai_lang_api::apply_constructor_result_type_aliases(&mut idx);
bonsai_lang_api::apply_class_field_type_aliases(&mut idx);
idx
}
fn extract_imports(&self, file: FileId, ctx: &AdapterContext<'_>) -> ImportIndex {
extract_imports_via(PACK_NAME, file, ctx, parse_imports)
}
}
fn synthesize_csharp_expression_bodied_properties(
index: &mut DeclIndex,
tree: &Tree,
src: &[u8],
file: FileId,
) {
let mut next_symbol = index
.defs
.iter()
.map(|d| d.symbol.raw())
.max()
.map_or(1, |m| m + 1);
let mut synthesized: Vec<bonsai_lang_api::Decl> = Vec::new();
for prop in collect_kinds(tree, &["property_declaration"]) {
let mut pc = prop.walk();
let Some(arrow) = prop
.children(&mut pc)
.find(|c| c.kind() == "arrow_expression_clause")
else {
continue;
};
let Some(name_node) = prop.child_by_field_name("name") else {
continue;
};
let name = node_text(&name_node, src).trim().to_string();
if name.is_empty() {
continue;
}
let mut ac = arrow.walk();
let named: Vec<_> = arrow.children(&mut ac).filter(|c| c.is_named()).collect();
let Some(expr) = named.last().copied() else {
continue;
};
let body = node_text(&expr, src).trim().to_string();
if body.is_empty() {
continue;
}
let qualified = if body.starts_with("this.") || body.starts_with("base.") {
body.clone()
} else {
format!("this.{body}")
};
let Some((parent, module_path, visibility)) = csharp_enclosing_type_decl(index, prop, file) else {
continue;
};
if index
.defs
.iter()
.chain(synthesized.iter())
.any(|d| d.parent == parent && d.name == name && d.params.is_empty())
{
continue;
}
let body_span = span_of(file, &expr);
let flow_events =
if let Some((call_receiver, call_name)) = dotted_member_access_call_parts(&qualified) {
let lookup_member = csharp_receiver_member_lookup_name(&call_receiver);
let receiver_types = csharp_lookup_member_type(prop, lookup_member, src)
.into_iter()
.collect();
let mut return_flow = bonsai_lang_api::ExpressionFlow::from_place(qualified.clone());
return_flow.call_sites.push(body_span);
vec![
FlowEvent::Call {
span: body_span,
name: call_name.clone(),
receiver: Some(call_receiver),
receiver_types,
call_kind: CallKind::Method,
args: Vec::new(),
},
FlowEvent::Return {
span: body_span,
value_kind: Some(bonsai_lang_api::AssignValueKind::CallResult),
value_text: Some(call_name.clone()),
value_name: Some(call_name),
value_flow: return_flow,
},
]
} else {
vec![FlowEvent::Return {
span: body_span,
value_kind: Some(bonsai_lang_api::AssignValueKind::Compound),
value_text: Some(qualified.clone()),
value_name: Some(qualified.clone()),
value_flow: bonsai_lang_api::ExpressionFlow::from_place(qualified.clone()),
}]
};
synthesized.push(bonsai_lang_api::Decl {
symbol: bonsai_common::SymbolId::new(next_symbol),
kind: DeclKind::Method,
name,
qualified_name: None,
module_path,
span: span_of(file, &name_node),
name_span: span_of(file, &name_node),
visibility,
parent,
body_span: Some(body_span),
flow_events,
has_implicit_returns: false,
params: Vec::new(),
param_annotations: Vec::new(),
param_default_calls: Vec::new(),
type_aliases: Vec::new(),
bases: Vec::new(),
receiver_param_index: None,
receiver_field_writes: Vec::new(),
receiver_field_initializers: Vec::new(),
implicit_receiver_names: vec!["this".to_string(), "base".to_string()],
receiver_state_sources: vec![qualified],
return_type: None,
is_variadic: false,
});
next_symbol += 1;
}
index.defs.extend(synthesized);
}
fn synthesize_csharp_constructor_implicit_returns(
index: &mut DeclIndex,
tree: &Tree,
src: &[u8],
file: FileId,
) {
let class_info_by_symbol: std::collections::HashMap<_, _> = index
.defs
.iter()
.filter(|decl| is_class_like(decl.kind))
.map(|decl| (decl.symbol, (decl.name.clone(), decl.bases.clone())))
.collect();
for ctor_node in collect_kinds(tree, &["constructor_declaration"]) {
let ctor_span = span_of(file, &ctor_node);
let Some(decl) = index
.defs
.iter_mut()
.find(|d| matches!(d.kind, DeclKind::Constructor) && d.span == ctor_span)
else {
continue;
};
let parent_info = decl.parent.and_then(|parent| class_info_by_symbol.get(&parent));
let mut parts: Vec<String> = Vec::new();
let mut initializer_call: Option<FlowEvent> = None;
let mut cw = ctor_node.walk();
for child in ctor_node.children(&mut cw) {
if child.kind() == "constructor_initializer" {
let t = node_text(&child, src).trim().to_string();
if let Some((callee, args)) =
csharp_constructor_initializer_call(child, file, src, parent_info)
{
let span = span_of(file, &child);
initializer_call = Some(FlowEvent::Call {
span,
name: callee,
receiver: None,
receiver_types: Vec::new(),
call_kind: CallKind::Constructor,
args,
});
}
if !t.is_empty() {
parts.push(t);
}
} else if child.kind() == "block" {
let t = node_text(&child, src).trim().to_string();
if !t.is_empty() {
parts.push(t);
}
}
}
if let Some(call) = initializer_call {
let already_present = decl.flow_events.iter().any(|event| {
matches!(
(event, &call),
(
FlowEvent::Call { span: existing_span, name: existing_name, .. },
FlowEvent::Call { span, name, .. }
) if existing_span == span && existing_name == name
)
});
if !already_present {
decl.flow_events.insert(0, call);
}
}
if decl
.flow_events
.iter()
.any(|e| matches!(e, FlowEvent::Return { .. }))
|| parts.is_empty()
{
continue;
}
let value_text = parts.join(" ");
let body_span = ctor_node
.child_by_field_name("body")
.map(|b| span_of(file, &b))
.unwrap_or_else(|| span_of(file, &ctor_node));
decl.flow_events.push(FlowEvent::Return {
span: body_span,
value_kind: Some(bonsai_lang_api::AssignValueKind::Compound),
value_text: Some(value_text),
value_name: None,
value_flow: bonsai_lang_api::ExpressionFlow::from_source_names(decl.params.clone()),
});
}
}
fn csharp_constructor_initializer_call(
initializer: tree_sitter::Node<'_>,
file: FileId,
src: &[u8],
parent_info: Option<&(String, Vec<String>)>,
) -> Option<(String, Vec<CallArg>)> {
if initializer.kind() != "constructor_initializer" {
return None;
}
let mut children = initializer.walk();
let target = initializer
.children(&mut children)
.find_map(|child| match child.kind() {
"base" => parent_info.and_then(|(_, bases)| bases.first()).cloned(),
"this" => parent_info.map(|(name, _)| name.clone()),
_ => None,
})?;
let argument_list = initializer
.named_children(&mut initializer.walk())
.find(|child| child.kind() == "argument_list")?;
let mut args = Vec::new();
let mut cursor = argument_list.walk();
for argument in argument_list.named_children(&mut cursor) {
if argument.kind() != "argument" {
continue;
}
let name = argument
.child_by_field_name("name")
.map(|name| node_text(&name, src).trim().to_string())
.filter(|name| !name.is_empty());
if let Some(argument) = call_arg_from_node_with_handler(argument, file, src, name, &HANDLER) {
args.push(argument);
}
}
let callee = target;
Some((callee, args))
}
fn csharp_bare_identifier(text: &str) -> Option<&str> {
let trimmed = text.trim();
let mut chars = trimmed.chars();
let first = chars.next()?;
if !(first == '_' || first.is_ascii_alphabetic()) {
return None;
}
if chars.all(|ch| ch == '_' || ch.is_ascii_alphanumeric()) {
Some(trimmed)
} else {
None
}
}
fn csharp_receiver_member_lookup_name(receiver: &str) -> &str {
receiver
.trim()
.strip_prefix("this.")
.or_else(|| receiver.trim().strip_prefix("base."))
.unwrap_or_else(|| receiver.trim())
.rsplit('.')
.next()
.unwrap_or_else(|| receiver.trim())
}
fn csharp_lookup_member_type(prop: tree_sitter::Node<'_>, member: &str, src: &[u8]) -> Option<String> {
let mut cur = prop.parent();
let mut class_node = None;
while let Some(n) = cur {
if matches!(
n.kind(),
"class_declaration" | "struct_declaration" | "record_declaration" | "interface_declaration"
) {
class_node = Some(n);
break;
}
cur = n.parent();
}
let class_node = class_node?;
let body = class_node.child_by_field_name("body")?;
let mut walker = body.walk();
for child in body.children(&mut walker) {
match child.kind() {
"property_declaration" => {
let name_node = child.child_by_field_name("name")?;
if node_text(&name_node, src).trim() == member {
let type_node = child.child_by_field_name("type")?;
let raw = node_text(&type_node, src).trim();
if raw.is_empty() {
return None;
}
return Some(canonical_simple_type_name(raw).to_string());
}
}
"field_declaration" => {
let Some(type_node) = child.child_by_field_name("type") else {
continue;
};
let mut cw = child.walk();
for cc in child.children(&mut cw) {
if cc.kind() != "variable_declaration" {
continue;
}
let mut vw = cc.walk();
for v in cc.children(&mut vw) {
if v.kind() != "variable_declarator" {
continue;
}
if let Some(name_node) = v.child_by_field_name("name") {
if node_text(&name_node, src).trim() == member {
let raw = node_text(&type_node, src).trim();
if raw.is_empty() {
return None;
}
return Some(canonical_simple_type_name(raw).to_string());
}
}
}
}
}
_ => {}
}
}
None
}
fn dotted_member_access_call_parts(body: &str) -> Option<(String, String)> {
let trimmed = body.trim();
let inner = trimmed;
let segments: Vec<&str> = inner.split('.').collect();
if segments.len() < 2 {
return None;
}
for seg in &segments {
if seg.is_empty()
|| !seg.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
|| !seg
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
{
return None;
}
}
let last_dot = inner.rfind('.')?;
let receiver = inner[..last_dot].to_string();
Some((receiver, inner.to_string()))
}
fn csharp_enclosing_type_decl(
index: &DeclIndex,
node: tree_sitter::Node<'_>,
file: FileId,
) -> Option<(
Option<bonsai_common::SymbolId>,
bonsai_lang_api::ModulePath,
Visibility,
)> {
let mut cur = node.parent();
while let Some(n) = cur {
if matches!(
n.kind(),
"class_declaration" | "struct_declaration" | "record_declaration" | "interface_declaration"
) {
let span = span_of(file, &n);
return index
.defs
.iter()
.find(|d| d.span == span)
.map(|d| (Some(d.symbol), d.module_path.clone(), d.visibility));
}
cur = n.parent();
}
None
}
fn qualify_csharp_implicit_member_accesses(
index: &mut DeclIndex,
class_member_names_by_symbol: &std::collections::HashMap<
bonsai_common::SymbolId,
std::collections::HashSet<String>,
>,
) {
use std::collections::{HashMap, HashSet};
let mut getter_names_by_parent: HashMap<Option<bonsai_common::SymbolId>, HashSet<String>> =
HashMap::new();
let mut class_symbols_by_name: HashMap<String, Vec<bonsai_common::SymbolId>> = HashMap::new();
let mut class_bases_by_symbol: HashMap<bonsai_common::SymbolId, Vec<String>> = HashMap::new();
for decl in &index.defs {
if matches!(decl.kind, DeclKind::Method) && decl.params.is_empty() && !decl.name.is_empty() {
getter_names_by_parent
.entry(decl.parent)
.or_default()
.insert(decl.name.clone());
}
if is_class_like(decl.kind) {
class_symbols_by_name
.entry(decl.name.clone())
.or_default()
.push(decl.symbol);
class_bases_by_symbol.insert(decl.symbol, decl.bases.clone());
}
}
for decl in &mut index.defs {
if decl.flow_events.is_empty() {
continue;
}
let mut locals: HashSet<String> = decl.params.iter().cloned().collect();
collect_assign_targets(&decl.flow_events, &mut locals);
let mut getter_names = HashSet::new();
let mut member_names = HashSet::new();
let mut owner_stack: Vec<bonsai_common::SymbolId> = decl.parent.into_iter().collect();
let mut seen_owners = HashSet::new();
while let Some(owner) = owner_stack.pop() {
if !seen_owners.insert(owner) {
continue;
}
if let Some(names) = getter_names_by_parent.get(&Some(owner)) {
getter_names.extend(names.iter().cloned());
}
if let Some(names) = class_member_names_by_symbol.get(&owner) {
member_names.extend(names.iter().cloned());
}
for base in class_bases_by_symbol.get(&owner).into_iter().flatten() {
if let Some(symbols) = class_symbols_by_name.get(base) {
owner_stack.extend(symbols.iter().copied());
}
}
}
if decl.parent.is_none() {
if let Some(names) = getter_names_by_parent.get(&None) {
getter_names.extend(names.iter().cloned());
}
}
member_names.extend(getter_names.iter().cloned());
let params: HashSet<String> = decl.params.iter().cloned().collect();
bonsai_lang_api::qualify_implicit_member_assign_targets(
&mut decl.flow_events,
&member_names,
¶ms,
|name| csharp_bare_identifier(name).map(|_| format!("this.{name}")),
);
bonsai_lang_api::rewrite_implicit_member_reads(
&mut decl.flow_events,
&getter_names,
&locals,
|name| bonsai_lang_api::ImplicitMemberReadCall {
source_call: format!("this.{name}"),
call_name: format!("this.{name}"),
receiver: Some("this".to_string()),
call_kind: CallKind::Method,
},
);
}
}
fn enrich_csharp_receiver_field_writes(decl: &mut bonsai_lang_api::Decl) {
if !matches!(decl.kind, DeclKind::Constructor | DeclKind::Method) {
return;
}
let writes = collect_receiver_field_writes(
&decl.flow_events,
&decl.params,
decl.receiver_param_index,
&["this", "base"],
&[],
);
decl.receiver_field_writes.extend(writes);
dedup_csharp_receiver_field_writes(&mut decl.receiver_field_writes);
}
fn propagate_csharp_base_constructor_field_writes(index: &mut DeclIndex) {
for _ in 0..8 {
let snapshot = index.defs.clone();
let mut changed = false;
for decl in index
.defs
.iter_mut()
.filter(|decl| matches!(decl.kind, DeclKind::Constructor))
{
let mut inherited = csharp_inherited_constructor_field_writes(decl, &snapshot);
if inherited.is_empty() {
continue;
}
let before = decl.receiver_field_writes.len();
decl.receiver_field_writes.append(&mut inherited);
dedup_csharp_receiver_field_writes(&mut decl.receiver_field_writes);
changed |= decl.receiver_field_writes.len() != before;
}
if !changed {
break;
}
}
}
fn csharp_inherited_constructor_field_writes(
decl: &bonsai_lang_api::Decl,
snapshot: &[bonsai_lang_api::Decl],
) -> Vec<FieldWrite> {
let mut out = Vec::new();
collect_csharp_inherited_constructor_field_writes(&decl.flow_events, decl, snapshot, &mut out);
dedup_csharp_receiver_field_writes(&mut out);
out
}
fn collect_csharp_inherited_constructor_field_writes(
events: &[FlowEvent],
decl: &bonsai_lang_api::Decl,
snapshot: &[bonsai_lang_api::Decl],
out: &mut Vec<FieldWrite>,
) {
for event in events {
match event {
FlowEvent::Call {
name,
call_kind,
args,
..
} if *call_kind == CallKind::Constructor => {
let Some(callee) = snapshot.iter().find(|candidate| {
matches!(candidate.kind, DeclKind::Constructor) && candidate.name == *name
}) else {
continue;
};
for write in &callee.receiver_field_writes {
let mut mapped_sources = Vec::new();
for source_param in &write.source_param_indices {
let Some(arg) = args.get(*source_param) else {
continue;
};
if let Some(current_param) = csharp_param_index_for_bare_arg(decl, &arg.value_text) {
if !mapped_sources.contains(¤t_param) {
mapped_sources.push(current_param);
}
}
}
if !mapped_sources.is_empty() {
out.push(FieldWrite {
span: write.span,
target: write.target.clone(),
source_param_indices: mapped_sources,
});
}
}
}
FlowEvent::Branch {
then_events,
else_events,
..
} => {
collect_csharp_inherited_constructor_field_writes(then_events, decl, snapshot, out);
collect_csharp_inherited_constructor_field_writes(else_events, decl, snapshot, out);
}
FlowEvent::Loop { body, .. } | FlowEvent::Defer { body, .. } | FlowEvent::Using { body, .. } => {
collect_csharp_inherited_constructor_field_writes(body, decl, snapshot, out);
}
FlowEvent::Try {
body,
catch_events,
finally_events,
..
} => {
collect_csharp_inherited_constructor_field_writes(body, decl, snapshot, out);
collect_csharp_inherited_constructor_field_writes(catch_events, decl, snapshot, out);
collect_csharp_inherited_constructor_field_writes(finally_events, decl, snapshot, out);
}
_ => {}
}
}
}
fn csharp_param_index_for_bare_arg(decl: &bonsai_lang_api::Decl, arg: &str) -> Option<usize> {
let bare = csharp_bare_identifier(arg)?;
decl.params.iter().position(|param| param == bare)
}
fn dedup_csharp_receiver_field_writes(writes: &mut Vec<FieldWrite>) {
for write in writes.iter_mut() {
write.source_param_indices.sort_unstable();
write.source_param_indices.dedup();
}
writes.sort_by_key(|write| {
(
write.span.start,
write.target.clone(),
write.source_param_indices.clone(),
)
});
writes.dedup_by(|a, b| {
a.span == b.span && a.target == b.target && a.source_param_indices == b.source_param_indices
});
}
fn parse_imports(tree: &Tree, src: &[u8], file: FileId) -> Vec<ImportSpec> {
let mut imports = Vec::new();
for using_node in collect_kinds(tree, &["using_directive"]) {
let mut child_cursor = using_node.walk();
let mut last_path: Option<tree_sitter::Node<'_>> = None;
for child in using_node.named_children(&mut child_cursor) {
if matches!(child.kind(), "qualified_name" | "identifier")
&& Some(child) != using_node.child_by_field_name("name")
{
last_path = Some(child);
}
}
let Some(path_node) = last_path.or_else(|| using_node.child_by_field_name("name")) else {
continue;
};
let module = node_text(&path_node, src).trim().to_string();
if module.is_empty() {
continue;
}
let alias = using_node
.child_by_field_name("name")
.map(|alias_node| node_text(&alias_node, src).to_string());
imports.push(ImportSpec {
span: span_of(file, &using_node),
module: module.clone(),
alias,
is_wildcard: false,
original_name: None,
scope: ImportScope::Module,
});
if csharp_using_is_static(&using_node) {
imports.push(ImportSpec {
span: span_of(file, &using_node),
module,
alias: None,
is_wildcard: true,
original_name: None,
scope: ImportScope::Local,
});
}
}
imports
}
fn csharp_using_is_static(using_node: &tree_sitter::Node<'_>) -> bool {
(0..using_node.child_count())
.filter_map(|index| u32::try_from(index).ok())
.any(|index| {
using_node
.child(index)
.is_some_and(|child| child.kind() == "static")
})
}
fn collect_csharp_local_type_aliases(
tree: &Tree,
file: FileId,
src: &[u8],
) -> std::collections::HashMap<bonsai_common::Span, Vec<TypeAliasBinding>> {
let fn_kinds = &[
"method_declaration",
"constructor_declaration",
"local_function_statement",
];
let mut out = std::collections::HashMap::new();
for fn_node in collect_kinds(tree, fn_kinds) {
let mut aliases: Vec<TypeAliasBinding> = Vec::new();
let mut work = vec![fn_node];
while let Some(node) = work.pop() {
if node != fn_node && fn_kinds.contains(&node.kind()) {
continue;
}
if node.kind() == "local_declaration_statement" {
extend_aliases_from_field_or_event(node, src, &mut aliases);
extend_aliases_from_var_cast(node, src, &mut aliases);
}
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
work.push(child);
}
}
if !aliases.is_empty() {
out.insert(span_of(file, &fn_node), aliases);
}
}
out
}
fn extend_aliases_from_var_cast(
node: tree_sitter::Node<'_>,
src: &[u8],
aliases: &mut Vec<TypeAliasBinding>,
) {
let mut var_decl = node.child_by_field_name("declaration");
if var_decl.is_none() {
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
if child.kind() == "variable_declaration" {
var_decl = Some(child);
break;
}
}
}
let Some(var_decl) = var_decl else {
return;
};
let Some(type_node) = var_decl.child_by_field_name("type") else {
return;
};
if node_text(&type_node, src).trim() != "var" {
return;
}
let mut cursor = var_decl.walk();
for declarator in var_decl.named_children(&mut cursor) {
if declarator.kind() != "variable_declarator" {
continue;
}
let mut name_node = declarator.child_by_field_name("name");
if name_node.is_none() {
let mut inner = declarator.walk();
for child in declarator.named_children(&mut inner) {
if child.kind() == "identifier" {
name_node = Some(child);
break;
}
}
}
let Some(name_node) = name_node else {
continue;
};
let name = node_text(&name_node, src).trim().to_string();
if name.is_empty() {
continue;
}
let mut init = declarator.child_by_field_name("value");
if init.is_none() {
let mut inner = declarator.walk();
for child in declarator.named_children(&mut inner) {
if child.id() != name_node.id() {
init = Some(child);
}
}
}
let Some(init) = init else {
continue;
};
let Some(type_name) = csharp_cast_type_of_init(init, src) else {
continue;
};
let canonical = canonical_simple_type_name(&type_name);
if canonical.is_empty() {
continue;
}
aliases.retain(|a| a.name != name);
aliases.push(TypeAliasBinding {
name,
type_name: canonical,
});
}
}
fn csharp_cast_type_of_init(init: tree_sitter::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()?;
}
match n.kind() {
"cast_expression" => n
.child_by_field_name("type")
.map(|t| node_text(&t, src).to_string()),
"as_expression" => n
.child_by_field_name("type")
.or_else(|| n.child_by_field_name("right"))
.map(|t| node_text(&t, src).to_string()),
_ => None,
}
}
fn collect_csharp_class_field_aliases(
tree: &Tree,
file: FileId,
src: &[u8],
) -> Vec<(bonsai_common::Span, Vec<TypeAliasBinding>)> {
let class_kinds = &[
"class_declaration",
"struct_declaration",
"record_declaration",
"record_struct_declaration",
"interface_declaration",
];
let mut out = Vec::new();
for class_node in collect_kinds(tree, class_kinds) {
let mut aliases: Vec<TypeAliasBinding> = Vec::new();
let mut work = vec![class_node];
while let Some(node) = work.pop() {
if node != class_node && class_kinds.contains(&node.kind()) {
continue;
}
match node.kind() {
"field_declaration" | "event_field_declaration" => {
extend_aliases_from_field_or_event(node, src, &mut aliases);
}
"property_declaration" => {
if let Some(binding) = property_alias_from_node(node, src) {
if !aliases.contains(&binding) {
aliases.push(binding);
}
}
}
_ => {}
}
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
work.push(child);
}
}
if !aliases.is_empty() {
out.push((span_of(file, &class_node), aliases));
}
}
out
}
fn extend_aliases_from_field_or_event(
node: tree_sitter::Node<'_>,
src: &[u8],
aliases: &mut Vec<TypeAliasBinding>,
) {
let var_decl = node.child_by_field_name("declaration").or_else(|| {
let mut cursor = node.walk();
let mut found = None;
for child in node.named_children(&mut cursor) {
if child.kind() == "variable_declaration" {
found = Some(child);
break;
}
}
found
});
let Some(var_decl) = var_decl else {
return;
};
let Some(type_node) = var_decl.child_by_field_name("type") else {
return;
};
if type_node.kind() == "implicit_type" || node_text(&type_node, src).trim() == "var" {
return;
}
let canonical = canonical_simple_type_name(node_text(&type_node, src));
if canonical.is_empty() {
return;
}
let mut cursor = var_decl.walk();
for declarator in var_decl.named_children(&mut cursor) {
if declarator.kind() != "variable_declarator" {
continue;
}
let name_node = declarator.child_by_field_name("name").or_else(|| {
let mut inner = declarator.walk();
let mut found = None;
for child in declarator.named_children(&mut inner) {
if child.kind() == "identifier" {
found = Some(child);
break;
}
}
found
});
let Some(name_node) = name_node else {
continue;
};
let name = node_text(&name_node, src).trim().to_string();
if name.is_empty() || name == canonical {
continue;
}
let binding = TypeAliasBinding {
name,
type_name: canonical.clone(),
};
if !aliases.contains(&binding) {
aliases.push(binding);
}
}
}
fn property_alias_from_node(node: tree_sitter::Node<'_>, src: &[u8]) -> Option<TypeAliasBinding> {
let type_node = node.child_by_field_name("type")?;
let canonical = canonical_simple_type_name(node_text(&type_node, src));
if canonical.is_empty() {
return None;
}
let name_node = node.child_by_field_name("name")?;
let name = node_text(&name_node, src).trim().to_string();
if name.is_empty() || name == canonical {
return None;
}
Some(TypeAliasBinding {
name,
type_name: canonical,
})
}
fn collect_csharp_visibility(
root: tree_sitter::Node<'_>,
file: FileId,
src: &[u8],
) -> std::collections::HashMap<Span, Visibility> {
let mut visibility_by_span = std::collections::HashMap::new();
let mut work_stack = vec![root];
while let Some(node) = work_stack.pop() {
if CSHARP_DECL_KINDS.contains(&node.kind()) {
visibility_by_span.insert(span_of(file, &node), csharp_node_visibility(node, src));
}
let mut child_cursor = node.walk();
for child in node.children(&mut child_cursor) {
work_stack.push(child);
}
}
visibility_by_span
}
fn csharp_node_visibility(node: tree_sitter::Node<'_>, src: &[u8]) -> Visibility {
let mut keywords: Vec<&str> = Vec::new();
let mut child_cursor = node.walk();
for child in node.children(&mut child_cursor) {
if child.kind() == "modifier" {
let text = node_text(&child, src);
if matches!(text, "private" | "protected" | "internal" | "public") {
keywords.push(text);
}
}
}
let has_private = keywords.contains(&"private");
let has_protected = keywords.contains(&"protected");
let has_internal = keywords.contains(&"internal");
let has_public = keywords.contains(&"public");
if has_public {
return Visibility::Public;
}
if has_protected && has_internal {
return Visibility::Crate;
}
if has_private && has_protected {
return Visibility::Protected;
}
if has_protected {
return Visibility::Protected;
}
if has_internal {
return Visibility::Crate;
}
if has_private {
return Visibility::Private;
}
CSHARP_DEFAULT_VISIBILITY
}
fn is_class_like(kind: DeclKind) -> bool {
matches!(
kind,
DeclKind::Class | DeclKind::Interface | DeclKind::Trait | DeclKind::Struct | DeclKind::Enum
)
}
fn collect_csharp_class_bases(
tree: &Tree,
file: FileId,
src: &[u8],
) -> Vec<(bonsai_common::Span, Vec<String>)> {
let mut bases_table = Vec::new();
let class_kinds = &[
"class_declaration",
"struct_declaration",
"record_declaration",
"record_struct_declaration",
"interface_declaration",
];
for class_node in collect_kinds(tree, class_kinds) {
let mut bases: Vec<String> = Vec::new();
let mut class_cursor = class_node.walk();
for child in class_node.named_children(&mut class_cursor) {
if child.kind() != "base_list" {
continue;
}
let mut entry_cursor = child.walk();
for entry in child.named_children(&mut entry_cursor) {
let raw = node_text(&entry, src);
if let Some(name) = canonical_csharp_base_name(raw) {
if !bases.iter().any(|existing| existing == &name) {
bases.push(name);
}
}
}
}
if !bases.is_empty() {
bases_table.push((span_of(file, &class_node), bases));
}
}
bases_table
}
fn canonical_csharp_base_name(raw: &str) -> Option<String> {
let trimmed = raw.trim();
let head = trimmed.split('<').next().unwrap_or(trimmed).trim();
let bare = head.rsplit('.').next().unwrap_or(head).trim();
if bare.is_empty() {
return None;
}
Some(bare.to_string())
}
fn populate_csharp_exception_types(
events: &mut [bonsai_lang_api::FlowEvent],
tree: &tree_sitter::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", "throw_expression"],
) {
if let Some(name) = csharp_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_csharp_catch_types(node, src);
}
if let Some(name) = collect_csharp_catch_param_name(node, src) {
*catch_param = Some(name);
}
}
populate_csharp_exception_types(body, tree, src);
populate_csharp_exception_types(catch_events, tree, src);
populate_csharp_exception_types(finally_events, tree, src);
}
FlowEvent::Branch {
then_events,
else_events,
..
} => {
populate_csharp_exception_types(then_events, tree, src);
populate_csharp_exception_types(else_events, tree, src);
}
FlowEvent::Loop { body, .. } | FlowEvent::Defer { body, .. } | FlowEvent::Using { body, .. } => {
populate_csharp_exception_types(body, tree, src);
}
_ => {}
}
}
}
fn csharp_thrown_type_for_node(throw_node: tree_sitter::Node<'_>, src: &[u8]) -> Option<String> {
let mut throw_cursor = throw_node.walk();
for child in throw_node.named_children(&mut throw_cursor) {
if child.kind() == "object_creation_expression" {
if let Some(type_node) = child.child_by_field_name("type") {
return Some(bonsai_lang_api::kit::canonical_simple_type_name(node_text(
&type_node, src,
)));
}
let mut type_cursor = child.walk();
for descendant in child.named_children(&mut type_cursor) {
if matches!(
descendant.kind(),
"identifier" | "qualified_name" | "generic_name"
) {
return Some(bonsai_lang_api::kit::canonical_simple_type_name(node_text(
&descendant,
src,
)));
}
}
}
}
None
}
fn collect_csharp_catch_param_name(try_node: tree_sitter::Node<'_>, src: &[u8]) -> Option<String> {
let mut try_cursor = try_node.walk();
for child in try_node.named_children(&mut try_cursor) {
if child.kind() != "catch_clause" {
continue;
}
let mut clause_cursor = child.walk();
for sub in child.named_children(&mut clause_cursor) {
if sub.kind() != "catch_declaration" {
continue;
}
if let Some(name_node) = sub.child_by_field_name("name") {
return Some(node_text(&name_node, src).trim().to_string());
}
let mut pcur = sub.walk();
let mut last_ident: Option<tree_sitter::Node<'_>> = None;
for n in sub.named_children(&mut pcur) {
if n.kind() == "identifier" {
last_ident = Some(n);
}
}
if let Some(n) = last_ident {
return Some(node_text(&n, src).trim().to_string());
}
}
}
None
}
fn collect_csharp_catch_types(try_node: tree_sitter::Node<'_>, src: &[u8]) -> Vec<String> {
let mut catch_types: Vec<String> = Vec::new();
let mut try_cursor = try_node.walk();
for child in try_node.named_children(&mut try_cursor) {
if child.kind() != "catch_clause" {
continue;
}
let mut clause_cursor = child.walk();
for sub in child.named_children(&mut clause_cursor) {
if sub.kind() != "catch_declaration" {
continue;
}
if let Some(type_node) = sub.child_by_field_name("type") {
let name = bonsai_lang_api::kit::canonical_simple_type_name(node_text(&type_node, src));
if !name.is_empty() && !catch_types.iter().any(|existing| existing == &name) {
catch_types.push(name);
}
}
}
}
catch_types
}
fn extract_csharp_namespace(root: tree_sitter::Node<'_>, src: &[u8]) -> Option<Vec<String>> {
let mut child_cursor = root.walk();
for child in root.children(&mut child_cursor) {
if !matches!(
child.kind(),
"namespace_declaration" | "file_scoped_namespace_declaration"
) {
continue;
}
if let Some(name_node) = child.child_by_field_name("name") {
let text = node_text(&name_node, src);
let segments: Vec<String> = text
.split('.')
.map(str::trim)
.filter(|segment| !segment.is_empty())
.map(str::to_string)
.collect();
if !segments.is_empty() {
return Some(segments);
}
}
}
None
}
#[cfg(test)]
#[path = "tests.rs"]
mod tests;