From 7028640495e60c3696e8f593678d5a94b8efbe07 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Thu, 22 Oct 2026 02:26:28 +0200 Subject: [PATCH] fix(csharp): preserve escaped constructors and conditional extension members Signed-off-by: Martin Vogel --- internal/cbm/extract_defs.c | 21 ++++++++++------ tests/test_extraction.c | 48 +++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index dd8fcca53..a2c367677 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -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 diff --git a/tests/test_extraction.c b/tests/test_extraction.c index a3c6e8f34..aadde696d 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -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);