fix(csharp): preserve escaped constructors and conditional extension members

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-10-22 02:26:28 +02:00
1 parent 5e6b1bb3f6
commit 7028640495
2 files changed
+62 -7

No files matched your search

+14 -7
View File
@@ -8943,9 +8943,15 @@ static bool csharp_is_extension_block(TSNode node, TSNode class_node, const char
}
TSNode type_name = ts_node_child_by_field_name(class_node, TS_FIELD("name"));
if (!ts_node_is_null(type_name)) {
const char *type_text = source + ts_node_start_byte(type_name);
uint32_t tlen = ts_node_end_byte(type_name) - ts_node_start_byte(type_name);
if (tlen == kw_len &&
strncmp(source + ts_node_start_byte(type_name), "extension", kw_len) == 0) {
/* A verbatim identifier names the same type: class @extension may
* declare a genuine constructor spelled extension(). */
if (tlen > 0 && type_text[0] == '@') {
type_text++;
tlen--;
}
if (tlen == kw_len && strncmp(type_text, "extension", kw_len) == 0) {
return false;
}
}
@@ -8963,11 +8969,11 @@ static void extract_csharp_extension_members(CBMExtractCtx *ctx, TSNode ext_node
if (ts_node_is_null(body)) {
return;
}
uint32_t count = ts_node_child_count(body);
for (uint32_t i = 0; i < count; i++) {
TSNode member = ts_node_child(body, i);
if (ts_node_is_null(member) ||
strcmp(ts_node_type(member), "local_function_statement") != 0) {
member_iter_t it;
member_iter_init(&it, body, ctx->language, false);
TSNode member;
while (member_iter_next(&it, &member)) {
if (strcmp(ts_node_type(member), "local_function_statement") != 0) {
continue;
}
TSNode name = resolve_method_name(member, ctx->language);
@@ -8976,6 +8982,7 @@ static void extract_csharp_extension_members(CBMExtractCtx *ctx, TSNode ext_node
}
push_method_def(ctx, member, class_node, class_qn, spec, name);
}
member_iter_done(&it);
}
// Extract methods inside a class body
+48
View File
@@ -916,6 +916,52 @@ TEST(csharp_constructor_of_type_named_extension) {
PASS();
}
/* Escaping a type identifier does not change its constructor's identity. */
TEST(csharp_constructor_of_escaped_type_named_extension) {
const char *src = "namespace App { public class @extension { "
"public extension() { int ConstructorLocal() => 1; } } }";
CBMFileResult *r = extract(src, CBM_LANG_CSHARP, "t", "extension.cs");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
ASSERT(has_def(r, "Method", "extension"));
ASSERT_FALSE(has_def(r, "Method", "ConstructorLocal"));
cbm_free_result(r);
PASS();
}
/* All conditional branches expose extension members, but method-local
* functions must never be promoted to members of the enclosing type. */
TEST(csharp_extension_block_conditional_members) {
const char *src = "public static class TextExtensions\n"
"{\n"
" extension(string text)\n"
" {\n"
"#if FIRST\n"
" public int First() { int Nested() => 1; return Nested(); }\n"
"#elif SECOND\n"
" public int Second() => 2;\n"
"#else\n"
"#if NESTED\n"
" public int Third() => 3;\n"
"#else\n"
" public int Fourth() => 4;\n"
"#endif\n"
"#endif\n"
" }\n"
"}\n";
CBMFileResult *r = extract(src, CBM_LANG_CSHARP, "t", "TextExtensions.cs");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
ASSERT(has_def(r, "Method", "First"));
ASSERT(has_def(r, "Method", "Second"));
ASSERT(has_def(r, "Method", "Third"));
ASSERT(has_def(r, "Method", "Fourth"));
ASSERT_FALSE(has_def_any(r, "extension"));
ASSERT_FALSE(has_def(r, "Method", "Nested"));
cbm_free_result(r);
PASS();
}
/* --- Swift --- */
TEST(swift_class) {
CBMFileResult *r = extract("class Vehicle {\n var speed: Int = 0\n func accelerate() { "
@@ -13195,6 +13241,8 @@ SUITE(extraction) {
RUN_TEST(csharp_interface);
RUN_TEST(csharp_extension_block_members);
RUN_TEST(csharp_constructor_of_type_named_extension);
RUN_TEST(csharp_constructor_of_escaped_type_named_extension);
RUN_TEST(csharp_extension_block_conditional_members);
RUN_TEST(swift_class);
RUN_TEST(swift_protocol);
RUN_TEST(kotlin_function);