feat(impact): Razor/Blazor (.cshtml/.razor) markup parser

New standalone RazorExtractor (follows the svelte/vue/liquid pattern) links
markup-driven C# that previously had no static in-repo caller:
- `@model Foo` / `@inherits Bar<Foo>` → the view-model / base type (.cshtml + .razor)
- `@inject IService svc` → the injected service type
- `@typeof(X)` → the referenced type
- Blazor `<MyComponent/>` tags + generic `TItem="Foo"` args → the component class

Wiring: `.cshtml`/`.razor` → new `razor` language (types.ts, grammars.ts
EXTENSION_MAP + isLanguageSupported/isGrammarLoaded + GrammarLanguage exclude +
display name); dispatch in tree-sitter.ts `extractFromSource`.

Key risks (per docs/design/template-markup-parser.md) all handled:
1. FAMILY GATE — `razor`+`csharp` share a new `dotnet` family in name-matcher,
   so `@model Foo`→`Foo.cs` isn't dropped by the cross-family gate (082353e).
2. PASCALCASE vs HTML — only `[A-Z]`-initial tags are components (HTML is
   lowercase); known Blazor framework components are skipped.
3. NODE DISCIPLINE — exactly one `component` node per file; tags → `references`
   edges, never nodes (no per-tag explosion).
4. INDEXING — `.cshtml`/`.razor` registered so they're indexed.

Validated: eShopOnWeb — 61 razor nodes, 97 resolved markup→code edges
(`App.razor`→MainLayout, `Create.razor`→CatalogBrand/ICatalogItemService,
ToastComponent covered). FAIR coverage 77.2% → 79.9% (.cshtml/.razor counted as
view entries in the metric). No regression (gin/requests/Express unchanged,
cs-mediatr/cs-polly fine). Full suite 1175; 1 regression test.

