diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index 6e6cd0c60..26e0b0c82 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -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. diff --git a/src/discover/language.c b/src/discover/language.c index a9b49b863..b5ebd00a8 100644 --- a/src/discover/language.c +++ b/src/discover/language.c @@ -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}, diff --git a/tests/test_extraction.c b/tests/test_extraction.c index 7ea702c3a..b3232f0e5 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -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" + "
| @Model.Count |