Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile.cbm
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,7 @@ TEST_REPRO_SRCS = \
tests/repro/repro_issue787.c \
tests/repro/repro_issue842.c \
tests/repro/repro_issue964.c \
tests/repro/repro_issue1692.c \
tests/repro/repro_invariant_calls.c \
tests/repro/repro_invariant_graph.c \
tests/repro/repro_invariant_breadth.c \
Expand Down
61 changes: 32 additions & 29 deletions internal/cbm/extract_defs.c
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
#define MAX_PARAMS_MINUS_1 31
#define MAX_RETURN_TYPES 16
#define MAX_RETURN_TYPES_MINUS_1 15
#define MAX_ATTR_WRAPPERS 16 // stacked C#/PHP attribute_list siblings per declaration (#1692)
#define MAX_ATTR_WRAPPERS_MINUS_1 15

// Tree traversal limits.
enum {
Expand Down Expand Up @@ -1287,7 +1289,7 @@ static const char *extract_docstring(CBMArena *a, TSNode node, const char *sourc
return NULL;
}

static TSNode find_jvm_modifiers(TSNode node, CBMLanguage lang);
static int find_jvm_modifiers(TSNode node, CBMLanguage lang, TSNode *out, int max);

/* HTTP method names recognized in decorator calls (e.g., @router.post → "POST") */
static const char *decorator_method_name(const char *attr_text) {
Expand Down Expand Up @@ -1673,12 +1675,9 @@ static void scan_route_annotations(CBMArena *a, TSNode owner, const char *source
*out_method = NULL;
*out_jax_path = NULL;

TSNode wrappers[2];
int wn = 0;
TSNode modifiers = find_jvm_modifiers(owner, spec->language);
if (!ts_node_is_null(modifiers)) {
wrappers[wn++] = modifiers;
}
/* MINUS_1: the owner node itself is appended below, after the wrappers. */
TSNode wrappers[MAX_ATTR_WRAPPERS];
int wn = find_jvm_modifiers(owner, spec->language, wrappers, MAX_ATTR_WRAPPERS_MINUS_1);
/* Direct-child annotations (some grammars attach the annotation as a child
* of the method node rather than under `modifiers`). */
wrappers[wn++] = owner;
Expand Down Expand Up @@ -1832,13 +1831,23 @@ static int count_modifier_annotations(TSNode modifiers, const CBMLangSpec *spec)
return count;
}

// Find the wrapper child 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};
// Find every wrapper child that holds annotations/attributes for languages
// where they are nested under an intermediate node rather than being a
// prev-sibling:
// Java/Kotlin/Swift → `modifiers` (one node, contains every annotation)
// C#/PHP 8 → `attribute_list` (contains attribute/attribute_group)
//
// C#/PHP attribute stacks are NOT a single wrapper: each bracketed group
// (`[Foo]`, `[Bar]`, ...) compiles to its own `attribute_list` node, so
// `[A] [B] [C]` above a declaration produces three separate `attribute_list`
// siblings among that declaration's children — not one `attribute_list`
// holding three entries. A field-name lookup (`ts_node_child_by_field_name`)
// only ever returns the first child registered under a given field, so using
// it here silently dropped every attribute after the first bracket group
// (#1692). Scanning all children by kind fixes that for C#/PHP and is a
// no-op change for Java/Kotlin/Swift, where `modifiers` never repeats.
// Writes up to `max` wrapper nodes into `out`; returns how many were found.
static int find_jvm_modifiers(TSNode node, CBMLanguage lang, TSNode *out, int max) {
const char *wrapper = NULL;
switch (lang) {
case CBM_LANG_JAVA:
Expand All @@ -1848,19 +1857,12 @@ static TSNode find_jvm_modifiers(TSNode node, CBMLanguage lang) {
break;
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;
default:
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);
return 0;
}
return w;
return cbm_find_children_by_kind(node, wrapper, out, max);
}

// Count direct children of `node` that are decorator/annotation nodes (used by
Expand Down Expand Up @@ -1938,13 +1940,14 @@ static const char **extract_decorators(CBMArena *a, TSNode node, const char *sou
prev = ts_node_prev_sibling(prev);
}

TSNode modifiers = {0};
TSNode wrappers[MAX_ATTR_WRAPPERS];
int wn = 0;
int mod_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);
wn = find_jvm_modifiers(node, lang, wrappers, MAX_ATTR_WRAPPERS);
for (int w = 0; w < wn; w++) {
mod_count += count_modifier_annotations(wrappers[w], spec);
}
/* Languages like Scala attach the annotation directly as a child of the
* definition node (no wrapper, no prev-sibling). */
Expand Down Expand Up @@ -1974,8 +1977,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);
for (int w = 0; w < wn && mod_count > 0; w++) {
idx = collect_modifier_decorators(a, wrappers[w], source, spec, result, idx, total);
}
if (child_count > 0) {
idx = collect_child_decorators(a, node, source, spec, result, idx, total);
Expand Down
12 changes: 12 additions & 0 deletions internal/cbm/helpers.c
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,18 @@ TSNode cbm_find_child_by_kind(TSNode parent, const char *kind) {
return null_node;
}

int cbm_find_children_by_kind(TSNode parent, const char *kind, TSNode *out, int max) {
int n = 0;
uint32_t count = ts_node_child_count(parent);
for (uint32_t i = 0; i < count && n < max; i++) {
TSNode child = ts_node_child(parent, i);
if (strcmp(ts_node_type(child), kind) == 0) {
out[n++] = child;
}
}
return n;
}

/* ── Node-type classification: TSSymbol bitset acceleration ───────────────
* cbm_kind_in_set is called for nearly every AST node (function/class/call/
* import/branching sets), so a linear strcmp over the type-name array is a hot
Expand Down
7 changes: 7 additions & 0 deletions internal/cbm/helpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,13 @@ char *cbm_cpp_out_of_line_parent_class(CBMArena *a, TSNode node, const char *sou
// Find a child node by kind string.
TSNode cbm_find_child_by_kind(TSNode parent, const char *kind);

// Find every child node matching `kind` (unlike cbm_find_child_by_kind,
// which stops at the first match). Writes up to `max` nodes into `out`;
// returns how many were found. Repeated sibling nodes of the same kind are
// common in some grammars (e.g. C#/PHP stack each `[Attr]` as its own
// attribute_list child) — see #1692.
int cbm_find_children_by_kind(TSNode parent, const char *kind, TSNode *out, int max);

// Check if node kind matches a set of types (NULL-terminated array of strings).
bool cbm_kind_in_set(TSNode node, const char **types);

Expand Down
91 changes: 91 additions & 0 deletions tests/repro/repro_issue1692.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* repro_issue1692.c — Reproduce-first case for OPEN bug #1692.
*
* Issue: #1692 — "C#: ... ASP.NET Core [Route]/[HttpGet] attribute routes
* not extracted (cross-repo-intelligence returns 0 edges)". The reporter's
* repro 2 shows a class carrying three stacked attributes
* ([ServiceFilter]/[Route]/[ApiController]) where DECORATES exists 13,964
* times elsewhere in the same graph but zero times for that declaration.
*
* Root cause (traced further than the issue body): C# does not attach one
* `attribute_list` wrapper holding every bracket group above a declaration —
* each `[A]` / `[B]` / `[C]` line compiles to its OWN `attribute_list` node,
* so a stack of three produces three separate `attribute_list` siblings
* among the declaration's children. `find_jvm_modifiers()`
* (internal/cbm/extract_defs.c) located that wrapper with
* `ts_node_child_by_field_name(node, "attribute_list", ...)`, which only
* ever returns the FIRST child registered under a repeated field — so only
* the first bracket group's attribute(s) ever produced a DECORATES edge;
* every attribute after the first was silently dropped, with no warning and
* no staleness signal (confirmed indexed, not stale, via grafo_coverage in
* the downstream McpDevGrafo-bugs.md BUG-004 report that led here).
*
* Expected (correct) behaviour:
* A method carrying N stacked attributes gets exactly N outbound
* DECORATES edges — not just one for the first bracket group.
*
* Why RED on current code: only the first attribute's DECORATES edge
* exists, so the project-wide DECORATES count is 1, not 3.
*
* Scope — why this file is C#-only:
* find_jvm_modifiers() shares ONE switch across Java/Kotlin/Swift/C#/PHP,
* but only C# can reach this bug. Verified against the vendored grammars:
* `[A] [B] [C]` in C# parses as THREE sibling `attribute_list` nodes, while
* PHP's `#[A] #[B] #[C]` parses as ONE `attribute_list` holding three
* `attribute_group` children — so the first-child lookup already found them
* all and PHP never dropped an attribute. Java/Kotlin/Swift use the
* `modifiers` wrapper, a single node that never repeats. PHP's DECORATES
* path stays covered by mkc_c3_php8_attribute (test_matrix_known_classes.c);
* a stacked-PHP fixture here would pass with or without the fix and prove
* nothing. No other language reaches this function (`default` returns 0).
*/

#include <foundation/compat.h>
#include "test_framework.h"
#include "repro_harness.h"

#include <string.h>
#include <stdlib.h>
#include <stdio.h>

/* ── Fixture: C# ───────────────────────────────────────────────────────────
*
* One C# method, three stacked attributes, nothing else in the file that
* could carry a DECORATES edge — so a project-wide edge count is as precise
* as scoping to the method node, without the extra node lookup.
* ──────────────────────────────────────────────────────────────────────── */
static const char *kStackedAttrsCs = "using System;\n"
"\n"
"namespace ReproNS\n"
"{\n"
" public class AttrController\n"
" {\n"
" [Foo]\n"
" [Bar(\"x\")]\n"
" [Baz]\n"
" public void StackedAttrsMethod() { }\n"
" }\n"
"}\n";

TEST(repro_issue1692_csharp_stacked_attributes_all_decorate) {
RProj lp;
cbm_store_t *store = rh_index(&lp, "StackedAttrs.cs", kStackedAttrsCs);
ASSERT_NOT_NULL(store);

int decorates_count = rh_count_edges(store, lp.project, "DECORATES");
if (decorates_count != 3) {
fprintf(stderr,
" [1692] FAIL decorates=%d (expected 3 — one per stacked [Foo][Bar][Baz]; "
"only the first attribute_list's attribute(s) survive find_jvm_modifiers' "
"single-field lookup)\n",
decorates_count);
}
ASSERT_EQ(3, decorates_count); /* one DECORATES per stacked attribute, not just the first */

rh_cleanup(&lp, store);
PASS();
}

SUITE(repro_issue1692) {
RUN_TEST(repro_issue1692_csharp_stacked_attributes_all_decorate);
}
2 changes: 2 additions & 0 deletions tests/repro/repro_main.c
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ extern void suite_repro_issue581(void);
extern void suite_repro_issue787(void);
extern void suite_repro_issue842(void);
extern void suite_repro_issue964(void);
extern void suite_repro_issue1692(void);
/* NEW bugs found by the discovery sweep */
extern void suite_repro_new_ts_class_field_arrow(void);
extern void suite_repro_new_py_tuple_unpack(void);
Expand Down Expand Up @@ -192,6 +193,7 @@ int main(void) {
RUN_SUITE(repro_issue787);
RUN_SUITE(repro_issue842);
RUN_SUITE(repro_issue964);
RUN_SUITE(repro_issue1692);
RUN_SUITE(repro_invariant_calls);
RUN_SUITE(repro_invariant_graph);
RUN_SUITE(repro_invariant_breadth);
Expand Down
Loading