Skip to content

Sonic 3.2: Activate the decl/impl split - #83887

Merged
chsienki merged 13 commits into
dotnet:features/sonicfrom
chsienki:sonic/3_2_activate_impl_decl_split
May 29, 2026
Merged

Sonic 3.2: Activate the decl/impl split#83887
chsienki merged 13 commits into
dotnet:features/sonicfrom
chsienki:sonic/3_2_activate_impl_decl_split

Conversation

@chsienki

@chsienki chsienki commented May 26, 2026

Copy link
Copy Markdown
Member

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.

Important

Baseline regeneration commit intentionally omitted

The original 5th commit Regenerate component baselines and update remaining SG tests for the split (1,233 files, ~17K insertions of regenerated *.decl.codegen.cs / *.decl.mappings.txt baselines) has been dropped from this PR at reviewer request so the diff stays reviewable.

Consequence: compiler integration tests that diff against on-disk codegen / mapping baselines will fail in CI, because the source generator now emits a split decl/impl pair while the baselines on disk still reflect the pre-split single-document format. These failures are expected and known.

The dropped commit is preserved on branch backup/sonic-3.2-with-baselines (sha 997fdab148f) and will be re-applied before this work ships.

Commits (cherry-picked from sonic/3_partial_decls)

  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.

Build

Verified locally:

  • Microsoft.CodeAnalysis.Razor.Compiler -- builds clean
  • Microsoft.AspNetCore.Razor.Language.UnitTests -- builds clean
  • Microsoft.NET.Sdk.Razor.SourceGenerators.UnitTests -- builds clean
  • Microsoft.CodeAnalysis.Razor.Workspaces -- builds clean
  • Microsoft.CodeAnalysis.Remote.Razor -- builds clean

Tests not run locally yet -- relying on CI.

Deferred IDE work: 209 cohosting tests skipped with PROTOTYPE markers

The 4 commits above are sufficient to activate the decl/impl split for the compiler / source generator. However the cohosting layer in the IDE builds its own local view of the generated C# (GetCSharpSourceText) that is no longer in sync with what the source generator now produces. 209 cohosting tests across 34 test classes fail as a result.

Rather than re-flatten the documents for the IDE (which would defeat the purpose of the split and prevent the next Sonic phases from running per-document analysis), this PR marks the affected tests with Skip = "PROTOTYPE(sonic): cohosting feature not yet decl/impl split aware; see PR #83887".

Each skipped test is a hand-off to the IDE migration follow-up PR(s). To find the work, grep -r "PROTOTYPE(sonic)" src/Razor.

Distribution by test class (distinct methods skipped):

Feature area Test class Methods
Formatting OnTypeFormattingTest 19
Formatting DocumentFormattingTest 2
Code actions GenerateMethodTests 16
Code actions CSharpCodeActionTests 14
Code actions GenerateConstructorTests 10
Code actions CohostRoslynCodeActionTest 10
Code actions GenerateEventHandlerTests 9
Code actions AddUsingTests 8
Code actions GenerateTypeTests 7
Code actions GenerateFieldTests 4
Code actions GenerateDeconstructMethodTests 3
Code actions GenerateConversionTests 3
Code actions GeneratePropertyTests 2
Code actions OrganizeUsingsCommandTest 12
Diagnostics CohostDocumentPullDiagnosticsTest 10
Semantic tokens CohostSemanticTokensRangeEndpointTest 6
Call hierarchy CohostCallHierarchyEndpointTest 12
Rename CohostRenameEndpointTest 11
Rename CohostRoslynRenameTest 3
Rename CohostPrepareRenameEndpointTest 1
Document symbols CohostDocumentSymbolEndpointTest 6
Folding CohostFoldingRangeEndpointTest 6
Auto-insert CohostOnAutoInsertEndpointTest 6
Highlight CohostDocumentHighlightEndpointTest 4
Inlay hints CohostInlayHintEndpointTest 4
Go to def / impl CohostGoToDefinitionEndpointTest 3
Go to def / impl CohostGoToImplementationEndpointTest 3
Go to def / impl CohostRoslynGoToDefTest 1
Completion CohostDocumentCompletionEndpointTest 3
Completion CohostInlineCompletionEndpointTest 2
Spell check CohostDocumentSpellCheckEndpointTest 2
Debug info RemoteDebugInfoServiceTest 3
Selection range CohostSelectionRangeEndpointTest 2
Find references CohostFindAllReferencesEndpointTest 2
Total 34 classes 209
Microsoft Reviewers: Open in CodeFlow

