From c36b4fbc446f9085704a6073b81ec743fd4180fc Mon Sep 17 00:00:00 2001 From: Joshua Richter Date: Tue, 1 Sep 2026 22:30:54 -0400 Subject: [PATCH] fix(routes): find a route path wherever it sits in a decorator's arguments Java and Kotlin put no order on annotation attributes, so `path` can appear after `method`, `produces` and `consumes`: @RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE, path = "/orders") extract_route_path_from_args stopped after the third argument, so the path was never read and no Route node formed. The endpoint was in the source and not in the graph. Four call sites read this function, so the miss covered plain decorator arguments, Spring mappings, and both JAX-RS paths. The loop now checks every argument. That matches find_drf_kwarg_in_args a few lines below, which walks the same kind of node with no cap. Cost stays small: the loop returns on the first path-shaped string, and each argument's own subtree walk is still bounded by find_route_path_literal. The recursive breadth guard inside find_route_path_literal keeps DECORATOR_SCAN_LIMIT. That loop descends to CBM_DESCENDANT_MAX_DEPTH, so breadth 3 by depth 6 is 729 visits at worst and unbounded breadth there is not bounded at all. The test fails without the change, reporting the class-level "/api" route present and "/api/orders" missing. Signed-off-by: Joshua Richter --- internal/cbm/extract_defs.c | 6 +++++- tests/test_edge_types_probe.c | 25 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index 2e359af67..2de509833 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -1450,8 +1450,12 @@ static const char *find_route_path_literal(CBMArena *a, TSNode node, const char // Extract route path from decorator arguments (first string that starts with /). static const char *extract_route_path_from_args(CBMArena *a, TSNode args, const char *source) { + /* Every argument is checked. Java and Kotlin put no order on annotation + * attributes, so `path` can sit anywhere in the list. Stopping early left + * a real route unread and formed no Route node. Each argument's own + * subtree walk stays bounded by find_route_path_literal below. */ uint32_t nc = ts_node_named_child_count(args); - for (uint32_t ai = 0; ai < nc && ai < DECORATOR_SCAN_LIMIT; ai++) { + for (uint32_t ai = 0; ai < nc; ai++) { TSNode arg = ts_node_named_child(args, ai); /* Spring/Kotlin frequently uses named or array-valued annotation args: * @RequestMapping(value = ["/internal/v1"]) diff --git a/tests/test_edge_types_probe.c b/tests/test_edge_types_probe.c index f71091ae4..84d7713ee 100644 --- a/tests/test_edge_types_probe.c +++ b/tests/test_edge_types_probe.c @@ -416,6 +416,30 @@ TEST(handles_spring_java) { PASS(); } +/* Spring (Java) — the path attribute may sit anywhere in the annotation. + * Java puts no order on annotation attributes, so `path` after `name`, + * `produces` and `consumes` is ordinary source. The argument scan stopped + * after the third attribute, so the path was never read and no Route node + * formed. A HANDLES count alone cannot catch that, because the class-level + * @RequestMapping still produces one route on its own. */ +TEST(handles_spring_java_path_attribute_fourth) { + static const char *routes[] = {"/api/orders", NULL}; + static const EtFile f[] = { + {"OrderController.java", + "package com.example;\n\n" + "import org.springframework.web.bind.annotation.RequestMapping;\n" + "import org.springframework.web.bind.annotation.GetMapping;\n\n" + "@RequestMapping(\"/api\")\npublic class OrderController {\n" + " @GetMapping(name = \"listOrders\",\n" + " produces = \"application/json\",\n" + " consumes = \"application/json\",\n" + " path = \"/orders\")\n" + " public String listOrders() {\n" + " return \"orders\";\n }\n}\n"}}; + ASSERT_TRUE(et_routes_exact(f, 1, routes)); + PASS(); +} + /* Spring (Kotlin) — same prefix contract, including Kotlin's named array form * for class-level RequestMapping values. */ TEST(handles_spring_kotlin) { @@ -1578,6 +1602,7 @@ SUITE(edge_types_probe) { RUN_TEST(handles_fastify_js); RUN_TEST(handles_gin_go); RUN_TEST(handles_spring_java); + RUN_TEST(handles_spring_java_path_attribute_fourth); RUN_TEST(handles_spring_kotlin); RUN_TEST(handles_jaxrs_java); RUN_TEST(handles_aspnet_csharp);