Skip to content

Commit 592894a

Browse files
fix(routes): take a route's handler from the last argument, not the first
Express, Fastify, gin and Laravel all put middleware between the route path and the handler, and the handler comes last: routerGet("/users", requireAuth, rateLimit, listUsers); extract_handler_arg returned the FIRST argument that looked like a function reference, so requireAuth won and the HANDLES edge pointed at the middleware instead of listUsers. That is a wrong edge, not a missing one, and it misleads anyone tracing a request through the graph. The scan bound hid a second fault. An arrow function matches none of the accepted node kinds, so three inline middlewares pushed the real handler past MAX_HANDLER_SCAN and no handler came back at all. The loop now examines every argument and keeps the last eligible one. Nothing that follows a handler matches the accepted kinds either, since an options argument is an object node, so the last match is the handler. HANDLER_START_IDX stays, because argument 0 really is the path. MAX_HANDLER_SCAN had no other use and is gone. A two-argument route has one eligible argument, so first and last agree and the existing Express, Fastify, gin and Laravel route tests are unchanged. Both tests fail without the change, the first reporting "requireAuth" where "listUsers" belongs and the second reporting no handler at all. Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
1 parent 5fbab7b commit 592894a

2 files changed

Lines changed: 59 additions & 6 deletions

File tree

internal/cbm/extract_calls.c

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@
2121
enum { LEAN_MAX_PARENT_DEPTH = 20 };
2222
/* Max positional args to scan for URL/string. */
2323
enum { MAX_POSITIONAL_SCAN = 3 };
24-
/* Max positional args to scan for handler ref. */
25-
enum { MAX_HANDLER_SCAN = 4 };
2624
/* Max string arg length before rejection. */
2725
enum { MAX_STRING_ARG_LEN = CBM_SZ_512 };
2826
/* Min printable ASCII (space). */
@@ -2349,8 +2347,17 @@ static const char *normalize_string_handler(CBMArena *a, const char *raw) {
23492347
}
23502348

