Split Razor components into decl/impl before tag-helper resolution - #84577
Conversation
Introduce MarkupSplitter, the decision layer for the component decl/impl split. For a component's @code class body it builds a parse-only analysis document -- each markup transition replaced by a position-aware marker, plus a lightweight per-child placement mapping every span back to its IR node -- recovers member boundaries by parsing it, then classifies and routes each member in a single pass into the pieces each half emits. The decision is a pure function of the class-body IR and the C# parse options: it never consults resolved tag helpers and never branches on the language version, so it can run before tag-helper resolution. Only a plain markup method is routable; a property, a field-like member, a preprocessor directive, an unsupported node, or unrecoverable syntax reports a fallback so the transform cannot change declaration order or mask a diagnostic. Covered by MarkupSplitterTest. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c45f67d5-6fe0-4f6b-8c68-c268dc7c0621
Partition a component whose @code contains markup into two partial documents before tag-helper resolution: a markup-free decl document (the descriptor surface) and an impl document (the render body and the markup-bearing members) that flows through the rest of the pipeline. A single phase runs after IR lowering and before resolution: it decides the split, produces the decl document immediately by reusing the engine's own classifier, directive- classifier, and decl-lowering phases -- so the decl bytes match what the single-document surface would produce -- and rewrites the working node into the impl half. DocumentIntermediateNode.IsSplitImplDocument marks that node so the final C# lowering phase emits it directly. Producing the decl half before resolution is the point: it is markup-free and depends only on user source, so it is resolution-independent and can be consumed early by incremental tag-helper discovery. The decl and impl C# lowering phases handle a markup-free component and serve as the fallback for a component whose raw @code shape the split phase cannot partition early (statement markup in a method, a CSS-scoped component, whitespace in a functions block, single-line control flow in @code) -- routing markup over the classified tree. A header- or arity-shaping directive (@inherits, @implements, @typeparam) combined with markup takes the single-document path, since the move-based partition cannot yet keep the base type, interfaces, and type parameters consistent across the two halves. Integration coverage verifies compilation, decl byte stability across markup edits, that the decl document exists before tag-helper resolution, source mappings, and the fully lowered component IR shape. Component baselines record the resulting member placement. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c45f67d5-6fe0-4f6b-8c68-c268dc7c0621
|
Azure Pipelines: Successfully started running 2 pipeline(s). There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
Pull request overview
This PR introduces an early (pre–tag-helper-resolution) decl/impl split for Razor components whose @code mixes C# with markup, by adding a resolution-independent “markup split” analysis layer and inserting a new engine phase to produce the decl document before tag-helper resolution.
Changes:
- Add
MarkupSplitter+SplitDecisionto classify/routably partition a component class body into decl/impl halves without consulting resolved tag helpers. - Add
DefaultRazorMarkupSplitPhaseand insert it into the default phase pipeline beforeDefaultTagHelperResolutionPhase; update lowering phases to respect the early-produced decl and to emit impl directly when the document is pre-partitioned. - Add unit/integration tests and update component codegen baselines to reflect the new split timing and routing.
Reviewed changes
Copilot reviewed 31 out of 31 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Razor/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/RazorIntegrationTestBase.cs | Adds a helper to run the engine up to a specific phase for pipeline-state assertions. |
| src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/SplitDecision.cs | Introduces the decision/plan/fallback model and routed member representation for splitting. |
| src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorProjectEngine.cs | Inserts the new markup-split phase into the default engine phase ordering. |
| src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/MarkupSplitter.Slicing.cs | Implements slicing of C# IR chunks and source-span recomputation for routed pieces. |
| src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/MarkupSplitter.cs | Adds the split entry points and safety gates (markup detection, directives detection, supported node checks). |
| src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/MarkupSplitter.Classify.cs | Parses an analysis document and routes members into decl vs impl pieces (or falls back). |
| src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/MarkupSplitter.Analysis.cs | Builds the parse-only analysis text with position-aware markers and child span tracking. |
| src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/DocumentIntermediateNode.cs | Adds IsSplitImplDocument flag to signal “impl already built” to the final lowering phase. |
| src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorMarkupSplitPhase.cs | New early phase that decides the split, produces decl immediately, and rewrites the working node into impl. |
| src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorDeclCSharpLoweringPhase.cs | Skips decl production when an early decl document already exists; adds late-tier routing fallback. |
| src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorCSharpLoweringPhase.cs | Emits impl directly when IsSplitImplDocument is set; adds late-tier routing fallback for markup methods. |
| src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/WhiteSpace_InMarkupInFunctionsBlock/TestComponent.mappings.txt | Updated baseline mappings reflecting moved markup-bearing members to impl. |
| src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/WhiteSpace_InMarkupInFunctionsBlock/TestComponent.decl.mappings.txt | Updated decl mappings baseline after early split behavior. |
| src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/WhiteSpace_InMarkupInFunctionsBlock/TestComponent.decl.codegen.cs | Updated decl codegen baseline (markup-bearing method removed from decl). |
| src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/WhiteSpace_InMarkupInFunctionsBlock/TestComponent.codegen.cs | Updated impl codegen baseline (markup-bearing method moved into impl). |
| src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/SingleLineControlFlowStatements_InCodeDirective/TestComponent.mappings.txt | Updated baseline mappings reflecting moved markup-bearing members to impl. |
| src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/SingleLineControlFlowStatements_InCodeDirective/TestComponent.decl.mappings.txt | Updated decl mappings baseline after early split behavior. |
| src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/SingleLineControlFlowStatements_InCodeDirective/TestComponent.decl.codegen.cs | Updated decl codegen baseline (markup-bearing method removed from decl). |
| src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/SingleLineControlFlowStatements_InCodeDirective/TestComponent.codegen.cs | Updated impl codegen baseline (markup-bearing method moved into impl). |
| src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Legacy_3_1_WhiteSpace_InMarkupInFunctionsBlock/TestComponent.mappings.txt | Updated legacy baseline mappings reflecting moved markup-bearing members to impl. |
| src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Legacy_3_1_WhiteSpace_InMarkupInFunctionsBlock/TestComponent.decl.mappings.txt | Updated legacy decl mappings baseline after early split behavior. |
| src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Legacy_3_1_WhiteSpace_InMarkupInFunctionsBlock/TestComponent.decl.codegen.cs | Updated legacy decl codegen baseline (markup-bearing method removed from decl). |
| src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Legacy_3_1_WhiteSpace_InMarkupInFunctionsBlock/TestComponent.codegen.cs | Updated legacy impl codegen baseline (markup-bearing method moved into impl). |
| src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Component_WithCssScope/TestComponent.mappings.txt | Updated baseline mappings reflecting moved markup-bearing members to impl. |
| src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Component_WithCssScope/TestComponent.decl.mappings.txt | Updated decl mappings baseline after early split behavior. |
| src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Component_WithCssScope/TestComponent.decl.codegen.cs | Updated decl codegen baseline (markup-bearing method removed from decl). |
| src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Component_WithCssScope/TestComponent.codegen.cs | Updated impl codegen baseline (markup-bearing method moved into impl). |
| src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Component_WithCssScope/TestComponent.builder.txt | Updated builder baseline to reflect reordered builder operations due to method relocation. |
| src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/RazorProjectEngineTest.cs | Updates default phase assertions to include DefaultRazorMarkupSplitPhase. |
| src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/MarkupSplitterTest.cs | Adds unit tests for analysis building, classification, routing, and span slicing helpers. |
| src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/IntegrationTests/MarkupSplitterComponentTest.cs | Adds integration tests validating split behavior across compilation, mappings, and phase ordering. |
| if (i + 1 < text.Length && text[i + 1] == '\n') | ||
| { | ||
| // Consume the paired newline as a single line break. | ||
| absolute++; | ||
| i++; | ||
| } |
| /// The decl document carries the user's component API surface: the partial class declaration with | ||
| /// base type / interfaces / type parameters / user-authored class-level attributes (route, | ||
| /// layout), all properties / fields / parameters / inject members / sibling methods, and any | ||
| /// document-level metadata (source-checksum attributes, etc.). It deliberately omits the render | ||
| /// method body and any compiler-synthesized plumbing (marked with | ||
| /// <see cref="IntermediateNode.IsSynthesizedHelper"/>) so it depends only on user source -- not | ||
| /// on tag helper resolution -- and can therefore run earlier in the pipeline than the final | ||
| /// C# lowering phase. | ||
| /// layout), all properties / fields / parameters / inject members and sibling methods, and any | ||
| /// document-level metadata (source-checksum attributes, etc.). It omits the render method body and | ||
| /// compiler-synthesized plumbing (marked with <see cref="IntermediateNode.IsSynthesizedHelper"/>). |
|
@copilot address the correctness errors |
…splitter Parse the throwaway analysis document from a SourceText: the string-based CSharpSyntaxTree.ParseText overload is banned by RS0030 (Correctness_Analyzers build leg). The analysis document is never emitted, so its encoding is irrelevant. Bound the CR/LF pair detection in AdvanceLocation by the slice end rather than the whole token length. A slice that ends exactly on the '\r' now treats it as a lone break and leaves the paired '\n' for the next slice, so a boundary-aligned slice can no longer over-advance its absolute/line/character indices and corrupt source mappings. Correct the decl-phase XML doc: under the early split the decl document is built from usings plus the markup-free @code pieces before directive classification, so directive-authored members such as @Inject ride in the impl half rather than the decl half. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 536d6383-16e8-4134-b429-d4ec73043662
| [Fact] | ||
| public void MarkupMethod_WithInject_CompilesUnderNewFlow() | ||
| { | ||
| // Under the early split, @inject is a document-level directive (not part of the @code analysis), |
There was a problem hiding this comment.
Nit: Seen this a few times, copilot loves to write comments in relation to work its doing, but doesn't make sense in isolation. Would be good to remove mentions of "early split" if it's easy. Not a big deal though, being only a comment.
There was a problem hiding this comment.
I haven't looked at the parser yet (and might not be able to understand it anyway 😛) but is it worth adding test coverage for a class defined inside a @code block, which has a method/property, which has markup?
| var builder = new StringBuilder(); | ||
| builder.Append(AnalysisClassHeader); | ||
|
|
||
| var spans = ImmutableArray.CreateBuilder<ChildSpan>(children.Length); |
There was a problem hiding this comment.
Nit: Here, and for builder above, use a pooled collection.
| { | ||
| return true; | ||
| } | ||
| else if (!char.IsWhiteSpace(c)) |
There was a problem hiding this comment.
If we're not at the start of a line, may as well save the call to IsWhiteSpace. Might even be simpler to just add an else if (!atLineStart) continue; as the second branch of the if.
Unless my logic is wrong of course :)
| tokenStart = tokenEnd; | ||
| } | ||
|
|
||
| return ImmutableArray.Create(pieces); |
There was a problem hiding this comment.
Nit: I think this copies, and there is a CollectionMarshal method that can do it without copying
…arkup split The markup-bearing RenderBadge method now lifts wholesale to the impl half, so it leaves the markup-free decl half and its lowered __builder.AddMarkupContent calls move into the implementation document. Update the codegen and source-mapping baselines to match; the emitted render tree is unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 536d6383-16e8-4134-b429-d4ec73043662
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 35 out of 35 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/MarkupSplitter.Slicing.cs:124
- AdvanceLocation currently double-counts CRLF when a slice starts at the '\n' of a '\r\n' pair and the preceding slice ended on '\r'. This differs from CodeWriter.WriteCore(), which treats CRLF as a single line break even across chunk boundaries, and can produce incorrect LineIndex/LineCount/endCharacterIndex for the sliced token (e.g. the content after the leading '\n' appears one line later than it should). Consider special-casing a slice that begins with '\n' when preceded by '\r' outside the slice so the leading '\n' advances AbsoluteIndex but does not advance Line/Character.
var (_, endLine, endCharacter) =
AdvanceLocation(startAbsolute, startLine, startCharacter, content, localStart, localLength);
| Assert.Equal(MarkupSplitter.MarkerMethodName + "()", sliced); | ||
|
|
||
| // The whole document parses as valid C# (a get-only expression-bodied property). | ||
| var tree = CSharpSyntaxTree.ParseText(analysis.Text); |
| /// <summary> | ||
| /// Set by <see cref="DefaultRazorMarkupSplitPhase"/> once it has produced the decl C# document and | ||
| /// rewritten this node into the impl half (before tag-helper resolution). Signals the final C# | ||
| /// lowering phase to emit this node directly as the impl half instead of deriving an impl spine from | ||
| /// a single classified tree. | ||
| /// </summary> |
| /// lowering phase to emit this node directly as the impl half instead of deriving an impl spine from | ||
| /// a single classified tree. | ||
| /// </summary> | ||
| public bool IsSplitImplDocument { get; set; } |
| private static bool IsCoveredByMember(TextSpan[] memberSpans, int index) | ||
| { | ||
| foreach (var span in memberSpans) | ||
| { | ||
| if (span.Contains(index)) | ||
| { | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } |
Addresses reviewer feedback (davidwengier + Copilot): - Parse the throwaway analysis document from a SourceText in the unit test, matching the product code (the string-based ParseText overload is banned). - Add coverage for a type declared inside @code whose member carries markup: it falls back (UnsupportedMarkupMember) and still compiles. - Reword a test comment/name to describe behaviour in isolation. - Pool the StringBuilder and span builder in BuildAnalysisDocument. - Skip the IsWhiteSpace check when not at the start of a line in HasPreprocessorDirective. - Return the sliced pieces via ImmutableCollectionsMarshal.AsImmutableArray to avoid an array copy. - Binary-search the source-ordered member spans in IsCoveredByMember rather than a per-character linear scan. - Make DocumentIntermediateNode.IsSplitImplDocument internal; it is an internal pipeline flag with no external consumer. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 536d6383-16e8-4134-b429-d4ec73043662
| /// <remarks> | ||
| /// This is a closed hierarchy, produced once per primary class and shared by both lowering phases. The | ||
| /// decision is a pure function of the class body's IR and the parse options, and it never branches on | ||
| /// the language version, so all callers reach the same decision for the same document. Split plans are | ||
| /// consumed by the lowering phases; an explicit fallback result lets pipeline callers preserve unsplit | ||
| /// processing for shapes that cannot be routed safely. | ||
| /// </remarks> |
| // A synthesized structured declaration (e.g. an injected property). It carries no | ||
| // markup and is surface, so it contributes no analysis text -- but is still recorded | ||
| // (as a zero-length span) so routing can place it. | ||
| break; |
| // A markup marker or a zero-length synthesized declaration lives entirely inside one | ||
| // member; route the original node there by reference (keeping its source mappings). | ||
| var owner = FindMemberIndex(memberSpans, child.Start); |
| // Builds a markup-free decl document from the surface parts of the raw tree and lowers it to C# by | ||
| // running the engine's own classifier, directive-classifier, and decl C# lowering phases on it. Reusing | ||
| // the engine's phases (rather than a separate configuration) keeps the decl bytes identical to what the | ||
| // single-document path would produce for the same surface. |
| foreach (var phase in projectEngine.Engine.Phases) | ||
| { | ||
| if (phase is TStopBefore) | ||
| { | ||
| break; | ||
| } | ||
|
|
||
| codeDocument = phase.Execute(codeDocument); | ||
| } | ||
|
|
#84670) Builds on the decl/impl split now in `features/sonic` (#84577, #84605). ## What this does Wires the resolution-independent **declaration half** of a splittable Razor component through `RegisterPreCompilationSourceOutput`, so tag-helper discovery runs over the real compilation instead of a separate declaration compilation parsed from generated text. This is the step that unlocks reusing the standard compilation's tag helpers. Components the markup split can't partition (a header/arity directive like `@inherits`/`@implements`/`@typeparam`, or class-body markup it can't route) still go through the separate declaration engine. To keep their **type** resolvable in the pre-compilation compilation — so a split component that references one in C# (e.g. `[Parameter] public Widget Child`) doesn't bind to an error type and fail consumers with CS0246 — the split phase emits a bodiless **type shell** for them. ## Commits 1. **Consume the split declaration through pre-compilation source output** — the core rewire: fast/slow tag-helper discovery split, decl `#pragma checksum` suppression so markup-only edits keep discovery cached, set-based test matching. 2. **Strip the generated-code banner from files a delegated code action creates** — cohost fix: generate-type-in-a-new-file no longer copies the `// <auto-generated/>` banner (from the decl's suppressed-checksum first line) onto authored `.cs` files. 3. **Emit a type-shell declaration for fallback components** — the bodiless type shell (namespace / class / modifiers / type-parameter names only) so fallback types resolve without contributing a discoverable surface. 4. **Add decl-baseline files for fallback component code-generation tests** — `.decl.codegen.cs` / `.decl.ir.txt` baselines. 5. **Distinguish a fallback type-shell declaration from a real declaration half** — `RazorCSharpDocument.IsStubDocument`, so tooling (diagnostic translation) treats the member-less shell as absent; fixes dropped IDE0005 unused-usings for fallback components. 6. **Add a skipped test for the fallback nested-delegate limitation** — a known gap (a delegate nested in a fallback binds to an error type → CS1503), tracked by #84646 and marked `PROTOTYPE(sonic)` so it can't reach `main` unfixed. ## Testing - Razor SG unit suite: **218 passed / 1 skipped** (`net10.0`). - Cohost pull-diagnostics: **19/19**. - Each commit builds in isolation (bisect-safe). ## Known follow-ups - #84646 — nested delegate in a fallback component (the skipped test above). ###### Microsoft Reviewers: [Open in CodeFlow](https://microsoft.github.io/open-pr/?codeflow=https://github.com/dotnet/roslyn/pull/84670) --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c45f67d5-6fe0-4f6b-8c68-c268dc7c0621
What
For a component whose
@codemixes markup with C#, partition the class body into a markup-free decl half (the tag-helper descriptor surface) and a markup-bearing impl half — and produce the decl half before tag-helper resolution, so it is resolution-independent.Two commits:
MarkupSplitter, a pure decision layer. It builds a parse-only analysis document for the@codeclass body (each markup transition replaced by a position-aware marker), recovers member boundaries by parsing it, then classifies + routes each member in a single pass into the pieces each half emits. The decision is a pure function of the class-body IR and the C# parse options — never consults resolved tag helpers, never branches on language version. Only a plain markup-bearing method is routable; a property, field-like member, preprocessor directive, unsupported node, or unrecoverable syntax reports a fallback so the transform can't reorder declarations or mask a diagnostic.DefaultRazorMarkupSplitPhase, after IR lowering / before resolution) decides the split, produces the decl document in-engine (reusing the engine's classifier + decl-lowering phases, so decl bytes match the single-document surface), and rewrites the working node into the impl half (markedDocumentIntermediateNode.IsSplitImplDocument), which flows through the rest of the pipeline.Why
The resolution-independent decl half is the enabler for the Sonic incremental source generator: a later PR feeds it into
RegisterPreCompilationSourceOutputso tag-helper discovery stays incremental without paying for full tag-helper resolution per keystroke.Endgame (this PR is a step toward it)
The target is a single decision made entirely by the early phase:
This PR is an interim two-tier. The pre-existing late
DeclCSharpLoweringPhase/CSharpLoweringPhasestill produce decl/impl for (a) markup-free components and (b) markup shapes the early phase can't yet partition (statement markup in a method, CSS-scoped components, whitespace in a functions block, single-line control flow in@code) — routing those over the already-classified tree as a fallback. As the early analysis closes those gaps and markup-free components are produced early, that late re-derivation is removed, collapsing to the binary above. A header/arity directive (@inherits/@implements/@typeparam) combined with markup also takes the single-document path, pending base-type/interface/type-parameter reconciliation across the two halves.Perf
The decl half is produced but not yet consumed (the SG pre-compilation wiring is the follow-up), so this adds a small amount of currently-unused work per markup component. The win lands when that wiring consumes the decl.
Testing
MarkupSplitterTest(unit) andMarkupSplitterComponentTest(integration: compilation, decl byte-stability across markup-only edits, decl-exists-before-resolution, source mappings, fully lowered IR shape).@codecomponent baselines regenerated; all other baselines byte-identical.Microsoft.AspNetCore.Razor.Language.UnitTestssuite green (net10.0 + net472).Microsoft Reviewers: Open in CodeFlow