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
94 changes: 94 additions & 0 deletions internal/cbm/extract_defs.c
Original file line number Diff line number Diff line change
Expand Up @@ -7524,6 +7524,89 @@ static void walk_defs(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec,
free(s.data);
}

/* True when rel_path names a Blazor component file. */
static bool cbm_path_is_razor(const char *rel_path) {
if (!rel_path) {
return false;
}
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;
}

/* Match `@page "/route"` on ONE line; returns the route text or NULL.
*
* Deliberately strict: the directive must be the first token on the line and be
* followed by whitespace and a double-quoted path beginning with '/', so
* neither `@pageSize` nor a `@page` mentioned in markup prose can match.
*
* A blank line is rejected up front rather than falling through the length
* check, which keeps every later comparison reachable on some path — the
* all-whitespace case would otherwise leave `line_end - p` provably zero. */
static const char *razor_page_route_on_line(CBMArena *a, const char *line, const char *line_end) {
static const char directive[] = "@page";
const size_t dlen = sizeof(directive) - 1U;

const char *p = line;
while (p < line_end && (*p == ' ' || *p == '\t')) {
p++;
}
if (p == line_end) {
return NULL; /* blank line — nothing can follow */
}
if ((size_t)(line_end - p) <= dlen || strncmp(p, directive, dlen) != 0) {
return NULL;
}
p += dlen;
if (*p != ' ' && *p != '\t') {
return NULL; /* `@pageSize` and friends */
}
while (p < line_end && (*p == ' ' || *p == '\t')) {
p++;
}
if (p == line_end || *p != '"') {
return NULL;
}
p++;
const char *route = p;
while (p < line_end && *p != '"') {
p++;
}
if (p == line_end || p == route || *route != '/') {
return NULL; /* unterminated, empty, or not a rooted path */
}
return cbm_arena_strndup(a, route, (size_t)(p - route));
}

/* Blazor route directive: `@page "/counter"` lives in MARKUP above the `@code`
* block. Tree-sitter's C# grammar recovers `@code` but never parses the
* directive, so there is no AST node to read it from — this scans the raw
* source instead. That is why routes need no Razor grammar.
*
* A component may declare several routes; the first is taken, because
* CBMDefinition carries a single route_path. */
static const char *cbm_razor_page_route(CBMArena *a, const char *source, int source_len) {
if (!source || source_len <= 0) {
return NULL;
}
const char *end = source + source_len;
const char *line = source;

while (line < end) {
const char *nl = memchr(line, '\n', (size_t)(end - line));
const char *route = razor_page_route_on_line(a, line, nl ? nl : end);
if (route) {
return route;
}
if (!nl) {
break;
}
line = nl + 1;
}
return NULL;
}

