Sonic 3.1: Generate Decl Files - #83688
Conversation
Threads a second optional RazorCSharpDocument? _declCSharpDocument field through the constructor, the Create factory, and every existing With* method, and exposes internal GetDeclCSharpDocument() and WithDeclCSharpDocument(RazorCSharpDocument). Pure infrastructure: the slot is null today. The upcoming DefaultRazorDeclCSharpLoweringPhase (Sonic part 3) will populate it for components. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Lifts WriteDocument and the private Visitor nested class out of DefaultRazorCSharpLoweringPhase into a new internal static RazorCSharpDocumentWriter so neither C# lowering phase owns it. The impl phase shrinks back to a thin wrapper that calls RazorCSharpDocumentWriter.Write(...). While moving, the entry point gains an explicit DocumentIntermediateNode parameter (instead of fetching it from the codeDocument internally) so callers can lower a tree they have already prepared -- the upcoming DefaultRazorDeclCSharpLoweringPhase needs that to write the decl half from a tree it has stripped of the primary render method. Pure refactor: no semantic change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds bool reportDiagnostics = true parameters to the CodeRenderingContext constructor and RazorCSharpDocumentWriter.Write entry point. When false, the constructor skips collecting diagnostics from documentNode via GetAllDiagnostics(); when true (the default), behavior is unchanged. Pure infrastructure for the upcoming DefaultRazorDeclCSharpLoweringPhase (Sonic part 3). The decl write needs to suppress diagnostic collection so any diagnostics attached to nodes shared between decl + impl synthetic trees aren't reported twice; the impl write keeps the default true. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds an internal IsSynthesizedHelper init-only bool on the base IntermediateNode so any node type can be flagged as compiler plumbing rather than content derived from user-authored source. ComponentRenderModeDirectivePass sets it on both nodes it synthesizes: the __PrivateComponentRenderModeAttribute helper class and the [__PrivateComponentRenderModeAttribute] decoration it adds to the primary class. Together this lets the upcoming decl/impl partial-file split route both nodes to the impl half so they stay colocated -- which keeps the file-scoped variant (Razor 11 + generic + C# 11) in a single file and keeps the nested variant'"'"'s decoration with its definition. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds DefaultRazorDeclCSharpLoweringPhase, registered immediately before
DefaultRazorCSharpLoweringPhase. For Razor components whose primary method
body is not being suppressed, the phase produces just the decl half --
the user'"'"'s component API surface, free of render-method body and any
compiler-synthesized plumbing -- by constructing a synthetic
DocumentIntermediateNode spine that shares children with the original
tree by reference. The original IR tree is never mutated, so callers
that walk it (IDE/cohosting code paths like ComponentNamespaceMatches
and ExtractToCodeBehindCodeActionResolver) continue to see the
pre-split shape.
The decl half is everything reachable from the user'"'"'s component API
surface: the partial class declaration with type parameters, base type,
interfaces, user-authored class-level attributes, properties, fields,
parameters, inject members, sibling methods, and document-level
metadata. It deliberately excludes:
- the render method body
- any IsSynthesizedHelper node (the
__PrivateComponentRenderModeAttribute helper class together with its
[__PrivateComponentRenderModeAttribute] decoration)
- IsGenericTyped helper namespaces (__Blazor.X.Y.TypeInference)
Routing all those into the impl half keeps the file-scoped rendermode
variant in a single file, keeps the nested variant'"'"'s decoration
colocated with its definition, and -- importantly -- means the decl
synthesis depends only on user source. It does not need tag helpers
to have been resolved, so the phase can be moved earlier in the
pipeline in future Sonic stages without any further restructuring.
Diagnostics attached to documentNode / primaryNamespace / primaryClass
themselves are seeded onto the synthetic root so they still surface.
The decl writer suppresses diagnostic collection because every
diagnostic on the decl tree is also reachable from the original tree
the impl half lowers from; letting both writers report would surface
every Razor-detected issue twice.
This commit is infrastructure only: GetDeclCSharpDocument() is now
populated for splittable components, but no consumer reads it yet and
GetCSharpDocument() is unaffected (DefaultRazorCSharpLoweringPhase
still produces the original single-file output from the unmutated IR).
A follow-up commit teaches the lowering phase to switch to producing
the impl half when a decl document is present, and updates the source
generator to emit both halves as separate .g.cs files.
Two representative .decl.codegen.cs baselines are included as
illustrations (the impl baselines are unchanged from main, since
GetCSharpDocument() output is unchanged at this commit):
* Component_WithDocType -- the simplest case: a minimal API-surface
file containing just the partial class header.
* RenderMode_Directive_FullyQualified -- a component with @rendermode:
decl omits both the helper class and its decoration. The remaining
component baselines and the test-infra change that wires
.decl.codegen.cs comparisons in are follow-up commits.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR is the first installment of the "Sonic 3" component-codegen split for Razor. It introduces a new lowering phase that produces a "decl" C# document (the partial-class API surface for a Razor component) alongside the existing single-file lowering, but does not yet redirect tooling to use it. A reusable RazorCSharpDocumentWriter is extracted from DefaultRazorCSharpLoweringPhase so both lowering phases share the visitor logic, and RazorCodeDocument gains a parallel _declCSharpDocument slot. New baselines exercise the produced decl content.
Changes:
- Add
DefaultRazorDeclCSharpLoweringPhase(component-only; no-op when the document isn't splittable) that builds a synthetic doc/namespace/class spine excluding the render method, synthesized helper nodes, and generic-typed helper namespaces, then writes a declRazorCSharpDocument. - Extract
RazorCSharpDocumentWriterand addIntermediateNode.IsSynthesizedHelper(set on the render-mode helper class and its attribute) plus areportDiagnosticsknob onCodeRenderingContextto suppress duplicate diagnostics. - Add
_declCSharpDocumentstorage andGet/WithDeclCSharpDocumentaccessors onRazorCodeDocument; register the new phase inRazorProjectEngineand assert it inRazorProjectEngineTest.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorProjectEngine.cs | Inserts the new decl lowering phase before the existing C# lowering phase. |
| src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCSharpDocumentWriter.cs | New shared writer that turns a DocumentIntermediateNode into a RazorCSharpDocument; called by both lowering phases. |
| src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorCSharpLoweringPhase.cs | Removes the inlined visitor/writer and delegates to RazorCSharpDocumentWriter.Write. |
| src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorDeclCSharpLoweringPhase.cs | New phase that builds the decl synthetic IR tree and stashes the resulting decl C# document on the code document. |
| src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCodeDocument.cs | Adds _declCSharpDocument field plus Get/WithDeclCSharpDocument and threads it through every existing With* clone. |
| src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/IntermediateNode.cs | Adds IsSynthesizedHelper flag used to mark compiler-plumbing nodes the decl writer should skip. |
| src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/ClassDeclarationIntermediateNode.cs | Trivial blank-line addition. |
| src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentRenderModeDirectivePass.cs | Marks the synthesized render-mode helper class and attribute node with IsSynthesizedHelper. |
| src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/CodeGeneration/CodeRenderingContext.cs | Adds reportDiagnostics constructor option to suppress the initial diagnostic copy. |
| .../ComponentCodeGenerationTest/RenderMode_Directive_FullyQualified/TestComponent.decl.codegen.cs | New decl-output baseline for the render-mode test. |
| .../ComponentCodeGenerationTest/Component_WithDocType/TestComponent.decl.codegen.cs | New decl-output baseline for the doctype test. |
| src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/RazorProjectEngineTest.cs | Updates the default-phase assertion to include the new decl phase. |
| // Seed the decl root with the full diagnostic set (deduped by checksum) so any | ||
| // diagnostics attached to documentNode / primaryNamespace / primaryClass themselves | ||
| // -- which aren't reachable from the synthetic clone -- still surface on the decl | ||
| // document. | ||
| foreach (var diagnostic in documentNode.GetAllDiagnostics()) | ||
| { | ||
| declDocNode.AddDiagnostic(diagnostic); | ||
| } | ||
|
|
||
| // The decl writer suppresses diagnostic collection because every diagnostic on the | ||
| // decl tree is also reachable from the original tree the impl half lowers from; | ||
| // letting both writers report would surface every Razor-detected issue twice. |
| foreach (var docChild in documentNode.Children) | ||
| { | ||
| if (docChild is NamespaceDeclarationIntermediateNode { IsGenericTyped: true }) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| declDocNode.Children.Add(docChild == primaryNamespace ? declNamespace : docChild); | ||
| } |
| /// surface" from "generator plumbing" -- such as the decl/impl partial-file split | ||
| /// for components -- use this flag to decide where a node belongs. | ||
| /// </summary> | ||
| internal bool IsSynthesizedHelper { get; init; } |
There was a problem hiding this comment.
Should we follow up to add this to things like the type inference helpers just so its used consistently?
| // and skip nodes that belong in the impl half: | ||
| // - renderMethod | ||
| // - any IsSynthesizedHelper node (compiler plumbing) | ||
| // - IsGenericTyped namespaces (type-inference helpers) |
There was a problem hiding this comment.
I think this speaks to my previous comment: If we tag these with IsSynthesizedHelper, they don't need special handling, right?
There was a problem hiding this comment.
Yep that sounds sensible. I'll open an issue to follow up on that as its slightly orthogonal to this change.
Sonic 3.2: activate the decl/impl split. Follows on from #83688 (Sonic 3.1: Generate Decl Files), which only landed the infrastructure to produce a second (decl) C# document. This PR brings over the remaining commits from the original work branch that switch the Razor files supporting the split from emitting a single `main` document to emitting an `impl` document alongside the `decl` document, and updates tooling/tests to be aware of the split. `backup/sonic-3.2-with-baselines` (sha `997fdab148f`) and will be re-applied before this work ships. 1. `Activate the decl/impl split in the lowering phase and source generator` -- turns on the split in the lowering pipeline and wires up the source generator to emit both documents. 2. `Make IDE/cohosting mapping layer split-aware` -- updates `RemoteDocumentMappingService` / `RemoteSpanMappingService` and related extensions so source mappings consider both documents. 3. `Make integration test infrastructure split-aware` -- updates `IntegrationTestBase` / `RazorBaselineIntegrationTestBase` / `RazorIntegrationTestBase` to handle decl + impl baselines. 4. `Update tests for the decl/impl split` -- updates `ComponentCodeGenerationTestBase` and related compiler tests.
This is a smaller subset of #83681 that stops at the point where we are generating the decl file, but not yet generating the impl file, or replacing the regular C# file with it.
It has a couple of new tests with baselines that show the state of the decl doc.
Once we have this in, we can add the commits that switch over to generating the impl docs, and flesh out how the tooling works with multiple files.
Microsoft Reviewers: Open in CodeFlow