import { Node, Edge, ExtractionResult, ExtractionError, UnresolvedReference } from '../types'; import { generateNodeId } from './tree-sitter-helpers'; import { TreeSitterExtractor } from './tree-sitter'; import { isLanguageSupported } from './grammars'; /** * 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` → the view-model / base type (.cshtml + .razor) * - `@inject IService svc` → the injected service type * - `@typeof(MainLayout)` → the referenced type * - `` (Blazor only) → the component class (.razor or `.cs : ComponentBase`) * - `` → 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 edges: Edge[] = []; 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); } // Delegate the C# in `@code { }` / `@functions { }` / `@{ }` blocks to the // C# tree-sitter extractor (the Blazor analog of Svelte's