Skip to content

Split Razor components into decl/impl before tag-helper resolution - #84577

Merged
chsienki merged 6 commits into
dotnet:features/sonicfrom
chsienki:sonic/decl-impl-early-split
Jul 22, 2026
Merged

Split Razor components into decl/impl before tag-helper resolution#84577
chsienki merged 6 commits into
dotnet:features/sonicfrom
chsienki:sonic/decl-impl-early-split

Conversation

@chsienki

@chsienki chsienki commented Jul 21, 2026

Copy link
Copy Markdown
Member

What

For a component whose @code mixes 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:

  1. Add the Razor markup-split analysis libraryMarkupSplitter, a pure decision layer. It builds a parse-only analysis document for the @code class 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.
  2. Split components into decl/impl before tag-helper resolution — a single early phase (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 (marked DocumentIntermediateNode.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 RegisterPreCompilationSourceOutput so 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:

  • splittable → decl half → pre-compilation output; impl half → implementation output
  • not splittable → one single document

This PR is an interim two-tier. The pre-existing late DeclCSharpLoweringPhase / CSharpLoweringPhase still 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

  • New MarkupSplitterTest (unit) and MarkupSplitterComponentTest (integration: compilation, decl byte-stability across markup-only edits, decl-exists-before-resolution, source mappings, fully lowered IR shape).
  • Four markup-in-@code component baselines regenerated; all other baselines byte-identical.
  • Full Microsoft.AspNetCore.Razor.Language.UnitTests suite green (net10.0 + net472).
Microsoft Reviewers: Open in CodeFlow

chsienki and others added 2 commits July 21, 2026 16:26
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
Copilot AI review requested due to automatic review settings July 21, 2026 23:34
@chsienki
chsienki requested a review from a team as a code owner July 21, 2026 23:34
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 2 pipeline(s).
There may be pipelines that require an authorized user to comment /azp run to run.

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 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 + SplitDecision to classify/routably partition a component class body into decl/impl halves without consulting resolved tag helpers.
  • Add DefaultRazorMarkupSplitPhase and insert it into the default phase pipeline before DefaultTagHelperResolutionPhase; 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.

Comment on lines +157 to +162
if (i + 1 < text.Length && text[i + 1] == '\n')
{
// Consume the paired newline as a single line break.
absolute++;
i++;
}
Comment on lines +23 to +27
/// 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"/>).
@chsienki

Copy link
Copy Markdown
Member Author

@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
Copilot AI review requested due to automatic review settings July 22, 2026 05:06

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

LGTM, just a few nits.

[Fact]
public void MarkupMethod_WithInject_CompilesUnderNewFlow()
{
// Under the early split, @inject is a document-level directive (not part of the @code analysis),

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.

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.

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 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);

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.

Nit: Here, and for builder above, use a pooled collection.

{
return true;
}
else if (!char.IsWhiteSpace(c))

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.

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);

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.

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

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

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);
Copilot AI review requested due to automatic review settings July 22, 2026 05:16

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

Copilot reviewed 35 out of 35 changed files in this pull request and generated 3 comments.

Comment on lines +17 to +22
/// <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; }
Comment on lines +246 to +257
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
Copilot AI review requested due to automatic review settings July 22, 2026 19:38

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

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

Comment on lines +21 to +27
/// <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>
Comment on lines +54 to +57
// 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;
Comment on lines +144 to +146
// 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);
Comment on lines +180 to +183
// 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.
Comment on lines +220 to +229
foreach (var phase in projectEngine.Engine.Phases)
{
if (phase is TStopBefore)
{
break;
}

codeDocument = phase.Execute(codeDocument);
}

@chsienki
chsienki merged commit 1b7e529 into dotnet:features/sonic Jul 22, 2026
25 checks passed
chsienki added a commit that referenced this pull request Jul 29, 2026
#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
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