From 2ecf202b3ca166ce7c8e727aaef67043971ae31b Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Sun, 18 Oct 2026 16:42:21 +0200 Subject: [PATCH] fix(routes): keep every overload's decorator route on both resolvers (#1692) An ASP.NET MVC GET form action and its POST submit share one method name, so both overloads collapse into one Method node whose merged properties keep only one route. The parallel resolver emitted each definition's route at node creation and kept both; the sequential one rebuilt decorator routes from the merged properties afterwards and kept only the last. On six ASP.NET corpora the sequential run lost 28 HANDLES edges this way. Both resolvers now call cbm_pipeline_emit_def_route() per definition at node creation (the parallel block moved into it unchanged), so the venues agree. The later decorator-route pass still skips handlers it already has. Signed-off-by: Martin Vogel --- src/pipeline/pass_definitions.c | 5 +++++ src/pipeline/pass_parallel.c | 18 +----------------- src/pipeline/pass_route_nodes.c | 21 +++++++++++++++++++++ src/pipeline/pipeline_internal.h | 9 +++++++++ tests/test_edge_types_probe.c | 29 +++++++++++++++++++++++++++++ 5 files changed, 65 insertions(+), 17 deletions(-) diff --git a/src/pipeline/pass_definitions.c b/src/pipeline/pass_definitions.c index 0f95c7989..6088a5c4b 100644 --- a/src/pipeline/pass_definitions.c +++ b/src/pipeline/pass_definitions.c @@ -452,6 +452,11 @@ static void process_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *def, const if (props != stack) { cbm_free(CBM_MEM_CLASS_GBUF_STRING, props); } + /* Per definition, like pass_parallel.c: overloads sharing this QN each keep + * their own route (the merged node properties hold only one of them). */ + if (node_id > 0) { + cbm_pipeline_emit_def_route(ctx->gbuf, node_id, def, def_rel); + } /* Registry membership is defined ONCE by cbm_label_is_registry_symbol * (helpers.c): callables + type-like containers (INHERITS/IMPLEMENTS/method/ * field resolution), Variable/Field (READS/WRITES resolution), and Table/View diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index 2c0a9023a..9da3f5999 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -779,23 +779,7 @@ static void insert_def_into_gbuf(extract_worker_state_t *ws, const cbm_stem_mark cbm_free(CBM_MEM_CLASS_GBUF_STRING, props); } ws->nodes_created++; - if (def->route_path && def->route_path[0] != '\0') { - const char *rm = def->route_method ? def->route_method : "ANY"; - char route_qn[CBM_ROUTE_QN_SIZE]; - char cpath[CBM_SZ_256]; - snprintf(route_qn, sizeof(route_qn), "__route__%s__%s", rm, - cbm_route_canon_path(def->route_path, cpath, sizeof(cpath))); - char rprops[CBM_SZ_256]; - snprintf(rprops, sizeof(rprops), "{\"method\":\"%s\",\"source\":\"decorator\"}", rm); - int64_t route_id = - cbm_gbuf_upsert_node(ws->local_gbuf, "Route", def->route_path, route_qn, - def->file_path ? def->file_path : fi->rel_path, 0, 0, rprops); - char hprops[CBM_SZ_512]; - char esc_h[CBM_SZ_512]; - cbm_json_escape(esc_h, sizeof(esc_h), def->qualified_name); - snprintf(hprops, sizeof(hprops), "{\"handler\":\"%s\"}", esc_h); - cbm_gbuf_insert_edge(ws->local_gbuf, func_id, route_id, "HANDLES", hprops); - } + cbm_pipeline_emit_def_route(ws->local_gbuf, func_id, def, fi->rel_path); } static void log_extract_fail(int pos, uint64_t ms, const char *path) { diff --git a/src/pipeline/pass_route_nodes.c b/src/pipeline/pass_route_nodes.c index 9f0c57ddb..c8d99e7a0 100644 --- a/src/pipeline/pass_route_nodes.c +++ b/src/pipeline/pass_route_nodes.c @@ -395,6 +395,27 @@ static void match_infra_routes(cbm_gbuf_t *gb) { } } +void cbm_pipeline_emit_def_route(cbm_gbuf_t *gb, int64_t func_id, const CBMDefinition *def, + const char *rel) { + if (!def->route_path || def->route_path[0] == '\0') { + return; + } + const char *rm = def->route_method ? def->route_method : "ANY"; + char route_qn[CBM_ROUTE_QN_SIZE]; + char cpath[CBM_SZ_256]; + snprintf(route_qn, sizeof(route_qn), "__route__%s__%s", rm, + cbm_route_canon_path(def->route_path, cpath, sizeof(cpath))); + char rprops[CBM_SZ_256]; + snprintf(rprops, sizeof(rprops), "{\"method\":\"%s\",\"source\":\"decorator\"}", rm); + int64_t route_id = cbm_gbuf_upsert_node(gb, "Route", def->route_path, route_qn, + def->file_path ? def->file_path : rel, 0, 0, rprops); + char hprops[CBM_SZ_512]; + char esc_h[CBM_SZ_512]; + cbm_json_escape(esc_h, sizeof(esc_h), def->qualified_name); + snprintf(hprops, sizeof(hprops), "{\"handler\":\"%s\"}", esc_h); + cbm_gbuf_insert_edge(gb, func_id, route_id, "HANDLES", hprops); +} + /* Phase 2a: Ensure all functions with route_path properties have Route+HANDLES edges. * During incremental indexing, only changed files get Route nodes from extraction. * This pass scans ALL Function/Method nodes and creates missing Route+HANDLES. */ diff --git a/src/pipeline/pipeline_internal.h b/src/pipeline/pipeline_internal.h index fbae8044f..b79763f3c 100644 --- a/src/pipeline/pipeline_internal.h +++ b/src/pipeline/pipeline_internal.h @@ -284,6 +284,15 @@ void cbm_pxc_test_import_probe_fail_after(int successful_allocations); /* cbm_route_canon_path: cbm.h (route_path.c). */ +/* A definition's decorator route (def->route_path, e.g. an ASP.NET [HttpGet("x")] + * action): upsert its Route node and the definition -> Route HANDLES edge. Both + * resolvers call it per DEFINITION at node creation (pass_parallel.c, + * pass_definitions.c), so overloads sharing one QN -- a GET form action and its + * POST submit -- each keep their route; the merged node properties hold only one + * of them. Defined in pass_route_nodes.c. */ +void cbm_pipeline_emit_def_route(cbm_gbuf_t *gb, int64_t func_id, const CBMDefinition *def, + const char *rel); + /* A file and the directory beside it share a stem (`pkg/foo.sh` beside * `pkg/foo/`): the file's Module QN equals the directory's Folder QN, and the * graph buffer keeps only the Folder. One side yields: diff --git a/tests/test_edge_types_probe.c b/tests/test_edge_types_probe.c index 704c1a2d6..a9904001d 100644 --- a/tests/test_edge_types_probe.c +++ b/tests/test_edge_types_probe.c @@ -1208,6 +1208,34 @@ static int et_handles_exact(const EtFile *files, int nfiles, const EtHandles *wa return et_handles_exact_routes(files, nfiles, want, parallel, NULL); } +/* ASP.NET Core MVC (#1692): a GET form action and its POST submit share one + * method name, so both overloads collapse into one Method node whose merged + * properties keep only one route. The parallel resolver emitted each + * definition's route at node creation and kept both; the sequential one rebuilt + * routes from the merged properties afterwards and kept only the last (28 + * HANDLES on 6 ASP.NET corpora, 2026-10-18). Both venues keep both. */ +TEST(handles_aspnet_overload_verbs_both_venues_issue1692) { + static const EtHandles want[] = {{"AccountController.Login", "/Account/login"}, + {"AccountController.Login", "/Account/login"}, + {NULL, NULL}}; + static const char *const route_qns[] = {"__route__GET__/Account/login", + "__route__POST__/Account/login", NULL}; + static const EtFile f[] = { + {"Controllers/AccountController.cs", + "using Microsoft.AspNetCore.Mvc;\n\n" + "namespace Shop.Web.Controllers\n{\n" + " [Route(\"[controller]\")]\n" + " public class AccountController : Controller\n {\n" + " [HttpGet(\"login\")]\n" + " public IActionResult Login() { return View(); }\n\n" + " [HttpPost(\"login\")]\n" + " public IActionResult Login(LoginModel model) { return Redirect(\"/\"); }\n" + " }\n}\n"}}; + ASSERT_TRUE(et_handles_exact_routes(f, 1, want, 0, route_qns)); + ASSERT_TRUE(et_handles_exact_routes(f, 1, want, 1, route_qns)); + PASS(); +} + /* #1146: Laravel's two class-based handler forms — * Route::get('/x', [UserController::class, 'show']) (controller method) * Route::get('/x', GetCurrentUserController::class) (invokable: __invoke) @@ -2721,6 +2749,7 @@ SUITE(edge_types_probe) { RUN_TEST(handles_aspnet_route_tokens_issue1692); RUN_TEST(handles_aspnet_override_constraints_issue1692); RUN_TEST(handles_aspnet_non_controller_control_issue1692); + RUN_TEST(handles_aspnet_overload_verbs_both_venues_issue1692); RUN_TEST(handles_laravel_php); RUN_TEST(handles_laravel_facade_routes_issue952); RUN_TEST(handles_laravel_facade_no_junk_routes_issue952);