chsienki and others added 4 commits May 26, 2026 12:15
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>
chsienki and others added 2 commits May 27, 2026 11:51
The Correctness_Analyzers leg was failing because four calls to
string.EndsWith(string) in the new decl/impl-split test code did not
specify a StringComparison. Add StringComparison.Ordinal -- the
matched value is a literal file-extension suffix (.decl.g.cs) where
ordinal comparison is the correct semantic and matches the existing
convention in adjacent EndsWith calls in these files.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PROTOTYPE(sonic): The cohosting feature stack currently builds its own
view of the C# document text (GetCSharpSourceText) that does not match
the decl/impl-split text produced by the source generator. This mismatch
causes formatting, code actions, completion, rename, and several other
cohosting endpoint tests to fail.

Per-feature migration to the split layout will follow in subsequent PRs.
Until then, mark the 209 affected test methods with a Skip reason that
clearly identifies the deferred work for follow-up authors and reviewers.

No production code is changed. All Skip strings carry the marker
'PROTOTYPE(sonic):' so they can be grepped and removed once each
feature is migrated.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@chsienki
chsienki force-pushed the sonic/3_2_activate_impl_decl_split branch from 10913b1 to c999634 Compare May 27, 2026 18:54
@chsienki
chsienki marked this pull request as ready for review May 27, 2026 19:02
@chsienki
chsienki requested a review from a team as a code owner May 27, 2026 19:02
Copilot AI review requested due to automatic review settings May 27, 2026 19:02

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 switches Razor component code generation from a single generated C# document to a decl/impl split (a decl half plus an impl half), then updates span/document mapping and test infrastructure so both halves participate correctly in compilations and baseline verification.

Changes:

  • Emit and propagate both impl and decl generated C# halves (including hint-name handling).
  • Update remote mapping services and test compilation helpers to select the correct half and add both syntax trees when needed.
  • Adjust / add tests and baselines for the split (and mark a large set of cohosting tests as skipped while cohosting catches up).

Reviewed changes

