Skip to content

Commit b00e1f7

Browse files
committed
AST-based route registration + remove prescan infrastructure
Route registration detection during AST extraction: callee QN matched against router frameworks (gin, chi, Express, Laravel, Ktor, etc.), handler reference captured as second_arg_name, Route + HANDLES edges created during resolution. Replaces disk-reading route discovery for non-decorator frameworks. Remove prescan infrastructure entirely (-350 net lines): - cbm_prescan_t, cbm_prescan_http_site_t, cbm_prescan_route_t, cbm_prescan_config_ref_t structs removed from pipeline_internal.h - prescan_http_sites, prescan_routes, prescan_config_refs, prescan_add_route removed from pass_parallel.c - prescan fast-paths removed from pass_httplinks.c and pass_configlink.c - prescan allocation/cleanup removed from pipeline.c All post-passes now work without prescan: - HTTP/async edges: service_patterns during resolution - Route registration: CBM_SVC_ROUTE_REG during resolution - Decorator routes: gbuf properties (no disk reads) - Config file refs: disk fallback (few files)
1 parent 80680ea commit b00e1f7

11 files changed

Lines changed: 258 additions & 608 deletions

internal/cbm/cbm.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ typedef struct {
111111
const char *callee_name; // raw callee text ("pkg.Func", "foo")
112112
const char *enclosing_func_qn; // QN of enclosing function (or module QN)
113113
const char *first_string_arg; // first string literal argument (URL, topic, key) or NULL
114+
const char *second_arg_name; // second argument identifier (handler ref) or NULL
114115
} CBMCall;
115116

116117
typedef struct {

internal/cbm/extract_calls.c

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ static void walk_calls(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec)
267267
if (callee && callee[0]) {
268268
// Skip keywords
269269
if (!cbm_is_keyword(callee, ctx->language)) {
270-
CBMCall call;
270+
CBMCall call = {0};
271271
call.callee_name = callee;
272272
call.enclosing_func_qn = cbm_enclosing_func_qn_cached(ctx, node);
273273
call.first_string_arg = NULL;
@@ -504,6 +504,26 @@ void handle_calls(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, Walk
504504
}
505505
}
506506

507+
/* Extract second argument name (handler ref for route registrations).
508+
* Pattern: router.GET("/path", handlerFunc) → second_arg_name = "handlerFunc"
509+
* Only extracted when first_string_arg looks like a path. */
510+
if (call.first_string_arg != NULL && call.first_string_arg[0] == '/' &&
511+
!ts_node_is_null(args)) {
512+
uint32_t nc2 = ts_node_named_child_count(args);
513+
for (uint32_t ai = 1; ai < nc2 && ai < 4 && !call.second_arg_name; ai++) {
514+
TSNode arg2 = ts_node_named_child(args, ai);
515+
const char *ak2 = ts_node_type(arg2);
516+
if (strcmp(ak2, "identifier") == 0) {
517+
call.second_arg_name = cbm_node_text(ctx->arena, arg2, ctx->source);
518+
} else if (strcmp(ak2, "member_expression") == 0 ||
519+
strcmp(ak2, "selector_expression") == 0 ||
520+
strcmp(ak2, "attribute") == 0 ||
521+
strcmp(ak2, "field_expression") == 0) {
522+
call.second_arg_name = cbm_node_text(ctx->arena, arg2, ctx->source);
523+
}
524+
}
525+
}
526+
507527
cbm_calls_push(&ctx->result->calls, ctx->arena, call);
508528
}
509529
}

internal/cbm/service_patterns.c

Lines changed: 132 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -282,13 +282,122 @@ static const lib_pattern_t config_libraries[] = {
282282
{NULL, CBM_SVC_NONE, NULL},
283283
};
284284

