diff --git a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/IntegrationTests/MarkupSplitterComponentTest.cs b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/IntegrationTests/MarkupSplitterComponentTest.cs
new file mode 100644
index 0000000000000..74a04f36aaeeb
--- /dev/null
+++ b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/IntegrationTests/MarkupSplitterComponentTest.cs
@@ -0,0 +1,450 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.Razor.Language.Extensions;
+using Microsoft.AspNetCore.Razor.Language.Intermediate;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests;
+
+// Verifies the markup splitter against real, fully-lowered component IR (the shape it sees when the
+// decl/impl lowering phases run). The whole design rests on class-body markup still being present as
+// markup IR nodes at that point and on every class-body child being a kind the splitter can route.
+public class MarkupSplitterComponentTest : RazorIntegrationTestBase
+{
+ internal override RazorFileKind? FileKind => RazorFileKind.Component;
+
+ internal override bool UseTwoPhaseCompilation => true;
+
+ [Fact]
+ public void MarkupField_FallsBackAndCompiles()
+ {
+ // A field initializer with markup can't be lifted (declaration-order side effects), so the file
+ // falls back and still compiles.
+ var generated = CompileToCSharp("""
+ @code {
+ private Microsoft.AspNetCore.Components.RenderFragment _frag = @
Hi
;
+ }
+ """);
+
+ CompileToAssembly(generated);
+ }
+
+ [Fact]
+ public void MarkupMethod_LiftsToImpl_AndCompiles()
+ {
+ var generated = CompileToCSharp("""
+ @code {
+ private Microsoft.AspNetCore.Components.RenderFragment Make() => @Hi
;
+ }
+ """);
+
+ // The markup method lifts wholesale to the impl half; the markup-free decl half keeps none of it.
+ Assert.NotNull(generated.DeclCode);
+ Assert.DoesNotContain("Make", generated.DeclCode);
+ Assert.Contains("Make", generated.Code);
+
+ // decl + impl are emitted as partial halves that recombine and compile.
+ CompileToAssembly(generated);
+ }
+
+ [Fact]
+ public void MarkupMethod_AlongsideMarkupFreeMembers_RoutesEachHalf()
+ {
+ var generated = CompileToCSharp("""
+ @code {
+ [Microsoft.AspNetCore.Components.Parameter] public int Count { get; set; }
+ private Microsoft.AspNetCore.Components.RenderFragment Make() => @Hi
;
+ }
+ """);
+
+ // The parameter (descriptor surface) stays in decl; the markup method lifts to impl.
+ Assert.NotNull(generated.DeclCode);
+ Assert.Contains("Count", generated.DeclCode);
+ Assert.DoesNotContain("Make", generated.DeclCode);
+ Assert.Contains("Make", generated.Code);
+
+ CompileToAssembly(generated);
+ }
+
+ [Fact]
+ public void AccessorBodiedMarkupProperty_FallsBackAndCompiles()
+ {
+ // A property with markup falls back (a property is descriptor surface that must stay in decl,
+ // where markup cannot live). The original class-body layout still compiles.
+ var generated = CompileToCSharp("""
+ @code {
+ public Microsoft.AspNetCore.Components.RenderFragment Foo { get => @Hi
; }
+ }
+ """);
+
+ CompileToAssembly(generated);
+ }
+
+ [Fact]
+ public void MarkupInitializerProperty_FallsBackAndCompiles()
+ {
+ // Markup in a property (here its initializer) produces a fallback decision. The original
+ // class-body layout must still compile.
+ var generated = CompileToCSharp("""
+ @code {
+ public Microsoft.AspNetCore.Components.RenderFragment Foo { get; set; } = @Hi
;
+ }
+ """);
+
+ CompileToAssembly(generated);
+ }
+
+ [Fact]
+ public void MultipleMarkupMethods_AllLiftToImplAndCompile()
+ {
+ var generated = CompileToCSharp("""
+ @code {
+ private Microsoft.AspNetCore.Components.RenderFragment A() => @A
;
+ private Microsoft.AspNetCore.Components.RenderFragment B() => @B
;
+ }
+ """);
+
+ Assert.NotNull(generated.DeclCode);
+ Assert.DoesNotContain("A
", generated.DeclCode);
+ Assert.DoesNotContain("B
", generated.DeclCode);
+ Assert.Contains("A
", generated.Code);
+ Assert.Contains("B
", generated.Code);
+ CompileToAssembly(generated);
+ }
+
+ [Fact]
+ public void ConditionalCompilation_AroundMarkupMethod_Compiles()
+ {
+ // If a #if/#endif around a lifted member were split across halves, the halves would have
+ // unbalanced directives and fail to compile. Verify the split keeps each half well-formed.
+ var generated = CompileToCSharp("""
+ @code {
+ #if true
+ private Microsoft.AspNetCore.Components.RenderFragment M() => @Hi
;
+ #endif
+ }
+ """);
+
+ CompileToAssembly(generated);
+ }
+
+ [Fact]
+ public void MarkupProperty_FallsBackAndCompiles()
+ {
+ var generated = CompileToCSharp("""
+ @code {
+ public Microsoft.AspNetCore.Components.RenderFragment Foo => @Hello
;
+ }
+ """);
+
+ var documentNode = generated.CodeDocument.GetDocumentNode();
+ Assert.NotNull(documentNode);
+ var primaryClass = documentNode.FindPrimaryClass();
+ var renderMethod = documentNode.FindPrimaryMethod();
+ Assert.NotNull(primaryClass);
+ Assert.NotNull(renderMethod);
+
+ // A markup property produces a fallback decision on every language version.
+ var decision = MarkupSplitter.Split(primaryClass, renderMethod, generated.CodeDocument.ParserOptions);
+ var fallback = Assert.IsType(decision);
+ Assert.Equal(FallbackReason.MarkupProperty, fallback.Reason);
+
+ // The unrouted output still compiles.
+ CompileToAssembly(generated);
+ }
+
+ [Fact]
+ public void MarkupFreeProperty_WithMarkupMethod_StaysInDeclAndSplits()
+ {
+ // A markup-free property is descriptor surface and stays in the decl half (the fast path); the
+ // markup method that forces the split lifts to impl.
+ var generated = CompileToCSharp("""
+ @code {
+ [Microsoft.AspNetCore.Components.Parameter] public int Count { get; set; }
+ private Microsoft.AspNetCore.Components.RenderFragment Make() => @@Count
;
+ }
+ """);
+
+ Assert.NotNull(generated.DeclCode);
+ Assert.Contains("Count", generated.DeclCode);
+ Assert.DoesNotContain("Make", generated.DeclCode);
+ Assert.Contains("Make", generated.Code);
+
+ CompileToAssembly(generated);
+ }
+
+ [Fact]
+ public void ExpressionTemplateProperty_SurvivesAsTemplateMarkup_ButFallsBack()
+ {
+ var generated = CompileToCSharp("""
+ @code {
+ public Microsoft.AspNetCore.Components.RenderFragment Header => @Hello
;
+ }
+ """);
+
+ var documentNode = generated.CodeDocument.GetDocumentNode();
+ Assert.NotNull(documentNode);
+ var primaryClass = documentNode.FindPrimaryClass();
+ var renderMethod = documentNode.FindPrimaryMethod();
+ Assert.NotNull(primaryClass);
+ Assert.NotNull(renderMethod);
+
+ // Invariant: the `@<...>` markup is still an IR node at class-body scope (not pre-lowered to
+ // __builder C#), and specifically an expression-position TemplateIntermediateNode.
+ Assert.True(MarkupSplitter.HasClassBodyMarkup(primaryClass, renderMethod));
+ var children = MarkupSplitter.CollectClassBodyChildren(primaryClass, renderMethod);
+ Assert.Contains(children, static c => c is TemplateIntermediateNode);
+
+ // But because the markup is in a property, the file falls back rather than splits.
+ var decision = MarkupSplitter.Split(primaryClass, renderMethod, generated.CodeDocument.ParserOptions);
+ var fallback = Assert.IsType(decision);
+ Assert.Equal(FallbackReason.MarkupProperty, fallback.Reason);
+ }
+
+ [Fact]
+ public void PureCSharpCode_HasNoClassBodyMarkup_AndDoesNotSplit()
+ {
+ var generated = CompileToCSharp("""
+ @code {
+ private int _count;
+ private void Increment() => _count++;
+ }
+ """);
+
+ var documentNode = generated.CodeDocument.GetDocumentNode();
+ Assert.NotNull(documentNode);
+ var primaryClass = documentNode.FindPrimaryClass();
+ var renderMethod = documentNode.FindPrimaryMethod();
+ Assert.NotNull(primaryClass);
+ Assert.NotNull(renderMethod);
+
+ Assert.False(MarkupSplitter.HasClassBodyMarkup(primaryClass, renderMethod));
+ Assert.Same(
+ SplitDecision.NoSplit,
+ MarkupSplitter.Split(primaryClass, renderMethod, generated.CodeDocument.ParserOptions));
+ }
+
+ [Fact]
+ public void MarkupMethod_WithTypeParam_FallsBackAndCompiles()
+ {
+ // A generic component (@typeparam) with a markup method takes the single-document path: the early
+ // move-based split would give the two partial halves inconsistent arity. Verify it still compiles.
+ var generated = CompileToCSharp("""
+ @typeparam T
+ @code {
+ private Microsoft.AspNetCore.Components.RenderFragment Make() => @Hi
;
+ }
+ """);
+
+ CompileToAssembly(generated);
+ }
+
+ [Fact]
+ public void MarkupMethod_WithInject_Compiles()
+ {
+ // @inject is a document-level directive, not part of the @code class body, so it doesn't prevent
+ // the markup method from being routed to the impl half. Verify the split halves recombine and
+ // compile with the injected member present.
+ var generated = CompileToCSharp("""
+ @inject System.IServiceProvider Services
+ @code {
+ private Microsoft.AspNetCore.Components.RenderFragment Make() => @Hi
;
+ }
+ """);
+
+ CompileToAssembly(generated);
+ }
+
+ [Fact]
+ public void MarkupMethod_InNestedClass_FallsBackAndCompiles()
+ {
+ // A type declared inside @code is a single class-body member; when it carries markup (here in a
+ // method) it can't be lifted -- a nested type may be referenced from the decl half or itself hold
+ // markup members -- so the component falls back to the single-document path. Verify it compiles.
+ var generated = CompileToCSharp("""
+ @code {
+ class Nested
+ {
+ public Microsoft.AspNetCore.Components.RenderFragment Make() => @Hi
;
+ }
+ }
+ """);
+
+ var documentNode = generated.CodeDocument.GetDocumentNode();
+ Assert.NotNull(documentNode);
+ var primaryClass = documentNode.FindPrimaryClass();
+ var renderMethod = documentNode.FindPrimaryMethod();
+ Assert.NotNull(primaryClass);
+ Assert.NotNull(renderMethod);
+
+ var decision = MarkupSplitter.Split(primaryClass, renderMethod, generated.CodeDocument.ParserOptions);
+ var fallback = Assert.IsType(decision);
+ Assert.Equal(FallbackReason.UnsupportedMarkupMember, fallback.Reason);
+
+ CompileToAssembly(generated);
+ }
+
+ [Fact]
+ public void MarkupProperty_InNestedClass_FallsBackAndCompiles()
+ {
+ // Same as the nested-class method case, but the markup lives in a property of the nested type. The
+ // nested type still can't be lifted, so the component falls back and compiles.
+ var generated = CompileToCSharp("""
+ @code {
+ class Nested
+ {
+ public Microsoft.AspNetCore.Components.RenderFragment Header => @Hi
;
+ }
+ }
+ """);
+
+ var documentNode = generated.CodeDocument.GetDocumentNode();
+ Assert.NotNull(documentNode);
+ var primaryClass = documentNode.FindPrimaryClass();
+ var renderMethod = documentNode.FindPrimaryMethod();
+ Assert.NotNull(primaryClass);
+ Assert.NotNull(renderMethod);
+
+ var decision = MarkupSplitter.Split(primaryClass, renderMethod, generated.CodeDocument.ParserOptions);
+ var fallback = Assert.IsType(decision);
+ Assert.Equal(FallbackReason.UnsupportedMarkupMember, fallback.Reason);
+
+ CompileToAssembly(generated);
+ }
+
+ [Fact]
+ public void Inject_AlongsideMarkup_FallsBack()
+ {
+ // @inject lowers to a ComponentInjectIntermediateNode (surface, an ExtensionIntermediateNode
+ // like a template). The splitter can't route it, so a component mixing it with markup falls back.
+ // A markup *method* is used so the inject is the unambiguous cause (a markup property would fall
+ // back on its own).
+ var generated = CompileToCSharp("""
+ @inject System.IServiceProvider Services
+ @code {
+ private Microsoft.AspNetCore.Components.RenderFragment Make() => @Hello
;
+ }
+ """);
+
+ var documentNode = generated.CodeDocument.GetDocumentNode();
+ Assert.NotNull(documentNode);
+ var primaryClass = documentNode.FindPrimaryClass();
+ var renderMethod = documentNode.FindPrimaryMethod();
+ Assert.NotNull(primaryClass);
+ Assert.NotNull(renderMethod);
+
+ var decision = MarkupSplitter.Split(primaryClass, renderMethod, generated.CodeDocument.ParserOptions);
+ var fallback = Assert.IsType(decision);
+ Assert.Equal(FallbackReason.UnsupportedClassBodyNode, fallback.Reason);
+ }
+
+ [Fact]
+ public void MarkupEdit_InMethodBody_LeavesDeclHalfByteIdentical()
+ {
+ // The core value of the split: editing markup (which lives in the impl half) must not perturb the
+ // decl half, so incremental tag-helper discovery can reuse it. The markup-free `[Parameter]`
+ // stays in decl; the markup method lives wholly in impl. The two sources differ only in the markup
+ // on one line, so nothing in decl shifts.
+ var a = CompileToCSharp("""
+ @code {
+ [Microsoft.AspNetCore.Components.Parameter] public int Count { get; set; }
+ private Microsoft.AspNetCore.Components.RenderFragment Make() => @Hello
;
+ }
+ """);
+
+ var b = CompileToCSharp("""
+ @code {
+ [Microsoft.AspNetCore.Components.Parameter] public int Count { get; set; }
+ private Microsoft.AspNetCore.Components.RenderFragment Make() => @Bye;
+ }
+ """);
+
+ Assert.NotNull(a.DeclCode);
+ Assert.NotNull(b.DeclCode);
+
+ // The decl halves are byte-identical despite the differing markup, and neither leaks the markup.
+ Assert.Equal(a.DeclCode, b.DeclCode);
+ Assert.DoesNotContain("Hello", a.DeclCode);
+ Assert.DoesNotContain("Bye", b.DeclCode);
+
+ // The impl halves do differ (that's where the edited markup lives).
+ Assert.NotEqual(a.Code, b.Code);
+ }
+
+ [Fact]
+ public void MarkupEdit_InMethodBody_AddingLines_LeavesDeclHalfByteIdentical()
+ {
+ // Same as above but the edit adds lines to the method body (which follows the decl member), to
+ // confirm the decl half's line mappings don't shift with impl-only growth.
+ var a = CompileToCSharp("""
+ @code {
+ [Microsoft.AspNetCore.Components.Parameter] public int Count { get; set; }
+ private void Make(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder)
+ {
+ Hello
+ }
+ }
+ """);
+
+ var b = CompileToCSharp("""
+ @code {
+ [Microsoft.AspNetCore.Components.Parameter] public int Count { get; set; }
+ private void Make(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder)
+ {
+ Hello
+ An extra line of markup
+ And another
+ }
+ }
+ """);
+
+ Assert.NotNull(a.DeclCode);
+ Assert.NotNull(b.DeclCode);
+ Assert.Equal(a.DeclCode, b.DeclCode);
+ }
+
+ [Fact]
+ public void LiftedMarkupMethod_ContentIsSourceMappedToRazor()
+ {
+ // A diagnostic inside a lifted markup method must map back to the .razor, which relies on the
+ // method's nodes keeping their source mappings when they move to the impl half. Assert that
+ // directly: the `@Count` inside the lifted method has a source mapping whose original text (read
+ // from the .razor) and generated text (read from the impl half) both read "Count" -- so the
+ // lifted content points at the user's source, not unmapped scaffolding.
+ var generated = CompileToCSharp("""
+ @code {
+ [Microsoft.AspNetCore.Components.Parameter] public int Count { get; set; }
+ private Microsoft.AspNetCore.Components.RenderFragment Make() => @@Count
;
+ }
+ """);
+
+ var impl = generated.CodeDocument.GetRequiredImplCSharpDocument();
+ var implText = impl.Text.ToString();
+ var sourceText = generated.CodeDocument.Source.Text.ToString();
+
+ Assert.Contains(impl.SourceMappingsSortedByGenerated, m =>
+ Slice(sourceText, m.OriginalSpan) == "Count" &&
+ Slice(implText, m.GeneratedSpan) == "Count");
+ }
+
+ [Fact]
+ public void DeclDocument_IsProducedBeforeTagHelperResolution()
+ {
+ // The whole point of the split: the decl half is emitted *before* tag-helper resolution, so
+ // discovery can consume a resolution-independent, byte-stable decl and stay incremental. Drive the
+ // pipeline only up to (not including) tag-helper resolution and assert the decl already exists and
+ // is markup-free -- if decl production ever regressed to run after resolution, this would see null.
+ var codeDocument = ProcessComponentUpToPhase("""
+ @code {
+ private Microsoft.AspNetCore.Components.RenderFragment Make() => @Hi
;
+ }
+ """);
+
+ var decl = codeDocument.GetDeclCSharpDocument();
+ Assert.NotNull(decl);
+ Assert.DoesNotContain("Make", decl.Text.ToString());
+ }
+
+ private static string Slice(string text, SourceSpan span) => text.Substring(span.AbsoluteIndex, span.Length);
+}
diff --git a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/MarkupSplitterTest.cs b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/MarkupSplitterTest.cs
new file mode 100644
index 0000000000000..716008d95f586
--- /dev/null
+++ b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/MarkupSplitterTest.cs
@@ -0,0 +1,685 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.Razor.Language.Extensions;
+using Microsoft.AspNetCore.Razor.Language.Intermediate;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Text;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Razor.Language;
+
+public class MarkupSplitterTest
+{
+ private static MethodDeclarationIntermediateNode CreateRenderMethod()
+ => new() { IsPrimaryMethod = true, Name = "BuildRenderTree" };
+
+ private static ClassDeclarationIntermediateNode CreatePrimaryClass(params IntermediateNode[] children)
+ {
+ var @class = new ClassDeclarationIntermediateNode { IsPrimaryClass = true, Name = "TestComponent" };
+
+ foreach (var child in children)
+ {
+ @class.Children.Add(child);
+ }
+
+ return @class;
+ }
+
+ private static CSharpCodeIntermediateNode CreateCSharpCode(string content)
+ {
+ var node = new CSharpCodeIntermediateNode();
+ node.Children.Add(new CSharpIntermediateToken(content, source: null));
+ return node;
+ }
+
+ [Fact]
+ public void HasClassBodyMarkup_PureCSharp_ReturnsFalse()
+ {
+ var renderMethod = CreateRenderMethod();
+ var primaryClass = CreatePrimaryClass(
+ CreateCSharpCode("[Parameter] public int Count { get; set; }"),
+ CreateCSharpCode("private void Increment() => Count++;"),
+ renderMethod);
+
+ Assert.False(MarkupSplitter.HasClassBodyMarkup(primaryClass, renderMethod));
+ }
+
+ [Fact]
+ public void HasClassBodyMarkup_ClassBodyMarkupNode_ReturnsTrue()
+ {
+ var renderMethod = CreateRenderMethod();
+ var primaryClass = CreatePrimaryClass(
+ CreateCSharpCode("public RenderFragment Header => "),
+ new MarkupElementIntermediateNode { TagName = "div" },
+ CreateCSharpCode(";"),
+ renderMethod);
+
+ Assert.True(MarkupSplitter.HasClassBodyMarkup(primaryClass, renderMethod));
+ }
+
+ [Fact]
+ public void HasClassBodyMarkup_SkipsRenderMethodAndSynthesizedHelpers()
+ {
+ var renderMethod = CreateRenderMethod();
+
+ // The render method carries the component's top-level markup as its own children; that markup
+ // is nested, not a class-body child, so it must not trip the gate. A synthesized helper that
+ // happens to be markup-shaped must also be skipped.
+ renderMethod.Children.Add(new MarkupElementIntermediateNode { TagName = "p" });
+
+ var primaryClass = CreatePrimaryClass(
+ CreateCSharpCode("private int _count;"),
+ new MarkupElementIntermediateNode { TagName = "span", IsSynthesizedHelper = true },
+ renderMethod);
+
+ Assert.False(MarkupSplitter.HasClassBodyMarkup(primaryClass, renderMethod));
+ }
+
+ [Fact]
+ public void IsClassBodyMarkup_CSharpAndStructuredDeclarations_AreNotMarkup()
+ {
+ Assert.False(MarkupSplitter.IsClassBodyMarkup(new CSharpCodeIntermediateNode()));
+ Assert.False(MarkupSplitter.IsClassBodyMarkup(new FieldDeclarationIntermediateNode { Name = "_f", Type = "int" }));
+ Assert.False(MarkupSplitter.IsClassBodyMarkup(
+ new PropertyDeclarationIntermediateNode
+ {
+ Name = "P",
+ Type = new CSharpIntermediateToken("int", source: null),
+ ExpressionBody = "0",
+ }));
+ Assert.False(MarkupSplitter.IsClassBodyMarkup(new MethodDeclarationIntermediateNode()));
+ }
+
+ [Fact]
+ public void IsClassBodyMarkup_MarkupNodes_AreMarkup()
+ {
+ Assert.True(MarkupSplitter.IsClassBodyMarkup(new MarkupElementIntermediateNode()));
+ Assert.True(MarkupSplitter.IsClassBodyMarkup(new MarkupBlockIntermediateNode()));
+ Assert.True(MarkupSplitter.IsClassBodyMarkup(new HtmlContentIntermediateNode()));
+ }
+
+ [Fact]
+ public void Split_NoMarkup_ReturnsNoSplit()
+ {
+ var renderMethod = CreateRenderMethod();
+ var primaryClass = CreatePrimaryClass(CreateCSharpCode("private int _count;"), renderMethod);
+
+ var decision = MarkupSplitter.Split(primaryClass, renderMethod, RazorParserOptions.Default);
+
+ Assert.Same(SplitDecision.NoSplit, decision);
+ Assert.False(decision.RequiresSplit);
+ Assert.False(decision.IsFallback);
+ }
+
+ [Fact]
+ public void Fallback_IsFallbackAndDoesNotRequireSplit()
+ {
+ var decision = SplitDecision.Fallback(FallbackReason.MarkupProperty);
+
+ Assert.True(decision.IsFallback);
+ Assert.False(decision.RequiresSplit);
+ Assert.Equal(FallbackReason.MarkupProperty, decision.Reason);
+ }
+
+ [Fact]
+ public void CollectClassBodyChildren_PreservesOrderAndExcludesRenderAndSynthesized()
+ {
+ var renderMethod = CreateRenderMethod();
+ var first = CreateCSharpCode("private int _a;");
+ var markup = new MarkupElementIntermediateNode { TagName = "div" };
+ var last = CreateCSharpCode("private int _b;");
+ var synthesized = new CSharpCodeIntermediateNode { IsSynthesizedHelper = true };
+
+ var primaryClass = CreatePrimaryClass(first, synthesized, renderMethod, markup, last);
+
+ var collected = MarkupSplitter.CollectClassBodyChildren(primaryClass, renderMethod);
+
+ Assert.Equal(new IntermediateNode[] { first, markup, last }, collected);
+ }
+
+
+ [Fact]
+ public void BuildAnalysisDocument_PureCSharp_EmitsTextWithNoMarkers()
+ {
+ var chunk = CreateCSharpCode("[Parameter] public int Count { get; set; }");
+ var analysis = MarkupSplitter.BuildAnalysisDocument([chunk]);
+
+ Assert.Contains("[Parameter] public int Count { get; set; }", analysis.Text);
+ Assert.DoesNotContain(MarkupSplitter.MarkerMethodName, analysis.Text);
+ var child = Assert.Single(analysis.Children);
+ Assert.IsType(child.Node);
+ Assert.Same(chunk, child.Node);
+ }
+
+ [Fact]
+ public void BuildAnalysisDocument_StatementMarkup_EmitsStatementMarker()
+ {
+ var before = CreateCSharpCode("void M(RenderTreeBuilder __builder) { ");
+ var markup = new MarkupElementIntermediateNode { TagName = "ul" };
+ var after = CreateCSharpCode(" }");
+
+ var analysis = MarkupSplitter.BuildAnalysisDocument([before, markup, after]);
+
+ Assert.Contains(MarkupSplitter.MarkerMethodName + "();", analysis.Text);
+ Assert.Equal(3, analysis.Children.Length);
+
+ var markupChild = analysis.Children[1];
+ Assert.True(MarkupSplitter.IsMarkupNode(markupChild.Node));
+ Assert.Same(markup, markupChild.Node);
+
+ // Child offsets must index the emitted marker text exactly.
+ var sliced = analysis.Text.Substring(markupChild.Start, markupChild.Length);
+ Assert.Equal(MarkupSplitter.MarkerMethodName + "();", sliced);
+ }
+
+ [Fact]
+ public void BuildAnalysisDocument_ExpressionTemplate_EmitsExpressionMarker()
+ {
+ var before = CreateCSharpCode("public RenderFragment Header => ");
+ var template = new TemplateIntermediateNode();
+ var after = CreateCSharpCode(";");
+
+ var analysis = MarkupSplitter.BuildAnalysisDocument([before, template, after]);
+
+ var markupChild = analysis.Children[1];
+ Assert.True(MarkupSplitter.IsMarkupNode(markupChild.Node));
+
+ var sliced = analysis.Text.Substring(markupChild.Start, markupChild.Length);
+ Assert.Equal(MarkupSplitter.MarkerMethodName + "()", sliced);
+
+ // The whole document parses as valid C# (a get-only expression-bodied property). Parse from a
+ // SourceText to match the product code, which avoids the banned string-based ParseText overload.
+ var tree = CSharpSyntaxTree.ParseText(SourceText.From(analysis.Text));
+ Assert.Empty(tree.GetDiagnostics());
+ }
+
+ [Fact]
+ public void IsExpressionPositionMarkup_OnlyTemplateIsExpression()
+ {
+ Assert.True(MarkupSplitter.IsExpressionPositionMarkup(new TemplateIntermediateNode()));
+ Assert.False(MarkupSplitter.IsExpressionPositionMarkup(new MarkupElementIntermediateNode()));
+ Assert.False(MarkupSplitter.IsExpressionPositionMarkup(new MarkupBlockIntermediateNode()));
+ Assert.False(MarkupSplitter.IsExpressionPositionMarkup(new HtmlContentIntermediateNode()));
+ }
+
+ private static SplitDecision ClassifyChildren(params IntermediateNode[] children)
+ {
+ var analysis = MarkupSplitter.BuildAnalysisDocument([.. children]);
+ return MarkupSplitter.ClassifyFromAnalysis(analysis, CSharpParseOptions.Default);
+ }
+
+ [Fact]
+ public void ClassifyFromAnalysis_PureCSharpField_RoutesToDecl()
+ {
+ var decision = ClassifyChildren(CreateCSharpCode("private int _n;"));
+
+ var plan = Assert.IsType(decision);
+ var member = Assert.Single(plan.Members);
+ Assert.Empty(member.ImplPieces);
+ Assert.NotEmpty(member.DeclPieces);
+ }
+
+ [Fact]
+ public void ClassifyFromAnalysis_MarkupHelperMethod_RoutesToImpl()
+ {
+ var decision = ClassifyChildren(
+ CreateCSharpCode("void M(RenderTreeBuilder __builder) { "),
+ new MarkupElementIntermediateNode { TagName = "ul" },
+ CreateCSharpCode(" }"));
+
+ var plan = Assert.IsType(decision);
+ var member = Assert.Single(plan.Members);
+ Assert.Empty(member.DeclPieces);
+ Assert.NotEmpty(member.ImplPieces);
+ }
+
+ [Fact]
+ public void ClassifyFromAnalysis_MarkupExpressionProperty_FallsBack()
+ {
+ var decision = ClassifyChildren(
+ CreateCSharpCode("public RenderFragment Header => "),
+ new TemplateIntermediateNode(),
+ CreateCSharpCode(";"));
+
+ var fallback = Assert.IsType(decision);
+ Assert.Equal(FallbackReason.MarkupProperty, fallback.Reason);
+ }
+
+ [Fact]
+ public void ClassifyFromAnalysis_ExplicitInterfaceMarkupProperty_FallsBack()
+ {
+ // Any property/indexer with markup produces a fallback decision -- explicit-interface properties
+ // included.
+ var decision = ClassifyChildren(
+ CreateCSharpCode("global::Microsoft.AspNetCore.Components.RenderFragment IFoo.Bar => "),
+ new TemplateIntermediateNode(),
+ CreateCSharpCode(";"));
+
+ var fallback = Assert.IsType(decision);
+ Assert.Equal(FallbackReason.MarkupProperty, fallback.Reason);
+ }
+
+ [Fact]
+ public void ClassifyFromAnalysis_MarkupField_FallsBack()
+ {
+ // A field's initializer runs in declaration order; lifting it across partials could perturb that,
+ // so a markup field produces a fallback decision.
+ var decision = ClassifyChildren(
+ CreateCSharpCode("private RenderFragment _frag = "),
+ new TemplateIntermediateNode(),
+ CreateCSharpCode(";"));
+
+ var fallback = Assert.IsType(decision);
+ Assert.Equal(FallbackReason.UnsupportedMarkupMember, fallback.Reason);
+ }
+
+ [Fact]
+ public void ClassifyFromAnalysis_NestedTypeWithMarkup_FallsBack()
+ {
+ // A nested type carrying markup can't be lifted as if it were a method (it may be referenced from
+ // decl, and its own markup members aren't handled), so it forces fallback.
+ var decision = ClassifyChildren(
+ CreateCSharpCode("public class Nested { public RenderFragment View => "),
+ new TemplateIntermediateNode(),
+ CreateCSharpCode("; }"));
+
+ var fallback = Assert.IsType(decision);
+ Assert.Equal(FallbackReason.UnsupportedMarkupMember, fallback.Reason);
+ }
+
+ [Fact]
+ public void Split_MarkupField_FallsBack()
+ {
+ var renderMethod = CreateRenderMethod();
+ var primaryClass = CreatePrimaryClass(
+ CreateCSharpCode("private RenderFragment _frag = "),
+ new TemplateIntermediateNode(),
+ CreateCSharpCode(";"),
+ renderMethod);
+
+ var decision = MarkupSplitter.Split(primaryClass, renderMethod, ParserOptions(LanguageVersion.CSharp13));
+
+ var fallback = Assert.IsType(decision);
+ Assert.Equal(FallbackReason.UnsupportedMarkupMember, fallback.Reason);
+ }
+
+ [Fact]
+ public void ClassifyFromAnalysis_MarkupPropertyAmongMembers_FallsBack()
+ {
+ // A clean field and a markup method pass, but the trailing markup property forces the whole file
+ // to fall back rather than route.
+ var decision = ClassifyChildren(
+ CreateCSharpCode("[Parameter] public int Count { get; set; } private void Helper(RenderTreeBuilder __builder) { "),
+ new MarkupElementIntermediateNode { TagName = "div" },
+ CreateCSharpCode(" } public RenderFragment Foo => "),
+ new TemplateIntermediateNode(),
+ CreateCSharpCode(";"));
+
+ var fallback = Assert.IsType(decision);
+ Assert.Equal(FallbackReason.MarkupProperty, fallback.Reason);
+ }
+
+ [Fact]
+ public void ClassifyFromAnalysis_UnrecoverableBraces_FallsBack()
+ {
+ // An unmatched open brace before markup: the class-body close brace goes missing, so the marker
+ // ends up outside any recoverable member.
+ var analysis = MarkupSplitter.BuildAnalysisDocument([
+ CreateCSharpCode("void M(RenderTreeBuilder __builder) { "),
+ new MarkupElementIntermediateNode { TagName = "div" }]);
+ var decision = MarkupSplitter.ClassifyFromAnalysis(analysis, CSharpParseOptions.Default);
+
+ var fallback = Assert.IsType(decision);
+ Assert.Equal(FallbackReason.UnrecoverableParse, fallback.Reason);
+ }
+
+ [Fact]
+ public void SplitCSharpNode_NoCuts_ReturnsSameNode()
+ {
+ var node = CreateCSharpCode("private int _a;");
+ var pieces = MarkupSplitter.SplitCSharpNode(node, System.Collections.Immutable.ImmutableArray.Empty);
+ Assert.Same(node, Assert.Single(pieces));
+ }
+
+ [Fact]
+ public void SplitCSharpNode_SingleCut_SplitsContent()
+ {
+ var node = CreateCSharpCode("private int _a; void M() {");
+ var pieces = MarkupSplitter.SplitCSharpNode(node, [15]);
+
+ Assert.Equal(2, pieces.Length);
+ Assert.Equal("private int _a;", TokenText(pieces[0]));
+ Assert.Equal(" void M() {", TokenText(pieces[1]));
+ }
+
+ [Fact]
+ public void SliceToken_RecomputesLineAndCharacterAcrossNewline()
+ {
+ // A token spanning two source lines, starting at line 3, char 4.
+ var source = new SourceSpan(filePath: "C.razor", absoluteIndex: 100, lineIndex: 3, characterIndex: 4, length: 12);
+ var token = new CSharpIntermediateToken("ab\ncdefghij", source);
+
+ // Slice starting after the newline ("cdef...") must land on line 4, char 0.
+ var sliced = MarkupSplitter.SliceToken(token, localStart: 3, localLength: 4);
+
+ Assert.Equal("cdef", sliced.Content);
+ var s = sliced.Source!.Value;
+ Assert.Equal(103, s.AbsoluteIndex); // 100 + 3
+ Assert.Equal(4, s.LineIndex); // advanced past one newline
+ Assert.Equal(0, s.CharacterIndex); // reset after the newline
+ Assert.Equal(4, s.Length);
+ Assert.Equal(0, s.LineCount); // "cdef" is single-line: zero line breaks
+ Assert.Equal(4, s.EndCharacterIndex);
+ }
+
+ [Fact]
+ public void SliceToken_SpanningNewline_ReportsOneLineBreak()
+ {
+ // A slice that itself crosses a newline reports LineCount 1 (one line break), and its end
+ // character resets on the new line.
+ var source = new SourceSpan(filePath: "C.razor", absoluteIndex: 100, lineIndex: 3, characterIndex: 4, length: 12);
+ var token = new CSharpIntermediateToken("ab\ncdefghij", source);
+
+ var sliced = MarkupSplitter.SliceToken(token, localStart: 0, localLength: 5); // "ab\ncd"
+ var s = sliced.Source!.Value;
+
+ Assert.Equal("ab\ncd", sliced.Content);
+ Assert.Equal(3, s.LineIndex);
+ Assert.Equal(1, s.LineCount); // one newline crossed
+ Assert.Equal(2, s.EndCharacterIndex); // "cd" -> char 2 on the new line
+ }
+
+ [Fact]
+ public void SliceToken_BeforeNewline_KeepsStartLineAndCharacter()
+ {
+ var source = new SourceSpan(filePath: "C.razor", absoluteIndex: 100, lineIndex: 3, characterIndex: 4, length: 12);
+ var token = new CSharpIntermediateToken("ab\ncdefghij", source);
+
+ var sliced = MarkupSplitter.SliceToken(token, localStart: 0, localLength: 2);
+
+ Assert.Equal("ab", sliced.Content);
+ var s = sliced.Source!.Value;
+ Assert.Equal(100, s.AbsoluteIndex);
+ Assert.Equal(3, s.LineIndex);
+ Assert.Equal(4, s.CharacterIndex);
+ }
+
+ [Fact]
+ public void SliceToken_NullSource_ProducesNullSource()
+ {
+ var token = new CSharpIntermediateToken("abcdef", source: null);
+ var sliced = MarkupSplitter.SliceToken(token, 2, 3);
+ Assert.Equal("cde", sliced.Content);
+ Assert.Null(sliced.Source);
+ }
+
+ [Fact]
+ public void SliceToken_AtCarriageReturnNewlineBoundary_DoesNotOverAdvance()
+ {
+ // Slicing exactly between the \r and \n of a \r\n pair must not pull the \n into the preceding
+ // slice: the second slice's content begins with the \n, so its absolute/line/character must point
+ // at the \n, not one past it. (Advancing across the prefix used to consume the paired \n even when
+ // the slice ended on the \r, corrupting the boundary-aligned slice's source mapping.)
+ var source = new SourceSpan(filePath: "C.razor", absoluteIndex: 100, lineIndex: 3, characterIndex: 4, length: 6);
+ var token = new CSharpIntermediateToken("ab\r\ncd", source);
+
+ var before = MarkupSplitter.SliceToken(token, localStart: 0, localLength: 3); // "ab\r"
+ var after = MarkupSplitter.SliceToken(token, localStart: 3, localLength: 3); // "\ncd"
+
+ Assert.Equal("ab\r", before.Content);
+ Assert.Equal("\ncd", after.Content);
+
+ // The second slice starts at the \n (absolute 103), on the line the \r opened, at character 0 --
+ // not skipped past the \n to 104/'c'.
+ var s = after.Source!.Value;
+ Assert.Equal(103, s.AbsoluteIndex);
+ Assert.Equal(4, s.LineIndex);
+ Assert.Equal(0, s.CharacterIndex);
+ }
+
+ [Theory]
+ [InlineData("abc", 0, 3, 3, 0, 3)] // no newline: char advances by 3
+ [InlineData("a\nb", 0, 3, 3, 1, 1)] // one \n: line +1, char 1
+ [InlineData("a\r\nb", 0, 4, 4, 1, 1)] // \r\n counts once: line +1, char 1
+ [InlineData("a\rb", 0, 3, 3, 1, 1)] // lone \r counts as a break
+ [InlineData("a\r\nb", 0, 2, 2, 1, 0)] // range ends on the \r: lone break, the \n is left for the next slice (not over-advanced)
+ [InlineData("a\r\nb", 2, 2, 2, 1, 1)] // the matching next slice starts on that \n: it counts as the break for this range
+ public void AdvanceLocation_CountsLineBreaks(string text, int start, int count, int expectedAbs, int expectedLine, int expectedChar)
+ {
+ var (abs, line, ch) = MarkupSplitter.AdvanceLocation(absolute: 0, line: 0, character: 0, text, start, count);
+ Assert.Equal(expectedAbs, abs);
+ Assert.Equal(expectedLine, line);
+ Assert.Equal(expectedChar, ch);
+ }
+
+ [Fact]
+ public void IsMarkupNode_RecognizesMarkupKinds()
+ {
+ Assert.True(MarkupSplitter.IsMarkupNode(new TemplateIntermediateNode()));
+ Assert.True(MarkupSplitter.IsMarkupNode(new MarkupElementIntermediateNode()));
+ Assert.True(MarkupSplitter.IsMarkupNode(new MarkupBlockIntermediateNode()));
+ Assert.True(MarkupSplitter.IsMarkupNode(new HtmlContentIntermediateNode()));
+ }
+
+ [Fact]
+ public void IsMarkupNode_ExcludesCSharpAndSurfaceNodes()
+ {
+ // Raw C# and structured/extension members are not routable markup -- crucially an @inject node,
+ // which (like a template) is an ExtensionIntermediateNode but is surface, not markup.
+ Assert.False(MarkupSplitter.IsMarkupNode(new CSharpCodeIntermediateNode()));
+ Assert.False(MarkupSplitter.IsMarkupNode(new FieldDeclarationIntermediateNode { Name = "_f", Type = "int" }));
+ Assert.False(MarkupSplitter.IsMarkupNode(new MethodDeclarationIntermediateNode()));
+ Assert.False(MarkupSplitter.IsMarkupNode(
+ new Components.ComponentInjectIntermediateNode("Foo", "Bar", typeSpan: null, memberSpan: null)));
+ }
+
+ [Fact]
+ public void Split_ClassBodyWithInject_FallsBack()
+ {
+ // A component whose @code mixes markup with an @inject cannot be split: the inject is surface the
+ // splitter cannot route, so classification falls back rather than moving it to impl.
+ var renderMethod = CreateRenderMethod();
+ var primaryClass = CreatePrimaryClass(
+ new Components.ComponentInjectIntermediateNode("Foo", "Bar", typeSpan: null, memberSpan: null),
+ CreateCSharpCode("void M(RenderTreeBuilder __builder) { "),
+ new MarkupElementIntermediateNode { TagName = "ul" },
+ CreateCSharpCode(" }"),
+ renderMethod);
+
+ var decision = MarkupSplitter.Split(primaryClass, renderMethod, ParserOptions(LanguageVersion.CSharp13));
+
+ var fallback = Assert.IsType(decision);
+ Assert.Equal(FallbackReason.UnsupportedClassBodyNode, fallback.Reason);
+ }
+
+ [Fact]
+ public void Split_MarkupHelperMethod_ReturnsSplitPlanRoutedToImpl()
+ {
+ var renderMethod = CreateRenderMethod();
+ var markup = new MarkupElementIntermediateNode { TagName = "ul" };
+ var primaryClass = CreatePrimaryClass(
+ CreateCSharpCode("void M(RenderTreeBuilder __builder) { "),
+ markup,
+ CreateCSharpCode(" }"),
+ renderMethod);
+
+ var decision = MarkupSplitter.Split(primaryClass, renderMethod, ParserOptions(LanguageVersion.CSharp10));
+
+ var plan = Assert.IsType(decision);
+ Assert.True(plan.RequiresSplit);
+ var member = Assert.Single(plan.Members);
+ Assert.Empty(member.DeclPieces);
+
+ // The whole method lifts to impl: its C# chunks stay by reference and the markup node is carried
+ // through untouched.
+ Assert.Equal(3, member.ImplPieces.Length);
+ Assert.Same(markup, member.ImplPieces[1]);
+ }
+
+ [Fact]
+ public void Split_ClassBodyWithDirective_FallsBack()
+ {
+ var renderMethod = CreateRenderMethod();
+ var primaryClass = CreatePrimaryClass(
+ CreateCSharpCode("#nullable enable\n public RenderFragment Header => "),
+ new TemplateIntermediateNode(),
+ CreateCSharpCode(";"),
+ renderMethod);
+
+ var decision = MarkupSplitter.Split(primaryClass, renderMethod, ParserOptions(LanguageVersion.CSharp13));
+
+ var fallback = Assert.IsType(decision);
+ Assert.Equal(FallbackReason.ClassBodyHasDirectives, fallback.Reason);
+ }
+
+ [Theory]
+ [InlineData("#if DEBUG", true)]
+ [InlineData(" #pragma warning disable", true)] // leading whitespace before the directive
+ [InlineData("int x = 1;\n#endif", true)] // directive on a later line
+ [InlineData("int x = 1;", false)]
+ [InlineData("var s = \"not # a directive\";", false)] // hash mid-line is not a directive
+ public void HasPreprocessorDirective_DetectsLineAnchoredHash(string text, bool expected)
+ {
+ Assert.Equal(expected, MarkupSplitter.HasPreprocessorDirective(text));
+ }
+
+ [Fact]
+ public void Split_MarkupProperty_FallsBack()
+ {
+ // A property with markup cannot stay in the markup-free decl half but must (it is descriptor
+ // surface), so it produces a fallback decision on every language version.
+ var renderMethod = CreateRenderMethod();
+ var primaryClass = CreatePrimaryClass(
+ CreateCSharpCode("[Parameter] public RenderFragment Header => "),
+ new TemplateIntermediateNode(),
+ CreateCSharpCode(";"),
+ renderMethod);
+
+ var decision = MarkupSplitter.Split(primaryClass, renderMethod, ParserOptions(LanguageVersion.CSharp13));
+
+ var fallback = Assert.IsType(decision);
+ Assert.Equal(FallbackReason.MarkupProperty, fallback.Reason);
+ }
+
+ [Fact]
+ public void Split_MarkupFreeProperty_AlongsideMarkupMethod_StaysInDecl()
+ {
+ // A markup-free property is descriptor surface and stays in decl; the markup method that forces
+ // the split lifts to impl.
+ var renderMethod = CreateRenderMethod();
+ var property = CreateCSharpCode("[Parameter] public int Count { get; set; } void M(RenderTreeBuilder __builder) { ");
+ var primaryClass = CreatePrimaryClass(
+ property,
+ new MarkupElementIntermediateNode { TagName = "ul" },
+ CreateCSharpCode(" }"),
+ renderMethod);
+
+ var decision = MarkupSplitter.Split(primaryClass, renderMethod, ParserOptions(LanguageVersion.CSharp13));
+
+ var plan = Assert.IsType(decision);
+ Assert.Equal(2, plan.Members.Length);
+ // The markup-free property stays wholly in decl.
+ Assert.Empty(plan.Members[0].ImplPieces);
+ Assert.Contains("Count", TokenText((CSharpCodeIntermediateNode)Assert.Single(plan.Members[0].DeclPieces)));
+ // The markup method lifts wholly to impl.
+ Assert.Empty(plan.Members[1].DeclPieces);
+ Assert.NotEmpty(plan.Members[1].ImplPieces);
+ }
+
+ [Fact]
+ public void Split_MarkupProperty_BelowCSharp13_AlsoFallsBack()
+ {
+ // The fallback is version-independent: no C# 13 gate anymore.
+ var renderMethod = CreateRenderMethod();
+ var primaryClass = CreatePrimaryClass(
+ CreateCSharpCode("public RenderFragment Header => "),
+ new TemplateIntermediateNode(),
+ CreateCSharpCode(";"),
+ renderMethod);
+
+ var decision = MarkupSplitter.Split(primaryClass, renderMethod, ParserOptions(LanguageVersion.CSharp10));
+
+ var fallback = Assert.IsType(decision);
+ Assert.Equal(FallbackReason.MarkupProperty, fallback.Reason);
+ }
+
+ [Fact]
+ public void Split_MarkupMethod_BelowCSharp13_StillSplits()
+ {
+ // Only markup properties fall back; a markup method lifts wholesale on any C#.
+ var renderMethod = CreateRenderMethod();
+ var primaryClass = CreatePrimaryClass(
+ CreateCSharpCode("void M(RenderTreeBuilder __builder) { "),
+ new MarkupElementIntermediateNode { TagName = "ul" },
+ CreateCSharpCode(" }"),
+ renderMethod);
+
+ var decision = MarkupSplitter.Split(primaryClass, renderMethod, ParserOptions(LanguageVersion.CSharp10));
+
+ Assert.IsType(decision);
+ }
+
+ [Fact]
+ public void Split_UnrecoverableParse_FallsBack()
+ {
+ // An unmatched open brace before markup leaves the marker outside any recoverable member.
+ var renderMethod = CreateRenderMethod();
+ var primaryClass = CreatePrimaryClass(
+ CreateCSharpCode("void M(RenderTreeBuilder __builder) { "),
+ new MarkupElementIntermediateNode { TagName = "div" },
+ renderMethod);
+
+ var decision = MarkupSplitter.Split(primaryClass, renderMethod, ParserOptions(LanguageVersion.CSharp13));
+
+ var fallback = Assert.IsType(decision);
+ Assert.Equal(FallbackReason.UnrecoverableParse, fallback.Reason);
+ }
+
+ [Fact]
+ public void ClassifyFromAnalysis_MixedMembers_SlicesChunkAndRoutesEachInOrder()
+ {
+ // One C# chunk holds a whole field plus the start of a markup method, so it must be sliced at the
+ // member boundary and each slice routed to its own member. (A markup property produces a fallback
+ // decision before routing, so routing only sees a markup-free member and a markup method.)
+ var markup = new MarkupElementIntermediateNode { TagName = "div" };
+ var analysis = MarkupSplitter.BuildAnalysisDocument([
+ CreateCSharpCode("[Parameter] public int Count { get; set; } private void Helper(RenderTreeBuilder __builder) { "),
+ markup,
+ CreateCSharpCode(" }")]);
+
+ var plan = Assert.IsType(
+ MarkupSplitter.ClassifyFromAnalysis(analysis, CSharpParseOptions.Default));
+ var routed = plan.Members;
+
+ Assert.Equal(2, routed.Length);
+
+ // int Count -> decl only, one sliced C# piece.
+ Assert.Empty(routed[0].ImplPieces);
+ var countPiece = Assert.IsType(Assert.Single(routed[0].DeclPieces));
+ Assert.Contains("int Count", TokenText(countPiece));
+ Assert.DoesNotContain("Helper", TokenText(countPiece));
+
+ // Helper -> impl only, carrying the markup node by reference.
+ Assert.Empty(routed[1].DeclPieces);
+ Assert.Contains(routed[1].ImplPieces, p => ReferenceEquals(p, markup));
+ }
+
+ private static RazorParserOptions ParserOptions(LanguageVersion version)
+ => RazorParserOptions.Default.WithCSharpParseOptions(
+ CSharpParseOptions.Default.WithLanguageVersion(version));
+
+ private static string TokenText(CSharpCodeIntermediateNode node)
+ {
+ var sb = new System.Text.StringBuilder();
+ foreach (var child in node.Children)
+ {
+ if (child is IntermediateToken token)
+ {
+ sb.Append(token.Content);
+ }
+ }
+
+ return sb.ToString();
+ }
+}
diff --git a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/RazorProjectEngineTest.cs b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/RazorProjectEngineTest.cs
index 4f46559cfbc09..b47b22b73f388 100644
--- a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/RazorProjectEngineTest.cs
+++ b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/RazorProjectEngineTest.cs
@@ -39,6 +39,7 @@ private static void AssertDefaultPhases(RazorProjectEngine engine)
phase => Assert.IsType(phase),
phase => Assert.IsType(phase),
phase => Assert.IsType(phase),
+ phase => Assert.IsType(phase),
phase => Assert.IsType(phase),
phase => Assert.IsType(phase),
phase => Assert.IsType(phase),
diff --git a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Component_WithCssScope/TestComponent.builder.txt b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Component_WithCssScope/TestComponent.builder.txt
index c8f306d8bf846..3a6bf8c808339 100644
--- a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Component_WithCssScope/TestComponent.builder.txt
+++ b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Component_WithCssScope/TestComponent.builder.txt
@@ -1,13 +1,8 @@
[0000] AddMarkupContent("Element with no attributes
\r\n")
-[0000] OpenElement("li")
-[0001] AddAttribute("data-index", i)
[0001] OpenElement("parent")
-[0002] AddAttribute("TestCssScope")
[0002] AddAttribute("with-attributes", "yes")
[0003] AddAttribute("with-csharp-attribute-value", 123)
-[0003] AddContent("Something ")
[0004] AddAttribute("TestCssScope")
-[0004] AddContent(i)
[0005] AddMarkupContent("\r\n ")
[0006] AddMarkupContent("With text\r\n ")
[0007] OpenComponent()
@@ -25,4 +20,9 @@
[0019] AddAttribute("another-attr", "Another attr value")
[0020] AddAttribute("value", global::Microsoft.AspNetCore.Components.BindConverter.FormatValue( myVariable ))
[0021] AddAttribute("onchange", global::Microsoft.AspNetCore.Components.EventCallback.Factory.CreateBinder(this, __value => myVariable = __value, myVariable))
-[0022] AddAttribute("TestCssScope")
\ No newline at end of file
+[0022] AddAttribute("TestCssScope")
+[0023] OpenElement("li")
+[0024] AddAttribute("data-index", i)
+[0025] AddAttribute("TestCssScope")
+[0026] AddContent("Something ")
+[0027] AddContent(i)
\ No newline at end of file
diff --git a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Component_WithCssScope/TestComponent.codegen.cs b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Component_WithCssScope/TestComponent.codegen.cs
index 11605969b003f..63013db83cd3b 100644
--- a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Component_WithCssScope/TestComponent.codegen.cs
+++ b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Component_WithCssScope/TestComponent.codegen.cs
@@ -114,6 +114,52 @@ protected override void BuildRenderTree(global::Microsoft.AspNetCore.Components.
}
#pragma warning restore 1998
+#nullable restore
+#line (21,1)-(26,1) "x:\dir\subdir\Test\TestComponent.cshtml"
+
+ void MethodRenderingMarkup(RenderTreeBuilder __builder)
+ {
+ for (var i = 0; i < 10; i++)
+ {
+
+#line default
+#line hidden
+#nullable disable
+
+ __builder.OpenElement(23, "li");
+ __builder.AddAttribute(24, "data-index",
+#nullable restore
+#line (26,29)-(26,30) "x:\dir\subdir\Test\TestComponent.cshtml"
+i
+
+#line default
+#line hidden
+#nullable disable
+ );
+ __builder.AddAttribute(25, "TestCssScope");
+ __builder.AddContent(26, "Something ");
+#nullable restore
+#line (26,42)-(26,43) 25 "x:\dir\subdir\Test\TestComponent.cshtml"
+__builder.AddContent(27, i
+
+#line default
+#line hidden
+#nullable disable
+ );
+ __builder.CloseElement();
+#nullable restore
+#line (27,1)-(33,1) "x:\dir\subdir\Test\TestComponent.cshtml"
+ }
+
+ System.GC.KeepAlive(myElementReference);
+ System.GC.KeepAlive(myComponentReference);
+ System.GC.KeepAlive(myVariable);
+ }
+
+#line default
+#line hidden
+#nullable disable
+
}
}
#pragma warning restore 1591
diff --git a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Component_WithCssScope/TestComponent.decl.codegen.cs b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Component_WithCssScope/TestComponent.decl.codegen.cs
index d4317c428c923..50a6593d3b647 100644
--- a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Component_WithCssScope/TestComponent.decl.codegen.cs
+++ b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Component_WithCssScope/TestComponent.decl.codegen.cs
@@ -27,50 +27,25 @@ public partial class TestComponent : global::Microsoft.AspNetCore.Components.Com
#nullable disable
{
#nullable restore
-#line (17,8)-(26,1) "x:\dir\subdir\Test\TestComponent.cshtml"
+#line (17,8)-(19,1) "x:\dir\subdir\Test\TestComponent.cshtml"
ElementReference myElementReference;
- TemplatedComponent myComponentReference;
- string myVariable;
-
- void MethodRenderingMarkup(RenderTreeBuilder __builder)
- {
- for (var i = 0; i < 10; i++)
- {
#line default
#line hidden
#nullable disable
- __builder.OpenElement(0, "li");
- __builder.AddAttribute(1, "data-index",
#nullable restore
-#line (26,29)-(26,30) "x:\dir\subdir\Test\TestComponent.cshtml"
-i
+#line (19,1)-(20,1) "x:\dir\subdir\Test\TestComponent.cshtml"
+ TemplatedComponent myComponentReference;
#line default
#line hidden
#nullable disable
- );
- __builder.AddAttribute(2, "TestCssScope");
- __builder.AddContent(3, "Something ");
-#nullable restore
-#line (26,42)-(26,43) 24 "x:\dir\subdir\Test\TestComponent.cshtml"
-__builder.AddContent(4, i
-#line default
-#line hidden
-#nullable disable
- );
- __builder.CloseElement();
#nullable restore
-#line (27,1)-(33,1) "x:\dir\subdir\Test\TestComponent.cshtml"
- }
-
- System.GC.KeepAlive(myElementReference);
- System.GC.KeepAlive(myComponentReference);
- System.GC.KeepAlive(myVariable);
- }
+#line (20,1)-(21,1) "x:\dir\subdir\Test\TestComponent.cshtml"
+ string myVariable;
#line default
#line hidden
diff --git a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Component_WithCssScope/TestComponent.decl.mappings.txt b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Component_WithCssScope/TestComponent.decl.mappings.txt
index 38c5f1fc02575..a4fa693878f3f 100644
--- a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Component_WithCssScope/TestComponent.decl.mappings.txt
+++ b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Component_WithCssScope/TestComponent.decl.mappings.txt
@@ -8,53 +8,26 @@ Source Location: (45:1,1 [47] x:\dir\subdir\Test\TestComponent.cshtml)
Generated Location: (524:18,0 [47] )
|using Microsoft.AspNetCore.Components.Rendering|
-Source Location: (651:16,7 [233] x:\dir\subdir\Test\TestComponent.cshtml)
+Source Location: (651:16,7 [44] x:\dir\subdir\Test\TestComponent.cshtml)
|
ElementReference myElementReference;
- TemplatedComponent myComponentReference;
- string myVariable;
-
- void MethodRenderingMarkup(RenderTreeBuilder __builder)
- {
- for (var i = 0; i < 10; i++)
- {
|
-Generated Location: (869:30,0 [233] )
+Generated Location: (869:30,0 [44] )
|
ElementReference myElementReference;
- TemplatedComponent myComponentReference;
- string myVariable;
-
- void MethodRenderingMarkup(RenderTreeBuilder __builder)
- {
- for (var i = 0; i < 10; i++)
- {
|
-Source Location: (912:25,28 [1] x:\dir\subdir\Test\TestComponent.cshtml)
-|i|
-Generated Location: (1329:48,0 [1] )
-|i|
-
-Source Location: (925:25,41 [1] x:\dir\subdir\Test\TestComponent.cshtml)
-|i|
-Generated Location: (1605:58,24 [1] )
-|i|
-
-Source Location: (933:26,0 [164] x:\dir\subdir\Test\TestComponent.cshtml)
-| }
-
- System.GC.KeepAlive(myElementReference);
- System.GC.KeepAlive(myComponentReference);
- System.GC.KeepAlive(myVariable);
- }
+Source Location: (695:18,0 [46] x:\dir\subdir\Test\TestComponent.cshtml)
+| TemplatedComponent myComponentReference;
+|
+Generated Location: (1047:39,0 [46] )
+| TemplatedComponent myComponentReference;
|
-Generated Location: (1787:67,0 [164] )
-| }
- System.GC.KeepAlive(myElementReference);
- System.GC.KeepAlive(myComponentReference);
- System.GC.KeepAlive(myVariable);
- }
+Source Location: (741:19,0 [24] x:\dir\subdir\Test\TestComponent.cshtml)
+| string myVariable;
+|
+Generated Location: (1227:47,0 [24] )
+| string myVariable;
|
diff --git a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Component_WithCssScope/TestComponent.mappings.txt b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Component_WithCssScope/TestComponent.mappings.txt
index e910a5ddd489d..89c0c08713229 100644
--- a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Component_WithCssScope/TestComponent.mappings.txt
+++ b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Component_WithCssScope/TestComponent.mappings.txt
@@ -49,3 +49,45 @@ Generated Location: (4078:108,0 [3] )
|}
|
+Source Location: (765:20,0 [119] x:\dir\subdir\Test\TestComponent.cshtml)
+|
+ void MethodRenderingMarkup(RenderTreeBuilder __builder)
+ {
+ for (var i = 0; i < 10; i++)
+ {
+|
+Generated Location: (4264:118,0 [119] )
+|
+ void MethodRenderingMarkup(RenderTreeBuilder __builder)
+ {
+ for (var i = 0; i < 10; i++)
+ {
+|
+
+Source Location: (912:25,28 [1] x:\dir\subdir\Test\TestComponent.cshtml)
+|i|
+Generated Location: (4612:132,0 [1] )
+|i|
+
+Source Location: (925:25,41 [1] x:\dir\subdir\Test\TestComponent.cshtml)
+|i|
+Generated Location: (4891:142,25 [1] )
+|i|
+
+Source Location: (933:26,0 [164] x:\dir\subdir\Test\TestComponent.cshtml)
+| }
+
+ System.GC.KeepAlive(myElementReference);
+ System.GC.KeepAlive(myComponentReference);
+ System.GC.KeepAlive(myVariable);
+ }
+|
+Generated Location: (5073:151,0 [164] )
+| }
+
+ System.GC.KeepAlive(myElementReference);
+ System.GC.KeepAlive(myComponentReference);
+ System.GC.KeepAlive(myVariable);
+ }
+|
+
diff --git a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Legacy_3_1_WhiteSpace_InMarkupInFunctionsBlock/TestComponent.codegen.cs b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Legacy_3_1_WhiteSpace_InMarkupInFunctionsBlock/TestComponent.codegen.cs
index 05ce811a28039..5cea863294169 100644
--- a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Legacy_3_1_WhiteSpace_InMarkupInFunctionsBlock/TestComponent.codegen.cs
+++ b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Legacy_3_1_WhiteSpace_InMarkupInFunctionsBlock/TestComponent.codegen.cs
@@ -25,6 +25,69 @@ protected override void BuildRenderTree(global::Microsoft.AspNetCore.Components.
{
}
#pragma warning restore 1998
+#nullable restore
+#line (2,8)-(5,1) "x:\dir\subdir\Test\TestComponent.cshtml"
+
+ void MyMethod(RenderTreeBuilder __builder)
+ {
+
+#line default
+#line hidden
+#nullable disable
+
+ __builder.AddContent(0, " ");
+ __builder.OpenElement(1, "ul");
+ __builder.AddMarkupContent(2, "\r\n");
+#nullable restore
+#line (6,1)-(6,13) "x:\dir\subdir\Test\TestComponent.cshtml"
+
+
+#line default
+#line hidden
+#nullable disable
+
+#nullable restore
+#line (6,14)-(8,1) "x:\dir\subdir\Test\TestComponent.cshtml"
+for (var i = 0; i < 100; i++)
+ {
+
+#line default
+#line hidden
+#nullable disable
+
+ __builder.AddContent(3, " ");
+ __builder.OpenElement(4, "li");
+ __builder.AddMarkupContent(5, "\r\n ");
+#nullable restore
+#line (9,22)-(9,23) 24 "x:\dir\subdir\Test\TestComponent.cshtml"
+__builder.AddContent(6, i
+
+#line default
+#line hidden
+#nullable disable
+ );
+ __builder.AddMarkupContent(7, "\r\n ");
+ __builder.CloseElement();
+ __builder.AddMarkupContent(8, "\r\n");
+#nullable restore
+#line (11,1)-(12,1) "x:\dir\subdir\Test\TestComponent.cshtml"
+ }
+
+#line default
+#line hidden
+#nullable disable
+
+ __builder.AddContent(9, " ");
+ __builder.CloseElement();
+ __builder.AddMarkupContent(10, "\r\n");
+#nullable restore
+#line (13,1)-(14,1) "x:\dir\subdir\Test\TestComponent.cshtml"
+ }
+
+#line default
+#line hidden
+#nullable disable
+
}
}
#pragma warning restore 1591
diff --git a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Legacy_3_1_WhiteSpace_InMarkupInFunctionsBlock/TestComponent.decl.codegen.cs b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Legacy_3_1_WhiteSpace_InMarkupInFunctionsBlock/TestComponent.decl.codegen.cs
index 17b5196a42161..e4d3195ddb7a1 100644
--- a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Legacy_3_1_WhiteSpace_InMarkupInFunctionsBlock/TestComponent.decl.codegen.cs
+++ b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Legacy_3_1_WhiteSpace_InMarkupInFunctionsBlock/TestComponent.decl.codegen.cs
@@ -20,69 +20,6 @@ namespace Test
public partial class TestComponent : global::Microsoft.AspNetCore.Components.ComponentBase
#nullable disable
{
-#nullable restore
-#line (2,8)-(5,1) "x:\dir\subdir\Test\TestComponent.cshtml"
-
- void MyMethod(RenderTreeBuilder __builder)
- {
-
-#line default
-#line hidden
-#nullable disable
-
- __builder.AddContent(0, " ");
- __builder.OpenElement(1, "ul");
- __builder.AddMarkupContent(2, "\r\n");
-#nullable restore
-#line (6,1)-(6,13) "x:\dir\subdir\Test\TestComponent.cshtml"
-
-
-#line default
-#line hidden
-#nullable disable
-
-#nullable restore
-#line (6,14)-(8,1) "x:\dir\subdir\Test\TestComponent.cshtml"
-for (var i = 0; i < 100; i++)
- {
-
-#line default
-#line hidden
-#nullable disable
-
- __builder.AddContent(3, " ");
- __builder.OpenElement(4, "li");
- __builder.AddMarkupContent(5, "\r\n ");
-#nullable restore
-#line (9,22)-(9,23) 24 "x:\dir\subdir\Test\TestComponent.cshtml"
-__builder.AddContent(6, i
-
-#line default
-#line hidden
-#nullable disable
- );
- __builder.AddMarkupContent(7, "\r\n ");
- __builder.CloseElement();
- __builder.AddMarkupContent(8, "\r\n");
-#nullable restore
-#line (11,1)-(12,1) "x:\dir\subdir\Test\TestComponent.cshtml"
- }
-
-#line default
-#line hidden
-#nullable disable
-
- __builder.AddContent(9, " ");
- __builder.CloseElement();
- __builder.AddMarkupContent(10, "\r\n");
-#nullable restore
-#line (13,1)-(14,1) "x:\dir\subdir\Test\TestComponent.cshtml"
- }
-
-#line default
-#line hidden
-#nullable disable
-
}
}
#pragma warning restore 1591
diff --git a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Legacy_3_1_WhiteSpace_InMarkupInFunctionsBlock/TestComponent.decl.mappings.txt b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Legacy_3_1_WhiteSpace_InMarkupInFunctionsBlock/TestComponent.decl.mappings.txt
index 6e259d7c13a56..7eae02f423321 100644
--- a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Legacy_3_1_WhiteSpace_InMarkupInFunctionsBlock/TestComponent.decl.mappings.txt
+++ b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Legacy_3_1_WhiteSpace_InMarkupInFunctionsBlock/TestComponent.decl.mappings.txt
@@ -3,47 +3,3 @@
Generated Location: (372:12,0 [47] )
|using Microsoft.AspNetCore.Components.Rendering|
-Source Location: (57:1,7 [57] x:\dir\subdir\Test\TestComponent.cshtml)
-|
- void MyMethod(RenderTreeBuilder __builder)
- {
-|
-Generated Location: (715:24,0 [57] )
-|
- void MyMethod(RenderTreeBuilder __builder)
- {
-|
-
-Source Location: (128:5,0 [12] x:\dir\subdir\Test\TestComponent.cshtml)
-| |
-Generated Location: (1040:37,0 [12] )
-| |
-
-Source Location: (141:5,13 [46] x:\dir\subdir\Test\TestComponent.cshtml)
-|for (var i = 0; i < 100; i++)
- {
-|
-Generated Location: (1187:45,0 [46] )
-|for (var i = 0; i < 100; i++)
- {
-|
-
-Source Location: (230:8,21 [1] x:\dir\subdir\Test\TestComponent.cshtml)
-|i|
-Generated Location: (1557:57,24 [1] )
-|i|
-
-Source Location: (256:10,0 [15] x:\dir\subdir\Test\TestComponent.cshtml)
-| }
-|
-Generated Location: (1851:68,0 [15] )
-| }
-|
-
-Source Location: (286:12,0 [7] x:\dir\subdir\Test\TestComponent.cshtml)
-| }
-|
-Generated Location: (2130:79,0 [7] )
-| }
-|
-
diff --git a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Legacy_3_1_WhiteSpace_InMarkupInFunctionsBlock/TestComponent.mappings.txt b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Legacy_3_1_WhiteSpace_InMarkupInFunctionsBlock/TestComponent.mappings.txt
index 7eae02f423321..23ed4fd5eb9f3 100644
--- a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Legacy_3_1_WhiteSpace_InMarkupInFunctionsBlock/TestComponent.mappings.txt
+++ b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/Legacy_3_1_WhiteSpace_InMarkupInFunctionsBlock/TestComponent.mappings.txt
@@ -3,3 +3,47 @@
Generated Location: (372:12,0 [47] )
|using Microsoft.AspNetCore.Components.Rendering|
+Source Location: (57:1,7 [57] x:\dir\subdir\Test\TestComponent.cshtml)
+|
+ void MyMethod(RenderTreeBuilder __builder)
+ {
+|
+Generated Location: (941:29,0 [57] )
+|
+ void MyMethod(RenderTreeBuilder __builder)
+ {
+|
+
+Source Location: (128:5,0 [12] x:\dir\subdir\Test\TestComponent.cshtml)
+| |
+Generated Location: (1266:42,0 [12] )
+| |
+
+Source Location: (141:5,13 [46] x:\dir\subdir\Test\TestComponent.cshtml)
+|for (var i = 0; i < 100; i++)
+ {
+|
+Generated Location: (1413:50,0 [46] )
+|for (var i = 0; i < 100; i++)
+ {
+|
+
+Source Location: (230:8,21 [1] x:\dir\subdir\Test\TestComponent.cshtml)
+|i|
+Generated Location: (1783:62,24 [1] )
+|i|
+
+Source Location: (256:10,0 [15] x:\dir\subdir\Test\TestComponent.cshtml)
+| }
+|
+Generated Location: (2077:73,0 [15] )
+| }
+|
+
+Source Location: (286:12,0 [7] x:\dir\subdir\Test\TestComponent.cshtml)
+| }
+|
+Generated Location: (2356:84,0 [7] )
+| }
+|
+
diff --git a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/SingleLineControlFlowStatements_InCodeDirective/TestComponent.codegen.cs b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/SingleLineControlFlowStatements_InCodeDirective/TestComponent.codegen.cs
index 20e9aaf6a8da5..edd877af76c66 100644
--- a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/SingleLineControlFlowStatements_InCodeDirective/TestComponent.codegen.cs
+++ b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/SingleLineControlFlowStatements_InCodeDirective/TestComponent.codegen.cs
@@ -24,6 +24,38 @@ protected override void BuildRenderTree(global::Microsoft.AspNetCore.Components.
{
}
#pragma warning restore 1998
+#nullable restore
+#line (3,8)-(9,1) "x:\dir\subdir\Test\TestComponent.cshtml"
+
+ void RenderChildComponent(RenderTreeBuilder __builder)
+ {
+ var output = string.Empty;
+ if (__builder == null) output = "Builder is null!";
+ else output = "Builder is not null!";
+
+#line default
+#line hidden
+#nullable disable
+
+ __builder.OpenElement(0, "p");
+ __builder.AddContent(1, "Output: ");
+#nullable restore
+#line (9,21)-(9,27) 24 "x:\dir\subdir\Test\TestComponent.cshtml"
+__builder.AddContent(2, output
+
+#line default
+#line hidden
+#nullable disable
+ );
+ __builder.CloseElement();
+#nullable restore
+#line (10,1)-(11,1) "x:\dir\subdir\Test\TestComponent.cshtml"
+ }
+
+#line default
+#line hidden
+#nullable disable
+
}
}
#pragma warning restore 1591
diff --git a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/SingleLineControlFlowStatements_InCodeDirective/TestComponent.decl.codegen.cs b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/SingleLineControlFlowStatements_InCodeDirective/TestComponent.decl.codegen.cs
index ebb0b2ad852ac..262493e325900 100644
--- a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/SingleLineControlFlowStatements_InCodeDirective/TestComponent.decl.codegen.cs
+++ b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/SingleLineControlFlowStatements_InCodeDirective/TestComponent.decl.codegen.cs
@@ -19,38 +19,6 @@ namespace Test
public partial class TestComponent : global::Microsoft.AspNetCore.Components.ComponentBase
#nullable disable
{
-#nullable restore
-#line (3,8)-(9,1) "x:\dir\subdir\Test\TestComponent.cshtml"
-
- void RenderChildComponent(RenderTreeBuilder __builder)
- {
- var output = string.Empty;
- if (__builder == null) output = "Builder is null!";
- else output = "Builder is not null!";
-
-#line default
-#line hidden
-#nullable disable
-
- __builder.OpenElement(0, "p");
- __builder.AddContent(1, "Output: ");
-#nullable restore
-#line (9,21)-(9,27) 24 "x:\dir\subdir\Test\TestComponent.cshtml"
-__builder.AddContent(2, output
-
-#line default
-#line hidden
-#nullable disable
- );
- __builder.CloseElement();
-#nullable restore
-#line (10,1)-(11,1) "x:\dir\subdir\Test\TestComponent.cshtml"
- }
-
-#line default
-#line hidden
-#nullable disable
-
}
}
#pragma warning restore 1591
diff --git a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/SingleLineControlFlowStatements_InCodeDirective/TestComponent.decl.mappings.txt b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/SingleLineControlFlowStatements_InCodeDirective/TestComponent.decl.mappings.txt
index e098374fe0949..1c936962d3b9e 100644
--- a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/SingleLineControlFlowStatements_InCodeDirective/TestComponent.decl.mappings.txt
+++ b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/SingleLineControlFlowStatements_InCodeDirective/TestComponent.decl.mappings.txt
@@ -3,32 +3,3 @@
Generated Location: (372:12,0 [48] )
|using Microsoft.AspNetCore.Components.Rendering;|
-Source Location: (60:2,7 [213] x:\dir\subdir\Test\TestComponent.cshtml)
-|
- void RenderChildComponent(RenderTreeBuilder __builder)
- {
- var output = string.Empty;
- if (__builder == null) output = "Builder is null!";
- else output = "Builder is not null!";
-|
-Generated Location: (709:23,0 [213] )
-|
- void RenderChildComponent(RenderTreeBuilder __builder)
- {
- var output = string.Empty;
- if (__builder == null) output = "Builder is null!";
- else output = "Builder is not null!";
-|
-
-Source Location: (293:8,20 [6] x:\dir\subdir\Test\TestComponent.cshtml)
-|output|
-Generated Location: (1169:38,24 [6] )
-|output|
-
-Source Location: (305:9,0 [7] x:\dir\subdir\Test\TestComponent.cshtml)
-| }
-|
-Generated Location: (1356:47,0 [7] )
-| }
-|
-
diff --git a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/SingleLineControlFlowStatements_InCodeDirective/TestComponent.mappings.txt b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/SingleLineControlFlowStatements_InCodeDirective/TestComponent.mappings.txt
index 1c936962d3b9e..cba1b49db6c28 100644
--- a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/SingleLineControlFlowStatements_InCodeDirective/TestComponent.mappings.txt
+++ b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/SingleLineControlFlowStatements_InCodeDirective/TestComponent.mappings.txt
@@ -3,3 +3,32 @@
Generated Location: (372:12,0 [48] )
|using Microsoft.AspNetCore.Components.Rendering;|
+Source Location: (60:2,7 [213] x:\dir\subdir\Test\TestComponent.cshtml)
+|
+ void RenderChildComponent(RenderTreeBuilder __builder)
+ {
+ var output = string.Empty;
+ if (__builder == null) output = "Builder is null!";
+ else output = "Builder is not null!";
+|
+Generated Location: (935:28,0 [213] )
+|
+ void RenderChildComponent(RenderTreeBuilder __builder)
+ {
+ var output = string.Empty;
+ if (__builder == null) output = "Builder is null!";
+ else output = "Builder is not null!";
+|
+
+Source Location: (293:8,20 [6] x:\dir\subdir\Test\TestComponent.cshtml)
+|output|
+Generated Location: (1395:43,24 [6] )
+|output|
+
+Source Location: (305:9,0 [7] x:\dir\subdir\Test\TestComponent.cshtml)
+| }
+|
+Generated Location: (1582:52,0 [7] )
+| }
+|
+
diff --git a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/SwitchExpression_WithMarkupInLambdaArms/TestComponent.codegen.cs b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/SwitchExpression_WithMarkupInLambdaArms/TestComponent.codegen.cs
index bbd6ee69e7a12..29f06675c5c71 100644
--- a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/SwitchExpression_WithMarkupInLambdaArms/TestComponent.codegen.cs
+++ b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/SwitchExpression_WithMarkupInLambdaArms/TestComponent.codegen.cs
@@ -24,6 +24,55 @@ protected override void BuildRenderTree(global::Microsoft.AspNetCore.Components.
{
}
#pragma warning restore 1998
+#nullable restore
+#line (5,1)-(9,10) "x:\dir\subdir\Test\TestComponent.cshtml"
+
+ private static RenderFragment RenderBadge(SampleType type) => type switch
+ {
+ SampleType.Alpha => (__builder) =>
+ {
+
+#line default
+#line hidden
+#nullable disable
+
+#nullable restore
+#line (9,10)-(10,1) "x:\dir\subdir\Test\TestComponent.cshtml"
+
+
+#line default
+#line hidden
+#nullable disable
+
+ __builder.AddMarkupContent(0, "Alpha");
+#nullable restore
+#line (11,1)-(13,10) "x:\dir\subdir\Test\TestComponent.cshtml"
+ },
+ _ => (__builder) =>
+ {
+
+#line default
+#line hidden
+#nullable disable
+
+#nullable restore
+#line (13,10)-(14,1) "x:\dir\subdir\Test\TestComponent.cshtml"
+
+
+#line default
+#line hidden
+#nullable disable
+
+ __builder.AddMarkupContent(1, "Unknown");
+#nullable restore
+#line (15,1)-(17,1) "x:\dir\subdir\Test\TestComponent.cshtml"
+ }
+ };
+
+#line default
+#line hidden
+#nullable disable
+
}
}
#pragma warning restore 1591
diff --git a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/SwitchExpression_WithMarkupInLambdaArms/TestComponent.decl.codegen.cs b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/SwitchExpression_WithMarkupInLambdaArms/TestComponent.decl.codegen.cs
index 11750c707bbd2..b7c0e726ec920 100644
--- a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/SwitchExpression_WithMarkupInLambdaArms/TestComponent.decl.codegen.cs
+++ b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/SwitchExpression_WithMarkupInLambdaArms/TestComponent.decl.codegen.cs
@@ -20,52 +20,10 @@ public partial class TestComponent : global::Microsoft.AspNetCore.Components.Com
#nullable disable
{
#nullable restore
-#line (3,8)-(9,10) "x:\dir\subdir\Test\TestComponent.cshtml"
+#line (3,8)-(5,1) "x:\dir\subdir\Test\TestComponent.cshtml"
public enum SampleType { Alpha, Beta, Gamma }
- private static RenderFragment RenderBadge(SampleType type) => type switch
- {
- SampleType.Alpha => (__builder) =>
- {
-
-#line default
-#line hidden
-#nullable disable
-
-#nullable restore
-#line (9,10)-(10,1) "x:\dir\subdir\Test\TestComponent.cshtml"
-
-
-#line default
-#line hidden
-#nullable disable
-
- __builder.AddMarkupContent(0, "Alpha");
-#nullable restore
-#line (11,1)-(13,10) "x:\dir\subdir\Test\TestComponent.cshtml"
- },
- _ => (__builder) =>
- {
-
-#line default
-#line hidden
-#nullable disable
-
-#nullable restore
-#line (13,10)-(14,1) "x:\dir\subdir\Test\TestComponent.cshtml"
-
-
-#line default
-#line hidden
-#nullable disable
-
- __builder.AddMarkupContent(1, "Unknown");
-#nullable restore
-#line (15,1)-(17,1) "x:\dir\subdir\Test\TestComponent.cshtml"
- }
- };
-
#line default
#line hidden
#nullable disable
diff --git a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/SwitchExpression_WithMarkupInLambdaArms/TestComponent.decl.mappings.txt b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/SwitchExpression_WithMarkupInLambdaArms/TestComponent.decl.mappings.txt
index 9104888da88f0..ba199b293e158 100644
--- a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/SwitchExpression_WithMarkupInLambdaArms/TestComponent.decl.mappings.txt
+++ b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/SwitchExpression_WithMarkupInLambdaArms/TestComponent.decl.mappings.txt
@@ -3,52 +3,12 @@
Generated Location: (320:11,0 [37] )
|using Microsoft.AspNetCore.Components|
-Source Location: (49:2,7 [194] x:\dir\subdir\Test\TestComponent.cshtml)
+Source Location: (49:2,7 [53] x:\dir\subdir\Test\TestComponent.cshtml)
|
public enum SampleType { Alpha, Beta, Gamma }
-
- private static RenderFragment RenderBadge(SampleType type) => type switch
- {
- SampleType.Alpha => (__builder) =>
- {|
-Generated Location: (654:23,0 [194] )
-|
- public enum SampleType { Alpha, Beta, Gamma }
-
- private static RenderFragment RenderBadge(SampleType type) => type switch
- {
- SampleType.Alpha => (__builder) =>
- {|
-
-Source Location: (243:8,9 [2] x:\dir\subdir\Test\TestComponent.cshtml)
-|
-|
-Generated Location: (984:37,0 [2] )
-|
|
-
-Source Location: (277:10,0 [50] x:\dir\subdir\Test\TestComponent.cshtml)
-| },
- _ => (__builder) =>
- {|
-Generated Location: (1183:46,0 [50] )
-| },
- _ => (__builder) =>
- {|
-
-Source Location: (327:12,9 [2] x:\dir\subdir\Test\TestComponent.cshtml)
-|
-|
-Generated Location: (1370:56,0 [2] )
+Generated Location: (653:23,0 [53] )
|
-|
-
-Source Location: (363:14,0 [19] x:\dir\subdir\Test\TestComponent.cshtml)
-| }
- };
-|
-Generated Location: (1570:65,0 [19] )
-| }
- };
+ public enum SampleType { Alpha, Beta, Gamma }
|
diff --git a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/SwitchExpression_WithMarkupInLambdaArms/TestComponent.mappings.txt b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/SwitchExpression_WithMarkupInLambdaArms/TestComponent.mappings.txt
index bd712b2e103f1..0a5c85dff7051 100644
--- a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/SwitchExpression_WithMarkupInLambdaArms/TestComponent.mappings.txt
+++ b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/SwitchExpression_WithMarkupInLambdaArms/TestComponent.mappings.txt
@@ -3,3 +3,48 @@
Generated Location: (320:11,0 [37] )
|using Microsoft.AspNetCore.Components|
+Source Location: (102:4,0 [141] x:\dir\subdir\Test\TestComponent.cshtml)
+|
+ private static RenderFragment RenderBadge(SampleType type) => type switch
+ {
+ SampleType.Alpha => (__builder) =>
+ {|
+Generated Location: (880:28,0 [141] )
+|
+ private static RenderFragment RenderBadge(SampleType type) => type switch
+ {
+ SampleType.Alpha => (__builder) =>
+ {|
+
+Source Location: (243:8,9 [2] x:\dir\subdir\Test\TestComponent.cshtml)
+|
+|
+Generated Location: (1157:40,0 [2] )
+|
+|
+
+Source Location: (277:10,0 [50] x:\dir\subdir\Test\TestComponent.cshtml)
+| },
+ _ => (__builder) =>
+ {|
+Generated Location: (1356:49,0 [50] )
+| },
+ _ => (__builder) =>
+ {|
+
+Source Location: (327:12,9 [2] x:\dir\subdir\Test\TestComponent.cshtml)
+|
+|
+Generated Location: (1543:59,0 [2] )
+|
+|
+
+Source Location: (363:14,0 [19] x:\dir\subdir\Test\TestComponent.cshtml)
+| }
+ };
+|
+Generated Location: (1743:68,0 [19] )
+| }
+ };
+|
+
diff --git a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/WhiteSpace_InMarkupInFunctionsBlock/TestComponent.codegen.cs b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/WhiteSpace_InMarkupInFunctionsBlock/TestComponent.codegen.cs
index 05ce811a28039..91e7bc73127d3 100644
--- a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/WhiteSpace_InMarkupInFunctionsBlock/TestComponent.codegen.cs
+++ b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/WhiteSpace_InMarkupInFunctionsBlock/TestComponent.codegen.cs
@@ -25,6 +25,61 @@ protected override void BuildRenderTree(global::Microsoft.AspNetCore.Components.
{
}
#pragma warning restore 1998
+#nullable restore
+#line (2,8)-(5,1) "x:\dir\subdir\Test\TestComponent.cshtml"
+
+ void MyMethod(RenderTreeBuilder __builder)
+ {
+
+#line default
+#line hidden
+#nullable disable
+
+ __builder.OpenElement(0, "ul");
+#nullable restore
+#line (6,1)-(6,13) "x:\dir\subdir\Test\TestComponent.cshtml"
+
+
+#line default
+#line hidden
+#nullable disable
+
+#nullable restore
+#line (6,14)-(8,1) "x:\dir\subdir\Test\TestComponent.cshtml"
+for (var i = 0; i < 100; i++)
+ {
+
+#line default
+#line hidden
+#nullable disable
+
+ __builder.OpenElement(1, "li");
+#nullable restore
+#line (9,22)-(9,23) 24 "x:\dir\subdir\Test\TestComponent.cshtml"
+__builder.AddContent(2, i
+
+#line default
+#line hidden
+#nullable disable
+ );
+ __builder.CloseElement();
+#nullable restore
+#line (11,1)-(12,1) "x:\dir\subdir\Test\TestComponent.cshtml"
+ }
+
+#line default
+#line hidden
+#nullable disable
+
+ __builder.CloseElement();
+#nullable restore
+#line (13,1)-(14,1) "x:\dir\subdir\Test\TestComponent.cshtml"
+ }
+
+#line default
+#line hidden
+#nullable disable
+
}
}
#pragma warning restore 1591
diff --git a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/WhiteSpace_InMarkupInFunctionsBlock/TestComponent.decl.codegen.cs b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/WhiteSpace_InMarkupInFunctionsBlock/TestComponent.decl.codegen.cs
index b9310cbe535b9..e4d3195ddb7a1 100644
--- a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/WhiteSpace_InMarkupInFunctionsBlock/TestComponent.decl.codegen.cs
+++ b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/WhiteSpace_InMarkupInFunctionsBlock/TestComponent.decl.codegen.cs
@@ -20,61 +20,6 @@ namespace Test
public partial class TestComponent : global::Microsoft.AspNetCore.Components.ComponentBase
#nullable disable
{
-#nullable restore
-#line (2,8)-(5,1) "x:\dir\subdir\Test\TestComponent.cshtml"
-
- void MyMethod(RenderTreeBuilder __builder)
- {
-
-#line default
-#line hidden
-#nullable disable
-
- __builder.OpenElement(0, "ul");
-#nullable restore
-#line (6,1)-(6,13) "x:\dir\subdir\Test\TestComponent.cshtml"
-
-
-#line default
-#line hidden
-#nullable disable
-
-#nullable restore
-#line (6,14)-(8,1) "x:\dir\subdir\Test\TestComponent.cshtml"
-for (var i = 0; i < 100; i++)
- {
-
-#line default
-#line hidden
-#nullable disable
-
- __builder.OpenElement(1, "li");
-#nullable restore
-#line (9,22)-(9,23) 24 "x:\dir\subdir\Test\TestComponent.cshtml"
-__builder.AddContent(2, i
-
-#line default
-#line hidden
-#nullable disable
- );
- __builder.CloseElement();
-#nullable restore
-#line (11,1)-(12,1) "x:\dir\subdir\Test\TestComponent.cshtml"
- }
-
-#line default
-#line hidden
-#nullable disable
-
- __builder.CloseElement();
-#nullable restore
-#line (13,1)-(14,1) "x:\dir\subdir\Test\TestComponent.cshtml"
- }
-
-#line default
-#line hidden
-#nullable disable
-
}
}
#pragma warning restore 1591
diff --git a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/WhiteSpace_InMarkupInFunctionsBlock/TestComponent.decl.mappings.txt b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/WhiteSpace_InMarkupInFunctionsBlock/TestComponent.decl.mappings.txt
index 57932cc4f7f5d..7eae02f423321 100644
--- a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/WhiteSpace_InMarkupInFunctionsBlock/TestComponent.decl.mappings.txt
+++ b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/WhiteSpace_InMarkupInFunctionsBlock/TestComponent.decl.mappings.txt
@@ -3,47 +3,3 @@
Generated Location: (372:12,0 [47] )
|using Microsoft.AspNetCore.Components.Rendering|
-Source Location: (57:1,7 [57] x:\dir\subdir\Test\TestComponent.cshtml)
-|
- void MyMethod(RenderTreeBuilder __builder)
- {
-|
-Generated Location: (715:24,0 [57] )
-|
- void MyMethod(RenderTreeBuilder __builder)
- {
-|
-
-Source Location: (128:5,0 [12] x:\dir\subdir\Test\TestComponent.cshtml)
-| |
-Generated Location: (946:35,0 [12] )
-| |
-
-Source Location: (141:5,13 [46] x:\dir\subdir\Test\TestComponent.cshtml)
-|for (var i = 0; i < 100; i++)
- {
-|
-Generated Location: (1093:43,0 [46] )
-|for (var i = 0; i < 100; i++)
- {
-|
-
-Source Location: (230:8,21 [1] x:\dir\subdir\Test\TestComponent.cshtml)
-|i|
-Generated Location: (1341:53,24 [1] )
-|i|
-
-Source Location: (256:10,0 [15] x:\dir\subdir\Test\TestComponent.cshtml)
-| }
-|
-Generated Location: (1523:62,0 [15] )
-| }
-|
-
-Source Location: (286:12,0 [7] x:\dir\subdir\Test\TestComponent.cshtml)
-| }
-|
-Generated Location: (1707:71,0 [7] )
-| }
-|
-
diff --git a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/WhiteSpace_InMarkupInFunctionsBlock/TestComponent.mappings.txt b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/WhiteSpace_InMarkupInFunctionsBlock/TestComponent.mappings.txt
index 7eae02f423321..dc4bffdfbb3fa 100644
--- a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/WhiteSpace_InMarkupInFunctionsBlock/TestComponent.mappings.txt
+++ b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/TestFiles/IntegrationTests/ComponentCodeGenerationTest/WhiteSpace_InMarkupInFunctionsBlock/TestComponent.mappings.txt
@@ -3,3 +3,47 @@
Generated Location: (372:12,0 [47] )
|using Microsoft.AspNetCore.Components.Rendering|
+Source Location: (57:1,7 [57] x:\dir\subdir\Test\TestComponent.cshtml)
+|
+ void MyMethod(RenderTreeBuilder __builder)
+ {
+|
+Generated Location: (941:29,0 [57] )
+|
+ void MyMethod(RenderTreeBuilder __builder)
+ {
+|
+
+Source Location: (128:5,0 [12] x:\dir\subdir\Test\TestComponent.cshtml)
+| |
+Generated Location: (1172:40,0 [12] )
+| |
+
+Source Location: (141:5,13 [46] x:\dir\subdir\Test\TestComponent.cshtml)
+|for (var i = 0; i < 100; i++)
+ {
+|
+Generated Location: (1319:48,0 [46] )
+|for (var i = 0; i < 100; i++)
+ {
+|
+
+Source Location: (230:8,21 [1] x:\dir\subdir\Test\TestComponent.cshtml)
+|i|
+Generated Location: (1567:58,24 [1] )
+|i|
+
+Source Location: (256:10,0 [15] x:\dir\subdir\Test\TestComponent.cshtml)
+| }
+|
+Generated Location: (1749:67,0 [15] )
+| }
+|
+
+Source Location: (286:12,0 [7] x:\dir\subdir\Test\TestComponent.cshtml)
+| }
+|
+Generated Location: (1933:76,0 [7] )
+| }
+|
+
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorCSharpLoweringPhase.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorCSharpLoweringPhase.cs
index c0316286540f0..3360c1aa6c593 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorCSharpLoweringPhase.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorCSharpLoweringPhase.cs
@@ -25,11 +25,20 @@ protected override RazorCodeDocument ExecuteCore(RazorCodeDocument codeDocument,
throw new InvalidOperationException(message);
}
- // Fork: 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 compiler-synthesized plumbing. Otherwise (non-component, suppressed primary
- // method body, malformed primary structure, or the decl phase didn't run for any
- // other reason) fall through to the original single-file lowering path.
+ // When the split partitioned the component before resolution, the working node is already the
+ // impl half: write it directly rather than re-deriving an impl spine from a classified single tree.
+ if (documentNode.IsSplitImplDocument)
+ {
+ var implCSharpDocument = RazorCSharpDocumentWriter.Write(documentNode, codeDocument, cancellationToken: cancellationToken);
+ return codeDocument.WithImplCSharpDocument(implCSharpDocument);
+ }
+
+ // The decl phase produced its half from the classified tree (a markup-free component, or a markup
+ // component whose shape the split couldn't partition early). Produce the matching impl half -- a
+ // partial class with the render method body, any compiler-synthesized plumbing, and (for the
+ // fallback case) the markup-bearing methods lifted from the class body. Otherwise (non-component,
+ // suppressed primary method body, malformed primary structure, or the decl phase didn't run for
+ // any other reason) fall through to the original single-file lowering path.
if (codeDocument.GetDeclCSharpDocument() is not null &&
TryWriteImplDocument(documentNode, codeDocument, cancellationToken, out var implDocument))
{
@@ -81,6 +90,20 @@ private static bool TryWriteImplDocument(
}
}
+ // Fallback tier: a markup component the early split couldn't partition reaches here unsplit, so
+ // lift its markup-bearing methods into the impl half over the classified tree.
+ var plan = MarkupSplitter.GetRoutablePlan(primaryClass, renderMethod, codeDocument.ParserOptions);
+ if (plan is not null)
+ {
+ foreach (var member in plan.Members)
+ {
+ foreach (var piece in member.ImplPieces)
+ {
+ implClass.Children.Add(piece);
+ }
+ }
+ }
+
foreach (var usingDirective in usings)
{
implNamespace.Children.Add(usingDirective);
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorDeclCSharpLoweringPhase.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorDeclCSharpLoweringPhase.cs
index 8956e248d02c1..e49bc4fb8de25 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorDeclCSharpLoweringPhase.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorDeclCSharpLoweringPhase.cs
@@ -9,22 +9,33 @@
namespace Microsoft.AspNetCore.Razor.Language;
///
-/// For Razor components whose primary method body is not being suppressed, this phase produces
-/// the "decl" C# document and stashes it on via
-/// WithDeclCSharpDocument. The matching "impl" half is produced later by
-/// ; both halves are emitted as partial
-/// so they rejoin at compile time.
+/// Produces the "decl" C# document -- the component's public API surface -- for a Razor component and
+/// stashes it on via WithDeclCSharpDocument. The matching "impl"
+/// half is produced by ; both are emitted as partial
+/// so they rejoin at compile time. This is the late tier of the decl/impl split: most markup components
+/// are partitioned earlier (before tag-helper resolution), and for them this phase is a no-op (it skips
+/// when a decl document already exists). It handles markup-free components and, as a fallback, markup
+/// components whose raw shape the early analysis could not partition -- routing their markup over the
+/// already-classified tree.
///
///
///
-/// 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
-/// ) 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.
+/// When this phase produces the decl from the full classified tree (a markup-free component, or a
+/// markup component that fell back), 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 and sibling
+/// methods, and any document-level metadata (source-checksum attributes, etc.). It omits the render
+/// method body and compiler-synthesized plumbing (marked with
+/// ).
+///
+///
+/// For a component the early split partitioned, the decl is instead produced by
+/// DefaultRazorMarkupSplitPhase.LowerDeclDocument from just the using directives and the
+/// markup-free @code pieces, before directive classification. Directive-authored members that
+/// live outside @code -- an @inject property, for instance -- are not part of that decl
+/// input, so they ride along in the impl half. That is invisible to tag-helper discovery (an injected
+/// member shapes no bound attribute) and both halves are emitted as partial, so the recombined
+/// class is unchanged.
///
///
/// The split affects only the generated C# (GetDeclCSharpDocument() gives the decl half;
@@ -41,6 +52,13 @@ namespace Microsoft.AspNetCore.Razor.Language;
/// stays null and falls through to the prior
/// single-file behavior.
///
+///
+/// This two-tier arrangement is interim. The target is a single decision made entirely by the early
+/// markup-split phase: a component either splits -- its decl half feeding the pre-compilation output and
+/// its impl half the implementation output -- or stays a single document. Reaching it requires the early
+/// phase to partition the shapes this tier handles as a fallback and to produce the decl for markup-free
+/// components too, after which this late decl/impl production can be removed.
+///
///
internal sealed class DefaultRazorDeclCSharpLoweringPhase : RazorEnginePhaseBase, IRazorCSharpLoweringPhase
{
@@ -59,6 +77,13 @@ protected override RazorCodeDocument ExecuteCore(RazorCodeDocument codeDocument,
throw new InvalidOperationException(message);
}
+ // If the early decl/impl split already produced the decl document (a component with class-body
+ // markup), don't produce it again from the impl-shaped working tree.
+ if (codeDocument.GetCSharpDocument(declarationDocument: true) is not null)
+ {
+ return codeDocument;
+ }
+
// Skip the split for any document that shouldn't be split:
// - Non-components: the split is component-only.
// - SuppressPrimaryMethodBody (e.g. ProcessDeclarationOnly): caller wants the
@@ -92,14 +117,33 @@ protected override RazorCodeDocument ExecuteCore(RazorCodeDocument codeDocument,
var declNamespace = RazorCSharpDocumentWriter.CloneContainer(primaryNamespace);
var declClass = RazorCSharpDocumentWriter.CloneContainer(primaryClass);
- foreach (var classChild in primaryClass.Children)
+ // Route the class body into the decl half. Most markup components are partitioned by the early
+ // split phases before resolution; a component whose raw @code the early analysis couldn't
+ // partition reaches here unsplit, so route its markup now over the classified tree (markup-free
+ // members stay in decl, markup-bearing methods lift to impl). Without a plan the class body is
+ // markup-free, so every non-render/non-synth child stays in decl.
+ var plan = MarkupSplitter.GetRoutablePlan(primaryClass, renderMethod, codeDocument.ParserOptions);
+ if (plan is not null)
{
- if (classChild == renderMethod || classChild.IsSynthesizedHelper)
+ foreach (var member in plan.Members)
{
- continue;
+ foreach (var piece in member.DeclPieces)
+ {
+ declClass.Children.Add(piece);
+ }
}
+ }
+ else
+ {
+ foreach (var classChild in primaryClass.Children)
+ {
+ if (classChild == renderMethod || classChild.IsSynthesizedHelper)
+ {
+ continue;
+ }
- declClass.Children.Add(classChild);
+ declClass.Children.Add(classChild);
+ }
}
foreach (var nsChild in primaryNamespace.Children)
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorMarkupSplitPhase.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorMarkupSplitPhase.cs
new file mode 100644
index 0000000000000..a05f37860995d
--- /dev/null
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/DefaultRazorMarkupSplitPhase.cs
@@ -0,0 +1,294 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Threading;
+using Microsoft.AspNetCore.Razor.Language.Components;
+using Microsoft.AspNetCore.Razor.Language.Extensions;
+using Microsoft.AspNetCore.Razor.Language.Intermediate;
+using Microsoft.AspNetCore.Razor.PooledObjects;
+using Microsoft.CodeAnalysis.CSharp;
+
+namespace Microsoft.AspNetCore.Razor.Language;
+
+///
+/// Splits a component into decl and impl halves before tag-helper resolution. For a component whose
+/// @code mixes markup with C#, it decides the split (a pure, resolution-independent function of
+/// the class-body IR -- see ), produces the markup-free decl C# document
+/// immediately by reusing the engine's classifier and decl-lowering phases, and rewrites the working
+/// node into the impl half (the render body plus the markup-bearing members), which flows through the
+/// rest of the pipeline.
+///
+///
+/// Producing the decl half here -- before tag-helper resolution -- is the point: the decl document is
+/// markup-free and depends only on user source, so tag-helper discovery can consume it early and stay
+/// incremental. A component with no class-body markup, or one whose shape can't be split safely (a
+/// markup property, an unsupported node, a preprocessor directive, a header/arity directive, or
+/// unrecoverable syntax), is left untouched for the single-document lowering. The final C# lowering
+/// phase emits the rewritten node directly as the impl half, gated on
+/// .
+///
+internal sealed class DefaultRazorMarkupSplitPhase : RazorEnginePhaseBase
+{
+ protected override RazorCodeDocument ExecuteCore(RazorCodeDocument codeDocument, CancellationToken cancellationToken)
+ {
+ var documentNode = codeDocument.GetDocumentNode();
+ ThrowForMissingDocumentDependency(documentNode);
+
+ // Gate cheaply, before any parsing: the split is component-only, a suppressed primary body already
+ // wants decl-shaped single-document output, and a header/arity directive (@inherits/@implements/
+ // @typeparam) can't be reconciled across the two partial halves by the move-based partition.
+ if (!codeDocument.FileKind.IsComponent() ||
+ codeDocument.CodeGenerationOptions.SuppressPrimaryMethodBody ||
+ HasUnsplittableDocumentDirective(documentNode))
+ {
+ return codeDocument;
+ }
+
+ // Before classification the class body still lives as raw C# interleaved with markup IR under the
+ // code-block directives -- the resolution-independent input the split reasons about. A class body
+ // with no markup needs no split and is filtered here without parsing anything.
+ var children = CollectCodeBlockChildren(documentNode);
+ if (!ContainsMarkup(children))
+ {
+ return codeDocument;
+ }
+
+ // Decide the split. A fallback (a markup property, an unsupported node, a directive, or
+ // unrecoverable syntax) leaves the document untouched for the late single-document lowering.
+ var analysis = MarkupSplitter.BuildAnalysisDocument(children);
+ var parseOptions = codeDocument.ParserOptions.CSharpParseOptions ?? CSharpParseOptions.Default;
+ if (MarkupSplitter.ClassifyFromAnalysis(analysis, parseOptions, cancellationToken) is not SplitDecision.SplitPlan plan)
+ {
+ return codeDocument;
+ }
+
+ using var declPiecesBuilder = new PooledArrayBuilder();
+ using var implPiecesBuilder = new PooledArrayBuilder();
+
+ foreach (var member in plan.Members)
+ {
+ foreach (var piece in member.DeclPieces)
+ {
+ declPiecesBuilder.Add(piece);
+ }
+
+ foreach (var piece in member.ImplPieces)
+ {
+ implPiecesBuilder.Add(piece);
+ }
+ }
+
+ // Produce the decl half now, before tag-helper resolution: it is markup-free and depends only on
+ // user source, which is what lets tag-helper discovery consume it early and stay incremental.
+ var declCSharp = LowerDeclDocument(codeDocument, documentNode, declPiecesBuilder.ToImmutable(), cancellationToken);
+
+ // Rewrite the working node into the impl half; it flows through the rest of the pipeline (including
+ // tag-helper resolution), and the final C# lowering phase emits it directly.
+ MakeImplInPlace(documentNode, implPiecesBuilder.ToImmutable());
+ documentNode.IsSplitImplDocument = true;
+
+ var result = codeDocument.WithDocumentNode(documentNode);
+ if (declCSharp is not null)
+ {
+ result = result.WithDeclCSharpDocument(declCSharp);
+ }
+
+ return result;
+ }
+
+ // The ordered children of every @code/@functions block in the document, in source order. Before
+ // classification these code-block directives hold the user's class body verbatim -- the same nodes
+ // FunctionsDirectivePass later moves into the primary class -- so this is the class body the split
+ // reasons about, gathered without depending on resolution or the classified structure.
+ private static ImmutableArray CollectCodeBlockChildren(DocumentIntermediateNode documentNode)
+ {
+ using var directives = new PooledArrayBuilder>();
+
+ documentNode.CollectDirectiveReferences(FunctionsDirective.Directive, ref directives.AsRef());
+ documentNode.CollectDirectiveReferences(ComponentCodeDirective.Directive, ref directives.AsRef());
+
+ if (directives.Count == 0)
+ {
+ return [];
+ }
+
+ directives.Sort(static (a, b) =>
+ Comparer.Default.Compare(a.Node.Source?.AbsoluteIndex, b.Node.Source?.AbsoluteIndex));
+
+ using var children = new PooledArrayBuilder();
+
+ foreach (var directive in directives)
+ {
+ foreach (var child in directive.Node.Children)
+ {
+ children.Add(child);
+ }
+ }
+
+ return children.ToImmutableAndClear();
+ }
+
+ // True if any collected class-body node (or a descendant) is a markup transition -- a node that can
+ // only be lowered after tag-helper resolution. O(nodes) with no parsing, so a markup-free class body
+ // is filtered cheaply.
+ private static bool ContainsMarkup(ImmutableArray nodes)
+ {
+ foreach (var node in nodes)
+ {
+ if (MarkupSplitter.IsMarkupNode(node) || ContainsMarkup(node))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static bool ContainsMarkup(IntermediateNode node)
+ {
+ foreach (var child in node.Children)
+ {
+ if (MarkupSplitter.IsMarkupNode(child) || ContainsMarkup(child))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ // Header- and arity-shaping directives (@inherits, @implements, @typeparam) must be consistent across
+ // the two partial halves. The move-based partition does not reconcile them -- it neither strips the
+ // base type and interfaces from the impl header nor duplicates the type parameters onto both halves --
+ // so a component that combines one of them with class-body markup takes the single-document path.
+ private static bool HasUnsplittableDocumentDirective(DocumentIntermediateNode documentNode)
+ {
+ foreach (var child in documentNode.Children)
+ {
+ if (child is DirectiveIntermediateNode { DirectiveName: "inherits" or "implements" or "typeparam" })
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ // 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.
+ private RazorCSharpDocument? LowerDeclDocument(
+ RazorCodeDocument codeDocument,
+ DocumentIntermediateNode source,
+ ImmutableArray declPieces,
+ CancellationToken cancellationToken)
+ {
+ var declNode = new DocumentIntermediateNode { Options = source.Options };
+
+ foreach (var child in source.Children)
+ {
+ // Usings are needed by both halves; duplicate them so classifying decl doesn't reparent the
+ // impl's copies.
+ if (child is UsingDirectiveIntermediateNode usingDirective)
+ {
+ declNode.Children.Add(CloneUsing(usingDirective));
+ }
+ }
+
+ if (declPieces.Length > 0)
+ {
+ var codeDirective = new DirectiveIntermediateNode
+ {
+ DirectiveName = ComponentCodeDirective.Directive.Directive,
+ Directive = ComponentCodeDirective.Directive,
+ };
+
+ foreach (var piece in declPieces)
+ {
+ codeDirective.Children.Add(piece);
+ }
+
+ declNode.Children.Add(codeDirective);
+ }
+
+ var declCodeDoc = codeDocument.WithDocumentNode(declNode);
+
+ // The split runs before the tag-helper rewrite phase, so the rewritten syntax tree isn't set yet.
+ // The decl half is markup-free and resolution-independent, so its rewritten tree is just the parsed
+ // syntax tree; seed it here so consumers that read it from the decl document (e.g. cohost diagnostic
+ // filtering, which walks it for using directives) don't see a null.
+ if (codeDocument.GetTagHelperRewrittenSyntaxTree() is null &&
+ codeDocument.GetSyntaxTree() is { } syntaxTree)
+ {
+ declCodeDoc = declCodeDoc.WithTagHelperRewrittenSyntaxTree(syntaxTree);
+ }
+
+ declCodeDoc = Engine.Phases.OfType().Single().Execute(declCodeDoc, cancellationToken);
+ declCodeDoc = Engine.Phases.OfType().Single().Execute(declCodeDoc, cancellationToken);
+ declCodeDoc = Engine.Phases.OfType().Single().Execute(declCodeDoc, cancellationToken);
+
+ return declCodeDoc.GetCSharpDocument(declarationDocument: true);
+ }
+
+ // Rewrites the working document into the impl half: the markup-bearing @code members replace the
+ // class-body content of a single consolidated code-block directive; every other code-block directive
+ // is dropped. The render body and any surface directives (e.g. @inject) remain in the impl half.
+ private static void MakeImplInPlace(DocumentIntermediateNode documentNode, ImmutableArray implPieces)
+ {
+ DirectiveIntermediateNode? primaryCodeDirective = null;
+
+ using var toRemove = new PooledArrayBuilder();
+
+ foreach (var child in documentNode.Children)
+ {
+ if (child is DirectiveIntermediateNode directive && IsCodeBlockDirective(directive))
+ {
+ if (primaryCodeDirective is null)
+ {
+ primaryCodeDirective = directive;
+ }
+ else
+ {
+ toRemove.Add(directive);
+ }
+ }
+ }
+
+ foreach (var node in toRemove)
+ {
+ documentNode.Children.Remove(node);
+ }
+
+ if (primaryCodeDirective is not null)
+ {
+ primaryCodeDirective.Children.Clear();
+
+ foreach (var piece in implPieces)
+ {
+ primaryCodeDirective.Children.Add(piece);
+ }
+
+ if (implPieces.Length == 0)
+ {
+ documentNode.Children.Remove(primaryCodeDirective);
+ }
+ }
+ }
+
+ private static bool IsCodeBlockDirective(DirectiveIntermediateNode directive)
+ => directive.Directive?.Kind == DirectiveKind.CodeBlock;
+
+ private static UsingDirectiveIntermediateNode CloneUsing(UsingDirectiveIntermediateNode node)
+ => new()
+ {
+ Content = node.Content,
+ HasExplicitSemicolon = node.HasExplicitSemicolon,
+ AppendLineDefaultAndHidden = node.AppendLineDefaultAndHidden,
+ Source = node.Source,
+ IsImported = node.IsImported,
+ };
+}
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/DocumentIntermediateNode.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/DocumentIntermediateNode.cs
index f96fbf67905e1..ee5c757584783 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/DocumentIntermediateNode.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/DocumentIntermediateNode.cs
@@ -14,6 +14,14 @@ public sealed class DocumentIntermediateNode : IntermediateNode
public string DocumentKind { get; set; }
+ ///
+ /// Set by 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.
+ ///
+ internal bool IsSplitImplDocument { get; set; }
+
public RazorCodeGenerationOptions Options { get; set; }
public CodeTarget Target { get; set; }
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/MarkupSplitter.Analysis.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/MarkupSplitter.Analysis.cs
new file mode 100644
index 0000000000000..23aafeed92fcd
--- /dev/null
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/MarkupSplitter.Analysis.cs
@@ -0,0 +1,125 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Immutable;
+using System.Text;
+using Microsoft.AspNetCore.Razor.Language.Extensions;
+using Microsoft.AspNetCore.Razor.Language.Intermediate;
+using Microsoft.AspNetCore.Razor.PooledObjects;
+
+namespace Microsoft.AspNetCore.Razor.Language;
+
+internal static partial class MarkupSplitter
+{
+ // A markup transition in expression position (an `@<...>` / `@:` template, which lowers to a
+ // RenderFragment value) is replaced by an expression; one in statement position (bare markup
+ // enabled by AllowRazorInAllCodeBlocks) is replaced by a statement. Substituting the right form
+ // for the markup's span yields text that parses iff the user's original C# was valid.
+ private const string ExpressionMarker = MarkerMethodName + "()";
+ private const string StatementMarker = MarkerMethodName + "();";
+
+ // The class-body children are wrapped in a throwaway class so they parse as member declarations.
+ // Recorded child offsets are relative to the full wrapped text so they line up with the parse tree.
+ private const string AnalysisClassHeader = "class __C {\n";
+ private const string AnalysisClassFooter = "\n}\n";
+
+ ///
+ /// Renders the collected class-body children into a parse-only C# document, replacing each markup
+ /// node with a position-aware marker, and records where each child landed (a )
+ /// so the parser's member boundaries can be mapped back to the original IR nodes. The document is
+ /// never emitted; it exists only to recover member boundaries and detect which members carry markup.
+ ///
+ internal static AnalysisDocument BuildAnalysisDocument(ImmutableArray children)
+ {
+ using var _ = StringBuilderPool.GetPooledObject(out var builder);
+ builder.Append(AnalysisClassHeader);
+
+ using var spans = new PooledArrayBuilder(capacity: children.Length);
+
+ foreach (var child in children)
+ {
+ var start = builder.Length;
+
+ switch (child)
+ {
+ case CSharpCodeIntermediateNode csharp:
+ AppendCSharpText(builder, csharp);
+ break;
+
+ case var markup when IsMarkupNode(markup):
+ builder.Append(IsExpressionPositionMarkup(markup) ? ExpressionMarker : StatementMarker);
+ break;
+
+ default:
+ // 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;
+ }
+
+ spans.Add(new ChildSpan(start, builder.Length - start, child));
+ }
+
+ builder.Append(AnalysisClassFooter);
+
+ return new AnalysisDocument(builder.ToString(), spans.ToImmutableAndClear());
+ }
+
+ ///
+ /// Expression-position markup is exactly a (from @<...>
+ /// / @:); every other class-body markup node sits in statement position. Keying on this single
+ /// node kind -- rather than an enumerated list of the statement-position kinds -- keeps the rule
+ /// correct as new markup node kinds are introduced.
+ ///
+ internal static bool IsExpressionPositionMarkup(IntermediateNode node)
+ => node is TemplateIntermediateNode;
+
+ private static void AppendCSharpText(StringBuilder builder, CSharpCodeIntermediateNode node)
+ {
+ foreach (var child in node.Children)
+ {
+ if (child is IntermediateToken token)
+ {
+ builder.Append(token.Content);
+ }
+ }
+ }
+}
+
+///
+/// Where one class-body child landed in the throwaway analysis text, so member boundaries the parser
+/// reports (in analysis-document coordinates) can be mapped back to the original IR node regardless of
+/// the length difference between a markup node and its marker. A child's role -- raw C#, markup, or a
+/// zero-length surface declaration -- is read from directly rather than stored.
+///
+internal readonly struct ChildSpan
+{
+ public ChildSpan(int start, int length, IntermediateNode node)
+ {
+ Start = start;
+ Length = length;
+ Node = node;
+ }
+
+ public int Start { get; }
+
+ public int Length { get; }
+
+ public int End => Start + Length;
+
+ public IntermediateNode Node { get; }
+}
+
+/// The throwaway analysis text plus the placements mapping its spans back to IR nodes.
+internal sealed class AnalysisDocument
+{
+ public AnalysisDocument(string text, ImmutableArray children)
+ {
+ Text = text;
+ Children = children;
+ }
+
+ public string Text { get; }
+
+ public ImmutableArray Children { get; }
+}
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/MarkupSplitter.Classify.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/MarkupSplitter.Classify.cs
new file mode 100644
index 0000000000000..8c3f9d3c4ecc8
--- /dev/null
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/MarkupSplitter.Classify.cs
@@ -0,0 +1,306 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Threading;
+using Microsoft.AspNetCore.Razor.Language.Intermediate;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Text;
+
+namespace Microsoft.AspNetCore.Razor.Language;
+
+internal static partial class MarkupSplitter
+{
+ ///
+ /// Turns a prepared into a in a single
+ /// pass: it applies the pre-parse safety gates (unsupported node, preprocessor directive), parses the
+ /// throwaway class, validates that every marker and every C# character is covered by a member, then
+ /// classifies and routes each member together -- bailing on the first member whose markup can't be
+ /// lifted, otherwise emitting the routed pieces. Classification and routing share the one parse and
+ /// the one set of member spans, so there is no intermediate classified-member table. The
+ /// analysis-building half runs in an earlier phase, so this is the resolution-independent decision the
+ /// split-classification phase drives; it never returns because a
+ /// document with no class-body markup never reaches analysis.
+ ///
+ internal static SplitDecision ClassifyFromAnalysis(
+ AnalysisDocument analysis,
+ CSharpParseOptions parseOptions,
+ CancellationToken cancellationToken = default)
+ {
+ // Every routable node is raw C# or recognized markup. A structured/extension member that reached
+ // the class body (e.g. an @inject) can't be placed, so leave the class body unrouted.
+ foreach (var child in analysis.Children)
+ {
+ if (!IsSupportedClassBodyNode(child.Node))
+ {
+ return SplitDecision.Fallback(FallbackReason.UnsupportedClassBodyNode);
+ }
+ }
+
+ // A preprocessor directive scopes across members; routing one to the other half would orphan it.
+ if (HasPreprocessorDirective(analysis.Text))
+ {
+ return SplitDecision.Fallback(FallbackReason.ClassBodyHasDirectives);
+ }
+
+ // Parse from a SourceText (the string-based ParseText overload is banned in this project). The
+ // analysis document is throwaway and never emitted, so its encoding is irrelevant.
+ var tree = CSharpSyntaxTree.ParseText(SourceText.From(analysis.Text), parseOptions, cancellationToken: cancellationToken);
+ var root = tree.GetCompilationUnitRoot(cancellationToken);
+
+ var markerClass = root.Members.OfType().FirstOrDefault();
+ if (markerClass is null || markerClass.OpenBraceToken.IsMissing || markerClass.CloseBraceToken.IsMissing)
+ {
+ return SplitDecision.Fallback(FallbackReason.UnrecoverableParse);
+ }
+
+ // Member spans, in source order. These and the child spans both live in analysis-document
+ // coordinates (a markup node contributes its marker's span), so intersecting them below never has
+ // to reconcile the markup/marker length difference.
+ var members = markerClass.Members;
+ var memberSpans = new TextSpan[members.Count];
+ for (var i = 0; i < members.Count; i++)
+ {
+ memberSpans[i] = members[i].FullSpan;
+ }
+
+ // Every markup marker and every non-whitespace C# character must land inside some member. A leak
+ // -- brace imbalance let a marker escape a member (or the class), or skipped tokens fell outside
+ // every member -- can't be routed without dropping content or leaking markup into decl, so it is
+ // unrecoverable. This, not an ordinary transient syntax error (which still recovers member
+ // boundaries), is what the catastrophic safety net exists for.
+ if (!AllMarkupCovered(analysis.Children, memberSpans) ||
+ !AllCSharpContentCovered(analysis, memberSpans))
+ {
+ return SplitDecision.Fallback(FallbackReason.UnrecoverableParse);
+ }
+
+ // Classify each member and, in the same pass, note whether it lifts to impl. A markup-bearing
+ // plain method lifts wholesale; markup anywhere else fails the whole file, so bail on the first
+ // such member before doing any routing work.
+ var liftToImpl = new bool[members.Count];
+ for (var i = 0; i < members.Count; i++)
+ {
+ if (!MemberCoversMarkup(analysis.Children, memberSpans[i]))
+ {
+ continue;
+ }
+
+ switch (members[i])
+ {
+ // Only a plain method can be lifted wholesale to impl: it has no field-initializer
+ // ordering to preserve and isn't descriptor surface, so its absence from decl is invisible.
+ case MethodDeclarationSyntax:
+ liftToImpl[i] = true;
+ break;
+
+ // A property/indexer is descriptor surface -- it must stay in decl, where markup can't
+ // live -- so markup in one forces fallback.
+ case PropertyDeclarationSyntax or IndexerDeclarationSyntax:
+ return SplitDecision.Fallback(FallbackReason.MarkupProperty);
+
+ // Anything else with markup -- a field/event (whose initializer runs in declaration
+ // order), a nested type (which may be referenced from decl, or itself contain markup
+ // members), a constructor/operator, or an incomplete member -- isn't safe to lift.
+ default:
+ return SplitDecision.Fallback(FallbackReason.UnsupportedMarkupMember);
+ }
+ }
+
+ return new SplitDecision.SplitPlan(RouteMembers(analysis, memberSpans, liftToImpl));
+ }
+
+ ///
+ /// Groups the analysis children under their owning parsed members, slicing straddling C# chunks at
+ /// member boundaries, to produce one per member in original order --
+ /// already resolved into the pieces each half emits. Parser member spans and IR nodes both live in
+ /// analysis-document offsets (a markup node contributes its marker span), so intersecting them here
+ /// means the markup/marker length difference never matters. A member either stays wholly in decl or
+ /// (a markup method) lifts wholly to impl, per .
+ ///
+ private static ImmutableArray RouteMembers(
+ AnalysisDocument analysis,
+ TextSpan[] memberSpans,
+ bool[] liftToImpl)
+ {
+ // Accumulate each member's pieces (sliced C# chunks and markup nodes) in source order.
+ var pieceBuilders = new List[memberSpans.Length];
+ for (var i = 0; i < memberSpans.Length; i++)
+ {
+ pieceBuilders[i] = [];
+ }
+
+ foreach (var child in analysis.Children)
+ {
+ if (child.Node is CSharpCodeIntermediateNode csharp)
+ {
+ RouteCSharpChild(csharp, child, memberSpans, pieceBuilders);
+ }
+ else
+ {
+ // 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);
+ if (owner >= 0)
+ {
+ pieceBuilders[owner].Add(child.Node);
+ }
+ }
+ }
+
+ var result = ImmutableArray.CreateBuilder(memberSpans.Length);
+ for (var i = 0; i < memberSpans.Length; i++)
+ {
+ var pieces = pieceBuilders[i].ToImmutableArray();
+
+ // A markup-free member stays in decl; a markup-bearing method lifts wholesale to impl.
+ // (Markup properties produced a fallback decision before this runs.)
+ result.Add(liftToImpl[i]
+ ? new RoutedMember(declPieces: [], implPieces: pieces)
+ : new RoutedMember(declPieces: pieces, implPieces: []));
+ }
+
+ return result.ToImmutable();
+ }
+
+ // Slices a raw C# chunk at any member boundaries that fall within it and routes each slice to the
+ // member that owns its start. A single class-body C# chunk commonly straddles several members (a
+ // field immediately followed by a markup-bearing method), so it can't be routed as a unit.
+ private static void RouteCSharpChild(
+ CSharpCodeIntermediateNode node,
+ ChildSpan child,
+ TextSpan[] memberSpans,
+ List[] pieceBuilders)
+ {
+ // Member boundaries strictly inside the child become node-local cut offsets. Members are in
+ // source order with contiguous, increasing spans, so the cuts come out strictly increasing.
+ var cuts = ImmutableArray.CreateBuilder();
+ foreach (var span in memberSpans)
+ {
+ var boundary = span.End;
+ if (boundary > child.Start && boundary < child.End)
+ {
+ cuts.Add(boundary - child.Start);
+ }
+ }
+
+ var cutOffsets = cuts.ToImmutable();
+ var slices = SplitCSharpNode(node, cutOffsets);
+
+ for (var i = 0; i < slices.Length; i++)
+ {
+ var localStart = i == 0 ? 0 : cutOffsets[i - 1];
+ var owner = FindMemberIndex(memberSpans, child.Start + localStart);
+ if (owner >= 0)
+ {
+ pieceBuilders[owner].Add(slices[i]);
+ }
+ }
+ }
+
+ // The index of the member whose analysis-document span contains the offset. Members partition the
+ // class body contiguously, so any interior offset has exactly one owner; a boundary offset belongs to
+ // the following member (spans are half-open), which is the member that content begins.
+ private static int FindMemberIndex(TextSpan[] memberSpans, int offset)
+ {
+ for (var i = 0; i < memberSpans.Length; i++)
+ {
+ if (memberSpans[i].Contains(offset))
+ {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ // True when every non-whitespace character of every C# child falls within some member's span. Members
+ // partition the class body contiguously in the common case, so this only fails on real gaps (leading/
+ // trailing skipped tokens or brace imbalance), which routing must not silently drop.
+ private static bool AllCSharpContentCovered(AnalysisDocument analysis, TextSpan[] memberSpans)
+ {
+ var text = analysis.Text;
+
+ foreach (var child in analysis.Children)
+ {
+ if (child.Node is not CSharpCodeIntermediateNode)
+ {
+ continue;
+ }
+
+ for (var index = child.Start; index < child.End; index++)
+ {
+ if (!char.IsWhiteSpace(text[index]) && !IsCoveredByMember(memberSpans, index))
+ {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ // Binary search the source-ordered, contiguous member spans for the one containing the offset. Called
+ // per character by AllCSharpContentCovered, so an O(log memberCount) probe keeps that scan near-linear
+ // in the text length instead of O(textLength * memberCount).
+ private static bool IsCoveredByMember(TextSpan[] memberSpans, int index)
+ {
+ var lo = 0;
+ var hi = memberSpans.Length - 1;
+
+ while (lo <= hi)
+ {
+ var mid = lo + (hi - lo) / 2;
+ var span = memberSpans[mid];
+
+ if (index < span.Start)
+ {
+ hi = mid - 1;
+ }
+ else if (index >= span.End)
+ {
+ lo = mid + 1;
+ }
+ else
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ // True when every markup marker starts within some member's span. A marker outside every member means
+ // brace imbalance let it leak out (unrecoverable): routing it would drop it or leak markup into decl.
+ private static bool AllMarkupCovered(ImmutableArray children, TextSpan[] memberSpans)
+ {
+ foreach (var child in children)
+ {
+ if (IsMarkupNode(child.Node) && !IsCoveredByMember(memberSpans, child.Start))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ // A member carries markup when a markup child's marker starts within the member's span. Detection is
+ // by node kind over the child spans, never by matching the marker identifier name -- user code may
+ // itself call a method of that name.
+ private static bool MemberCoversMarkup(ImmutableArray children, TextSpan memberSpan)
+ {
+ foreach (var child in children)
+ {
+ if (IsMarkupNode(child.Node) && memberSpan.Contains(child.Start))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/MarkupSplitter.Slicing.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/MarkupSplitter.Slicing.cs
new file mode 100644
index 0000000000000..e786ce4db29a5
--- /dev/null
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/MarkupSplitter.Slicing.cs
@@ -0,0 +1,186 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Runtime.InteropServices;
+using Microsoft.AspNetCore.Razor.Language.Intermediate;
+
+namespace Microsoft.AspNetCore.Razor.Language;
+
+internal static partial class MarkupSplitter
+{
+ ///
+ /// Splits a raw C# node at one or more offsets within its concatenated text, producing a piece per
+ /// sub-range. A single class-body C# chunk often straddles several parsed members (e.g. a field
+ /// immediately followed by a markup-bearing method), so it must be cut at the member boundaries and
+ /// each slice routed independently. Each produced token's is recomputed --
+ /// line and character indices, not just the absolute index -- because a slice that starts after a
+ /// newline maps to a different line, and getting that wrong corrupts source mappings.
+ ///
+ /// The C# node to split.
+ /// Strictly-increasing offsets in the node's concatenated text, each strictly
+ /// between 0 and the text length. Produces cuts.Length + 1 pieces.
+ internal static ImmutableArray SplitCSharpNode(
+ CSharpCodeIntermediateNode node,
+ ImmutableArray cuts)
+ {
+ if (cuts.IsDefaultOrEmpty)
+ {
+ return [node];
+ }
+
+ var pieces = new CSharpCodeIntermediateNode[cuts.Length + 1];
+ for (var i = 0; i < pieces.Length; i++)
+ {
+ pieces[i] = new CSharpCodeIntermediateNode { Source = node.Source, IsImported = node.IsImported };
+ }
+
+ // Walk each token, tracking its start offset in the node's concatenated text. A token that spans
+ // one or more cut points is itself sliced so each slice lands in the correct piece.
+ var tokenStart = 0;
+ foreach (var child in node.Children)
+ {
+ if (child is not IntermediateToken token)
+ {
+ continue;
+ }
+
+ var content = token.Content;
+ var tokenEnd = tokenStart + content.Length;
+
+ // The cut offsets that fall strictly inside this token become local slice boundaries.
+ var localBoundaries = new List { 0 };
+ var pieceOfFirstSlice = PieceIndexOf(cuts, tokenStart);
+
+ foreach (var cut in cuts)
+ {
+ if (cut > tokenStart && cut < tokenEnd)
+ {
+ localBoundaries.Add(cut - tokenStart);
+ }
+ }
+
+ localBoundaries.Add(content.Length);
+
+ for (var i = 0; i < localBoundaries.Count - 1; i++)
+ {
+ var localStart = localBoundaries[i];
+ var localLength = localBoundaries[i + 1] - localBoundaries[i];
+ if (localLength == 0)
+ {
+ continue;
+ }
+
+ var pieceIndex = pieceOfFirstSlice + i;
+ pieces[pieceIndex].Children.Add(SliceToken(token, localStart, localLength));
+ }
+
+ tokenStart = tokenEnd;
+ }
+
+ return ImmutableCollectionsMarshal.AsImmutableArray(pieces);
+ }
+
+ // The index of the piece that content at the given node-text offset belongs to: the number of cut
+ // points at or before the offset.
+ private static int PieceIndexOf(ImmutableArray cuts, int offset)
+ {
+ var index = 0;
+ foreach (var cut in cuts)
+ {
+ if (cut <= offset)
+ {
+ index++;
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ return index;
+ }
+
+ ///
+ /// Produces a token for the substring [localStart, localStart + localLength) of the given
+ /// token's content, with its advanced from the token's start across the
+ /// skipped prefix (so line/character indices are correct even across newlines).
+ ///
+ internal static CSharpIntermediateToken SliceToken(IntermediateToken token, int localStart, int localLength)
+ {
+ var content = token.Content;
+ var slicedContent = content.Substring(localStart, localLength);
+
+ if (token.Source is not { } source)
+ {
+ return new CSharpIntermediateToken(slicedContent, source: null);
+ }
+
+ var (startAbsolute, startLine, startCharacter) =
+ AdvanceLocation(source.AbsoluteIndex, source.LineIndex, source.CharacterIndex, content, 0, localStart);
+
+ var (_, endLine, endCharacter) =
+ AdvanceLocation(startAbsolute, startLine, startCharacter, content, localStart, localLength);
+
+ var slicedSource = new SourceSpan(
+ source.FilePath,
+ startAbsolute,
+ startLine,
+ startCharacter,
+ localLength,
+ // LineCount is the number of line breaks the span covers (0 for a single-line slice), matching
+ // how the writer derives the enhanced #line end line as LineIndex + 1 + LineCount.
+ lineCount: endLine - startLine,
+ endCharacterIndex: endCharacter);
+
+ return new CSharpIntermediateToken(slicedContent, slicedSource);
+ }
+
+ ///
+ /// Advances a source location across characters of
+ /// starting at . Matches the writer's line-break accounting: \r\n, a
+ /// lone \r, and a lone \n each count as one line break that resets the character index.
+ /// The \n of a \r\n pair is only consumed when it falls inside the requested range: a
+ /// range that ends exactly on the \r treats it as a lone break and leaves the \n for the
+ /// next slice, so a slice boundary that lands between the two characters can't over-advance past it.
+ ///
+ internal static (int Absolute, int Line, int Character) AdvanceLocation(
+ int absolute, int line, int character, string text, int start, int count)
+ {
+ var i = start;
+ var end = start + count;
+
+ while (i < end)
+ {
+ var c = text[i];
+ absolute++;
+
+ if (c == '\r')
+ {
+ if (i + 1 < end && text[i + 1] == '\n')
+ {
+ // Consume the paired newline as a single line break.
+ absolute++;
+ i++;
+ }
+
+ line++;
+ character = 0;
+ }
+ else if (c == '\n')
+ {
+ line++;
+ character = 0;
+ }
+ else
+ {
+ character++;
+ }
+
+ i++;
+ }
+
+ return (absolute, line, character);
+ }
+}
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/MarkupSplitter.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/MarkupSplitter.cs
new file mode 100644
index 0000000000000..c2173de8bc846
--- /dev/null
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/MarkupSplitter.cs
@@ -0,0 +1,201 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Immutable;
+using Microsoft.AspNetCore.Razor.Language.Extensions;
+using Microsoft.AspNetCore.Razor.Language.Intermediate;
+using Microsoft.AspNetCore.Razor.PooledObjects;
+
+namespace Microsoft.AspNetCore.Razor.Language;
+
+///
+/// Decides, for a component's primary class body, which parts of the user's @code belong in
+/// the markup-free "decl" half (the tag-helper descriptor surface) and which markup-bearing parts
+/// belong in the "impl" half (lowered after tag-helper resolution).
+///
+///
+///
+/// The @code contents arrive on the primary as a
+/// flat sequence of raw C# text ( holding
+/// ) interleaved with markup nodes. The vast majority of
+/// @code is pure C# with no markup, so a cheap structural gate ()
+/// runs first: with no class-body markup there is nothing to route to the impl half, so it reports
+/// without parsing anything -- the whole class body stays in the
+/// decl half.
+///
+///
+/// The split decision is a pure function of the class body's IR content and the parse options; it does
+/// not branch on the language version, so every caller reaches the same decision for the same document.
+/// The markup-split phase computes it once, produces the decl half, and rewrites the working node into
+/// the impl half -- all before tag-helper resolution.
+///
+///
+internal static partial class MarkupSplitter
+{
+ ///
+ /// Identifier emitted into the throwaway analysis document to stand in for a markup transition, so
+ /// the class body parses as ordinary C# without needing resolved tag helpers. It never appears in
+ /// generated output. Markup is detected from the analysis document's per-child placements, never by
+ /// matching this name (user code may legitimately contain a call of the same name).
+ ///
+ public const string MarkerMethodName = "__RazorMarkupTransition";
+
+ ///
+ /// Computes the split decision for the given primary class body: gates on class-body markup, builds
+ /// the analysis document, and classifies it. Pure and uncached.
+ ///
+ public static SplitDecision Split(
+ ClassDeclarationIntermediateNode primaryClass,
+ MethodDeclarationIntermediateNode renderMethod,
+ RazorParserOptions parserOptions)
+ {
+ ArgHelper.ThrowIfNull(primaryClass);
+ ArgHelper.ThrowIfNull(renderMethod);
+ ArgHelper.ThrowIfNull(parserOptions);
+
+ // Fast path: with no class-body markup there is nothing to move to the impl half.
+ if (!HasClassBodyMarkup(primaryClass, renderMethod))
+ {
+ return SplitDecision.NoSplit;
+ }
+
+ var children = CollectClassBodyChildren(primaryClass, renderMethod);
+ var analysis = BuildAnalysisDocument(children);
+ return ClassifyFromAnalysis(analysis, parserOptions.CSharpParseOptions);
+ }
+
+ ///
+ /// The routable plan for the given classified class body, or when the body
+ /// keeps its unsplit shape. This is the fallback entry point: the primary decl/impl split
+ /// runs early (before tag-helper resolution) over the raw @code, but a component whose raw
+ /// shape the early analysis can't partition falls through to the late lowering phases, which route
+ /// its markup here over the already-classified tree instead.
+ ///
+ public static SplitDecision.SplitPlan? GetRoutablePlan(
+ ClassDeclarationIntermediateNode primaryClass,
+ MethodDeclarationIntermediateNode renderMethod,
+ RazorParserOptions parserOptions)
+ => Split(primaryClass, renderMethod, parserOptions) as SplitDecision.SplitPlan;
+
+ ///
+ /// True if any line of the analysis text begins (after leading whitespace) with a preprocessor
+ /// directive. A line-anchored scan avoids misfiring on a # inside a string or interpolation;
+ /// a rare false positive only costs an unnecessary fallback, never a mis-split.
+ ///
+ internal static bool HasPreprocessorDirective(string text)
+ {
+ var atLineStart = true;
+
+ foreach (var c in text)
+ {
+ if (c is '\n' or '\r')
+ {
+ atLineStart = true;
+ }
+ else if (!atLineStart)
+ {
+ continue;
+ }
+ else if (c == '#')
+ {
+ return true;
+ }
+ else if (!char.IsWhiteSpace(c))
+ {
+ atLineStart = false;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// True if the primary class body contains a markup transition (a node that can only be lowered
+ /// after tag-helper resolution). Runs in O(children) with no parsing.
+ ///
+ public static bool HasClassBodyMarkup(
+ ClassDeclarationIntermediateNode primaryClass,
+ MethodDeclarationIntermediateNode renderMethod)
+ {
+ foreach (var child in primaryClass.Children)
+ {
+ if (ReferenceEquals(child, renderMethod) || child.IsSynthesizedHelper)
+ {
+ continue;
+ }
+
+ if (IsClassBodyMarkup(child))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Classifies a class-body child as markup rather than C#. Defined as the complement of the known
+ /// C#/structured-declaration node kinds so that an unrecognized (e.g. newly introduced) markup node
+ /// is treated as markup: erring toward running the split machinery is a harmless cost, whereas
+ /// missing a markup node would let it leak into the resolution-free decl half.
+ ///
+ ///
+ /// This is the deliberately over-eager gate classifier. It can flag a non-markup extension
+ /// node (an @inject) as "markup"; that only causes to run, which then sees
+ /// the node isn't a kind it can route () and falls back. Routing
+ /// itself uses the precise allow-list , never this predicate.
+ ///
+ internal static bool IsClassBodyMarkup(IntermediateNode node)
+ => node is not (CSharpCodeIntermediateNode or
+ FieldDeclarationIntermediateNode or
+ PropertyDeclarationIntermediateNode or
+ MethodDeclarationIntermediateNode);
+
+ ///
+ /// The precise allow-list of markup intermediate node kinds the splitter knows how to route to the
+ /// impl half: an expression-position (from @<...>)
+ /// and the statement-position markup nodes. Unlike the fail-safe gate,
+ /// this is positive: a class-body node that is neither raw C# nor one of these kinds -- e.g. an
+ /// @inject (ComponentInjectIntermediateNode, itself an
+ /// just like ) or a
+ /// structured member declaration -- is not treated as routable markup.
+ ///
+ internal static bool IsMarkupNode(IntermediateNode node)
+ => node is TemplateIntermediateNode or
+ MarkupElementIntermediateNode or
+ MarkupBlockIntermediateNode or
+ HtmlContentIntermediateNode;
+
+ ///
+ /// A class-body node the splitter can route: raw C# text (which stays in decl or lifts to impl with
+ /// its member) or a recognized markup node (which lifts to impl). Any other kind -- a structured or
+ /// extension member such as @inject -- means the file can't be split and must fall back.
+ ///
+ internal static bool IsSupportedClassBodyNode(IntermediateNode node)
+ => node is CSharpCodeIntermediateNode || IsMarkupNode(node);
+
+ ///
+ /// The ordered user-authored class-body children -- everything that isn't the render method or a
+ /// synthesized helper -- in source order. This is the flat sequence of raw C# chunks and markup
+ /// transitions the analysis document and routing operate over.
+ ///
+ internal static ImmutableArray CollectClassBodyChildren(
+ ClassDeclarationIntermediateNode primaryClass,
+ MethodDeclarationIntermediateNode renderMethod)
+ {
+ using var builder = new PooledArrayBuilder();
+
+ foreach (var child in primaryClass.Children)
+ {
+ if (ReferenceEquals(child, renderMethod) || child.IsSynthesizedHelper)
+ {
+ continue;
+ }
+
+ builder.Add(child);
+ }
+
+ return builder.ToImmutableAndClear();
+ }
+}
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorProjectEngine.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorProjectEngine.cs
index 8bc3ad3a16f7a..de87d0e4febbe 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorProjectEngine.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorProjectEngine.cs
@@ -260,6 +260,7 @@ private static void AddDefaultPhases(ImmutableArray.Builder p
phases.Add(new DefaultRazorSyntaxTreePhase());
phases.Add(new DefaultRazorTagHelperContextDiscoveryPhase());
phases.Add(new DefaultRazorIntermediateNodeLoweringPhase());
+ phases.Add(new DefaultRazorMarkupSplitPhase());
phases.Add(new DefaultTagHelperResolutionPhase());
phases.Add(new DefaultRazorTagHelperRewritePhase());
phases.Add(new DefaultRazorDocumentClassifierPhase());
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/SplitDecision.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/SplitDecision.cs
new file mode 100644
index 0000000000000..29cd12ce9c030
--- /dev/null
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/SplitDecision.cs
@@ -0,0 +1,157 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Immutable;
+using Microsoft.AspNetCore.Razor.Language.Intermediate;
+
+namespace Microsoft.AspNetCore.Razor.Language;
+
+///
+/// The outcome of analyzing a component's primary class body for the decl/impl markup split. One of
+/// three cases:
+///
+/// - -- no class-body markup, so the caller keeps the single-file behavior.
+/// - -- the class body mixes markup and C# and can be split safely into a
+/// markup-free decl half and a markup-bearing impl half; describes how the pieces route.
+/// - -- the class body has markup but cannot be split safely (a markup
+/// property, an unsupported node, a directive, or an unrecoverable parse), so the caller retains the
+/// original class-body layout and can select its fallback pipeline.
+///
+///
+///
+/// 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.
+///
+internal abstract class SplitDecision
+{
+ private protected SplitDecision()
+ {
+ }
+
+ ///
+ /// The class body has no markup (or nothing that needs splitting); the caller keeps the single-file
+ /// behavior. Shared singleton.
+ ///
+ public static SplitDecision NoSplit { get; } = new NoSplitDecision();
+
+ ///
+ /// The class body has markup but cannot be split safely; the caller retains the original class-body
+ /// layout and can select its fallback pipeline.
+ ///
+ public static SplitFallback Fallback(FallbackReason reason) => new(reason);
+
+ /// True when this decision requires the caller to build separate decl and impl halves.
+ public bool RequiresSplit => this is SplitPlan;
+
+ ///
+ /// True when the file has markup but cannot be split, so the caller must retain the original
+ /// class-body layout instead of routing members between declaration and implementation output.
+ ///
+ public bool IsFallback => this is SplitFallback;
+
+ private sealed class NoSplitDecision : SplitDecision
+ {
+ }
+
+ ///
+ /// The class body has markup that cannot be split safely, so its original layout must be retained.
+ /// This preserves correctness while allowing the surrounding pipeline to choose a fallback path.
+ ///
+ public sealed class SplitFallback : SplitDecision
+ {
+ public SplitFallback(FallbackReason reason)
+ {
+ Reason = reason;
+ }
+
+ /// Why the file falls back instead of splitting (for diagnostics/telemetry and tests).
+ public FallbackReason Reason { get; }
+ }
+
+ ///
+ /// Describes how each class-body member routes between the decl and impl halves. Produced only when
+ /// the class body mixes markup and C# and the file can be split safely.
+ ///
+ public sealed class SplitPlan : SplitDecision
+ {
+ public SplitPlan(ImmutableArray members)
+ {
+ Members = members.NullToEmpty();
+ }
+
+ /// The routed class-body members in original order; each drives what its half emits.
+ public ImmutableArray Members { get; }
+ }
+}
+
+///
+/// A user-authored class-body member after routing, already resolved into the IR pieces each half emits:
+/// for the decl half and for the impl half. Original
+/// nodes are shared by reference (keeping their source mappings). A member is either markup-free (all its
+/// pieces stay in decl) or a markup-bearing method (all its pieces lift to impl); a markup property
+/// never reaches routing because it produces a fallback decision. The lowering phases simply append the
+/// pieces for their half.
+///
+internal readonly struct RoutedMember
+{
+ public RoutedMember(
+ ImmutableArray declPieces,
+ ImmutableArray implPieces)
+ {
+ DeclPieces = declPieces.NullToEmpty();
+ ImplPieces = implPieces.NullToEmpty();
+ }
+
+ /// The pieces this member contributes to the decl half, in order.
+ public ImmutableArray DeclPieces { get; }
+
+ /// The pieces this member contributes to the impl half, in order.
+ public ImmutableArray ImplPieces { get; }
+}
+
+///
+/// Why a markup-bearing class body retains its original layout instead of being routed between
+/// declaration and implementation output.
+///
+internal enum FallbackReason
+{
+ ///
+ /// A property/indexer carries markup. A property is tag-helper descriptor surface (a
+ /// [Parameter] shapes the component's attributes), so it must stay in the decl half -- but
+ /// markup cannot live in the markup-free decl half. Rather than reshape the property, the splitter
+ /// reports fallback so the property remains in place.
+ ///
+ MarkupProperty,
+
+ ///
+ /// A non-method, non-property member carries markup -- a field/event (whose initializer runs in
+ /// declaration order, which splitting across partials would perturb), a nested type, a
+ /// constructor/operator, or an incomplete member. It cannot be safely lifted, so the splitter reports
+ /// fallback.
+ ///
+ UnsupportedMarkupMember,
+
+ ///
+ /// The analysis parse is unrecoverable (brace mismatch, or a markup marker isn't contained by any
+ /// member), so member boundaries can't be trusted. Not triggered by ordinary transient typos, which
+ /// still recover member boundaries.
+ ///
+ UnrecoverableParse,
+
+ ///
+ /// The class body contains a node the splitter can't route -- neither raw C# nor a recognized markup
+ /// node -- such as an @inject or another structured/extension member. Retaining the original
+ /// layout avoids moving descriptor surface into the impl half.
+ ///
+ UnsupportedClassBodyNode,
+
+ ///
+ /// The class body contains a preprocessor directive (#if/#endif, #region,
+ /// #pragma, #nullable). Splitting could route a member out of the directive's scope and
+ /// orphan it in the other half; retaining the original layout keeps directives balanced.
+ ///
+ ClassBodyHasDirectives,
+}
diff --git a/src/Razor/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/RazorIntegrationTestBase.cs b/src/Razor/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/RazorIntegrationTestBase.cs
index 4bbd5802fc750..151e6379767ff 100644
--- a/src/Razor/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/RazorIntegrationTestBase.cs
+++ b/src/Razor/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/RazorIntegrationTestBase.cs
@@ -207,6 +207,29 @@ protected CompileToCSharpResult CompileToCSharp(string cshtmlContent, params Dia
expectedCSharpDiagnostics: expectedCSharpDiagnostics);
}
+ // Runs the engine's phases over a component built from , stopping
+ // immediately before the first phase of type . Lets a test observe
+ // intermediate pipeline state -- e.g. that the decl document already exists before a later phase runs.
+ private protected RazorCodeDocument ProcessComponentUpToPhase(string content)
+ where TStopBefore : IRazorEnginePhase
+ {
+ var projectEngine = CreateProjectEngine(Configuration, Array.Empty(), supportLocalizedComponentNames: false, csharpParseOptions: null);
+ var projectItem = CreateProjectItem("TestComponent.razor", content, RazorFileKind.Component);
+ var codeDocument = projectEngine.CreateCodeDocument(projectItem);
+
+ foreach (var phase in projectEngine.Engine.Phases)
+ {
+ if (phase is TStopBefore)
+ {
+ break;
+ }
+
+ codeDocument = phase.Execute(codeDocument);
+ }
+
+ return codeDocument;
+ }
+
protected CompileToCSharpResult CompileToCSharp(
string cshtmlContent,
string? cssScope = null,