mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-28 07:03:20 +08:00
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>
This commit is contained in:
@@ -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]) {
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
@@ -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++) {
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user