Modest gain on this app is honest: residual DTOs are a name-collision
(`CatalogBrand` exists as both entity and DTO — edge resolves to the entity),
and many DTOs/services are used inside Blazor `@code { }` blocks not yet
delegated to the C# extractor — both documented as next increments in the scope.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby McHenry
2026-06-04 18:51:57 -04:00
parent 2a0b6e001d
commit 59b8de21fd
8 changed files with 402 additions and 2 deletions
+1
View File
@@ -11,6 +11,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### New Features
- ASP.NET Razor (`.cshtml`) and Blazor (`.razor`) markup are now parsed for code relationships. A `@model` / `@inherits` / `@inject` directive links the view to the C# view-model, base type, or service it names; a Blazor `<MyComponent/>` tag (plus `@typeof(...)` and generic `TItem="..."` arguments) links to the component class. So a view-model, component, or service referenced only from markup is no longer reported as having no dependents, and editing it surfaces the views that use it. (ASP.NET, Blazor)
- `codegraph status --json` now also reports the running CLI `version`, the index directory (`indexPath`), and a `lastIndexed` timestamp (ISO-8601, or null when nothing's indexed yet), so CI and scripts can pin the CLI version and check index freshness from a single command. A matching `CodeGraph.getLastIndexedAt()` library method exposes the same freshness check without shelling out. Thanks @12122J and @eddieran. (#329)
### Fixes
+54
View File
@@ -3953,6 +3953,60 @@ describe('Python absolute module import resolution', () => {
});
});
describe('Razor / Blazor markup extraction', () => {
let tempDir: string;
let cg: CodeGraph;
beforeEach(() => {
tempDir = createTempDir();
});
afterEach(() => {
if (cg) cg.close();
if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true });
});
it('links @model and Blazor component tags to their C# types; ignores HTML elements', async () => {
fs.mkdirSync(path.join(tempDir, 'Views'), { recursive: true });
fs.writeFileSync(
path.join(tempDir, 'LoginViewModel.cs'),
`namespace App { public class LoginViewModel { public string Email { get; set; } } }`
);
fs.writeFileSync(
path.join(tempDir, 'ToastComponent.cs'),
`namespace App { public class ToastComponent { } }`
);
fs.writeFileSync(
path.join(tempDir, 'Views/Login.cshtml'),
`@model LoginViewModel\n<div class="form">\n <input asp-for="Email" />\n</div>\n`
);
fs.writeFileSync(
path.join(tempDir, 'Index.razor'),
`<div>\n <ToastComponent />\n</div>\n`
);
cg = CodeGraph.initSync(tempDir);
await cg.indexAll();
cg.resolveReferences();
// `@model LoginViewModel` → the view-model class.
const vm = cg.getNodesByKind('class').find((n) => n.name === 'LoginViewModel');
expect(vm, 'LoginViewModel class').toBeDefined();
const vmDeps = [...cg.getImpactRadius(vm!.id, 2).nodes.values()].map((n) => n.filePath ?? '');
expect(vmDeps.some((p) => p.endsWith('Login.cshtml')), '@model links the view').toBe(true);
// `<ToastComponent />` → the component class.
const toast = cg.getNodesByKind('class').find((n) => n.name === 'ToastComponent');
expect(toast, 'ToastComponent class').toBeDefined();
const toastDeps = [...cg.getImpactRadius(toast!.id, 2).nodes.values()].map((n) => n.filePath ?? '');
expect(toastDeps.some((p) => p.endsWith('Index.razor')), 'Blazor tag links the component').toBe(true);
// HTML elements (`<div>`, `<input>`) must NOT become component references.
const htmlNodes = cg.getNodesByKind('class').filter((n) => n.name === 'div' || n.name === 'input');
expect(htmlNodes.length, 'no node for HTML elements').toBe(0);
});
});
describe('Default import resolution (renamed default export)', () => {
let tempDir: string;
let cg: CodeGraph;
+152
View File
@@ -0,0 +1,152 @@
# Scope: Template-markup parser (Razor / Blazor / Thymeleaf)
Status: **proposed** (scoping only — not implemented). Authored 2026-06-04 from the
cross-language impact-coverage campaign (`feat/cross-language-impact-coverage`).
## Problem
The impact graph is built from code the engine parses. **Template markup is not
parsed**, so any code-behind, component, view-model, or DTO that is referenced
*only* from markup looks like it has no in-repo dependent. On convention-heavy
frameworks this is the dominant residual gap after framework-entry exclusions:
| Framework | App | FAIR coverage (entries excluded) | Residual cause |
|---|---|---|---|
| ASP.NET | eShopOnWeb | **77.2%** (115/149) | Razor `.cshtml` + Blazor `.razor` reference `.cs` we don't parse |
| Spring | petclinic | 65.2% | mostly Spring Data proxies + JPA, **not** templates (Thymeleaf links are weak) |
| Django | django-realworld | 74.1% | signals / DRF / string-config, **not** templates |
**This feature is primarily an ASP.NET (Razor + Blazor) win.** Thymeleaf and Django
templates link to code only weakly (template→template fragments + fuzzy
model-attribute strings), and those frameworks' real gaps are elsewhere — so they
are explicitly lower priority here.
### Quantified target (eShopOnWeb, the 34 residual zeros after entry-exclusion)
- **~20 markup-coverable** by this feature:
- 5 MVC `ViewModels/*` ← Razor `@model X`
- 7 `BlazorShared/Models/*` (DTOs) ← Blazor `@bind` / component params
- 6 `BlazorAdmin/*` C# components ← Blazor `<Component/>` tags
- 1 `BasketComponent` ViewComponent ← `<vc:basket>` / `Component.InvokeAsync`
- 1 Razor page helper
- **~13 NOT covered** (separate frontier — reflection/proxy + value-reads): AutoMapper
`MappingProfile`, Swagger `CustomSchemaFilters`/`ImageValidators`, `ExceptionMiddleware`,
health checks, `Constants` (static-member reads), `Buyer` entity.
**Honest ceiling: ASP.NET ~77% → ~90%**, not 95%. The last ~10% is reflection/proxy
(AutoMapper, Swagger, DI/middleware registration) + C# static-const reads — a
*separate* feature (reflection modeling + extending the static-member pass to C#).
## Reference patterns to extract (prioritized)
| Pri | Format | Markup construct | Edge to emit | Resolves to |
|---|---|---|---|---|
| P1 | Razor `.cshtml`/`.razor` | `@model Foo` / `@inherits X<Foo>` | `references` | the model/VM class `Foo` |
| P1 | Razor/Blazor | `@inject IBar bar` | `references` | the service type `IBar` |
| P2 | Blazor `.razor` | `<MyComponent .../>` (PascalCase element) | `references` | component class (`.razor` or `.cs : ComponentBase`) |
| P2 | Blazor `.razor` | `@typeof(MainLayout)`, `@inherits LayoutBase` | `references` | the type |
| P3 | Razor `.cshtml` | `<partial name="_X"/>`, `<vc:basket>`, `Component.InvokeAsync("X")` | `references` | the partial view / `XViewComponent` |
| P3 | Razor `.cshtml` | `asp-page="./Register"`, `asp-controller`/`asp-action` | `references` | the page / controller action |
| P4 (defer) | Thymeleaf `.html` | `th:replace="~{frag :: x}"` | `references` | template fragment (template→template only) |
| P4 (defer) | Django `.html` | `{% extends %}` / `{% include %}` / `{% url 'n' %}` | `references` | template / named route |
`asp-for="Prop"`, `th:field="*{prop}"` (property-string bindings) are the data-flow
frontier — **out of scope** (would need model-type inference; low value, high noise).
## Architecture — follow the existing standalone-extractor pattern
The engine already has non-tree-sitter extractors (`svelte-extractor.ts`,
`vue-extractor.ts`, `liquid-extractor.ts`): a class taking `(filePath, source)`,
returning `{ nodes, references }`, wired in two places. Mirror exactly:
1. **`src/extraction/grammars.ts`** — map extensions to a synthetic language:
`.cshtml`/`.razor``'razor'`, (later) `.html` under `templates/``'thymeleaf'`.
(Django `.html` is ambiguous with plain HTML — gate on a `templates/` path or a
`{% %}`/`{{ }}` content sniff, like the framework resolvers do.)
2. **`src/extraction/tree-sitter.ts`** — dispatch by extension to a new
`RazorExtractor` (and `ThymeleafExtractor`), exactly as `SvelteExtractor` is
dispatched (~line 4025).
3. **`src/extraction/razor-extractor.ts`** (new) — regex/line scan (markup is
highly stylized; no grammar needed, same as Liquid/Svelte template scanning):
- Emit ONE `component` node for the file (so `.razor` components are linkable as
`<X/>` targets and the file is a graph citizen).
- Emit `references` per the P1P3 patterns above, `fromNodeId` = the file/component
node, `referenceKind: 'references'`, `language: 'razor'`.
- **Code-behind link:** a `Foo.razor` + `Foo.razor.cs` (partial class) — emit a
`references` (or rely on same-basename) so the markup's refs also credit the
code-behind. (eShop's Blazor components are plain `.cs : ComponentBase`, named
`<ToastComponent/>` → resolves by class name; the `.razor.cs` partial case is
the other shape.)
**Resolution: no new resolver needed.** The emitted refs are ordinary `references`
to a class/component by name; the existing name-matcher resolves them (`@model
RegisterModel` → class `RegisterModel`; `<ToastComponent/>` → class `ToastComponent`).
Apply the **same cross-family language gate** already in place — a `razor` ref must
resolve to a `csharp` symbol, so add `razor` to the `web`/dotnet family or treat
`razor``csharp` as same-family (otherwise the gate from commit 082353e drops it).
**This is the one resolver-side change** and must be done or every edge is gated away.
## Node/edge shape & invariants
- +1 `component` node per template file (real new symbol — like `.svelte`/`.vue`).
Node count grows by the template-file count only; **no per-tag node explosion**
(component tags become `references` edges, not nodes).
- All edges are `references` (counted by impact / `affected` / `getFileDependents`,
not by `callers`/`callees` — matches how `route`/`component` edges already behave).
- Idempotent re-index; node count stable across re-runs.
## Phasing
- **P1 (highest value/effort ratio):** Razor `@model` + `@inject` for `.cshtml` AND
`.razor`. Covers the 5 ViewModels + injected services. + the resolver family-gate fix.
- **P2:** Blazor `<PascalComponent/>` tags + `@typeof`/`@inherits` + code-behind link.
Covers the 6 Blazor `.cs` components + the 7 DTOs (via component params/`@bind`).
- **P3:** Razor `<partial>` / `<vc:>` / `Component.InvokeAsync` / `asp-page`.
- **P4 (defer / probably skip):** Thymeleaf + Django templates — weak code links,
low coverage payoff; revisit only if a Thymeleaf/Django app is a priority.
## Edge cases & risks
- **PascalCase tag vs HTML element:** only `[A-Z]`-initial tags are Blazor components
(HTML is lowercase) — safe discriminator. Skip known framework components
(`<Router>`, `<Found>`, `<LayoutView>`, `<RouteView>`, `<CascadingValue>`) via a
builtin set, or just let them fail to resolve (no false edge — they're not in-repo).
- **`_Imports.razor` `@using`:** namespace imports, not code refs — ignore (or emit
`imports` to the namespace, low value).
- **Generic components `<Grid TItem="CatalogItem">`:** capture the type-arg as a
`references` to `CatalogItem` (bonus DTO coverage).
- **Name collisions:** component/model names are usually unique; rely on the
name-matcher's existing proximity scoring. Same-named class in another language is
blocked by the family gate.
- **Razor `@{ ... }` C# blocks:** contain real C# (calls, `new`) — P-future; regex
scanning the C# inside markup is noisy. Defer (the directives above are the wins).
- **`.razor` is NOT `.cs`:** must add to `grammars.ts` + the indexer's include globs
(verify `.razor`/`.cshtml` aren't in a default-exclude).
## Validation (per the engine's methodology)
1. Build `RazorExtractor`; unit tests in `__tests__/extraction.test.ts` (a `.cshtml`
with `@model X` covers `X`; a `.razor` with `<ToastComponent/>` covers it; an HTML
`<div>` does NOT create an edge).
2. Re-measure eShopOnWeb FAIR coverage before/after (`/tmp/faircov.cjs`): target
77% → ~90%; **node count stable** (only +template-file component nodes); residual
zeros are the reflection/value-read set only.
3. No regression on a non-.NET control (gin/requests) and on the Razor-free C#
repos (cs-mediatr/cs-polly unchanged).
4. Record in this doc + the coverage handoff.
## Effort
- P1: ~0.5 day (extractor skeleton + `@model`/`@inject` scan + family-gate fix + tests).
- P2: ~1 day (Blazor tags + code-behind + generic type-args).
- P3: ~0.5 day. P4 (Thymeleaf/Django): ~12 days, low ROI — defer.
- **Total for the ASP.NET win (P1+P2+P3): ~2 days → ASP.NET ~90%.**
## Non-goals (and what's still needed for 95% on convention apps)
This feature does NOT close: reflection/proxy registration (Spring Data repository
proxies, AutoMapper profiles, Swagger filters, DI container / middleware), property-
string data bindings (`asp-for`/`th:field`), or C# static-const value reads
(`Constants.X`). Convention apps reaching literal 95% additionally need a **reflection/
DI-registration modeling** pass and **extending the static-member pass to C#/TS**
tracked separately. Markup parsing is the single biggest, most self-contained step.
+8 -2
View File
@@ -10,7 +10,7 @@ import * as path from 'path';
import { Parser, Language as WasmLanguage } from 'web-tree-sitter';
import { Language } from '../types';
export type GrammarLanguage = Exclude<Language, 'svelte' | 'vue' | 'liquid' | 'yaml' | 'twig' | 'xml' | 'properties' | 'unknown'>;
export type GrammarLanguage = Exclude<Language, 'svelte' | 'vue' | 'liquid' | 'razor' | 'yaml' | 'twig' | 'xml' | 'properties' | 'unknown'>;
/**
* WASM filename map — maps each language to its .wasm grammar file
@@ -69,6 +69,10 @@ export const EXTENSION_MAP: Record<string, Language> = {
'.hpp': 'cpp',
'.hxx': 'cpp',
'.cs': 'csharp',
// ASP.NET Razor / Blazor markup — custom RazorExtractor (links @model/@inject/
// component tags to their C# types; markup isn't a tree-sitter grammar).
'.cshtml': 'razor',
'.razor': 'razor',
'.php': 'php',
// Drupal-specific PHP file extensions
'.module': 'php',
@@ -278,6 +282,7 @@ export function isLanguageSupported(language: Language): boolean {
if (language === 'svelte') return true; // custom extractor (script block delegation)
if (language === 'vue') return true; // custom extractor (script block delegation)
if (language === 'liquid') return true; // custom regex extractor
if (language === 'razor') return true; // custom RazorExtractor (.cshtml/.razor markup)
if (language === 'yaml') return true; // file-level tracking only; Drupal routing extraction via framework resolver
if (language === 'twig') return true; // file-level tracking only
if (language === 'xml') return true; // MyBatis mapper extractor
@@ -290,7 +295,7 @@ export function isLanguageSupported(language: Language): boolean {
* Check if a grammar has been loaded and is ready for parsing.
*/
export function isGrammarLoaded(language: Language): boolean {
if (language === 'svelte' || language === 'vue' || language === 'liquid') return true;
if (language === 'svelte' || language === 'vue' || language === 'liquid' || language === 'razor') return true;
if (language === 'yaml' || language === 'twig') return true; // no WASM grammar needed
if (language === 'xml' || language === 'properties') return true; // no WASM grammar needed
return languageCache.has(language);
@@ -371,6 +376,7 @@ export function getLanguageDisplayName(language: Language): string {
c: 'C',
cpp: 'C++',
csharp: 'C#',
razor: 'Razor/Blazor',
php: 'PHP',
ruby: 'Ruby',
swift: 'Swift',
+178
View File
@@ -0,0 +1,178 @@
import { Node, ExtractionResult, ExtractionError, UnresolvedReference } from '../types';
import { generateNodeId } from './tree-sitter-helpers';
/**
* RazorExtractor — extracts code relationships from ASP.NET Razor (`.cshtml`)
* and Blazor (`.razor`) markup.
*
* Markup-driven code-behind, view-models, components, and DTOs are referenced
* only from markup the engine otherwise doesn't parse, so they look like nothing
* depends on them. This extractor links the markup → the C# types it names:
*
* - `@model Foo` / `@inherits Bar<Foo>` → the view-model / base type (.cshtml + .razor)
* - `@inject IService svc` → the injected service type
* - `@typeof(MainLayout)` → the referenced type
* - `<MyComponent .../>` (Blazor only) → the component class (.razor or `.cs : ComponentBase`)
* - `<Grid TItem="CatalogItem">` → the generic type argument
*
* Risk mitigations (see docs/design/template-markup-parser.md):
* - Only PascalCase (`[A-Z]`-initial) tags are treated as components — HTML
* elements are lowercase, so they never match. Known Blazor framework
* components are skipped (they aren't in-repo, so a ref would just dangle).
* - Exactly ONE `component` node per file; component tags become `references`
* EDGES, never nodes — no per-tag node explosion.
* - Emitted refs are ordinary by-name `references` resolved by the name-matcher;
* `razor` shares the `dotnet` language family with `csharp` (name-matcher.ts)
* so the cross-family gate doesn't drop them.
* - `.cshtml`/`.razor` are registered in grammars.ts so they're indexed.
*
* Out of scope (data-flow / low-value): `asp-for`/`th:field` property-string
* bindings; the C# inside `@code { }` / `@{ }` blocks (noisy regex on embedded C#).
*/
/**
* Blazor framework-provided components — invoked by the runtime, not defined
* in-repo, so a reference to them would never resolve. Skip to avoid dangling refs.
*/
const BLAZOR_BUILTIN_COMPONENTS = new Set([
'Router', 'Found', 'NotFound', 'RouteView', 'AuthorizeRouteView', 'LayoutView',
'CascadingValue', 'CascadingAuthenticationState', 'AuthorizeView', 'Authorized',
'NotAuthorized', 'Authorizing', 'EditForm', 'DataAnnotationsValidator',
'ValidationSummary', 'ValidationMessage', 'InputText', 'InputNumber',
'InputCheckbox', 'InputSelect', 'InputDate', 'InputTextArea', 'InputRadio',
'InputRadioGroup', 'InputFile', 'PageTitle', 'HeadContent', 'HeadOutlet',
'Virtualize', 'DynamicComponent', 'ErrorBoundary', 'SectionContent',
'SectionOutlet', 'FocusOnNavigate', 'NavLink', 'Microsoft',
]);
export class RazorExtractor {
private filePath: string;
private source: string;
private nodes: Node[] = [];
private unresolvedReferences: UnresolvedReference[] = [];
private errors: ExtractionError[] = [];
constructor(filePath: string, source: string) {
this.filePath = filePath;
this.source = source;
}
extract(): ExtractionResult {
const startTime = Date.now();
try {
const componentId = this.createComponentNode().id;
this.extractDirectives(componentId);
// Blazor component tags only — `.cshtml` uses HTML + tag helpers, not
// PascalCase component elements.
if (this.filePath.toLowerCase().endsWith('.razor')) {
this.extractComponentTags(componentId);
}
} catch (error) {
this.errors.push({
message: `Razor extraction error: ${error instanceof Error ? error.message : String(error)}`,
severity: 'error',
code: 'parse_error',
});
}
return {
nodes: this.nodes,
edges: [],
unresolvedReferences: this.unresolvedReferences,
errors: this.errors,
durationMs: Date.now() - startTime,
};
}
private createComponentNode(): Node {
const lines = this.source.split('\n');
const fileName = this.filePath.split(/[/\\]/).pop() || this.filePath;
const componentName = fileName.replace(/\.(razor|cshtml)$/i, '');
const node: Node = {
id: generateNodeId(this.filePath, 'component', componentName, 1),
kind: 'component',
name: componentName,
qualifiedName: `${this.filePath}::${componentName}`,
filePath: this.filePath,
language: 'razor',
startLine: 1,
endLine: lines.length,
startColumn: 0,
endColumn: lines[lines.length - 1]?.length || 0,
isExported: true,
updatedAt: Date.now(),
};
this.nodes.push(node);
return node;
}
/** Last `.`-segment (`App.ViewModels.RegisterModel` → `RegisterModel`). */
private lastSegment(s: string): string {
const i = s.lastIndexOf('.');
return i >= 0 ? s.slice(i + 1) : s;
}
/**
* Split a type expression into the capitalized type names it contains — base
* type plus any generic arguments (`Bar<Foo, Baz>` → `Bar`, `Foo`, `Baz`),
* each reduced to its last namespace segment. Lowercase/keyword tokens drop out.
*/
private typeNames(expr: string): string[] {
const out: string[] = [];
for (const raw of expr.split(/[<>,\s]+/)) {
const seg = this.lastSegment(raw.trim());
if (/^[A-Z][A-Za-z0-9_]*$/.test(seg)) out.push(seg);
}
return out;
}
private pushRef(componentId: string, name: string, line: number, column: number): void {
this.unresolvedReferences.push({
fromNodeId: componentId,
referenceName: name,
referenceKind: 'references',
line,
column,
filePath: this.filePath,
language: 'razor',
});
}
private extractDirectives(componentId: string): void {
const lines = this.source.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i]!;
// `@model Foo` / `@inherits Bar<Foo>` — directive followed by a type.
const dir = line.match(/^\s*@(?:model|inherits)\s+([A-Za-z_][\w.]*(?:\s*<[^>]+>)?)/);
if (dir) for (const t of this.typeNames(dir[1]!)) this.pushRef(componentId, t, i + 1, 0);
// `@inject IService name` — the type is the first token, a name follows.
const inj = line.match(/^\s*@inject\s+([A-Za-z_][\w.]*(?:\s*<[^>]+>)?)\s+[A-Za-z_]/);
if (inj) for (const t of this.typeNames(inj[1]!)) this.pushRef(componentId, t, i + 1, 0);
// `@typeof(X)` anywhere on the line.
for (const m of line.matchAll(/@typeof\(\s*([A-Za-z_][\w.]*)\s*\)/g)) {
const seg = this.lastSegment(m[1]!);
if (/^[A-Z]/.test(seg)) this.pushRef(componentId, seg, i + 1, m.index ?? 0);
}
}
}
private extractComponentTags(componentId: string): void {
const lines = this.source.split('\n');
// PascalCase opening / self-closing tags. Closing tags (`</Foo>`) start with
// `</` and are skipped. HTML elements are lowercase → never match.
const tagRe = /<([A-Z][A-Za-z0-9_]*)\b([^>]*)>/g;
for (let i = 0; i < lines.length; i++) {
const line = lines[i]!;
let m: RegExpExecArray | null;
while ((m = tagRe.exec(line)) !== null) {
const name = m[1]!;
if (BLAZOR_BUILTIN_COMPONENTS.has(name)) continue;
this.pushRef(componentId, name, i + 1, m.index + 1);
// Generic component type arg: `<Grid TItem="CatalogItem">`.
for (const t of (m[2] || '').matchAll(/\bT[A-Za-z]*\s*=\s*"([A-Za-z_][\w.]*)"/g)) {
const seg = this.lastSegment(t[1]!);
if (/^[A-Z]/.test(seg)) this.pushRef(componentId, seg, i + 1, 0);
}
}
}
}
}
+5
View File
@@ -20,6 +20,7 @@ import { generateNodeId, getNodeText, getChildByField, getPrecedingDocstring } f
import type { LanguageExtractor, ExtractorContext } from './tree-sitter-types';
import { EXTRACTORS } from './languages';
import { LiquidExtractor } from './liquid-extractor';
import { RazorExtractor } from './razor-extractor';
import { SvelteExtractor } from './svelte-extractor';
import { DfmExtractor } from './dfm-extractor';
import { VueExtractor } from './vue-extractor';
@@ -4032,6 +4033,10 @@ export function extractFromSource(
// Use custom extractor for Liquid
const extractor = new LiquidExtractor(filePath, source);
result = extractor.extract();
} else if (detectedLanguage === 'razor') {
// Use custom extractor for ASP.NET Razor (.cshtml) / Blazor (.razor) markup
const extractor = new RazorExtractor(filePath, source);
result = extractor.extract();
} else if (detectedLanguage === 'xml') {
// Custom extractor for MyBatis mapper XML. Non-mapper XML returns just a
// file node so the watcher tracks it without emitting symbols.
+3
View File
@@ -115,6 +115,9 @@ const LANGUAGE_FAMILY: Record<string, string> = {
swift: 'apple', objc: 'apple',
typescript: 'web', tsx: 'web', javascript: 'web', jsx: 'web',
c: 'c', cpp: 'c',
// Razor/Blazor markup names C# types — same family so `@model Foo` /
// `<MyComponent/>` resolve to their `.cs` class through the cross-family gate.
csharp: 'dotnet', razor: 'dotnet',
};
export function sameLanguageFamily(a: string, b: string): boolean {
if (a === b) return true;
+1
View File
@@ -75,6 +75,7 @@ export const LANGUAGES = [
'c',
'cpp',
'csharp',
'razor',
'php',
'ruby',
'swift',