Skip to content
Merged
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
16 changes: 13 additions & 3 deletions internal/cbm/extract_defs.c
Original file line number Diff line number Diff line change
Expand Up @@ -7908,13 +7908,23 @@ void cbm_extract_definitions_without_module(CBMExtractCtx *ctx) {

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

/* Match `@page "/route"` on ONE line; returns the route text or NULL.
Expand Down
8 changes: 8 additions & 0 deletions src/discover/language.c
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ static const ext_entry_t EXT_TABLE[] = {
* parse_partial, which is why this is a best-effort mapping rather
* than a dedicated grammar. */
{".razor", CBM_LANG_CSHARP},
/* Razor Pages / MVC views. Same Razor syntax and the same C# host as
* .razor, and equally unmapped before this: an ASP.NET Core app's views
* produced no nodes at all. The `@page` directive that defines a Razor
* Page lives in this file type, so the route extraction below matters
* more here than it does for components. Best-effort on the same terms:
* the C# grammar recovers the @{ } / @functions blocks, the surrounding
* markup lands in ERROR regions and is reported via parse_partial. */
{".cshtml", CBM_LANG_CSHARP},

/* Clojure */
{".clj", CBM_LANG_CLOJURE},
Expand Down
47 changes: 47 additions & 0 deletions tests/test_extraction.c
Original file line number Diff line number Diff line change
Expand Up @@ -4045,6 +4045,51 @@ TEST(extract_blazor_component_without_page_has_no_route) {
PASS();
}

/* Razor Pages: `@page` is what turns a .cshtml view INTO a page — it is the
* defining directive of the model, not an optional annotation as it is on a
* Blazor component. So an ASP.NET Core app's routable surface lives entirely
* in file types that were unmapped until now, and every one of those routes
* was invisible.
*
* Same mechanism as the .razor case: the directive sits in markup above any
* code block, where the C# grammar never reaches, so it is read from raw
* source and hangs off the file's Module definition. */
TEST(extract_razor_page_directive_routes_cshtml_view) {
CBMFileResult *r = extract("@page \"/orders\"\n"
"@model OrderIndexModel\n"
"\n"
"<h1>Orders</h1>\n"
"<table><tr><td>@Model.Count</td></tr></table>\n",
CBM_LANG_CSHARP, "t", "Pages/Orders/Index.cshtml");
ASSERT_NOT_NULL(r);
const CBMDefinition *mod = find_module_def(r);
ASSERT_NOT_NULL(mod);
ASSERT_NOT_NULL(mod->route_path);
ASSERT_STR_EQ(mod->route_path, "/orders");
/* A Razor Page is reached by navigation, i.e. GET — same as a component. */
ASSERT_NOT_NULL(mod->route_method);
ASSERT_STR_EQ(mod->route_method, "GET");
cbm_free_result(r);
PASS();
}

/* The overwhelming majority of .cshtml files are layouts, partials and views
* with no `@page` at all. If the scan fired on those, an ASP.NET app would
* gain a bogus Route node per view — worse than the missing routes it set out
* to fix, because a wrong route looks authoritative. */
TEST(extract_razor_layout_without_page_has_no_route) {
CBMFileResult *r = extract("@model LayoutModel\n"
"<!DOCTYPE html>\n"
"<html><body>@RenderBody()</body></html>\n",
CBM_LANG_CSHARP, "t", "Pages/Shared/_Layout.cshtml");
ASSERT_NOT_NULL(r);
const CBMDefinition *mod = find_module_def(r);
ASSERT_NOT_NULL(mod);
ASSERT_NULL(mod->route_path);
cbm_free_result(r);
PASS();
}

/* A comment between decorators must not drop the decorators above it.
* Comments are NAMED tree-sitter nodes, so the prev-sibling walk used to stop
* at one — a documented route (@Post + @HttpCode above an explanatory comment)
Expand Down Expand Up @@ -7208,6 +7253,8 @@ SUITE(extraction) {
RUN_TEST(extract_java_jaxrs_path_composition_issue1005);
RUN_TEST(extract_blazor_page_directive_routes_component);
RUN_TEST(extract_blazor_component_without_page_has_no_route);
RUN_TEST(extract_razor_page_directive_routes_cshtml_view);
RUN_TEST(extract_razor_layout_without_page_has_no_route);
RUN_TEST(extract_ts_template_string_url_issue1006);
RUN_TEST(extract_go_binary_concat_url_issue1249);
RUN_TEST(extract_go_binary_concat_url_no_literal_suffix_issue1249);
Expand Down
7 changes: 7 additions & 0 deletions tests/test_language.c
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ TEST(lang_ext_razor) {
ASSERT_EQ(cbm_language_for_extension(".razor"), CBM_LANG_CSHARP);
PASS();
}
/* Razor Pages / MVC views were unmapped for the same reason .razor was, so an
* ASP.NET Core app's entire view layer was invisible to discovery. */
TEST(lang_ext_cshtml) {
ASSERT_EQ(cbm_language_for_extension(".cshtml"), CBM_LANG_CSHARP);
PASS();
}
TEST(lang_ext_php) {
ASSERT_EQ(cbm_language_for_extension(".php"), CBM_LANG_PHP);
PASS();
Expand Down Expand Up @@ -1206,6 +1212,7 @@ SUITE(language) {
RUN_TEST(lang_ext_ixx);
RUN_TEST(lang_ext_csharp);
RUN_TEST(lang_ext_razor);
RUN_TEST(lang_ext_cshtml);
RUN_TEST(lang_ext_php);
RUN_TEST(lang_ext_lua);
RUN_TEST(lang_ext_scala);
Expand Down
Loading