void cbm_extract_definitions(CBMExtractCtx *ctx) {
const CBMLangSpec *spec = cbm_lang_spec(ctx->language);
if (!spec) {
Expand All @@ -7543,6 +7626,17 @@ void cbm_extract_definitions(CBMExtractCtx *ctx) {
mod.end_line = ts_node_end_point(ctx->root).row + TS_LINE_OFFSET;
mod.is_exported = true;
mod.is_test = ctx->result->is_test_file;
/* A routable Blazor component carries its route on the module def: the
* component's class is implicit in a .razor file, so there is no class node
* to hang it on, and the module QN already is the component's identity.
* insert_def_into_gbuf creates Route+HANDLES for any def with route_path. */
if (ctx->language == CBM_LANG_CSHARP && cbm_path_is_razor(ctx->rel_path)) {
const char *route = cbm_razor_page_route(a, ctx->source, ctx->source_len);
if (route) {
mod.route_path = route;
mod.route_method = "GET"; /* a routable page is reached by navigation */
}
}
cbm_defs_push(&ctx->result->defs, a, mod);

// Walk AST for function/class definitions
Expand Down
5 changes: 5 additions & 0 deletions src/discover/language.c
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ static const ext_entry_t EXT_TABLE[] = {

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

/* Clojure */
{".clj", CBM_LANG_CLOJURE},
Expand Down
13 changes: 11 additions & 2 deletions src/pipeline/pass_route_nodes.c
Original file line number Diff line number Diff line change
Expand Up @@ -481,10 +481,19 @@ static int ensure_one_decorator_route(cbm_gbuf_t *gb, const cbm_gbuf_node_t *fun

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

for (int li = 0; li < RN_STRIP_PASSES; li++) {
for (int li = 0; li < label_count; li++) {
const cbm_gbuf_node_t **nodes = NULL;
int count = 0;
if (cbm_gbuf_find_by_label(gb, labels[li], &nodes, &count) != 0) {
Expand Down
71 changes: 71 additions & 0 deletions tests/test_extraction.c
Original file line number Diff line number Diff line change
Expand Up @@ -3336,6 +3336,75 @@ TEST(extract_java_jaxrs_path_composition_issue1005) {
PASS();
}

/* Return the file's Module definition (extraction pushes it first), or NULL. */
static const CBMDefinition *find_module_def(CBMFileResult *r) {
for (int i = 0; i < r->defs.count; i++) {
if (r->defs.items[i].label && strcmp(r->defs.items[i].label, "Module") == 0) {
return &r->defs.items[i];
}
}
return NULL;
}

/* Blazor: a routable component declares its route with a `@page` directive in
* MARKUP, above the `@code` block. The C# grammar recovers `@code` (that is why
* .razor already yields methods via extra_extensions) but never sees the
* directive, so a routable page contributes no Route node and
* get_architecture(routes) is empty for a whole Blazor app.
*
* The route hangs off the file's Module definition, not off a class: a .razor
* component's class is implicit — it is never written in the source — so there
* is no class node to carry it. The Module's qualified name already IS the
* component's identity (t.Pages.Counter), and insert_def_into_gbuf creates
* Route+HANDLES for any definition carrying route_path, whatever its label. */
TEST(extract_blazor_page_directive_routes_component) {
CBMFileResult *r = extract("@page \"/counter\"\n"
"@inject NavigationManager Nav\n"
"\n"
"<h1>Counter</h1>\n"
"<button @onclick=\"Increment\">Click</button>\n"
"\n"
"@code {\n"
" private int count;\n"
" private void Increment() { count++; }\n"
"}\n",
CBM_LANG_CSHARP, "t", "Pages/Counter.razor");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
/* The markup must not cost us the @code block we already extract today. */
ASSERT_NOT_NULL(find_def_by_name(r, "Increment"));
const CBMDefinition *mod = find_module_def(r);
ASSERT_NOT_NULL(mod);
ASSERT_NOT_NULL(mod->route_path);
ASSERT_STR_EQ(mod->route_path, "/counter");
/* A routable Blazor page is reached by navigation, i.e. GET. */
ASSERT_NOT_NULL(mod->route_method);
ASSERT_STR_EQ(mod->route_method, "GET");
cbm_free_result(r);
PASS();
}

/* The directive scan must not fire on every .razor file. A non-routable
* component (no @page) has to stay route-free, or every shared component in the
* tree becomes a bogus Route node. */
TEST(extract_blazor_component_without_page_has_no_route) {
CBMFileResult *r = extract("@inject IJSRuntime JS\n"
"\n"
"<div class=\"card\">@Title</div>\n"
"\n"
"@code {\n"
" private void Refresh() { }\n"
"}\n",
CBM_LANG_CSHARP, "t", "Shared/Card.razor");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
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 @@ -5783,6 +5852,8 @@ SUITE(extraction) {
RUN_TEST(python_regular_module_qn_unchanged);
RUN_TEST(extract_java_method_annotations_issue382);
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_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
9 changes: 9 additions & 0 deletions tests/test_language.c
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,14 @@ TEST(lang_ext_csharp) {
ASSERT_EQ(cbm_language_for_extension(".cs"), CBM_LANG_CSHARP);
PASS();
}
/* Blazor components were unmapped, so a .razor file was never discovered at
* all: indexing a Blazor app produced no nodes for any component, and reaching
* them required an undocumented extra_extensions entry in a per-project
* .codebase-memory.json. */
TEST(lang_ext_razor) {
ASSERT_EQ(cbm_language_for_extension(".razor"), CBM_LANG_CSHARP);
PASS();
}
TEST(lang_ext_php) {
ASSERT_EQ(cbm_language_for_extension(".php"), CBM_LANG_PHP);
PASS();
Expand Down Expand Up @@ -1079,6 +1087,7 @@ SUITE(language) {
RUN_TEST(lang_ext_h);
RUN_TEST(lang_ext_ixx);
RUN_TEST(lang_ext_csharp);
RUN_TEST(lang_ext_razor);
RUN_TEST(lang_ext_php);
RUN_TEST(lang_ext_lua);
RUN_TEST(lang_ext_scala);
Expand Down
Loading