Copilot reviewed 54 out of 54 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/Razor/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/RazorIntegrationTestBase.cs Adds decl awareness to integration-test compilation and exposes DeclCode/AllCode.
src/Razor/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/RazorBaselineIntegrationTestBase.cs Baseline verification updated to validate impl + decl halves independently.
src/Razor/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntegrationTestBase.cs Includes decl syntax tree when compiling generated output to an assembly.
src/Razor/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.UnitTests/Cohost/RemoteDebugInfoServiceTest.cs Skips cohosting debug-info tests pending decl/impl split support.
src/Razor/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.UnitTests/Cohost/OrganizeUsingsCommandTest.cs Skips cohosting organize-usings tests pending split support.
src/Razor/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.UnitTests/Cohost/Formatting/OnTypeFormattingTest.cs Skips cohosting on-type formatting tests pending split support.
src/Razor/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.UnitTests/Cohost/CohostRoslynRenameTest.cs Skips cohosting rename tests pending split support.
src/Razor/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.UnitTests/Cohost/CohostRoslynGoToDefTest.cs Skips cohosting go-to-def tests pending split support.
src/Razor/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.UnitTests/Cohost/CohostRoslynCodeActionTest.cs Skips cohosting Roslyn code action tests pending split support.
src/Razor/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.UnitTests/Cohost/CohostInlineCompletionEndpointTest.cs Skips cohosting inline completion tests pending split support.
src/Razor/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.UnitTests/Cohost/CohostDocumentSpellCheckEndpointTest.cs Skips cohosting spell check tests pending split support.
src/Razor/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.UnitTests/Cohost/CohostDocumentPullDiagnosticsTest.cs Skips cohosting pull-diagnostics tests pending split support.
src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/Formatting/DocumentFormattingTest.cs Skips shared cohosting formatting tests pending split support.
src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/Endpoints/CohostSemanticTokensRangeEndpointTest.cs Skips shared cohosting semantic-tokens tests pending split support.
src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/Endpoints/CohostSelectionRangeEndpointTest.cs Skips shared cohosting selection-range tests pending split support.
src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/Endpoints/CohostRenameEndpointTest.cs Skips shared cohosting rename endpoint tests pending split support.
src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/Endpoints/CohostPrepareRenameEndpointTest.cs Skips shared cohosting prepare-rename tests pending split support.
src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/Endpoints/CohostOnAutoInsertEndpointTest.cs Skips shared cohosting auto-insert tests pending split support.
src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/Endpoints/CohostInlayHintEndpointTest.cs Skips shared cohosting inlay-hints tests pending split support.
src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/Endpoints/CohostGoToImplementationEndpointTest.cs Skips shared cohosting go-to-implementation tests pending split support.
src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/Endpoints/CohostGoToDefinitionEndpointTest.cs Skips shared cohosting go-to-definition tests pending split support.
src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/Endpoints/CohostFoldingRangeEndpointTest.cs Skips shared cohosting folding-range tests pending split support.
src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/Endpoints/CohostFindAllReferencesEndpointTest.cs Skips shared cohosting find-references tests pending split support.
src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/Endpoints/CohostDocumentSymbolEndpointTest.cs Skips shared cohosting document-symbol tests pending split support.
src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/Endpoints/CohostDocumentPullDiagnosticsTest.cs Skips shared cohosting pull-diagnostics tests pending split support.
src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/Endpoints/CohostDocumentHighlightEndpointTest.cs Skips shared cohosting document-highlight tests pending split support.
src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/Endpoints/CohostDocumentCompletionEndpointTest.cs Skips shared cohosting completion tests pending split support.
src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/Endpoints/CohostCallHierarchyEndpointTest.cs Skips shared cohosting call-hierarchy tests pending split support.
src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/CodeActions/GenerateTypeTests.cs Skips shared cohosting generate-type code action tests pending split support.
src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/CodeActions/GeneratePropertyTests.cs Skips shared cohosting generate-property tests pending split support.
src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/CodeActions/GenerateMethodTests.cs Skips shared cohosting generate-method tests pending split support.
src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/CodeActions/GenerateFieldTests.cs Skips shared cohosting generate-field tests pending split support.
src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/CodeActions/GenerateEventHandlerTests.cs Skips shared cohosting event-handler code action tests pending split support.
src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/CodeActions/GenerateDeconstructMethodTests.cs Skips shared cohosting deconstruct-method tests pending split support.
src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/CodeActions/GenerateConversionTests.cs Skips shared cohosting conversion tests pending split support.
src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/CodeActions/GenerateConstructorTests.cs Skips shared cohosting constructor tests pending split support.
src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/CodeActions/CSharpCodeActionTests.cs Skips shared cohosting C# code action tests pending split support.
src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.CohostingShared.UnitTests/CodeActions/AddUsingTests.cs Skips shared cohosting add-using tests pending split support.
src/Razor/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentMapping/RemoteSpanMappingService.cs Makes span mapping select mappings based on generated hint name (decl vs impl).
src/Razor/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentMapping/RemoteDocumentMappingService.cs Uses hint name to pick the right generated C# half for range mapping.
src/Razor/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions.cs Adds helper to choose the correct RazorCSharpDocument by hint name.
src/Razor/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.UnitTests/TestFiles/RazorSourceGeneratorComponentTests/Inject/Shared/Component1_razor.g.cs Updates impl baseline content after moving injected member to decl file.
src/Razor/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.UnitTests/TestFiles/RazorSourceGeneratorComponentTests/Inject/Shared/Component1_razor.decl.g.cs New decl baseline for split output.
src/Razor/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.UnitTests/RazorSourceGeneratorTestsBase.cs Adds helpers to filter impl/decl generated sources for tests.
src/Razor/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.UnitTests/RazorSourceGeneratorTests.cs Updates expected generated source counts and incremental assertions for split output.
src/Razor/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.UnitTests/RazorSourceGeneratorTagHelperTests.cs Updates assertions to target impl output where appropriate and adjusts expected counts.
src/Razor/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.UnitTests/RazorSourceGeneratorComponentTests.cs Updates expectations and adds split-specific regression tests.
src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.Helpers.cs Adds hint-name transformation for decl half (.g.cs.decl.g.cs).
src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.cs Emits decl source, compares decl/impl texts for caching, and records decl hint name mappings.
src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCSharpDocumentWriter.cs Centralizes shallow “container clone” helpers for decl/impl synthetic trees.
src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorDeclCSharpLoweringPhase.cs Produces decl synthetic tree + decl document and stores it on the code document.
src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorCSharpLoweringPhase.cs Produces impl synthetic tree when a decl document exists; otherwise falls back to single-file output.
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/IntegrationTests/ComponentRenderModeDirectiveIntegrationTests.cs Adjusts diagnostic expectations for decl/impl split (and adds deferred-work notes).
src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/IntegrationTests/ComponentCodeGenerationTestBase.cs Updates integration tests to compile with both halves and assert across AllCode.