23512349
static const char *extract_handler_arg(CBMExtractCtx *ctx, TSNode args) {
2350+
/* The LAST eligible argument wins, and every argument is examined.
2351+
* Express, Fastify, gin and Laravel all put middleware between the route
2352+
* path and the handler, so the first function-shaped argument is usually a
2353+
* middleware. Taking the first one pointed the HANDLES edge at the
2354+
* middleware; stopping the scan early missed the handler outright when the
2355+
* middleware was written inline, because an arrow function matches none of
2356+
* the kinds below. Nothing that follows a handler matches them either — an
2357+
* options argument is an object node — so the last match is the handler. */
2358+
const char *handler = NULL;
23522359
uint32_t nc = ts_node_named_child_count(args);
2353-
for (uint32_t ai = HANDLER_START_IDX; ai < nc && ai < MAX_HANDLER_SCAN; ai++) {
2360+
for (uint32_t ai = HANDLER_START_IDX; ai < nc; ai++) {
23542361
TSNode arg2 = ts_node_named_child(args, ai);
23552362
/* PHP wraps each argument in an `argument` node — unwrap to the value. */
23562363
if (strcmp(ts_node_type(arg2), "argument") == 0 && ts_node_named_child_count(arg2) > 0) {
@@ -2362,17 +2369,18 @@ static const char *extract_handler_arg(CBMExtractCtx *ctx, TSNode args) {
23622369
if (strcmp(ak2, "identifier") == 0 || strcmp(ak2, "member_expression") == 0 ||
23632370
strcmp(ak2, "selector_expression") == 0 || strcmp(ak2, "attribute") == 0 ||
23642371
strcmp(ak2, "field_expression") == 0 || strcmp(ak2, "name") == 0) {
2365-
return cbm_node_text(ctx->arena, arg2, ctx->source);
2372+
handler = cbm_node_text(ctx->arena, arg2, ctx->source);
2373+
continue;
23662374
}
23672375
if (is_string_like(ak2)) {
23682376
const char *h =
23692377
normalize_string_handler(ctx->arena, cbm_node_text(ctx->arena, arg2, ctx->source));
23702378
if (h && h[0]) {
2371-
return h;
2379+
handler = h;
23722380
}
23732381
}
23742382
}
2375-
return NULL;
2383+
return handler;
23762384
}
23772385

23782386
// Extract JSX component refs (uppercase tags) as CALLS edges.

tests/test_extraction.c

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3970,6 +3970,49 @@ TEST(extract_ts_url_builder_issue1009) {
39703970
PASS();
39713971
}
39723972

3973+
/* A route registration names its middleware before its handler, and every
3974+
* framework here puts the handler last. The handler scan took the FIRST
3975+
* argument that looked like a function reference, so a named middleware won
3976+
* and the HANDLES edge pointed at the middleware instead of the handler. */
3977+
TEST(extract_ts_route_handler_after_named_middleware) {
3978+
CBMFileResult *r = extract("function requireAuth(req: any, res: any, next: any) { next(); }\n"
3979+
"function rateLimit(req: any, res: any, next: any) { next(); }\n"
3980+
"function listUsers(req: any, res: any) { res.json([]); }\n"
3981+
"routerGet(\"/users\", requireAuth, rateLimit, listUsers);\n",
3982+
CBM_LANG_TYPESCRIPT, "t", "routes.ts");
3983+
ASSERT_NOT_NULL(r);
3984+
const CBMCall *c = find_call_by_callee(r, "routerGet");
3985+
ASSERT_NOT_NULL(c);
3986+
ASSERT_NOT_NULL(c->first_string_arg);
3987+
ASSERT_STR_EQ(c->first_string_arg, "/users");
3988+
ASSERT_NOT_NULL(c->second_arg_name);
3989+
ASSERT_STR_EQ(c->second_arg_name, "listUsers");
3990+
cbm_free_result(r);
3991+
PASS();
3992+
}
3993+
3994+
/* The same route with its middleware written inline. An arrow function is not
3995+
* one of the kinds the handler scan accepts, so three of them pushed the real
3996+
* handler past the scan bound and no handler came back at all. */
3997+
TEST(extract_ts_route_handler_after_inline_middleware) {
3998+
CBMFileResult *r = extract("function listOrders(req: any, res: any) { res.json([]); }\n"
3999+
"routerGet(\"/orders\",\n"
4000+
" (req: any, res: any, next: any) => { next(); },\n"
4001+
" (req: any, res: any, next: any) => { next(); },\n"
4002+
" (req: any, res: any, next: any) => { next(); },\n"
4003+
" listOrders);\n",
4004+
CBM_LANG_TYPESCRIPT, "t", "orders.ts");
4005+
ASSERT_NOT_NULL(r);
4006+
const CBMCall *c = find_call_by_callee(r, "routerGet");
4007+
ASSERT_NOT_NULL(c);
4008+
ASSERT_NOT_NULL(c->first_string_arg);
4009+
ASSERT_STR_EQ(c->first_string_arg, "/orders");
4010+
ASSERT_NOT_NULL(c->second_arg_name);
4011+
ASSERT_STR_EQ(c->second_arg_name, "listOrders");
4012+
cbm_free_result(r);
4013+
PASS();
4014+
}
4015+
39734016
/* Issue #1009 (composed builders): a builder whose template inlines an earlier
39744017
* builder's call plus a query string: `return \`${basePath(id)}?${params}\``.
39754018
* The known-substitution is inlined and the query string is truncated, so the
@@ -6812,6 +6855,8 @@ SUITE(extraction) {
68126855
RUN_TEST(extract_go_binary_concat_url_issue1249);
68136856
RUN_TEST(extract_go_binary_concat_url_no_literal_suffix_issue1249);
68146857
RUN_TEST(extract_ts_url_builder_issue1009);
6858+
RUN_TEST(extract_ts_route_handler_after_named_middleware);
6859+
RUN_TEST(extract_ts_route_handler_after_inline_middleware);
68156860
RUN_TEST(extract_ts_url_builder_composed_issue1009);
68166861
RUN_TEST(extract_c_url_builder_gated_issue1009);
68176862
RUN_TEST(extract_ts_url_builder_mixed_returns_issue1009);

0 commit comments

Comments
 (0)