Merge pull request #2254 from DeusData/distill/1245-patch-decorator-route

fix(extract): a decorator with arguments but no path is not a route
This commit is contained in:
Martin Vogel
2026-10-07 18:23:39 +02:00
committed by GitHub
2 changed files with 81 additions and 1 deletions
+8 -1
View File
@@ -2987,7 +2987,11 @@ static bool try_drf_action_decorator(CBMArena *a, TSNode dchild, const char *sou
}
// Try to extract a route from a single decorator call node.
// Returns true if a route method was found (even with fallback path "/").
// Returns true if a route method was found. The fallback path "/" applies only
// to a zero-argument call (`@app.route()`): a call that HAS arguments but none
// path-shaped is not a route -- unittest.mock's `@patch("subprocess.run")`
// shares its name with the HTTP verb and used to mint a PATCH "/" handler for
// every mocked test function (distilled from PR #1245).
static bool try_route_from_decorator_call(CBMArena *a, TSNode dchild, const char *source,
const char **out_path, const char **out_method) {
TSNode fn = ts_node_child_by_field_name(dchild, TS_FIELD("function"));
@@ -3012,6 +3016,9 @@ static bool try_route_from_decorator_call(CBMArena *a, TSNode dchild, const char
*out_method = method;
return true;
}
if (ts_node_named_child_count(args) > 0) {
return false;
}
}
*out_path = "/";
*out_method = method;
+73
View File
@@ -4715,6 +4715,77 @@ TEST(extract_java_method_annotations_issue382) {
PASS();
}
/* Distilled from PR #1245 (Andrew Hundt): unittest.mock's `@patch("x")` is a
* bare decorator whose name collides with the HTTP verb, and its only argument
* is a dotted target, not a path. The "/" fallback then minted a Route handler
* PATCH "/" for every mocked test function. Rule: a decorator call WITH
* arguments but no path-shaped one is not a route. */
TEST(extract_python_mock_patch_is_not_route) {
CBMFileResult *r = extract("from unittest.mock import patch\n\n"
"@patch(\"subprocess.run\")\n"
"def test_cmd(mock_run):\n"
" pass\n\n"
"@app.patch(\"/items/{id}\")\n"
"def update_item():\n"
" pass\n",
CBM_LANG_PYTHON, "t", "test_routes.py");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
const CBMDefinition *mocked = find_def_by_name(r, "test_cmd");
ASSERT_NOT_NULL(mocked);
ASSERT_NULL(mocked->route_path);
ASSERT_NULL(mocked->route_method);
const CBMDefinition *route = find_def_by_name(r, "update_item");
ASSERT_NOT_NULL(route);
ASSERT_STR_EQ(route->route_path, "/items/{id}");
ASSERT_STR_EQ(route->route_method, "PATCH");
cbm_free_result(r);
PASS();
}
/* Companion to the mock-patch case: the narrowing must not cost real routes.
* A receiver-less framework decorator with a path-shaped argument (Litestar /
* BlackSheep `@get("/x")`) stays a route, `@mock.patch("x")` (receiver, but
* no path) is not one, and a zero-argument `@app.route()` keeps the "/"
* default main emits today. */
TEST(extract_python_bare_decorator_route_rules) {
CBMFileResult *r = extract("from litestar import get\n"
"from unittest import mock\n\n"
"@get(\"/health\")\n"
"def health():\n"
" pass\n\n"
"@mock.patch(\"os.getcwd\")\n"
"def test_cwd(mock_cwd):\n"
" pass\n\n"
"@app.route()\n"
"def index():\n"
" pass\n",
CBM_LANG_PYTHON, "t", "routes.py");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
const CBMDefinition *health = find_def_by_name(r, "health");
ASSERT_NOT_NULL(health);
ASSERT_STR_EQ(health->route_path, "/health");
ASSERT_STR_EQ(health->route_method, "GET");
const CBMDefinition *mocked = find_def_by_name(r, "test_cwd");
ASSERT_NOT_NULL(mocked);
ASSERT_NULL(mocked->route_path);
ASSERT_NULL(mocked->route_method);
const CBMDefinition *index = find_def_by_name(r, "index");
ASSERT_NOT_NULL(index);
ASSERT_STR_EQ(index->route_path, "/");
ASSERT_STR_EQ(index->route_method, "ANY");
cbm_free_result(r);
PASS();
}
/* ── ArkTS (HarmonyOS .ets) ─────────────────────────────────────── */
TEST(arkts_component_struct) {
@@ -9829,6 +9900,8 @@ SUITE(extraction) {
RUN_TEST(js_index_module_qn_not_collide_with_folder);
RUN_TEST(python_regular_module_qn_unchanged);
RUN_TEST(extract_java_method_annotations_issue382);
RUN_TEST(extract_python_mock_patch_is_not_route);
RUN_TEST(extract_python_bare_decorator_route_rules);
RUN_TEST(arkts_component_struct);
RUN_TEST(arkts_exported_struct_decorators);
RUN_TEST(arkts_member_decorators);