Skip to content

Commit 9ed01a4

Browse files
authored
Merge pull request #2024 from DeusData/feat/cshtml-razor-pages
feat(extract): discover Razor Pages / MVC views (.cshtml) and route their @page
2 parents 91a1723 + 380b1c5 commit 9ed01a4

4 files changed

Lines changed: 75 additions & 3 deletions

File tree

‎internal/cbm/extract_defs.c‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7908,13 +7908,23 @@ void cbm_extract_definitions_without_module(CBMExtractCtx *ctx) {
79087908

79097909
/* True when rel_path names a Blazor component file. */
79107910
static bool cbm_path_is_razor(const char *rel_path) {
7911+
/* Both Razor file types, not just components. `@page` is what DEFINES a
7912+
* Razor Page, so a .cshtml route is at least as worth extracting as a
7913+
* .razor one. Deliberately not .aspx/.ascx: Web Forms is a different
7914+
* templating syntax (`<%@ %>`, `runat="server"`) with no `@page`
7915+
* directive, so neither the C# recovery nor the scan below applies. */
79117916
if (!rel_path) {
79127917
return false;
79137918
}
7919+
static const char *const suffixes[] = {".razor", ".cshtml"};
79147920
size_t len = strlen(rel_path);
7915-
static const char suffix[] = ".razor";
7916-
size_t slen = sizeof(suffix) - 1U;
7917-
return len > slen && strcmp(rel_path + (len - slen), suffix) == 0;
7921+
for (size_t i = 0; i < sizeof(suffixes) / sizeof(suffixes[0]); i++) {
7922+
size_t slen = strlen(suffixes[i]);
7923+
if (len > slen && strcmp(rel_path + (len - slen), suffixes[i]) == 0) {
7924+
return true;
7925+
}
7926+
}
7927+
return false;
79187928
}
79197929

79207930
/* Match `@page "/route"` on ONE line; returns the route text or NULL.

‎src/discover/language.c‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ static const ext_entry_t EXT_TABLE[] = {
5555
* parse_partial, which is why this is a best-effort mapping rather
5656
* than a dedicated grammar. */
5757
{".razor", CBM_LANG_CSHARP},
58+
/* Razor Pages / MVC views. Same Razor syntax and the same C# host as
59+
* .razor, and equally unmapped before this: an ASP.NET Core app's views
60+
* produced no nodes at all. The `@page` directive that defines a Razor
61+
* Page lives in this file type, so the route extraction below matters
62+
* more here than it does for components. Best-effort on the same terms:
63+
* the C# grammar recovers the @{ } / @functions blocks, the surrounding
64+
* markup lands in ERROR regions and is reported via parse_partial. */
65+
{".cshtml", CBM_LANG_CSHARP},
5866

5967
/* Clojure */
6068
{".clj", CBM_LANG_CLOJURE},

‎tests/test_extraction.c‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4045,6 +4045,51 @@ TEST(extract_blazor_component_without_page_has_no_route) {
40454045
PASS();
40464046
}
40474047

4048+
/* Razor Pages: `@page` is what turns a .cshtml view INTO a page — it is the
4049+
* defining directive of the model, not an optional annotation as it is on a
4050+
* Blazor component. So an ASP.NET Core app's routable surface lives entirely
4051+
* in file types that were unmapped until now, and every one of those routes
4052+
* was invisible.
4053+
*
4054+
* Same mechanism as the .razor case: the directive sits in markup above any
4055+
* code block, where the C# grammar never reaches, so it is read from raw
4056+
* source and hangs off the file's Module definition. */
4057+
TEST(extract_razor_page_directive_routes_cshtml_view) {
4058+
CBMFileResult *r = extract("@page \"/orders\"\n"
4059+
"@model OrderIndexModel\n"
4060+
"\n"
4061+
"<h1>Orders</h1>\n"
4062+
"<table><tr><td>@Model.Count</td></tr></table>\n",
4063+
CBM_LANG_CSHARP, "t", "Pages/Orders/Index.cshtml");
4064+
ASSERT_NOT_NULL(r);
4065+
const CBMDefinition *mod = find_module_def(r);
4066+
ASSERT_NOT_NULL(mod);
4067+
ASSERT_NOT_NULL(mod->route_path);
4068+
ASSERT_STR_EQ(mod->route_path, "/orders");
4069+
/* A Razor Page is reached by navigation, i.e. GET — same as a component. */
4070+
ASSERT_NOT_NULL(mod->route_method);
4071+
ASSERT_STR_EQ(mod->route_method, "GET");
4072+
cbm_free_result(r);
4073+
PASS();
4074+
}
4075+
4076+
/* The overwhelming majority of .cshtml files are layouts, partials and views
4077+
* with no `@page` at all. If the scan fired on those, an ASP.NET app would
4078+
* gain a bogus Route node per view — worse than the missing routes it set out
4079+
* to fix, because a wrong route looks authoritative. */
4080+
TEST(extract_razor_layout_without_page_has_no_route) {
4081+
CBMFileResult *r = extract("@model LayoutModel\n"
4082+
"<!DOCTYPE html>\n"
4083+
"<html><body>@RenderBody()</body></html>\n",
4084+
CBM_LANG_CSHARP, "t", "Pages/Shared/_Layout.cshtml");
4085+
ASSERT_NOT_NULL(r);
4086+
const CBMDefinition *mod = find_module_def(r);
4087+
ASSERT_NOT_NULL(mod);
4088+
ASSERT_NULL(mod->route_path);
4089+
cbm_free_result(r);
4090+
PASS();
4091+
}
4092+
40484093
/* A comment between decorators must not drop the decorators above it.
40494094
* Comments are NAMED tree-sitter nodes, so the prev-sibling walk used to stop
40504095
* at one — a documented route (@Post + @HttpCode above an explanatory comment)
@@ -7208,6 +7253,8 @@ SUITE(extraction) {
72087253
RUN_TEST(extract_java_jaxrs_path_composition_issue1005);
72097254
RUN_TEST(extract_blazor_page_directive_routes_component);
72107255
RUN_TEST(extract_blazor_component_without_page_has_no_route);
7256+
RUN_TEST(extract_razor_page_directive_routes_cshtml_view);
7257+
RUN_TEST(extract_razor_layout_without_page_has_no_route);
72117258
RUN_TEST(extract_ts_template_string_url_issue1006);
72127259
RUN_TEST(extract_go_binary_concat_url_issue1249);
72137260
RUN_TEST(extract_go_binary_concat_url_no_literal_suffix_issue1249);

‎tests/test_language.c‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ TEST(lang_ext_razor) {
9797
ASSERT_EQ(cbm_language_for_extension(".razor"), CBM_LANG_CSHARP);
9898
PASS();
9999
}
100+
/* Razor Pages / MVC views were unmapped for the same reason .razor was, so an
101+
* ASP.NET Core app's entire view layer was invisible to discovery. */
102+
TEST(lang_ext_cshtml) {
103+
ASSERT_EQ(cbm_language_for_extension(".cshtml"), CBM_LANG_CSHARP);
104+
PASS();
105+
}
100106
TEST(lang_ext_php) {
101107
ASSERT_EQ(cbm_language_for_extension(".php"), CBM_LANG_PHP);
102108
PASS();
@@ -1206,6 +1212,7 @@ SUITE(language) {
12061212
RUN_TEST(lang_ext_ixx);
12071213
RUN_TEST(lang_ext_csharp);
12081214
RUN_TEST(lang_ext_razor);
1215+
RUN_TEST(lang_ext_cshtml);
12091216
RUN_TEST(lang_ext_php);
12101217
RUN_TEST(lang_ext_lua);
12111218
RUN_TEST(lang_ext_scala);

0 commit comments

Comments
 (0)