285-
/* ── HTTP method inference from function/method name suffix ───── */
285+
/* Route registration frameworks — callee resolves to one of these AND
286+
* has an HTTP method suffix → CBM_SVC_ROUTE_REG.
287+
* Distinguished from HTTP clients: "gin.GET" registers a handler,
288+
* "requests.get" makes an outbound HTTP call. */
289+
static const lib_pattern_t route_reg_libraries[] = {
290+
/* Go */
291+
{"gin-gonic/gin", CBM_SVC_ROUTE_REG, NULL},
292+
{"gin.", CBM_SVC_ROUTE_REG, NULL},
293+
{"go-chi/chi", CBM_SVC_ROUTE_REG, NULL},
294+
{"chi.", CBM_SVC_ROUTE_REG, NULL},
295+
{"gorilla/mux", CBM_SVC_ROUTE_REG, NULL},
296+
{"labstack/echo", CBM_SVC_ROUTE_REG, NULL},
297+
{"echo.", CBM_SVC_ROUTE_REG, NULL},
298+
{"gofiber/fiber", CBM_SVC_ROUTE_REG, NULL},
299+
{"fiber.", CBM_SVC_ROUTE_REG, NULL},
300+
{"net/http.ServeMux", CBM_SVC_ROUTE_REG, NULL},
301+
{"http.ServeMux", CBM_SVC_ROUTE_REG, NULL},
302+
{"httprouter", CBM_SVC_ROUTE_REG, NULL},
303+
304+
/* JavaScript / TypeScript */
305+
{"express", CBM_SVC_ROUTE_REG, NULL},
306+
{"fastify", CBM_SVC_ROUTE_REG, NULL},
307+
{"koa-router", CBM_SVC_ROUTE_REG, NULL},
308+
{"hono", CBM_SVC_ROUTE_REG, NULL},
309+
{"hapi", CBM_SVC_ROUTE_REG, NULL},
310+
311+
/* Python (non-decorator, e.g., Flask add_url_rule) */
312+
{"flask", CBM_SVC_ROUTE_REG, NULL},
313+
{"FastAPI", CBM_SVC_ROUTE_REG, NULL},
314+
{"starlette", CBM_SVC_ROUTE_REG, NULL},
315+
316+
/* PHP */
317+
{"Laravel", CBM_SVC_ROUTE_REG, NULL},
318+
{"Illuminate.Routing", CBM_SVC_ROUTE_REG, NULL},
319+
{"Symfony.Routing", CBM_SVC_ROUTE_REG, NULL},
320+
321+
/* Kotlin */
322+
{"ktor.server", CBM_SVC_ROUTE_REG, NULL},
323+
{"ktor.routing", CBM_SVC_ROUTE_REG, NULL},
324+
325+
/* Rust */
326+
{"actix-web", CBM_SVC_ROUTE_REG, NULL},
327+
{"actix_web", CBM_SVC_ROUTE_REG, NULL},
328+
{"axum", CBM_SVC_ROUTE_REG, NULL},
329+
{"rocket", CBM_SVC_ROUTE_REG, NULL},
330+
331+
/* Java */
332+
{"Spring", CBM_SVC_ROUTE_REG, NULL},
333+
{"jakarta.ws.rs", CBM_SVC_ROUTE_REG, NULL},
334+
335+
/* C# */
336+
{"Microsoft.AspNetCore", CBM_SVC_ROUTE_REG, NULL},
337+
{"MapGet", CBM_SVC_ROUTE_REG, NULL},
338+
{"MapPost", CBM_SVC_ROUTE_REG, NULL},
339+
340+
/* Ruby */
341+
{"ActionDispatch", CBM_SVC_ROUTE_REG, NULL},
342+
{"Sinatra", CBM_SVC_ROUTE_REG, NULL},
343+
344+
/* Elixir */
345+
{"Phoenix.Router", CBM_SVC_ROUTE_REG, NULL},
346+
347+
/* Scala */
348+
{"akka.http.scaladsl.server", CBM_SVC_ROUTE_REG, NULL},
349+
{"play.api.routing", CBM_SVC_ROUTE_REG, NULL},
286350

351+
{NULL, CBM_SVC_NONE, NULL},
352+
};
353+
354+
/* Method suffix type (used by both route registration and HTTP client tables) */
287355
typedef struct {
288356
const char *suffix;
289357
const char *method;
290358
} method_suffix_t;
291359

