feat(extract): C# field/property + C# 12 primary-constructor support

Three C# extractor gaps that left modern .NET classes partially invisible
in the graph:

1. lang_specs.c CSHARP entry had `empty_types` for field_node_types, so
   walk_defs never descended into field_declaration / property_declaration
   nodes. Add cs_field_types and wire it in. Without this, no C# fields
   or properties were ever pushed as Field defs.

2. extract_class_fields assumed the field's "type" is on the immediate
   child via ts_node_child_by_field_name. Tree-sitter-c_sharp wraps fields
   as field_declaration > variable_declaration(type, variable_declarator),
   one level deeper than Java/Go/Rust. Detect the nested shape by walking
   named children for variable_declaration and resolve type + name from
   inside it. Direct shape continues unchanged.

3. C# 12 primary constructors (`class Foo(IBar bar) : Base { ... }`)
   declare implicit captured fields on the class line. Tree-sitter wraps
   them inside the hidden _class_declaration_initializer node, so the
   `parameters` field on class_declaration may not always resolve
   directly; iterate top-level children for parameter_list as a fallback.
   Emit each primary-ctor parameter as a Field def with parent_class set.

Effect: dependency-injected fields on .NET 8+/9+ controllers and any C#
auto-property (`public IFoo Foo { get; }`) are now reachable as Field
nodes in the graph.

Co-authored-by: sponger94 <45746997+sponger94@users.noreply.github.com>
This commit is contained in:
Martin Vogel
2026-05-10 18:52:34 +02:00
co-authored by sponger94
parent b2a71b80b9
commit beef06ec6d
2 changed files with 85 additions and 3 deletions
+83 -2
View File
@@ -1986,6 +1986,55 @@ static void extract_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
// Extract class-level variables (field declarations)
extract_class_variables(ctx, node, spec);
// C# 12 primary-constructor parameters: declared on the class line
// (`class Foo(IBar bar, IBaz baz) : Base { ... }`) and bound to implicit
// captured fields accessible from any instance member. Tree-sitter c-sharp
// wraps them inside the hidden _class_declaration_initializer node, so the
// `parameters` field on class_declaration may not always resolve directly;
// iterate top-level children for parameter_list as a robust fallback.
if (ctx->language == CBM_LANG_CSHARP) {
TSNode primary_params = ts_node_child_by_field_name(node, TS_FIELD("parameters"));
if (ts_node_is_null(primary_params)) {
uint32_t total = ts_node_child_count(node);
for (uint32_t i = 0; i < total; i++) {
TSNode c = ts_node_child(node, i);
if (!ts_node_is_null(c) && strcmp(ts_node_type(c), "parameter_list") == 0) {
primary_params = c;
break;
}
}
}
if (!ts_node_is_null(primary_params)) {
uint32_t pcount = ts_node_child_count(primary_params);
for (uint32_t k = 0; k < pcount; k++) {
TSNode p = ts_node_child(primary_params, k);
if (ts_node_is_null(p) || !ts_node_is_named(p)) {
continue;
}
char *pname = resolve_param_name(a, p, ctx->source);
if (!pname || !pname[0]) {
continue;
}
char *ptype = resolve_param_type_text(a, p, ctx->source, ctx->language);
if (!ptype || !ptype[0]) {
continue;
}
CBMDefinition pdef;
memset(&pdef, 0, sizeof(pdef));
pdef.name = pname;
pdef.qualified_name = cbm_arena_sprintf(a, "%s.%s", class_qn, pname);
pdef.label = "Field";
pdef.file_path = ctx->rel_path;
pdef.parent_class = class_qn;
pdef.return_type = ptype;
pdef.start_line = ts_node_start_point(p).row + TS_LINE_OFFSET;
pdef.end_line = ts_node_end_point(p).row + TS_LINE_OFFSET;
pdef.is_exported = false;
cbm_defs_push(&ctx->result->defs, a, pdef);
}
}
}
}
// Find the body/members node inside a class node
@@ -3304,8 +3353,41 @@ static void extract_class_fields(CBMExtractCtx *ctx, TSNode class_node, const ch
continue;
}
// Extract type from "type" field
/* Locate the field's "type" + name node. Two shapes:
* - direct (Java/Go/Rust/C/C++):
* field_declaration .type=identifier .declarator=variable_declarator(.name)
* - nested (C#):
* field_declaration > variable_declaration(.type=identifier,
* variable_declarator(.name))
* For the nested case, the child has no "type" field directly. Detect by
* walking named children for a variable_declaration. */
TSNode type_node = ts_node_child_by_field_name(child, TS_FIELD("type"));
TSNode name_node = ts_node_is_null(type_node) ? (TSNode){0} : resolve_field_name_node(child);
if (ts_node_is_null(type_node)) {
uint32_t cnc = ts_node_named_child_count(child);
for (uint32_t k = 0; k < cnc; k++) {
TSNode inner = ts_node_named_child(child, k);
if (strcmp(ts_node_type(inner), "variable_declaration") != 0) {
continue;
}
type_node = ts_node_child_by_field_name(inner, TS_FIELD("type"));
/* Find first variable_declarator child for the name. */
uint32_t nc = ts_node_named_child_count(inner);
for (uint32_t j = 0; j < nc; j++) {
TSNode vd = ts_node_named_child(inner, j);
if (strcmp(ts_node_type(vd), "variable_declarator") == 0) {
TSNode nm = ts_node_child_by_field_name(vd, TS_FIELD("name"));
if (!ts_node_is_null(nm)) {
name_node = nm;
break;
}
}
}
break;
}
}
if (ts_node_is_null(type_node)) {
continue;
}
@@ -3314,7 +3396,6 @@ static void extract_class_fields(CBMExtractCtx *ctx, TSNode class_node, const ch
continue;
}
TSNode name_node = resolve_field_name_node(child);
if (ts_node_is_null(name_node)) {
continue;
}
+2 -1
View File
@@ -333,6 +333,7 @@ static const char *cs_branch_types[] = {"if_statement", "for_statement", "
"while_statement", "switch_statement", "case_switch_label",
"try_statement", "catch_clause", NULL};
static const char *cs_var_types[] = {"field_declaration", "local_declaration_statement", NULL};
static const char *cs_field_types[] = {"field_declaration", "property_declaration", NULL};
static const char *cs_assign_types[] = {"assignment_expression", NULL};
static const char *cs_throw_types[] = {"throw_statement", "throw_expression", NULL};
static const char *cs_decorator_types[] = {"attribute", NULL};
@@ -1570,7 +1571,7 @@ static const CBMLangSpec lang_specs[CBM_LANG_COUNT] = {
empty_types, cpp_env_funcs, NULL, tree_sitter_cpp, NULL},
// CBM_LANG_CSHARP
[CBM_LANG_CSHARP] = {CBM_LANG_CSHARP, cs_func_types, cs_class_types, empty_types,
[CBM_LANG_CSHARP] = {CBM_LANG_CSHARP, cs_func_types, cs_class_types, cs_field_types,
cs_module_types, cs_call_types, cs_import_types, cs_import_types,
cs_branch_types, cs_var_types, cs_assign_types, cs_throw_types, NULL,
cs_decorator_types, cs_env_funcs, NULL, tree_sitter_c_sharp, NULL},