Comment on lines 305 to +309
AdditionalSyntaxTrees.RemoveAll(st => st.FilePath == item.FilePath);
AdditionalSyntaxTrees.Add(syntaxTree);
var implTree = Parse(codeDocument.GetRequiredCSharpDocument().Text, csharpParseOptions, path: item.FilePath);
AdditionalSyntaxTrees.Add(implTree);
if (codeDocument.GetDeclCSharpDocument() is { } declDocument)
{
Comment on lines +81 to +83
// exactly one half. As a side effect there is no .decl.diagnostics.txt baseline
// to commit -- the regen path treats an empty diagnostics list as "delete the
// baseline file" and the decl half always has one.
Comment on lines +38 to +42
/// Returns the generated <see cref="RazorCSharpDocument"/> that corresponds to the given
/// generated-source hint name. For Razor components the generator can emit two halves:
/// an impl (under the original hint) and a decl (under <c>hintName + ".decl.g.cs"</c>).
/// Mapping callers receive a hint name from Roslyn and need to consult the matching half
/// so that source mappings line up with positions in that generated document.
if (codeDocument.GetDeclCSharpDocument() is not null &&
TryWriteImplDocument(documentNode, codeDocument, cancellationToken, out var implDocument))
{
return codeDocument.WithCSharpDocument(implDocument);

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'm reviewing commit-at-a-time, and I'm 99% sure this will be fixed in a future commit, but just noting here that its super weird this says WithCSharpDocument when its one of two.

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.

Okay, this didn't get changed, so I guess I'll be explicit: Consider a future change here to change the API so its explicit that methods that deal with documents are specific to a decl or impl. ie, WithImplCSharpDocument etc.

private async ValueTask<ImmutableArray<RazorMappedSpanResult>> MapSpansAsync(Solution solution, DocumentId generatedDocumentId, ImmutableArray<TextSpan> spans, CancellationToken cancellationToken)
{
var razorDocument = await TryGetRazorDocumentForGeneratedDocumentIdAsync(generatedDocumentId, solution, cancellationToken).ConfigureAwait(false);
var generatedDocument = await solution.GetSourceGeneratedDocumentAsync(generatedDocumentId, cancellationToken).ConfigureAwait(false);

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.

It is surprising to me that this code changed, rather than TryGetRazorDocumentForSourceGeneratedDocumentId getting updated

// Convenience for assertions that need to look across both halves of a split
// generated component (e.g. ``Assert.Contains("AddComponentParameter", AllCode)``).
// For non-split documents this is identical to <see cref="Code"/>.
public string AllCode => DeclCode is null ? Code : Code + DeclCode;

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 get the convenience of this, but I feel like long term we should get rid of it, and have things assert not just that the code exists, but its in the file it should exist in.

chsienki and others added 3 commits May 28, 2026 15:20
Addresses davidwengier's review comment on DefaultRazorCSharpLoweringPhase.cs:

> Consider a future change here to change the API so its explicit that
> methods that deal with documents are specific to a decl or impl. ie,
> `WithImplCSharpDocument` etc.

The decl-side methods on RazorCodeDocument (WithDeclCSharpDocument,
GetDeclCSharpDocument) were already explicit, but the impl-side methods
were not, which made code like

    return codeDocument.WithCSharpDocument(implDocument);

read as "wait, which document is this?". This commit makes the impl-side
methods symmetric with the decl-side methods:

  WithCSharpDocument        -> WithImplCSharpDocument
  GetCSharpDocument         -> GetImplCSharpDocument
  GetRequiredCSharpDocument -> GetRequiredImplCSharpDocument
  TryGetCSharpDocument      -> TryGetImplCSharpDocument

Pure rename across the codebase; no behavior change. Includes the
`<see cref>` in DefaultRazorDeclCSharpLoweringPhase.cs doc comment.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…Async

Addresses davidwengier's review comment on RemoteSpanMappingService.cs:

> It is surprising to me that this code changed, rather than
> `TryGetRazorDocumentForSourceGeneratedDocumentId` getting updated

The original split-aware change inlined the helper's body inside
TryExcerptAsync and MapSpansAsync so the SourceGeneratedDocument could be
surfaced (needed to read .HintName for picking impl vs decl). MapTextChangesAsync
still called the helper directly. The result was three callers with
inconsistent shapes.

This commit fixes the asymmetry by updating the helper itself:

  // before
  private async Task<TextDocument?> TryGetRazorDocumentForGeneratedDocumentIdAsync(...)

  // after
  private async Task<(SourceGeneratedDocument Generated, TextDocument Razor)?>
      TryGetRazorDocumentForGeneratedDocumentIdAsync(...)

All three callers now consume the helper uniformly via `is not var (gen, raz)`
pattern matching. MapTextChangesAsync uses `is not var (_, raz)` since it
doesn't need the generated document. The two inlined fetch+map sequences in
TryExcerptAsync and MapSpansAsync go away.

Net -9 lines in the file (11+/20-); no behavior change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Addresses davidwengier's review comment on RazorIntegrationTestBase.cs:

> I get the convenience of this, but I feel like long term we should get
> rid of it, and have things assert not just that the code exists, but
> its in the file it should exist in.

The AllCode property on CompileToCSharpResult concatenated the impl and
decl halves so tests could use `Assert.Contains(x, generated.AllCode)`
without caring which half x landed in. That made the assertions weaker
than they should be -- if a string accidentally migrated from impl to
decl (or vice versa), the test wouldn't notice.

All four callers asserted strings that live unambiguously in the impl
half:

  - AddComponentParameter: "AddComponentParameter" and "AddAttribute"
    are emitted into BuildRenderTree, which lives only in impl.

  - GenericInference_DynamicallyAccessedMembers_{01,02}:
    "DynamicallyAccessedMembers" is emitted on the synthesized generic
    inference helper methods (e.g. CreateInputRadioGroup_0<...>), which
    are IsSynthesizedHelper plumbing that lives only in impl. Confirmed
    against the existing TestComponent.codegen.cs baseline.

Replaced all four callsites with `generated.Code` (the impl-only
property) and deleted AllCode.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@chsienki

Copy link
Copy Markdown
Member Author

@davidwengier I addressed your feedback, would you mind taking another look to make sure you're ok with the changes. If you are I'll add back the baseline commit so we can get green + merge to the feature branch.

@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.

I'm approving, but I'll be honest, I missed that generatedDocument was used elsewhere in the method when I suggested that TryGetRazorDocument.... should be updated, so I'd be totally happy if 923f8f9 was reverted.

My bad.

chsienki and others added 2 commits May 28, 2026 16:59
…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>
Copilot AI review requested due to automatic review settings May 29, 2026 00:01

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.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

Three tests were failing in CI for known impl/decl split reasons that the
test source already acknowledged inline with PROTOTYPE comments but
hadn't gated with Skip markers:

  - ComponentRenderModeDirectiveIntegrationTests.RenderMode_GenericComponent_CSharp10
    rendermode lowering doesn't emit a #line directive on the synthesized
    attribute decoration in the decl half, so the resulting diagnostic
    points at the generated file instead of the @rendermode token.

  - ComponentRenderModeDirectiveIntegrationTests.LanguageVersion_BreakingChange_8_0
    rendermode lowering doesn't emit a #line directive on the synthesized
    => <expr> arrow expression, so the diagnostic's line/column point
    inside the generated helper rather than at the user's @rendermode token.

  - CohostDocumentPullDiagnosticsTest.CSharpUnusedUsings_HintDiagnosticsInVSCode
    unused-using diagnostics are emitted from both the impl and decl
    halves of the split generated source; the test sees 2 RZ0005
    diagnostics back-mapped to the same razor position instead of 1.
    Needs decl/impl-aware diagnostic dedup.

All three are real follow-up items to address before merging this work
to main, matching the existing pattern of PROTOTYPE(sonic) skips for
other known broken tests on the branch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@chsienki

Copy link
Copy Markdown
Member Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 2 pipeline(s), but failed to run 1 pipeline(s).

The Generate{Method,Type,Field}_{NoCodeBlock,ExistingCodeBlock,ExistingCodeBlock_DifferentGenericArity}
tests on this branch fail intermittently in CI. Local reproduction on
sonic/3_2_activate_impl_decl_split: GenerateType_NoCodeBlock passes 3/5
runs. The same test on upstream/main passes 5/5 runs, confirming the
intermittency is a regression introduced by the impl/decl split in this
branch (not a pre-existing flake).

Most likely root cause: the request/resolve roundtrip in CohostCodeActions
doesn't deterministically route to a single half of the generated source.
Roslyn picks impl or decl on different runs depending on timing. The fix
will live in the cohosting layer (see canary work on
sonic/3_3_cohosting_canaries for the patterns) but is out of scope for
this PR.

Skipping the five remaining unskipped tests in CohostRoslynCodeActionTest
that exhibit the issue:

  - GenerateMethod_NoCodeBlock
  - GenerateMethod_ExistingCodeBlock
  - GenerateType_NoCodeBlock
  - GenerateType_ExistingCodeBlock_DifferentGenericArity
  - GenerateField_NoCodeBlock

All other Generate* tests in this class were already skipped with the
generic 'cohosting feature not yet decl/impl split aware' PROTOTYPE
marker. After this commit the entire class skips (15/15) on this branch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 29, 2026 16:41

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.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

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