diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index 2e359af67..0249f370e 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -1324,6 +1324,7 @@ static const char *extract_docstring(CBMArena *a, TSNode node, const char *sourc } static TSNode find_jvm_modifiers(TSNode node, CBMLanguage lang); +static const char *annotation_wrapper_kind(CBMLanguage lang); /* HTTP method names recognized in decorator calls (e.g., @router.post → "POST") */ static const char *decorator_method_name(const char *attr_text) { @@ -1868,35 +1869,52 @@ static int count_modifier_annotations(TSNode modifiers, const CBMLangSpec *spec) return count; } -// Find the wrapper child that holds annotations/attributes for languages where +// Return the wrapper kind that holds annotations/attributes for languages where // they are nested under an intermediate node rather than being a prev-sibling: // Java/Kotlin/C#/Swift → `modifiers` (contains annotation/attribute) // PHP 8 → `attribute_list` (contains attribute_group) -// Returns a null node when the language has no such wrapper. -static TSNode find_jvm_modifiers(TSNode node, CBMLanguage lang) { - TSNode null_node = {0}; - const char *wrapper = NULL; +// Returns NULL when the language has no such wrapper. +static const char *annotation_wrapper_kind(CBMLanguage lang) { switch (lang) { case CBM_LANG_JAVA: case CBM_LANG_KOTLIN: case CBM_LANG_SWIFT: - wrapper = "modifiers"; - break; + return "modifiers"; case CBM_LANG_CSHARP: case CBM_LANG_PHP: /* C# attributes live in an `attribute_list` child (modifiers like * `public` are separate `modifier` nodes); PHP 8 likewise nests * `attribute_group` under `attribute_list`. */ - wrapper = "attribute_list"; - break; + return "attribute_list"; default: + return NULL; + } +} + +static TSNode find_jvm_modifiers(TSNode node, CBMLanguage lang) { + TSNode null_node = {0}; + const char *wrapper = annotation_wrapper_kind(lang); + if (!wrapper) { return null_node; } - TSNode w = ts_node_child_by_field_name(node, wrapper, (uint32_t)strlen(wrapper)); - if (ts_node_is_null(w)) { - w = cbm_find_child_by_kind(node, wrapper); + TSNode result = ts_node_child_by_field_name(node, wrapper, (uint32_t)strlen(wrapper)); + if (ts_node_is_null(result)) { + result = cbm_find_child_by_kind(node, wrapper); + } + return result; +} + +static int count_wrapped_decorators(TSNode node, const char *wrapper_kind, + const CBMLangSpec *spec) { + int count = 0; + uint32_t cc = ts_node_child_count(node); + for (uint32_t ci = 0; ci < cc; ci++) { + TSNode child = ts_node_child(node, ci); + if (strcmp(ts_node_type(child), wrapper_kind) == 0) { + count += count_modifier_annotations(child, spec); + } } - return w; + return count; } // Count direct children of `node` that are decorator/annotation nodes (used by @@ -1941,6 +1959,19 @@ static int collect_modifier_decorators(CBMArena *a, TSNode modifiers, const char return idx; } +static int collect_wrapped_decorators(CBMArena *a, TSNode node, const char *source, + const char *wrapper_kind, const CBMLangSpec *spec, + const char **result, int idx, int max) { + uint32_t cc = ts_node_child_count(node); + for (uint32_t ci = 0; ci < cc && idx < max; ci++) { + TSNode child = ts_node_child(node, ci); + if (strcmp(ts_node_type(child), wrapper_kind) == 0) { + idx = collect_modifier_decorators(a, child, source, spec, result, idx, max); + } + } + return idx; +} + /* Comments are NAMED nodes in tree-sitter, so a comment interleaved in a * decorator run would end the walk and silently drop every decorator above it: * @@ -1974,22 +2005,22 @@ static const char **extract_decorators(CBMArena *a, TSNode node, const char *sou prev = ts_node_prev_sibling(prev); } - TSNode modifiers = {0}; - int mod_count = 0; + const char *wrapper_kind = NULL; + int wrapper_count = 0; int child_count = 0; if (count == 0) { - modifiers = find_jvm_modifiers(node, lang); - if (!ts_node_is_null(modifiers)) { - mod_count = count_modifier_annotations(modifiers, spec); + wrapper_kind = annotation_wrapper_kind(lang); + if (wrapper_kind) { + wrapper_count = count_wrapped_decorators(node, wrapper_kind, spec); } /* Languages like Scala attach the annotation directly as a child of the * definition node (no wrapper, no prev-sibling). */ - if (mod_count == 0) { + if (wrapper_count == 0) { child_count = count_child_decorators(node, spec); } } - int total = count + mod_count + child_count; + int total = count + wrapper_count + child_count; if (total == 0) { return NULL; } @@ -2010,8 +2041,8 @@ static const char **extract_decorators(CBMArena *a, TSNode node, const char *sou } prev = ts_node_prev_sibling(prev); } - if (!ts_node_is_null(modifiers)) { - idx = collect_modifier_decorators(a, modifiers, source, spec, result, idx, total); + if (wrapper_kind) { + idx = collect_wrapped_decorators(a, node, source, wrapper_kind, spec, result, idx, total); } if (child_count > 0) { idx = collect_child_decorators(a, node, source, spec, result, idx, total); diff --git a/tests/test_extraction.c b/tests/test_extraction.c index 5b8d16f61..549001500 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -3732,6 +3732,26 @@ TEST(extract_java_method_annotations_issue382) { PASS(); } +/* Issue #1865: tree-sitter-c-sharp emits one attribute_list child for each + * separate bracket group. Looking up a single wrapper silently dropped every + * group after the first one. */ +TEST(extract_csharp_multiple_attribute_lists_issue1865) { + CBMFileResult *r = extract("public class OrdersController {\n" + " [HttpPost]\n" + " [Route(\"orders/{id}/confirm\")]\n" + " public void Confirm(Guid id) {}\n" + "}\n", + CBM_LANG_CSHARP, "t", "OrdersController.cs"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + const CBMDefinition *m = find_def_by_name(r, "Confirm"); + ASSERT_NOT_NULL(m); + ASSERT(decorators_contain(m, "HttpPost")); + ASSERT(decorators_contain(m, "Route")); + cbm_free_result(r); + PASS(); +} + /* ── ArkTS (HarmonyOS .ets) ─────────────────────────────────────── */ TEST(arkts_component_struct) { @@ -6800,6 +6820,7 @@ SUITE(extraction) { RUN_TEST(js_index_module_qn_not_collide_with_folder); RUN_TEST(python_regular_module_qn_unchanged); RUN_TEST(extract_java_method_annotations_issue382); + RUN_TEST(extract_csharp_multiple_attribute_lists_issue1865); RUN_TEST(arkts_component_struct); RUN_TEST(arkts_exported_struct_decorators); RUN_TEST(arkts_member_decorators);