Skip to content

Commit 012be57

Browse files
fix(extract): keep decorators separated by a comment
Comments are NAMED tree-sitter nodes, so the prev-sibling walk in extract_decorators() stopped at one — silently dropping every decorator ABOVE an interleaved comment: @post('login') <-- dropped @httpcode(HttpStatus.OK) <-- dropped // throttled per IP and account @Throttle({ ... }) <-- kept async login(...) The route then vanished from decorator/route queries, so documenting a decorator made the endpoint disappear from the graph. Real-world impact: on a NestJS backend, 1 of 95 HTTP endpoints was missing — the one whose throttle policy carried an explanatory comment. Comments are now transparent to the walk, the same way anonymous tokens (e.g. TS `export`) already were. Reuses the existing is_comment_node() helper. Signed-off-by: KolisCode <jhohantma@gmail.com>
1 parent 90d7315 commit 012be57

2 files changed

Lines changed: 41 additions & 3 deletions

File tree

internal/cbm/extract_defs.c

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1886,6 +1886,18 @@ static int collect_modifier_decorators(CBMArena *a, TSNode modifiers, const char
18861886
return idx;
18871887
}
18881888

1889+
/* Comments are NAMED nodes in tree-sitter, so a comment interleaved in a
1890+
* decorator run would end the walk and silently drop every decorator above it:
1891+
*
1892+
* @Post('login') <-- lost
1893+
* @HttpCode(HttpStatus.OK) <-- lost
1894+
* // why this route is throttled <-- walk stopped here
1895+
* @Throttle({ ... }) <-- kept
1896+
* async login(...)
1897+
*
1898+
* Documenting a decorator must not make it disappear from the graph, so treat
1899+
* comments as transparent — like the anonymous tokens already skipped below.
1900+
* (is_comment_node() is defined above, near the docstring helpers.) */
18891901
static const char **extract_decorators(CBMArena *a, TSNode node, const char *source,
18901902
CBMLanguage lang, const CBMLangSpec *spec) {
18911903
if (!spec->decorator_node_types || !spec->decorator_node_types[0]) {
@@ -1897,10 +1909,11 @@ static const char **extract_decorators(CBMArena *a, TSNode node, const char *sou
18971909
while (!ts_node_is_null(prev)) {
18981910
if (cbm_kind_in_set(prev, spec->decorator_node_types)) {
18991911
count++;
1900-
} else if (ts_node_is_named(prev)) {
1912+
} else if (ts_node_is_named(prev) && !is_comment_node(ts_node_type(prev))) {
19011913
/* A real preceding construct ends the decorator run. Anonymous
19021914
* tokens (e.g. TS `export` between `@Decorator` and the
1903-
* `class_declaration`) are skipped so the decorator is still seen. */
1915+
* `class_declaration`) and comments are skipped so the decorator
1916+
* is still seen. */
19041917
break;
19051918
}
19061919
prev = ts_node_prev_sibling(prev);
@@ -1937,7 +1950,7 @@ static const char **extract_decorators(CBMArena *a, TSNode node, const char *sou
19371950
while (!ts_node_is_null(prev) && idx < count) {
19381951
if (cbm_kind_in_set(prev, spec->decorator_node_types)) {
19391952
result[idx++] = cbm_node_text(a, prev, source);
1940-
} else if (ts_node_is_named(prev)) {
1953+
} else if (ts_node_is_named(prev) && !is_comment_node(ts_node_type(prev))) {
19411954
break;
19421955
}
19431956
prev = ts_node_prev_sibling(prev);

tests/test_extraction.c

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2790,6 +2790,30 @@ TEST(extract_java_jaxrs_path_composition_issue1005) {
27902790
PASS();
27912791
}
27922792

2793+
/* A comment between decorators must not drop the decorators above it.
2794+
* Comments are NAMED tree-sitter nodes, so the prev-sibling walk used to stop
2795+
* at one — a documented route (@Post + @HttpCode above an explanatory comment)
2796+
* silently lost those decorators and disappeared from route/authz queries. */
2797+
TEST(extract_ts_decorators_survive_interleaved_comment) {
2798+
CBMFileResult *r = extract("class AuthController {\n"
2799+
" @Post('login')\n"
2800+
" @HttpCode(HttpStatus.OK)\n"
2801+
" // throttled per IP and per account\n"
2802+
" @Throttle({ default: { ttl: 900_000, limit: 5 } })\n"
2803+
" async login(dto: LoginDto) { return 1; }\n"
2804+
"}\n",
2805+
CBM_LANG_TYPESCRIPT, "t", "auth.controller.ts");
2806+
ASSERT_NOT_NULL(r);
2807+
ASSERT_FALSE(r->has_error);
2808+
const CBMDefinition *m = find_def_by_name(r, "login");
2809+
ASSERT_NOT_NULL(m);
2810+
ASSERT(decorators_contain(m, "Throttle")); /* below the comment — always worked */
2811+
ASSERT(decorators_contain(m, "HttpCode")); /* above the comment — was dropped */
2812+
ASSERT(decorators_contain(m, "Post")); /* above the comment — was dropped */
2813+
cbm_free_result(r);
2814+
PASS();
2815+
}
2816+
27932817
/* Find an in-body call by its raw callee text; returns the call or NULL. */
27942818
static const CBMCall *find_call_by_callee(CBMFileResult *r, const char *callee) {
27952819
for (int i = 0; i < r->calls.count; i++) {
@@ -4912,6 +4936,7 @@ SUITE(extraction) {
49124936
RUN_TEST(walk_defs_no_truncation_over_4096_issue668);
49134937
RUN_TEST(extract_rust_test_attr_marks_is_test_issue855);
49144938
RUN_TEST(docstring_utf8_truncation_boundary_issue1017);
4939+
RUN_TEST(extract_ts_decorators_survive_interleaved_comment);
49154940

49164941
cbm_shutdown();
49174942
}

0 commit comments

Comments
 (0)