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 <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel
2026-10-18 16:42:21 +02:00
parent 753df46b78
commit 2ecf202b3c
5 changed files with 65 additions and 17 deletions
+5
View File
@@ -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
+1 -17
View File
@@ -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) {
+21
View File
@@ -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. */
+9
View File
@@ -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:
+29
View File
@@ -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);