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
19 changes: 16 additions & 3 deletions internal/cbm/extract_defs.c
Original file line number Diff line number Diff line change
Expand Up @@ -1886,6 +1886,18 @@ static int collect_modifier_decorators(CBMArena *a, TSNode modifiers, const char
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:
*
* @Post('login') <-- lost
* @HttpCode(HttpStatus.OK) <-- lost
* // why this route is throttled <-- walk stopped here
* @Throttle({ ... }) <-- kept
* async login(...)
*
* Documenting a decorator must not make it disappear from the graph, so treat
* comments as transparent — like the anonymous tokens already skipped below.
* (is_comment_node() is defined above, near the docstring helpers.) */
static const char **extract_decorators(CBMArena *a, TSNode node, const char *source,
CBMLanguage lang, const CBMLangSpec *spec) {
if (!spec->decorator_node_types || !spec->decorator_node_types[0]) {
Expand All @@ -1897,10 +1909,11 @@ static const char **extract_decorators(CBMArena *a, TSNode node, const char *sou
while (!ts_node_is_null(prev)) {
if (cbm_kind_in_set(prev, spec->decorator_node_types)) {
count++;
} else if (ts_node_is_named(prev)) {
} else if (ts_node_is_named(prev) && !is_comment_node(ts_node_type(prev))) {
/* A real preceding construct ends the decorator run. Anonymous
* tokens (e.g. TS `export` between `@Decorator` and the
* `class_declaration`) are skipped so the decorator is still seen. */
* `class_declaration`) and comments are skipped so the decorator
* is still seen. */
break;
}
prev = ts_node_prev_sibling(prev);
Expand Down Expand Up @@ -1937,7 +1950,7 @@ static const char **extract_decorators(CBMArena *a, TSNode node, const char *sou
while (!ts_node_is_null(prev) && idx < count) {
if (cbm_kind_in_set(prev, spec->decorator_node_types)) {
result[idx++] = cbm_node_text(a, prev, source);
} else if (ts_node_is_named(prev)) {
} else if (ts_node_is_named(prev) && !is_comment_node(ts_node_type(prev))) {
break;
}
prev = ts_node_prev_sibling(prev);
Expand Down
25 changes: 25 additions & 0 deletions tests/test_extraction.c
Original file line number Diff line number Diff line change
Expand Up @@ -2790,6 +2790,30 @@ TEST(extract_java_jaxrs_path_composition_issue1005) {
PASS();
}

/* A comment between decorators must not drop the decorators above it.
* Comments are NAMED tree-sitter nodes, so the prev-sibling walk used to stop
* at one — a documented route (@Post + @HttpCode above an explanatory comment)
* silently lost those decorators and disappeared from route/authz queries. */
TEST(extract_ts_decorators_survive_interleaved_comment) {
CBMFileResult *r = extract("class AuthController {\n"
" @Post('login')\n"
" @HttpCode(HttpStatus.OK)\n"
" // throttled per IP and per account\n"
" @Throttle({ default: { ttl: 900_000, limit: 5 } })\n"
" async login(dto: LoginDto) { return 1; }\n"
"}\n",
CBM_LANG_TYPESCRIPT, "t", "auth.controller.ts");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
const CBMDefinition *m = find_def_by_name(r, "login");
ASSERT_NOT_NULL(m);
ASSERT(decorators_contain(m, "Throttle")); /* below the comment — always worked */
ASSERT(decorators_contain(m, "HttpCode")); /* above the comment — was dropped */
ASSERT(decorators_contain(m, "Post")); /* above the comment — was dropped */
cbm_free_result(r);
PASS();
}

/* Find an in-body call by its raw callee text; returns the call or NULL. */
static const CBMCall *find_call_by_callee(CBMFileResult *r, const char *callee) {
for (int i = 0; i < r->calls.count; i++) {
Expand Down Expand Up @@ -4912,6 +4936,7 @@ SUITE(extraction) {
RUN_TEST(walk_defs_no_truncation_over_4096_issue668);
RUN_TEST(extract_rust_test_attr_marks_is_test_issue855);
RUN_TEST(docstring_utf8_truncation_boundary_issue1017);
RUN_TEST(extract_ts_decorators_survive_interleaved_comment);

cbm_shutdown();
}
Loading