Skip to content

Commit e6a34fc

Browse files
authored
Merge pull request #1824 from The1nk/feat/blazor-razor-routes
feat(extract): discover Blazor .razor components and emit Route nodes for @page
2 parents 2a0e9b9 + fd73c34 commit e6a34fc

5 files changed

Lines changed: 190 additions & 12 deletions

File tree

‎internal/cbm/extract_defs.c‎

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7906,6 +7906,89 @@ void cbm_extract_definitions_without_module(CBMExtractCtx *ctx) {
79067906
extract_variables(ctx, ctx->root, spec);
79077907
}
79087908

7909+
/* True when rel_path names a Blazor component file. */
7910+
static bool cbm_path_is_razor(const char *rel_path) {
7911+
if (!rel_path) {
7912+
return false;
7913+
}
7914+
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;
7918+
}
7919+
7920+
/* Match `@page "/route"` on ONE line; returns the route text or NULL.
7921+
*
7922+
* Deliberately strict: the directive must be the first token on the line and be
7923+
* followed by whitespace and a double-quoted path beginning with '/', so
7924+
* neither `@pageSize` nor a `@page` mentioned in markup prose can match.
7925+
*
7926+
* A blank line is rejected up front rather than falling through the length
7927+
* check, which keeps every later comparison reachable on some path — the
7928+
* all-whitespace case would otherwise leave `line_end - p` provably zero. */
7929+
static const char *razor_page_route_on_line(CBMArena *a, const char *line, const char *line_end) {
7930+
static const char directive[] = "@page";
7931+
const size_t dlen = sizeof(directive) - 1U;
7932+
7933+
const char *p = line;
7934+
while (p < line_end && (*p == ' ' || *p == '\t')) {
7935+
p++;
7936+
}
7937+
if (p == line_end) {
7938+
return NULL; /* blank line — nothing can follow */
7939+
}
7940+
if ((size_t)(line_end - p) <= dlen || strncmp(p, directive, dlen) != 0) {
7941+
return NULL;
7942+
}
7943+
p += dlen;
7944+
if (*p != ' ' && *p != '\t') {
7945+
return NULL; /* `@pageSize` and friends */
7946+
}
7947+
while (p < line_end && (*p == ' ' || *p == '\t')) {
7948+
p++;
7949+
}
7950+
if (p == line_end || *p != '"') {
7951+
return NULL;
7952+
}
7953+
p++;
7954+
const char *route = p;
7955+
while (p < line_end && *p != '"') {
7956+
p++;
7957+
}
7958+
if (p == line_end || p == route || *route != '/') {
7959+
return NULL; /* unterminated, empty, or not a rooted path */
7960+
}
7961+
return cbm_arena_strndup(a, route, (size_t)(p - route));
7962+
}
7963+
7964+
/* Blazor route directive: `@page "/counter"` lives in MARKUP above the `@code`
7965+
* block. Tree-sitter's C# grammar recovers `@code` but never parses the
7966+
* directive, so there is no AST node to read it from — this scans the raw
7967+
* source instead. That is why routes need no Razor grammar.
7968+
*
7969+
* A component may declare several routes; the first is taken, because
7970+
* CBMDefinition carries a single route_path. */
7971+
static const char *cbm_razor_page_route(CBMArena *a, const char *source, int source_len) {
7972+
if (!source || source_len <= 0) {
7973+
return NULL;
7974+
}
7975+
const char *end = source + source_len;
7976+
const char *line = source;
7977+
7978+
while (line < end) {
7979+
const char *nl = memchr(line, '\n', (size_t)(end - line));
7980+
const char *route = razor_page_route_on_line(a, line, nl ? nl : end);
7981+
if (route) {
7982+
return route;
7983+
}
7984+
if (!nl) {
7985+
break;
7986+
}
7987+
line = nl + 1;
7988+
}
7989+
return NULL;
7990+
}
7991+
79097992
void cbm_extract_definitions(CBMExtractCtx *ctx) {
79107993
const CBMLangSpec *spec = cbm_lang_spec(ctx->language);
79117994
if (!spec) {
@@ -7927,6 +8010,17 @@ void cbm_extract_definitions(CBMExtractCtx *ctx) {
79278010
mod.is_test = ctx->result->is_test_file;
79288011
// #519: index what a config file declares itself to be, not only its path.
79298012
mod.docstring = extract_config_module_description(ctx);
8013+
/* A routable Blazor component carries its route on the module def: the
8014+
* component's class is implicit in a .razor file, so there is no class node
8015+
* to hang it on, and the module QN already is the component's identity.
8016+
* insert_def_into_gbuf creates Route+HANDLES for any def with route_path. */
8017+
if (ctx->language == CBM_LANG_CSHARP && cbm_path_is_razor(ctx->rel_path)) {
8018+
const char *route = cbm_razor_page_route(a, ctx->source, ctx->source_len);
8019+
if (route) {
8020+
mod.route_path = route;
8021+
mod.route_method = "GET"; /* a routable page is reached by navigation */
8022+
}
8023+
}
79308024
cbm_defs_push(&ctx->result->defs, a, mod);
79318025

79328026
cbm_extract_definitions_without_module(ctx);

‎src/discover/language.c‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ static const ext_entry_t EXT_TABLE[] = {
5050

5151
/* C# */
5252
{".cs", CBM_LANG_CSHARP},
53+
/* Blazor components. The C# grammar recovers the @code block; the
54+
* surrounding markup parses as ERROR regions and is reported via
55+
* parse_partial, which is why this is a best-effort mapping rather
56+
* than a dedicated grammar. */
57+
{".razor", CBM_LANG_CSHARP},
5358

5459
/* Clojure */
5560
{".clj", CBM_LANG_CLOJURE},

‎src/pipeline/pass_route_nodes.c‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -480,10 +480,19 @@ static int ensure_one_decorator_route(cbm_gbuf_t *gb, const cbm_gbuf_node_t *fun
480480

481481
/* Phase 2a: Ensure all functions with route_path properties have Route+HANDLES edges. */
482482
static void ensure_decorator_routes(cbm_gbuf_t *gb) {
483-
const char *labels[] = {"Function", "Method"};
483+
/* "Module" is here for Blazor: a .razor component's class is implicit, so
484+
* its @page route is carried by the file's Module def. Extraction's own
485+
* insert_def_into_gbuf is label-agnostic and creates the Route either way —
486+
* this backstop is what runs on an INCREMENTAL re-index, so leaving Module
487+
* out would make a component's Route appear on a full index and vanish the
488+
* next time that one file changed.
489+
* Bound comes from the array, not the unrelated RN_STRIP_PASSES it used to
490+
* borrow, so adding a label cannot silently skip it. */
491+
const char *labels[] = {"Function", "Method", "Module"};
492+
const int label_count = (int)(sizeof(labels) / sizeof(labels[0]));
484493
int created = 0;
485494

486-
for (int li = 0; li < RN_STRIP_PASSES; li++) {
495+
for (int li = 0; li < label_count; li++) {
487496
const cbm_gbuf_node_t **nodes = NULL;
488497
int count = 0;
489498
if (cbm_gbuf_find_by_label(gb, labels[li], &nodes, &count) != 0) {

‎tests/test_extraction.c‎

Lines changed: 71 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3924,6 +3924,75 @@ TEST(extract_java_jaxrs_path_composition_issue1005) {
39243924
PASS();
39253925
}
39263926

3927+
/* Return the file's Module definition (extraction pushes it first), or NULL. */
3928+
static const CBMDefinition *find_module_def(CBMFileResult *r) {
3929+
for (int i = 0; i < r->defs.count; i++) {
3930+
if (r->defs.items[i].label && strcmp(r->defs.items[i].label, "Module") == 0) {
3931+
return &r->defs.items[i];
3932+
}
3933+
}
3934+
return NULL;
3935+
}
3936+
3937+
/* Blazor: a routable component declares its route with a `@page` directive in
3938+
* MARKUP, above the `@code` block. The C# grammar recovers `@code` (that is why
3939+
* .razor already yields methods via extra_extensions) but never sees the
3940+
* directive, so a routable page contributes no Route node and
3941+
* get_architecture(routes) is empty for a whole Blazor app.
3942+
*
3943+
* The route hangs off the file's Module definition, not off a class: a .razor
3944+
* component's class is implicit — it is never written in the source — so there
3945+
* is no class node to carry it. The Module's qualified name already IS the
3946+
* component's identity (t.Pages.Counter), and insert_def_into_gbuf creates
3947+
* Route+HANDLES for any definition carrying route_path, whatever its label. */
3948+
TEST(extract_blazor_page_directive_routes_component) {
3949+
CBMFileResult *r = extract("@page \"/counter\"\n"
3950+
"@inject NavigationManager Nav\n"
3951+
"\n"
3952+
"<h1>Counter</h1>\n"
3953+
"<button @onclick=\"Increment\">Click</button>\n"
3954+
"\n"
3955+
"@code {\n"
3956+
" private int count;\n"
3957+
" private void Increment() { count++; }\n"
3958+
"}\n",
3959+
CBM_LANG_CSHARP, "t", "Pages/Counter.razor");
3960+
ASSERT_NOT_NULL(r);
3961+
ASSERT_FALSE(r->has_error);
3962+
/* The markup must not cost us the @code block we already extract today. */
3963+
ASSERT_NOT_NULL(find_def_by_name(r, "Increment"));
3964+
const CBMDefinition *mod = find_module_def(r);
3965+
ASSERT_NOT_NULL(mod);
3966+
ASSERT_NOT_NULL(mod->route_path);
3967+
ASSERT_STR_EQ(mod->route_path, "/counter");
3968+
/* A routable Blazor page is reached by navigation, i.e. GET. */
3969+
ASSERT_NOT_NULL(mod->route_method);
3970+
ASSERT_STR_EQ(mod->route_method, "GET");
3971+
cbm_free_result(r);
3972+
PASS();
3973+
}
3974+
3975+
/* The directive scan must not fire on every .razor file. A non-routable
3976+
* component (no @page) has to stay route-free, or every shared component in the
3977+
* tree becomes a bogus Route node. */
3978+
TEST(extract_blazor_component_without_page_has_no_route) {
3979+
CBMFileResult *r = extract("@inject IJSRuntime JS\n"
3980+
"\n"
3981+
"<div class=\"card\">@Title</div>\n"
3982+
"\n"
3983+
"@code {\n"
3984+
" private void Refresh() { }\n"
3985+
"}\n",
3986+
CBM_LANG_CSHARP, "t", "Shared/Card.razor");
3987+
ASSERT_NOT_NULL(r);
3988+
ASSERT_FALSE(r->has_error);
3989+
const CBMDefinition *mod = find_module_def(r);
3990+
ASSERT_NOT_NULL(mod);
3991+
ASSERT_NULL(mod->route_path);
3992+
cbm_free_result(r);
3993+
PASS();
3994+
}
3995+
39273996
/* A comment between decorators must not drop the decorators above it.
39283997
* Comments are NAMED tree-sitter nodes, so the prev-sibling walk used to stop
39293998
* at one — a documented route (@Post + @HttpCode above an explanatory comment)
@@ -6309,16 +6378,6 @@ TEST(iris_export_xml_multi_class) {
63096378
* question asked in words could not reach either. Both now carry the prose in
63106379
* `docstring`, which is what nodes_fts indexes into its `body` column. */
63116380

6312-
/* First Module-labelled definition, or NULL. */
6313-
static const CBMDefinition *find_module_def(CBMFileResult *r) {
6314-
for (int i = 0; i < r->defs.count; i++) {
6315-
if (strcmp(r->defs.items[i].label, "Module") == 0) {
6316-
return &r->defs.items[i];
6317-
}
6318-
}
6319-
return NULL;
6320-
}
6321-
63226381
TEST(markdown_section_body_becomes_docstring_issue518) {
63236382
CBMFileResult *r = extract("# Installation\n"
63246383
"Run the bootstrap script to provision a workstation.\n"
@@ -6827,6 +6886,8 @@ SUITE(extraction) {
68276886
RUN_TEST(arkts_lazy_import);
68286887
RUN_TEST(arkts_ts_compat);
68296888
RUN_TEST(extract_java_jaxrs_path_composition_issue1005);
6889+
RUN_TEST(extract_blazor_page_directive_routes_component);
6890+
RUN_TEST(extract_blazor_component_without_page_has_no_route);
68306891
RUN_TEST(extract_ts_template_string_url_issue1006);
68316892
RUN_TEST(extract_go_binary_concat_url_issue1249);
68326893
RUN_TEST(extract_go_binary_concat_url_no_literal_suffix_issue1249);

‎tests/test_language.c‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,14 @@ TEST(lang_ext_csharp) {
8989
ASSERT_EQ(cbm_language_for_extension(".cs"), CBM_LANG_CSHARP);
9090
PASS();
9191
}
92+
/* Blazor components were unmapped, so a .razor file was never discovered at
93+
* all: indexing a Blazor app produced no nodes for any component, and reaching
94+
* them required an undocumented extra_extensions entry in a per-project
95+
* .codebase-memory.json. */
96+
TEST(lang_ext_razor) {
97+
ASSERT_EQ(cbm_language_for_extension(".razor"), CBM_LANG_CSHARP);
98+
PASS();
99+
}
92100
TEST(lang_ext_php) {
93101
ASSERT_EQ(cbm_language_for_extension(".php"), CBM_LANG_PHP);
94102
PASS();
@@ -1197,6 +1205,7 @@ SUITE(language) {
11971205
RUN_TEST(lang_ext_h);
11981206
RUN_TEST(lang_ext_ixx);
11991207
RUN_TEST(lang_ext_csharp);
1208+
RUN_TEST(lang_ext_razor);
12001209
RUN_TEST(lang_ext_php);
12011210
RUN_TEST(lang_ext_lua);
12021211
RUN_TEST(lang_ext_scala);

0 commit comments

Comments
 (0)