Skip to content

feat(extract): discover Blazor .razor components and emit Route nodes for @page - #1824

Open
The1nk wants to merge 2 commits into
DeusData:mainfrom
The1nk:feat/blazor-razor-routes
Open

feat(extract): discover Blazor .razor components and emit Route nodes for @page#1824
The1nk wants to merge 2 commits into
DeusData:mainfrom
The1nk:feat/blazor-razor-routes

Conversation

@The1nk

@The1nk The1nk commented Aug 25, 2026

Copy link
Copy Markdown

What does this PR do?

Makes Blazor .razor components visible to the indexer, and turns their
@page directives into Route nodes.

Two changes, in dependency order:

1. feat(discover): map .razor to C#.razor had no entry in
EXT_TABLE, so discovery skipped it and a Blazor application produced no
graph nodes for any component. The only way in was an undocumented
extra_extensions entry in a per-project .codebase-memory.json. This maps
.razor to CBM_LANG_CSHARP.

It is best-effort by design: the C# grammar recovers the @code block, while
the surrounding markup lands in ERROR regions and is reported through
parse_partial. That is still strictly more than the Module-and-imports the
other markup-hosted languages extract today, and it needs no new grammar.

2. feat(extract): emit Route nodes for Blazor @page directives
get_architecture's routes aspect returned nothing for a Blazor
application: the entry surface of the app was invisible even though every
@page names one.

@page lives in markup above the @code block, which tree-sitter's C#
grammar never parses, so there is no AST node to read it from. This scans the
raw source instead. The match is deliberately strict — the directive must be
the first token on its line, followed by whitespace and a double-quoted path
beginning with / — so @pageSize and prose mentions cannot match.

The route is attached to the file's module def. A .razor component's
class is implicit, so there is no class node to carry it, and the module QN
already is the component's identity. insert_def_into_gbuf is label-agnostic
and creates Route + HANDLES from route_path.

pass_route_nodes.c's ensure_decorator_routes gains "Module" for the
same reason. Extraction covers the full-index path on its own; this backstop
is what runs on an incremental re-index, so without it a component's
Route would appear on a full index and vanish the next time that one file
changed. Its loop bound now derives from the labels array rather than
borrowing the unrelated RN_STRIP_PASSES, so adding a label cannot silently
skip it. Verified there is no double-creation when both paths can fire (60
files: 60 Routes and 60 HANDLES either way).

Measured

Paired control on a real Blazor application (36 .razor files, 261 .cs),
same source, same day, isolated caches:

binary nodes Route HANDLES
0.9.0 (upstream) 4555 0 0
this branch 4566 11 11

+11 nodes is exactly the 11 Route nodes. get_architecture(routes) went
from the aspect being absent to 11 entries. No regression in what was already
extracted from that project's C#: 363 Class, 1775 Method, 3805 CALLS
unchanged, no store schema change.

End-to-end on a dedicated fixture, with a negative control:

Route /counter          <- HANDLES <- Module Pages/Counter.razor
Route /weather/forecast <- HANDLES <- Module Pages/Weather.razor
NoRoute.razor (no @page)  ->  Module, and NO Route

Stable across a re-index.

What this does not claim

  • @code methods are not reliably extracted from .razor. Real markup
    (class=, role= attributes) defeats recovery and the file comes back
    parse_partial as a whole. Bare @code fields remain a separate gap.
  • Two pre-existing gaps are untouched and are not caused by this change —
    they reproduce on the unmodified binary too: routes report handler:""
    although the HANDLES edge exists, and the language census counts .razor
    as C# rather than as its own surface.
  • Only the first @page on a component is taken, because CBMDefinition
    carries a single route_path.

Checklist

  • Every commit is signed off (git commit -s) — required, CI rejects
    unsigned commits (DCO, see CONTRIBUTING.md)
  • Tests pass locally (make -f Makefile.cbm test)
  • Lint passes (make -f Makefile.cbm lint-ci)
  • New behavior is covered by a test (reproduce-first for bug fixes)

Full suite on this branch, rebased onto 010569fa (current main at the time
of writing): 7415 passed / 0 failed / 8 skipped, 139 suites, via
scripts/test.sh. The branch itself adds 3 of those tests —
extract_blazor_page_directive_routes_component,
extract_blazor_component_without_page_has_no_route and lang_ext_razor, the
middle one being the negative control.

An earlier draft of this description quoted 7335/138 against base 49d928be.
That base is now 129 commits behind, so the branch was rebased and re-measured
rather than shipped with stale numbers; the difference is upstream's, not this
change's.

On lint, stated plainly

make -f Makefile.cbm lint-ci — the gate this checklist names — passes: exit
0, "CI linters passed", run in a clean container against this branch rebased
onto 010569fa.

