mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-27 06:53:11 +08:00
feat(extract): add Route+HANDLES for Django REST Framework @action methods
Django REST Framework ViewSets expose extra endpoints through @action-decorated methods, e.g. @action(detail=True, methods=["post"], url_path="approve"). On main these methods carried no route_path, so Phase 2a created no Route node and no HANDLES edge for them -- the endpoint was invisible in the graph (#603). Add try_drf_action_decorator (internal/cbm/extract_defs.c): when a method's decorator identifier is "action", read the methods / url_path / detail kwargs and set *out_path/*out_method. The existing decorator-route pipeline (ensure_one_decorator_route, Phase 2a) then creates the Route + handler->Route HANDLES edge in the established direction. Fallbacks: method name for url_path, "GET" for methods; detail=True -> /{pk}/<seg>, else /<seg>. Reproduce-first guard: handles_drf_action_python in the edge_types_probe suite -- a DRF @action method must produce >=1 HANDLES edge (RED with 0 edges before the extractor, GREEN after). Known limitation: multi-method actions (methods=["get","post"]) capture only the first method -> a single Route. Distilled from #647 (thanks @muskiteer). Drops that PR's unrelated project-wide HANDLES edge-direction reversal, which was unnecessary for the fix and broke the handler->Route convention relied on by pass_cross_repo.c's find_route_handler. Closes #603 Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> Co-Authored-By: muskiteer <agarwal.vansh3110@gmail.com>
This commit is contained in:
co-authored by
muskiteer
parent
2d48c06042
commit
024c0c5f76
@@ -1353,6 +1353,124 @@ 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 a route from a Django REST Framework @action decorator on a
|
||||
// ViewSet method:
|
||||
// @action(detail=True, methods=["post"], url_path="approve")
|
||||
// Sets *out_path/*out_method so the downstream Route+HANDLES pipeline (Phase 2a
|
||||
// ensure_one_decorator_route) emits the handler->Route edge in the normal
|
||||
// direction. Falls back to the method name for url_path and "GET" for methods.
|
||||
// Known limitation: multi-method actions (methods=["get","post"]) capture only
|
||||
// the first method -> a single Route rather than one per method.
|
||||
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,
|
||||
@@ -1516,6 +1634,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. */
|
||||
|
||||
@@ -198,6 +198,23 @@ TEST(handles_fastapi_python) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* DRF @action (Python) — issue #603. Before the try_drf_action_decorator
|
||||
* extractor, an @action-decorated ViewSet method carried no route_path, so
|
||||
* Phase 2a emitted 0 Route/HANDLES edges (this asserts >=1 → RED without the
|
||||
* fix, GREEN with it). Direction-agnostic count via cbm_store_count_edges_by_type. */
|
||||
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 +1388,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);
|
||||
|
||||
Reference in New Issue
Block a user