Repository navigation
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>
chsienki
marked this pull request as draft
May 13, 2026 23:41
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>
Two paired changes that flip the split from "decl phase populates a sidecar" to "decl + impl emitted as separate .g.cs files": 1. Teach DefaultRazorCSharpLoweringPhase to fork on GetDeclCSharpDocument() being non-null. When the decl phase produced its half, this phase produces the matching impl half -- a minimal partial class containing just the render method body and any IsSynthesizedHelper plumbing lifted from the primary class / namespace, plus IsGenericTyped helper namespaces lifted as siblings. The impl synthetic spine is built brand-new (not by mutating the original IR) so callers that walk documentNode continue to see its pre-split shape. The fork is gated on the decl document specifically rather than any pre-existing csharpDocument, so a future test or caller that pre-populates only the impl half doesn'"'"'t accidentally suppress this phase. When no decl document is present (non-component, suppressed primary method body, malformed primary structure) the phase falls through to its original single-file lowering path. 2. Thread declCSharpDocument through RazorSourceGenerator'"'"'s post-codegen projection tuple, teach the cache comparer to short-circuit when either document carries diagnostics and to compare both texts, and update the RegisterImplementationSourceOutput callback to report decl diagnostics and emit the decl half as a separate generated source. When the decl phase produced no document only the single impl output is emitted, preserving prior behavior. The decl half is named via GetDeclIdentifierFromHintName, which substitutes ".decl.g.cs" for the trailing ".g.cs" of the impl hint name (e.g. Component1_razor.g.cs -> Component1_razor.decl.g.cs). Both halves keep the .g.cs suffix that the editor and MSBuild use to identify generated files, without stacking it. The CloneContainer helpers used to build synthetic spines move from DefaultRazorDeclCSharpLoweringPhase to RazorCSharpDocumentWriter so both consumers (the decl phase and the new impl-clone path) share one copy. Includes the Inject SG test as a representative baseline: bumps its GeneratedSources count from 1 to 2 and adds the new decl baseline; the existing impl baseline is trimmed to just the BuildRenderTree wrapper. The remaining ~290 component-test count assertions and decl baselines are follow-up commits. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Razor source generator now emits two files per component (impl + decl), but the mapping layer between Roslyn and the Razor source still assumed one generated document per .razor file. That left the decl half invisible to feature round-trips: any IDE feature that takes a position in the decl generated document and tries to map it back to the original .razor source (go-to-def, find-references, hover, diagnostics translation, code excerpts) silently failed. * RazorSourceGenerator now also registers the decl hint name in hintNameToFilePath so reverse lookups (decl hint -> Razor file path) succeed. * Adds RazorCodeDocument.GetCSharpDocumentForHintName(hintName) extension that returns the decl half when the hint name has the .decl.g.cs suffix, otherwise the impl. Mapping callers receive a hint name from Roslyn and need to consult the matching half so source mappings line up with positions in that generated document. * Updates the two mapping entry points -- RemoteDocumentMappingService.TryMapToHostDocumentRangeAsync and RemoteSpanMappingService.TryExcerptAsync / MapSpansAsync -- to thread the hint name through and pick the correct generated document via the new helper. Other consumers of GetRequiredCSharpDocument operate on impl-side positions only (formatting, completion, hover invoked at user cursor positions, etc.) and don't need the dispatch -- the impl is the right answer for them. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The baseline assertion and compilation helpers in
RazorBaselineIntegrationTestBase / RazorIntegrationTestBase used to
treat csharpDocument as the canonical full generated source. With the
decl/impl split, csharpDocument is just the impl half and
declCSharpDocument carries the decl half; both halves need to be
visible to baseline assertions and to the C# compilations the test
harness builds.
Test base changes:
* RazorIntegrationTestBase.CompileToCSharp adds both halves to
AdditionalSyntaxTrees with distinct paths so file-local types
(e.g. __PrivateComponentRenderModeAttribute) stay unambiguous.
CompileToAssembly does the same when consuming a CompileToCSharpResult.
* CompileToCSharpResult gets two new properties:
* DeclCode -- the decl half text, null when the document wasn't split
* AllCode -- impl + decl concatenation, for assertions that need to
look across both halves (e.g. Assert.Contains for content that may
have moved to the decl half).
* New AddGeneratedSyntaxTrees(result, primaryPath?) helper for
multi-step tests that compose generated output into a downstream
compilation.
* RazorBaselineIntegrationTestBase.AssertCSharpDocumentMatchesBaseline
extracts per-half assertion logic into AssertCSharpHalfMatchesBaseline.
Always asserts the impl half against the existing
.codegen.cs / .diagnostics.txt / .mappings.txt baselines; when the
document was split, also asserts the decl half against
.decl.codegen.cs / .decl.diagnostics.txt / .decl.mappings.txt. The
decl writer suppresses diagnostic reporting (the impl writer reports
the full seeded set), so the .decl.diagnostics.txt baseline is never
produced.
Caller updates in ComponentCodeGenerationTestBase:
* Four multi-step tests (GenericComponent_GenericChildContent_*,
GenericInheritsTagHelper, GenericComponentInherits_TypeArguments)
switch their AdditionalSyntaxTrees.Add(Parse(generated.Code)) calls to
AddGeneratedSyntaxTrees(generated) so the decl half also makes it into
the downstream compilation.
* AddComponentParameter and GenericInference_DynamicallyAccessedMembers
_01/_02 switch from generated.Code to generated.AllCode -- the content
they search for now lives in the decl half.
The two representative .decl.codegen.cs baselines added previously
(Component_WithDocType, RenderMode_Directive_FullyQualified) are now
actively validated by the split-aware assertion. Other ~290 component
baselines still need regenerating; that is a follow-up commit.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Updates the SG and integration test suites to recognize that
splittable components now produce two generated sources (impl + decl)
instead of one. Where a test asserts on count, content, diagnostics,
or generated-source ordering, it learns the new shape.
* RazorSourceGeneratorComponentTests:
* Adds three regression tests -- DeclImplSplit_ProducesTwoFilesPerComponent,
DeclImplSplit_DoesNotApplyToCshtml,
DeclImplSplit_DocumentLevelDiagnosticReportedOnce -- pinning the
behavior the rest of the changes assume.
* Bumps Assert.Single / Assert.Equal(N, ...) count assertions on
component tests to account for the extra decl source.
* RazorSourceGeneratorTests / RazorSourceGeneratorTestsBase:
* Adds ImplGeneratedSources() / DeclGeneratedSources() extension
helpers so tests can grab the half they care about.
* SourceGenerator_UppercaseRazor_GeneratesComponent and
IncrementalCompilation_RazorFiles_WhenChildComponentsAreAdded
are updated as representative examples of the inline expected-text
shape: each VerifyPageOutput call now passes one expected string
per generated source (impl + decl), and VerifyOutputsMatch diff
indices are updated for the new source positions. The remaining
inline-expected SG tests get the same treatment as part of the
bulk baseline regen commit.
* WhenProjectReferencesChange expects the duplicate compile
diagnostic now produced because the offending base type appears
in both partial halves of the declaration -- marked PROTOTYPE
because the duplication is a temporary issue to fix before main.
* RazorSourceGeneratorCshtmlTests / RazorSourceGeneratorTagHelperTests:
Apply the new assertion helpers where mixed-document tests need the
impl half specifically.
* ComponentCodeGenerationTestBase / ComponentRenderModeDirectiveIntegrationTests:
Update a handful of tests where the generated diagnostic location
shifts because the partial class header (or the synthesized
__PrivateComponentRenderModeAttribute helper) lives in a different
file path under the split. PROTOTYPE markers note the line-mapping
and duplicate-diagnostic gaps that need to be fixed before main.
The bulk component baselines (~290 .codegen.cs / .mappings.txt
regenerations and matching .decl.codegen.cs / .decl.mappings.txt
additions) plus the matching SG decl baselines and the inline
expected-text updates for the remaining 21+ SG tests land in a
follow-up commit -- so most affected tests will fail at this commit
until those baselines arrive.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…split The bulk regen step that finishes the decl/impl split work: * Regenerate ~290 component .codegen.cs and .mappings.txt baselines to reflect the impl half (now without @code blocks, properties, etc., which moved to the decl half). * Add ~290 matching .decl.codegen.cs and .decl.mappings.txt baselines carrying the user-API-surface content the impl half no longer holds. Remove ~14 .mappings.txt files whose impl halves no longer have source-mapped content. * Add the matching SG decl baselines (.razor.decl.g.cs) for the component SG tests, and trim the existing .razor.g.cs impl baselines for tests where the impl half is now smaller. * Update the remaining inline-expected SG tests in RazorSourceGeneratorTests.cs to pass the full impl + decl expected text per generated source. VerifyOutputsMatch callers that previously diff'd a single position now supply diffs at the post-split positions for both halves of the changed component. The test helpers in RazorSourceGeneratorTestsBase.cs are unchanged from the previous commit -- the simple shape (strict positional comparison in VerifyPageOutput, strict positional pair-by-position with optional diff overrides in VerifyOutputsMatch) is what the test author works against. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
chsienki
force-pushed
the
sonic/3_partial_decls
branch
from
May 14, 2026 02:02
87ec53b to
0cc4b1b
Compare
Member
Author
|
Replaced by #83688 |
chsienki
added a commit
that referenced
this pull request
May 14, 2026
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is the last of the really big sonic PRs, and despite the file count, I promise this one is actually smaller than the previous one.
This PR splits the generation of razor components into two files: a declaration file and an implementation file. The declaration is just the public API shape of the component (broadly, the stuff inside a @code block). The implementation file contains the
BuildRenderTreemethod (broadly, the html/razor stuff in a file).The key insight is that we can generate the declaration of a component without needing to know what tag helpers the file uses, as they are only used inside the
BuildRenderTreemethod. This PR doesn't take advantage of that yet, it just generates the decl, then immediately generates the impl, but they are in different stages so that we can move it earlier in the next PR.One wrinkle is right now, due to some code being shared between the two files, if there is a diagnostic on the shared code you end up with duplicate diagnostics (one from each file). It's really, really expensive to fix that until we've done some of the structural moves in the future PR, so instead I've opted to just call it out with a
//PROTOTYPEcomment. As such this PR can't go into main as it's not a finished product. I've made a feature branch where we can merge this PR, then merge the follow ups onto, to address the//PROTOTYPEcomments. I think Roslyn even has a CI check that there aren't//PROTOTYPEcomments in main, so we can't accidentally merge until its done.I highly recommend a commit-by-commit review of this PR. I've tried to structure it so that it follows logically with each step building on the one before.
Most of the 1000+ changes in this PR are just test baseline updates. Rather than update them at each commit and make noisy diff's, I've just updated 1 or 2 representative tests with each change so you can see the difference without having to crawl through 100s of files. The very final commit just updates all the baselines to match, so feel free to just skim that, there shouldn't be anything new in them that you didn't already see in earlier commits.
CI Toolset build: https://dev.azure.com/dnceng/internal/_build/results?buildId=2974324&view=results
Microsoft Reviewers: Open in CodeFlow