Skip to content

Commit 70bc64f

Browse files
author
Your Name
committed
fix(routes): deduplicate route nodes - eliminate ghost nodes and real duplicates
Route extraction produced 3x duplicates for every JS/TS route: - 2 real copies (express_routes + hapi_routes both matching same pattern) - 1 ghost copy (module-level extraction with empty qualified_name) Root causes in pass_parallel.c prescan_routes(): 1. Function-level: cbm_extract_express_routes and cbm_extract_hapi_routes both extract the same Express-style routes from the same function body. 2. Module-level: cbm_extract_express_routes/hapi_routes called with "" as qualified_name, producing ghost nodes with empty file_path and QN starting with a leading dot (.route.GET.path). Fix: deduplicate routes by (method, path) after Phase 1 collection, before prefix resolution. When duplicates exist, prefer the entry with a non-empty qualified_name (function-level entry wins over module-level ghost). Verified: 3:1 dedup ratio. 0 ghosts, 0 duplicates after fix. All routes have correct file_path and qualified_name.
1 parent 1ee8771 commit 70bc64f

File tree

1 file changed

+39
-0
lines changed

1 file changed

+39
-0
lines changed

src/pipeline/pass_httplinks.c

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1402,6 +1402,45 @@ int cbm_pipeline_pass_httplinks(cbm_pipeline_ctx_t *ctx) {
14021402

14031403
cbm_log_info("httplink.routes", "count", itoa_hl(route_count));
14041404

1405+
/* ── Phase 1b: Deduplicate routes by (method, path) ──────── */
1406+
/* Three sources of route duplication:
1407+
* 1. Module-level extraction (empty QN) re-discovers routes already found
1408+
* at function level (non-empty QN) for the same (method, path).
1409+
* 2. Both extract_express_routes and extract_hapi_routes may match the
1410+
* same route patterns in the same function body.
1411+
* Strategy: for each (method, path) group, keep the entry with the best
1412+
* qualified_name (non-empty wins over empty; longer wins over shorter). */
1413+
{
1414+
int deduped = 0;
1415+
for (int i = 0; i < route_count; i++) {
1416+
cbm_route_handler_t *a = &routes[i];
1417+
/* Check if a better or equal entry already exists */
1418+
bool dominated = false;
1419+
for (int j = 0; j < deduped; j++) {
1420+
cbm_route_handler_t *b = &routes[j];
1421+
if (strcmp(a->method, b->method) == 0 &&
1422+
strcmp(a->path, b->path) == 0) {
1423+
/* Same route — keep whichever has a better QN */
1424+
if (a->qualified_name[0] && !b->qualified_name[0]) {
1425+
/* a is better — replace b */
1426+
*b = *a;
1427+
}
1428+
/* else b is better or equal — drop a */
1429+
dominated = true;
1430+
break;
1431+
}
1432+
}
1433+
if (!dominated) {
1434+
routes[deduped++] = *a;
1435+
}
1436+
}
1437+
if (deduped < route_count) {
1438+
cbm_log_info("httplink.dedup", "before", itoa_hl(route_count),
1439+
"after", itoa_hl(deduped));
1440+
route_count = deduped;
1441+
}
1442+
}
1443+
14051444
/* ── Phase 2: Resolve cross-file prefixes (serial) ────────── */
14061445
resolve_cross_file_group_prefixes(ctx, routes, route_count);
14071446
resolve_fastapi_prefixes(ctx, routes, route_count);

0 commit comments

Comments
 (0)