360+
/* Route registration method suffixes — matched on callee name.
361+
* These are methods on router objects that register handlers. */
362+
static const method_suffix_t route_reg_suffixes[] = {
363+
/* HTTP method registrations */
364+
{".GET", "GET"},
365+
{".Get", "GET"},
366+
{".get", "GET"},
367+
{".POST", "POST"},
368+
{".Post", "POST"},
369+
{".post", "POST"},
370+
{".PUT", "PUT"},
371+
{".Put", "PUT"},
372+
{".put", "PUT"},
373+
{".DELETE", "DELETE"},
374+
{".Delete", "DELETE"},
375+
{".delete", "DELETE"},
376+
{".PATCH", "PATCH"},
377+
{".Patch", "PATCH"},
378+
{".patch", "PATCH"},
379+
/* Handle/HandleFunc (Go stdlib, gorilla) */
380+
{".Handle", "ANY"},
381+
{".HandleFunc", "ANY"},
382+
{".handle", "ANY"},
383+
/* Framework-specific route registration */
384+
{".Route", "ANY"},
385+
{".route", "ANY"},
386+
{"::get", "GET"},
387+
{"::post", "POST"},
388+
{"::put", "PUT"},
389+
{"::delete", "DELETE"},
390+
{"::patch", "PATCH"},
391+
/* Minimal API (C# ASP.NET) */
392+
{".MapGet", "GET"},
393+
{".MapPost", "POST"},
394+
{".MapPut", "PUT"},
395+
{".MapDelete", "DELETE"},
396+
{NULL, NULL},
397+
};
398+
399+
/* ── HTTP method inference from function/method name suffix ───── */
400+
292401
static const method_suffix_t method_suffixes[] = {
293402
{".get", "GET"}, {".Get", "GET"}, {".GET", "GET"},
294403
{".post", "POST"}, {".Post", "POST"}, {".POST", "POST"},
@@ -331,7 +440,14 @@ cbm_svc_kind_t cbm_service_pattern_match(const char *resolved_qn) {
331440
return CBM_SVC_NONE;
332441
}
333442

334-
const lib_pattern_t *p = match_qn(resolved_qn, http_libraries);
443+
/* Route registration checked first — prevents gin/echo from matching
444+
* as HTTP clients (both have .get/.post suffixes). */
445+
const lib_pattern_t *p = match_qn(resolved_qn, route_reg_libraries);
446+
if (p) {
447+
return p->kind;
448+
}
449+
450+
p = match_qn(resolved_qn, http_libraries);
335451
if (p) {
336452
return p->kind;
337453
}
@@ -363,6 +479,20 @@ const char *cbm_service_pattern_http_method(const char *callee_name) {
363479
return NULL;
364480
}
365481

482+
const char *cbm_service_pattern_route_method(const char *callee_name) {
483+
if (!callee_name) {
484+
return NULL;
485+
}
486+
size_t clen = strlen(callee_name);
487+
for (int i = 0; route_reg_suffixes[i].suffix != NULL; i++) {
488+
size_t slen = strlen(route_reg_suffixes[i].suffix);
489+
if (clen >= slen && strcmp(callee_name + clen - slen, route_reg_suffixes[i].suffix) == 0) {
490+
return route_reg_suffixes[i].method;
491+
}
492+
}
493+
return NULL;
494+
}
495+
366496
const char *cbm_service_pattern_broker(const char *resolved_qn) {
367497
if (!resolved_qn) {
368498
return NULL;

internal/cbm/service_patterns.h

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@
1313

1414
/* Edge type returned by pattern match. */
1515
typedef enum {
16-
CBM_SVC_NONE = 0, /* Not a service pattern — use normal CALLS */
17-
CBM_SVC_HTTP = 1, /* Synchronous HTTP client call */
18-
CBM_SVC_ASYNC = 2, /* Async dispatch (message broker, task queue) */
19-
CBM_SVC_CONFIG = 3, /* Config/env accessor */
16+
CBM_SVC_NONE = 0, /* Not a service pattern — use normal CALLS */
17+
CBM_SVC_HTTP = 1, /* Synchronous HTTP client call */
18+
CBM_SVC_ASYNC = 2, /* Async dispatch (message broker, task queue) */
19+
CBM_SVC_CONFIG = 3, /* Config/env accessor */
20+
CBM_SVC_ROUTE_REG = 4, /* Route registration (router.GET, app.get, Route::post) */
2021
} cbm_svc_kind_t;
2122

2223
/* Initialize the pattern lookup tables. Call once at startup. Thread-safe after init. */
@@ -32,6 +33,11 @@ cbm_svc_kind_t cbm_service_pattern_match(const char *resolved_qn);
3233
* Returns NULL if method cannot be inferred. */
3334
const char *cbm_service_pattern_http_method(const char *callee_name);
3435

36+
/* Get the HTTP method from a route registration callee name suffix
37+
* (e.g., "router.GET" → "GET", "app.post" → "POST").
38+
* Returns NULL if not a known route registration method. */
39+
const char *cbm_service_pattern_route_method(const char *callee_name);
40+
3541
/* Get the broker name for an async QN (e.g., "pubsub" from a Pub/Sub QN).
3642
* Returns NULL if not an async pattern. */
3743
const char *cbm_service_pattern_broker(const char *resolved_qn);

src/pipeline/pass_calls.c

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,46 @@ int cbm_pipeline_pass_calls(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *file
275275
/* Classify edge type by library in resolved QN */
276276
cbm_svc_kind_t svc = cbm_service_pattern_match(res.qualified_name);
277277

278+
if (svc == CBM_SVC_ROUTE_REG && call->first_string_arg != NULL &&
279+
call->first_string_arg[0] == '/') {
280+
/* Route registration: router.GET("/path", handler) → Route + HANDLES */
281+
const char *method = cbm_service_pattern_route_method(call->callee_name);
282+
char route_qn[CBM_ROUTE_QN_SIZE];
283+
snprintf(route_qn, sizeof(route_qn), "__route__%s__%s", method ? method : "ANY",
284+
call->first_string_arg);
285+
char route_props[256];
286+
snprintf(route_props, sizeof(route_props), "{\"method\":\"%s\"}",
287+
method ? method : "ANY");
288+
int64_t route_id = cbm_gbuf_upsert_node(ctx->gbuf, "Route", call->first_string_arg,
289+
route_qn, "", 0, 0, route_props);
290+
291+
char props[512];
292+
snprintf(props, sizeof(props),
293+
"{\"callee\":\"%s\",\"url_path\":\"%s\",\"via\":\"route_registration\"}",
294+
call->callee_name, call->first_string_arg);
295+
cbm_gbuf_insert_edge(ctx->gbuf, source_node->id, route_id, "CALLS", props);
296+
297+
/* Resolve handler and create HANDLES edge */
298+
if (call->second_arg_name != NULL && call->second_arg_name[0] != '\0') {
299+
cbm_resolution_t hres =
300+
cbm_registry_resolve(ctx->registry, call->second_arg_name, module_qn,
301+
imp_keys, imp_vals, imp_count);
302+
if (hres.qualified_name != NULL && hres.qualified_name[0] != '\0') {
303+
const cbm_gbuf_node_t *handler =
304+
cbm_gbuf_find_by_qn(ctx->gbuf, hres.qualified_name);
305+
if (handler != NULL) {
306+
char hprops[256];
307+
snprintf(hprops, sizeof(hprops), "{\"handler\":\"%s\"}",
308+
hres.qualified_name);
309+
cbm_gbuf_insert_edge(ctx->gbuf, handler->id, route_id, "HANDLES",
310+
hprops);
311+
}
312+
}
313+
}
314+
resolved++;
315+
continue;
316+
}
317+
278318
if (svc == CBM_SVC_HTTP || svc == CBM_SVC_ASYNC) {
279319
/* HTTP/async call — route through Route node for cross-service traversal.
280320
* Only create Route if string looks like a URL (HTTP) or topic name (async). */

0 commit comments

Comments
 (0)