Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions internal/cbm/extract_defs.c
Original file line number Diff line number Diff line change
Expand Up @@ -1186,6 +1186,119 @@ static const char *extract_route_path_from_args(CBMArena *a, TSNode args, const
return NULL;
}

// Find a keyword argument by name in an argument_list node and return its value child.
static TSNode find_drf_kwarg_in_args(CBMArena *a, TSNode args, const char *kwarg_name,
const char *source) {
uint32_t nc = ts_node_named_child_count(args);
for (uint32_t ai = 0; ai < nc; ai++) {
TSNode child = ts_node_named_child(args, ai);
if (strcmp(ts_node_type(child), "keyword_argument") != 0)
continue;
TSNode name_node = ts_node_child_by_field_name(child, TS_FIELD("name"));
if (ts_node_is_null(name_node))
continue;
char *name = cbm_node_text(a, name_node, source);
if (name && strcmp(name, kwarg_name) == 0) {
return ts_node_child_by_field_name(child, TS_FIELD("value"));
}
}
TSNode null_node = {0};
return null_node;
}

// Try to extract route from a DRF @action decorator.
// @action(detail=True, methods=["post"], url_path="approve")
// Falls back to the method name for url_path and "GET" for methods.
static bool try_drf_action_decorator(CBMArena *a, TSNode dchild, const char *source,
TSNode func_node, const char **out_path,
const char **out_method) {
TSNode fn = ts_node_child_by_field_name(dchild, TS_FIELD("function"));
if (ts_node_is_null(fn)) {
fn = ts_node_named_child(dchild, 0);
}
if (ts_node_is_null(fn)) {
return false;
}
const char *fn_type = ts_node_type(fn);
if (strcmp(fn_type, "identifier") != 0) {
return false;
}
char *fn_text = cbm_node_text(a, fn, source);
if (!fn_text || strcmp(fn_text, "action") != 0) {
return false;
}
TSNode args = find_decorator_args(dchild);
if (ts_node_is_null(args)) {
return false;
}
const char *method = NULL;
TSNode methods_val = find_drf_kwarg_in_args(a, args, "methods", source);
if (!ts_node_is_null(methods_val) && strcmp(ts_node_type(methods_val), "list") == 0) {
uint32_t mc = ts_node_named_child_count(methods_val);
for (uint32_t mi = 0; mi < mc && !method; mi++) {
TSNode item = ts_node_named_child(methods_val, mi);
if (strcmp(ts_node_type(item), "string") != 0)
continue;
char *text = cbm_node_text(a, item, source);
if (!text)
continue;
int tlen = (int)strlen(text);
if (tlen < PAIR_CHARS || (text[0] != '"' && text[0] != '\''))
continue;
char inner[CBM_SZ_16];
int ilen = tlen - PAIR_CHARS;
if (ilen <= 0 || ilen >= (int)sizeof(inner))
continue;
memcpy(inner, text + SKIP_CHAR, (size_t)ilen);
inner[ilen] = '\0';
for (int ci = 0; inner[ci]; ci++) {
if (inner[ci] >= 'a' && inner[ci] <= 'z')
inner[ci] -= 32;
}
method = cbm_arena_strdup(a, inner);
}
}
if (!method) {
method = "GET";
}
const char *segment = NULL;
TSNode url_path_val = find_drf_kwarg_in_args(a, args, "url_path", source);
if (!ts_node_is_null(url_path_val) && strcmp(ts_node_type(url_path_val), "string") == 0) {
char *text = cbm_node_text(a, url_path_val, source);
if (text) {
int tlen = (int)strlen(text);
if (tlen >= PAIR_CHARS && (text[0] == '"' || text[0] == '\'')) {
segment = cbm_arena_strndup(a, text + SKIP_CHAR, (size_t)(tlen - PAIR_CHARS));
}
}
}
if (!segment) {
TSNode name_node = func_name_node(func_node);
if (!ts_node_is_null(name_node)) {
segment = cbm_node_text(a, name_node, source);
}
}
if (!segment) {
return false;
}
// Extract detail kwarg (default True in DRF)
bool detail = true;
TSNode detail_val = find_drf_kwarg_in_args(a, args, "detail", source);
if (!ts_node_is_null(detail_val)) {
const char *dv = ts_node_type(detail_val);
if (strcmp(dv, "false") == 0) {
detail = false;
}
}
if (detail) {
*out_path = cbm_arena_sprintf(a, "/{pk}/%s", segment);
} else {
*out_path = cbm_arena_sprintf(a, "/%s", segment);
}
*out_method = method;
return true;
}

