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
9 changes: 8 additions & 1 deletion internal/cbm/extract_defs.c
Original file line number Diff line number Diff line change
Expand Up @@ -1591,7 +1591,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"));
Expand All @@ -1616,6 +1620,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;
Expand Down
73 changes: 73 additions & 0 deletions tests/test_extraction.c
Original file line number Diff line number Diff line change
Expand Up @@ -4001,6 +4001,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) {
Expand Down Expand Up @@ -7926,6 +7997,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);
Expand Down
Loading