diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index 7e7fd5fd..38a6af4c 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -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, @@ -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. */ diff --git a/src/pipeline/pass_calls.c b/src/pipeline/pass_calls.c index 4f4d7b54..9ce0e2d2 100644 --- a/src/pipeline/pass_calls.c +++ b/src/pipeline/pass_calls.c @@ -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); } } } diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index 0471cbe0..431b045c 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -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); } } @@ -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); } } } diff --git a/src/pipeline/pass_route_nodes.c b/src/pipeline/pass_route_nodes.c index 664c8252..a27f0911 100644 --- a/src/pipeline/pass_route_nodes.c +++ b/src/pipeline/pass_route_nodes.c @@ -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; @@ -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; } } @@ -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; } @@ -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++; } @@ -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; @@ -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)++; @@ -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) { @@ -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++; } } diff --git a/tests/test_edge_types_probe.c b/tests/test_edge_types_probe.c index f6967943..8c82d6cf 100644 --- a/tests/test_edge_types_probe.c +++ b/tests/test_edge_types_probe.c @@ -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 @@ -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);