// Try to extract a route from a single decorator call node.
// Returns true if a route method was found (even with fallback path "/").
static bool try_route_from_decorator_call(CBMArena *a, TSNode dchild, const char *source,
Expand Down Expand Up @@ -1349,6 +1462,9 @@ static void extract_route_from_decorators(CBMArena *a, TSNode func_node, const c
if (try_route_from_decorator_call(a, dchild, source, out_path, out_method)) {
return;
}
if (try_drf_action_decorator(a, dchild, source, func_node, out_path, out_method)) {
return;
}
}
/* JVM/C# annotation-form route mapping (Spring @GetMapping etc.) — the
* prev-sibling itself may be the annotation node. */
Expand Down
2 changes: 1 addition & 1 deletion src/pipeline/pass_calls.c
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ static void handle_route_registration(cbm_pipeline_ctx_t *ctx, const CBMCall *ca
char esc_h[CBM_SZ_512];
cbm_json_escape(esc_h, sizeof(esc_h), hres.qualified_name);
snprintf(hprops, sizeof(hprops), "{\"handler\":\"%s\"}", esc_h);
cbm_gbuf_insert_edge(ctx->gbuf, handler->id, route_id, "HANDLES", hprops);
cbm_gbuf_insert_edge(ctx->gbuf, route_id, handler->id, "HANDLES", hprops);
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/pipeline/pass_parallel.c
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,7 @@ static void insert_def_into_gbuf(extract_worker_state_t *ws, const cbm_file_info
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_gbuf_insert_edge(ws->local_gbuf, route_id, func_id, "HANDLES", hprops);
}
}

Expand Down Expand Up @@ -1321,7 +1321,7 @@ static void emit_route_registration(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *sou
char esc_h2[CBM_SZ_512];
cbm_json_escape(esc_h2, sizeof(esc_h2), hres.qualified_name);
snprintf(hp, sizeof(hp), "{\"handler\":\"%s\"}", esc_h2);
cbm_gbuf_insert_edge(gbuf, h->id, rid, "HANDLES", hp);
cbm_gbuf_insert_edge(gbuf, rid, h->id, "HANDLES", hp);
}
}
}
Expand Down
24 changes: 12 additions & 12 deletions src/pipeline/pass_route_nodes.c
Original file line number Diff line number Diff line change
Expand Up @@ -355,10 +355,10 @@ static int match_one_infra_route(cbm_gbuf_t *gb, const cbm_gbuf_node_t *infra,
if (path_match || root_svc_match) {
const cbm_gbuf_edge_t **fn_handles = NULL;
int fn_hcount = 0;
cbm_gbuf_find_edges_by_target_type(gb, handler_route->id, "HANDLES", &fn_handles,
cbm_gbuf_find_edges_by_source_type(gb, handler_route->id, "HANDLES", &fn_handles,
&fn_hcount);
for (int fh = 0; fh < fn_hcount; fh++) {
cbm_gbuf_insert_edge(gb, fn_handles[fh]->source_id, infra->id, "HANDLES",
cbm_gbuf_insert_edge(gb, infra->id, fn_handles[fh]->target_id, "HANDLES",
"{\"source\":\"infra_match\"}");
}
return SKIP_ONE;
Expand Down Expand Up @@ -464,9 +464,9 @@ static int ensure_one_decorator_route(cbm_gbuf_t *gb, const cbm_gbuf_node_t *fun
if (existing) {
const cbm_gbuf_edge_t **existing_handles = NULL;
int eh_count = 0;
cbm_gbuf_find_edges_by_target_type(gb, route_id, "HANDLES", &existing_handles, &eh_count);
cbm_gbuf_find_edges_by_source_type(gb, route_id, "HANDLES", &existing_handles, &eh_count);
for (int eh = 0; eh < eh_count; eh++) {
if (existing_handles[eh]->source_id == func->id) {
if (existing_handles[eh]->target_id == func->id) {
return 0;
}
}
Expand All @@ -475,7 +475,7 @@ static int ensure_one_decorator_route(cbm_gbuf_t *gb, const cbm_gbuf_node_t *fun
char hprops[CBM_SZ_512];
snprintf(hprops, sizeof(hprops), "{\"handler\":\"%s\"}",
func->qualified_name ? func->qualified_name : "");
cbm_gbuf_insert_edge(gb, func->id, route_id, "HANDLES", hprops);
cbm_gbuf_insert_edge(gb, route_id, func->id, "HANDLES", hprops);
return SKIP_ONE;
}

Expand Down Expand Up @@ -530,7 +530,7 @@ static int bridge_funcs_to_prefix(cbm_gbuf_t *gb, const cbm_gbuf_node_t *prefix_
if (prefix_segs && prefix_segs[0] && !strstr(func->file_path, prefix_segs)) {
continue;
}
cbm_gbuf_insert_edge(gb, func->id, prefix_route->id, "HANDLES",
cbm_gbuf_insert_edge(gb, prefix_route->id, func->id, "HANDLES",
"{\"source\":\"prefix_decorator_bridge\"}");
connected++;
}
Expand Down Expand Up @@ -749,10 +749,10 @@ static int collect_infra_handlers(cbm_gbuf_t *gb, int64_t route_id, int64_t *out
for (int ie = 0; ie < infra_count; ie++) {
const cbm_gbuf_edge_t **ep_handles = NULL;
int ep_hcount = 0;
cbm_gbuf_find_edges_by_target_type(gb, infra_edges[ie]->target_id, "HANDLES", &ep_handles,
cbm_gbuf_find_edges_by_source_type(gb, infra_edges[ie]->target_id, "HANDLES", &ep_handles,
&ep_hcount);
for (int eh = 0; eh < ep_hcount && n < max_out; eh++) {
out[n++] = ep_handles[eh]->source_id;
out[n++] = ep_handles[eh]->target_id;
}
}
return n;
Expand Down Expand Up @@ -792,14 +792,14 @@ static void create_route_data_flows(cbm_gbuf_t *gb, const cbm_gbuf_node_t *route
int *skipped) {
const cbm_gbuf_edge_t **handles_edges = NULL;
int handles_count = 0;
cbm_gbuf_find_edges_by_target_type(gb, route->id, "HANDLES", &handles_edges, &handles_count);
cbm_gbuf_find_edges_by_source_type(gb, route->id, "HANDLES", &handles_edges, &handles_count);

int64_t extra_handlers[CBM_SZ_32];
int n_extra = collect_infra_handlers(gb, route->id, extra_handlers, CBM_SZ_32);

for (int ci = 0; ci < n_callers; ci++) {
for (int hi = 0; hi < handles_count; hi++) {
int rc = try_create_data_flow(gb, callers[ci].source_id, handles_edges[hi]->source_id,
int rc = try_create_data_flow(gb, callers[ci].source_id, handles_edges[hi]->target_id,
route, callers[ci].edge_type, callers[ci].props, false);
if (rc > 0) {
(*flows)++;
Expand Down Expand Up @@ -877,7 +877,7 @@ static void create_grpc_routes(cbm_gbuf_t *gb) {

int64_t route_id = cbm_gbuf_upsert_node(gb, "Route", fn->name, route_qn, fn->file_path,
fn->start_line, fn->end_line, props);
cbm_gbuf_insert_edge(gb, fn->id, route_id, "HANDLES", "{\"via\":\"proto_rpc\"}");
cbm_gbuf_insert_edge(gb, route_id, fn->id, "HANDLES", "{\"via\":\"proto_rpc\"}");
grpc_routes++;
}
if (grpc_routes > 0) {
Expand Down Expand Up @@ -1166,7 +1166,7 @@ static void sveltekit_file_visitor(const cbm_gbuf_node_t *node, void *userdata)
snprintf(hprops, sizeof(hprops), "{\"handler\":\"%s\",\"framework\":\"sveltekit\"%s}",
child->qualified_name ? child->qualified_name : child->name,
is_actions ? ",\"via\":\"actions_object\"" : "");
cbm_gbuf_insert_edge(ctx->gb, child->id, route_id, "HANDLES", hprops);
cbm_gbuf_insert_edge(ctx->gb, route_id, child->id, "HANDLES", hprops);
ctx->handles_created++;
}
}
Expand Down
15 changes: 15 additions & 0 deletions tests/test_edge_types_probe.c
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,20 @@ TEST(handles_fastapi_python) {
PASS();
}

/* DRF action (Python) — @action decorator on ViewSet methods */
TEST(handles_drf_action_python) {
static const EtFile f[] = {
{"viewsets.py",
"from rest_framework.decorators import action\n"
"from rest_framework.viewsets import ViewSet\n\n"
"class CustomerTaskViewSet(ViewSet):\n"
" @action(detail=True, methods=[\"post\"])\n"
" def approve_draft_with_charge(self, request, pk=None):\n"
" pass\n"}};
ASSERT_TRUE(et_edge_present(f, 1, "HANDLES", 1));
PASS();
}

/* Express (JS/TS) — route registration must resolve to a callee QN containing
* the "express" library substring AND pass the handler as an identifier (not an
* inline-object method, which is never registered as a resolvable node). We use
Expand Down Expand Up @@ -1371,6 +1385,7 @@ SUITE(edge_types_probe) {
/* HANDLES — route→handler across web frameworks (8 frameworks) */
RUN_TEST(handles_flask_python);
RUN_TEST(handles_fastapi_python);
RUN_TEST(handles_drf_action_python);
RUN_TEST(handles_express_ts);
RUN_TEST(handles_fastify_js);
RUN_TEST(handles_gin_go);
Expand Down
Loading