scripts/lint.sh in full mode does not pass, and I do not believe any of
it is mine. Full mode adds clang-tidy, which fails repo-wide on rules like
readability-magic-numbers, readability-braces-around-statements and
misc-no-recursion, in files this branch never touches —
src/ui/httpd.h, src/pipeline/lsp_resolve.h, src/pipeline/pass_lsp_cross.h,
src/foundation/*.h among others.

internal/cbm/extract_defs.c does appear in that output, but not at this
change's lines: the diagnostics sit at lines 281-1245 (including
cbm_resolve_func_name's cognitive complexity of 396), while this branch's
hunks are at 7524-7613 and 7626-7643. Happy to be corrected if you read it
differently.

One scope question I would rather raise than assume

CONTRIBUTING.md asks for prior design discussion on "new pipeline passes or
indexing algorithms — anything that changes what gets extracted or how", and
this does change what gets extracted. What I took as the go-ahead was #1667
being open for eight days and labelled bug / language-request /
priority/high, plus the standing exception for focused bug fixes. If you
would rather this went through a design discussion first, say so and I will
close it and move the conversation back to the issue — no hard feelings.

Fixes #1667


Built and measured with Claude Code; all numbers above are from real runs on
a real application, not generated.

The1nk and others added 2 commits August 24, 2026 19:00
A .razor file had no entry in EXT_TABLE, so discovery skipped it entirely
and a Blazor application produced no graph nodes for any of its
components. The only way in was an undocumented extra_extensions entry in
a per-project .codebase-memory.json.

Map .razor to CBM_LANG_CSHARP. This is best-effort by design: the C#
grammar recovers the @code block, while the surrounding markup lands in
ERROR regions and is reported through parse_partial. That is strictly
more than the Module-and-imports the other markup-hosted languages
(Vue, Svelte, Astro) extract today, and it needs no new grammar.

Covered by tests/test_language.c:lang_ext_razor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: The1nk <23087600+The1nk@users.noreply.github.com>
get_architecture's routes aspect returned nothing for a Blazor
application: the entry surface of the app was invisible in the graph
even though every @page directive names one.

@page lives in markup above the @code block, which tree-sitter's C#
grammar never parses, so there is no AST node to read it from. Scan the
raw source for it instead. The match is deliberately strict — the
directive must be the first token on its line, followed by whitespace
and a double-quoted path beginning with '/' — so @pageSize and prose
mentions cannot match.

The route is attached to the file's module def. A .razor component's
class is implicit, so there is no class node to carry it, and the module
QN already is the component's identity. insert_def_into_gbuf is
label-agnostic and creates Route + HANDLES from route_path.

pass_route_nodes.c's ensure_decorator_routes gains "Module" for the same
reason. Extraction covers the full-index path on its own; this backstop
is what runs on an incremental re-index, so without it a component's
Route would appear on a full index and vanish the next time that one
file changed. Its loop bound now derives from the labels array rather
than borrowing the unrelated RN_STRIP_PASSES, so adding a label cannot
silently skip it. Verified no double-creation when both paths can fire.

Measured on a real Blazor application (36 .razor files): routes went
from 0 to 11, with 11 matching HANDLES edges, and no change to the
363 Class / 1775 Method / 3805 CALLS already extracted from its C#.

Covered by two new tests in tests/test_extraction.c.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: The1nk <23087600+The1nk@users.noreply.github.com>
@The1nk
The1nk requested a review from DeusData as a code owner August 25, 2026 00:53
@github-actions

Copy link
Copy Markdown

Thanks for opening this — it has been seen, and it is queued.

This note is automated, but it is not a brush-off: it exists so you know where your PR stands instead of having to guess from silence.

Current review status: working through a backlog. 0.9.1-rc.1 is out, so the release freeze that held reviews is over — but it left a large queue of open pull requests behind it, and we are reading through them oldest-first. The background is in discussion #1144.

What that means for this PR, concretely:

  • It will not be closed for inactivity. No stale bot touches pull requests here.
  • It may still sit a while before a human reads it. That is on us, not on you.
  • Older PRs are read first, so a recent one is not being skipped — it is behind a queue.

Things that will genuinely speed it up whenever review does happen:

  • Keep it rebased on main — the tree is moving quickly right now, and a conflicting branch cannot be reviewed as the diff you intended.
  • Get CI green, or say which failures you believe are pre-existing.
  • Keep the change to one claim. Bundled features and refactors get split before they get merged, which costs you a round trip.
  • Every commit needs a sign-off (git commit -s) — CI enforces DCO.

If this fixes a bug, a reproduction we can run is worth more than a description of the symptom.

Thanks for contributing, and sorry in advance for the wait.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Blazor: .razor files are never discovered, and @page routes produce no Route nodes

1 participant