Skip to content

Sonic 3.1: Generate Decl Files - #83688

Merged
chsienki merged 5 commits into
dotnet:features/sonicfrom
chsienki:sonic/3_1_generate_only_decls
May 14, 2026
Merged

Sonic 3.1: Generate Decl Files#83688
chsienki merged 5 commits into
dotnet:features/sonicfrom
chsienki:sonic/3_1_generate_only_decls

Conversation

@chsienki

@chsienki chsienki commented May 14, 2026

Copy link
Copy Markdown
Member

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

chsienki and others added 5 commits May 12, 2026 14:11
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>
Copilot AI review requested due to automatic review settings May 14, 2026 04:48
@chsienki
chsienki requested a review from a team as a code owner May 14, 2026 04:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 decl RazorCSharpDocument.
  • Extract RazorCSharpDocumentWriter and add IntermediateNode.IsSynthesizedHelper (set on the render-mode helper class and its attribute) plus a reportDiagnostics knob on CodeRenderingContext to suppress duplicate diagnostics.
  • Add _declCSharpDocument storage and Get/WithDeclCSharpDocument accessors on RazorCodeDocument; register the new phase in RazorProjectEngine and assert it in RazorProjectEngineTest.

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.

Comment on lines +125 to +136
// 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.
Comment on lines +115 to +123
foreach (var docChild in documentNode.Children)
{
if (docChild is NamespaceDeclarationIntermediateNode { IsGenericTyped: true })
{
continue;
}

declDocNode.Children.Add(docChild == primaryNamespace ? declNamespace : docChild);
}

@davidwengier davidwengier left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense so far

/// 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; }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this speaks to my previous comment: If we tag these with IsSynthesizedHelper, they don't need special handling, right?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep that sounds sensible. I'll open an issue to follow up on that as its slightly orthogonal to this change.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filed as #83891.

@chsienki
chsienki merged commit 3bd751d into dotnet:features/sonic May 14, 2026
25 checks passed
chsienki added a commit that referenced this pull request May 29, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants