2323#define MAX_PARAMS_MINUS_1 31
2424#define MAX_RETURN_TYPES 16
2525#define MAX_RETURN_TYPES_MINUS_1 15
26+ #define MAX_ATTR_WRAPPERS 16 // stacked C#/PHP attribute_list siblings per declaration (#1692)
27+ #define MAX_ATTR_WRAPPERS_MINUS_1 15
2628
2729// Tree traversal limits.
2830enum {
@@ -1323,7 +1325,7 @@ static const char *extract_docstring(CBMArena *a, TSNode node, const char *sourc
13231325 return NULL ;
13241326}
13251327
1326- static TSNode find_jvm_modifiers (TSNode node , CBMLanguage lang );
1328+ static int find_jvm_modifiers (TSNode node , CBMLanguage lang , TSNode * out , int max );
13271329
13281330/* HTTP method names recognized in decorator calls (e.g., @router.post → "POST") */
13291331static const char * decorator_method_name (const char * attr_text ) {
@@ -1713,12 +1715,9 @@ static void scan_route_annotations(CBMArena *a, TSNode owner, const char *source
17131715 * out_method = NULL ;
17141716 * out_jax_path = NULL ;
17151717
1716- TSNode wrappers [2 ];
1717- int wn = 0 ;
1718- TSNode modifiers = find_jvm_modifiers (owner , spec -> language );
1719- if (!ts_node_is_null (modifiers )) {
1720- wrappers [wn ++ ] = modifiers ;
1721- }
1718+ /* MINUS_1: the owner node itself is appended below, after the wrappers. */
1719+ TSNode wrappers [MAX_ATTR_WRAPPERS ];
1720+ int wn = find_jvm_modifiers (owner , spec -> language , wrappers , MAX_ATTR_WRAPPERS_MINUS_1 );
17221721 /* Direct-child annotations (some grammars attach the annotation as a child
17231722 * of the method node rather than under `modifiers`). */
17241723 wrappers [wn ++ ] = owner ;
@@ -1872,13 +1871,23 @@ static int count_modifier_annotations(TSNode modifiers, const CBMLangSpec *spec)
18721871 return count ;
18731872}
18741873
1875- // Find the wrapper child that holds annotations/attributes for languages where
1876- // they are nested under an intermediate node rather than being a prev-sibling:
1877- // Java/Kotlin/C#/Swift → `modifiers` (contains annotation/attribute)
1878- // PHP 8 → `attribute_list` (contains attribute_group)
1879- // Returns a null node when the language has no such wrapper.
1880- static TSNode find_jvm_modifiers (TSNode node , CBMLanguage lang ) {
1881- TSNode null_node = {0 };
1874+ // Find every wrapper child that holds annotations/attributes for languages
1875+ // where they are nested under an intermediate node rather than being a
1876+ // prev-sibling:
1877+ // Java/Kotlin/Swift → `modifiers` (one node, contains every annotation)
1878+ // C#/PHP 8 → `attribute_list` (contains attribute/attribute_group)
1879+ //
1880+ // C#/PHP attribute stacks are NOT a single wrapper: each bracketed group
1881+ // (`[Foo]`, `[Bar]`, ...) compiles to its own `attribute_list` node, so
1882+ // `[A] [B] [C]` above a declaration produces three separate `attribute_list`
1883+ // siblings among that declaration's children — not one `attribute_list`
1884+ // holding three entries. A field-name lookup (`ts_node_child_by_field_name`)
1885+ // only ever returns the first child registered under a given field, so using
1886+ // it here silently dropped every attribute after the first bracket group
1887+ // (#1692). Scanning all children by kind fixes that for C#/PHP and is a
1888+ // no-op change for Java/Kotlin/Swift, where `modifiers` never repeats.
1889+ // Writes up to `max` wrapper nodes into `out`; returns how many were found.
1890+ static int find_jvm_modifiers (TSNode node , CBMLanguage lang , TSNode * out , int max ) {
18821891 const char * wrapper = NULL ;
18831892 switch (lang ) {
18841893 case CBM_LANG_JAVA :
@@ -1888,19 +1897,12 @@ static TSNode find_jvm_modifiers(TSNode node, CBMLanguage lang) {
18881897 break ;
18891898 case CBM_LANG_CSHARP :
18901899 case CBM_LANG_PHP :
1891- /* C# attributes live in an `attribute_list` child (modifiers like
1892- * `public` are separate `modifier` nodes); PHP 8 likewise nests
1893- * `attribute_group` under `attribute_list`. */
18941900 wrapper = "attribute_list" ;
18951901 break ;
18961902 default :
1897- return null_node ;
1898- }
1899- TSNode w = ts_node_child_by_field_name (node , wrapper , (uint32_t )strlen (wrapper ));
1900- if (ts_node_is_null (w )) {
1901- w = cbm_find_child_by_kind (node , wrapper );
1903+ return 0 ;
19021904 }
1903- return w ;
1905+ return cbm_find_children_by_kind ( node , wrapper , out , max ) ;
19041906}
19051907
19061908// Count direct children of `node` that are decorator/annotation nodes (used by
@@ -1978,13 +1980,14 @@ static const char **extract_decorators(CBMArena *a, TSNode node, const char *sou
19781980 prev = ts_node_prev_sibling (prev );
19791981 }
19801982
1981- TSNode modifiers = {0 };
1983+ TSNode wrappers [MAX_ATTR_WRAPPERS ];
1984+ int wn = 0 ;
19821985 int mod_count = 0 ;
19831986 int child_count = 0 ;
19841987 if (count == 0 ) {
1985- modifiers = find_jvm_modifiers (node , lang );
1986- if (! ts_node_is_null ( modifiers ) ) {
1987- mod_count = count_modifier_annotations (modifiers , spec );
1988+ wn = find_jvm_modifiers (node , lang , wrappers , MAX_ATTR_WRAPPERS );
1989+ for ( int w = 0 ; w < wn ; w ++ ) {
1990+ mod_count + = count_modifier_annotations (wrappers [ w ] , spec );
19881991 }
19891992 /* Languages like Scala attach the annotation directly as a child of the
19901993 * definition node (no wrapper, no prev-sibling). */
@@ -2014,8 +2017,8 @@ static const char **extract_decorators(CBMArena *a, TSNode node, const char *sou
20142017 }
20152018 prev = ts_node_prev_sibling (prev );
20162019 }
2017- if (! ts_node_is_null ( modifiers ) ) {
2018- idx = collect_modifier_decorators (a , modifiers , source , spec , result , idx , total );
2020+ for ( int w = 0 ; w < wn && mod_count > 0 ; w ++ ) {
2021+ idx = collect_modifier_decorators (a , wrappers [ w ] , source , spec , result , idx , total );
20192022 }
20202023 if (child_count > 0 ) {
20212024 idx = collect_child_decorators (a , node , source , spec , result , idx , total );
0 commit comments