diff --git a/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/Intermediate/IntermediateNodeCloneTest.cs b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/Intermediate/IntermediateNodeCloneTest.cs
new file mode 100644
index 0000000000000..88bf248d41f30
--- /dev/null
+++ b/src/Razor/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/Intermediate/IntermediateNodeCloneTest.cs
@@ -0,0 +1,321 @@
+// 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;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Razor.Language.Intermediate;
+
+// Verifies that IntermediateNode.Clone() produces a faithful deep copy -- every field, child, and
+// deep-cloned side reference. The dump used for the comparison reflects over every property rather than
+// reusing FormatNode (which writes only the handful of properties chosen for display), so a dropped field
+// is caught.
+public class IntermediateNodeCloneTest
+{
+ private readonly RazorProjectEngine _projectEngine = RazorProjectEngine.Create(
+ RazorConfiguration.Default,
+ RazorProjectFileSystem.Create(AppContext.BaseDirectory));
+
+ [Fact]
+ public void Clone_Component_ProducesFaithfulDeepCopy()
+ {
+ var source = """
+ @page "/counter"
+ @using System.Text
+ @inject System.IServiceProvider Services
+ @typeparam TItem
+
+
Hello @Name
+
+
+ @code {
+ [Parameter] public string Name { get; set; }
+ private int Value = 1;
+ private void OnClick() { Value++; }
+ }
+ """;
+
+ // The tree contains at least one aliased child (a tag helper's unbound HTML attribute), so the
+ // clone's alias preservation is actually exercised rather than vacuously passing.
+ var aliasCount = AssertClone(source, RazorFileKind.Component);
+ Assert.True(aliasCount > 0);
+ }
+
+ [Fact]
+ public void Clone_LegacyView_ProducesFaithfulDeepCopy()
+ {
+ var source = """
+ @using System.Text
+
+
+ Test
+
+ @DateTime.Now
+ @{ var x = 1; }
+ value is @x
+
+
+ """;
+
+ // The tree contains at least one aliased child (a tag helper's unbound HTML attribute), so the
+ // clone's alias preservation is actually exercised rather than vacuously passing.
+ var aliasCount = AssertClone(source, RazorFileKind.Legacy);
+ Assert.True(aliasCount > 0);
+ }
+
+ [Fact]
+ public void Clone_EdgeConstructs_ProducesFaithfulDeepCopy()
+ {
+ // Malformed directives and markup-element fallback containers are lowered node kinds that the other
+ // documents don't produce; a malformed @addTagHelper yields a MalformedDirectiveIntermediateNode and
+ // the mixed literal/expression attribute value yields a MarkupElementIntermediateNode fallback.
+ var source = """
+ @addTagHelper *
+ body
+ """;
+
+ var kinds = CollectKinds(Lower(source, RazorFileKind.Legacy));
+ Assert.Contains(nameof(MalformedDirectiveIntermediateNode), kinds);
+ Assert.Contains(nameof(MarkupElementIntermediateNode), kinds);
+
+ AssertClone(source, RazorFileKind.Legacy);
+ }
+
+ private int AssertClone(string content, RazorFileKind fileKind)
+ {
+ var documentNode = Lower(content, fileKind);
+
+ var clone = (DocumentIntermediateNode)documentNode.Clone();
+
+ Assert.Equal(Dump(documentNode), Dump(clone));
+
+ // The declaration subtree, options, and code target are shared by reference on purpose.
+ Assert.Same(documentNode.DeclDocumentNode, clone.DeclDocumentNode);
+ Assert.Same(documentNode.Options, clone.Options);
+ Assert.Same(documentNode.Target, clone.Target);
+
+ return AssertAliasesPreserved(documentNode, clone);
+ }
+
+ // A node-typed property that holds one of the node's own Children is an alias (e.g.
+ // UnresolvedAttributeIntermediateNode.HtmlAttributeNode == Children[^1]). Cloning must preserve that
+ // aliasing: the cloned property has to point at the cloned child, not an independent copy. Otherwise the
+ // clone carries two divergent instances and a phase that mutates one while walking the other sees stale
+ // state. Walk the original and clone trees in lockstep and assert every alias is preserved by reference.
+ // Returns the number of aliases verified so a test can assert the tree actually exercised one.
+ private static int AssertAliasesPreserved(IntermediateNode original, IntermediateNode clone)
+ {
+ Assert.Equal(original.GetType(), clone.GetType());
+ Assert.Equal(original.Children.Count, clone.Children.Count);
+
+ var aliasCount = 0;
+
+ foreach (var (name, node) in NodeProperties(original))
+ {
+ var index = IndexOfReference(original.Children, node);
+ if (index < 0)
+ {
+ continue;
+ }
+
+ Assert.Same(clone.Children[index], GetPropertyValue(clone, name));
+ aliasCount++;
+ }
+
+ for (var i = 0; i < original.Children.Count; i++)
+ {
+ aliasCount += AssertAliasesPreserved(original.Children[i], clone.Children[i]);
+ }
+
+ return aliasCount;
+ }
+
+ // Enumerates the node-typed properties of a node, using the same reflection/exclusion rules as the dump.
+ private static IEnumerable<(string Name, IntermediateNode Node)> NodeProperties(IntermediateNode node)
+ {
+ foreach (var property in node.GetType()
+ .GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
+ .Where(p => p.CanRead && p.GetIndexParameters().Length == 0)
+ .OrderBy(p => p.Name, StringComparer.Ordinal))
+ {
+ if (property.Name is "Children" or "Parent" or "DeclDocumentNode")
+ {
+ continue;
+ }
+
+ object? value;
+ try
+ {
+ value = property.GetValue(node);
+ }
+ catch
+ {
+ continue;
+ }
+
+ if (value is IntermediateNode childNode)
+ {
+ yield return (property.Name, childNode);
+ }
+ }
+ }
+
+ private static IntermediateNode GetPropertyValue(IntermediateNode node, string name)
+ => (IntermediateNode)node.GetType()
+ .GetProperty(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)!
+ .GetValue(node)!;
+
+ private static int IndexOfReference(IntermediateNodeCollection children, IntermediateNode node)
+ {
+ for (var i = 0; i < children.Count; i++)
+ {
+ if (ReferenceEquals(children[i], node))
+ {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ // Collects the distinct node-kind names present in a tree, walking node-typed side references and
+ // Children, so a test can assert a document actually exercises a given kind.
+ private static HashSet CollectKinds(IntermediateNode root)
+ {
+ var kinds = new HashSet();
+ Collect(root);
+ return kinds;
+
+ void Collect(IntermediateNode node)
+ {
+ kinds.Add(node.GetType().Name);
+
+ foreach (var (_, child) in NodeProperties(node))
+ {
+ Collect(child);
+ }
+
+ foreach (var child in node.Children)
+ {
+ Collect(child);
+ }
+ }
+ }
+
+ // Runs the engine phases up to (but not including) tag-helper discovery, producing the lowered but
+ // still unresolved intermediate tree.
+ private DocumentIntermediateNode Lower(string content, RazorFileKind fileKind)
+ {
+ var source = RazorSourceDocument.Create(content, "test.razor");
+ var codeDocument = _projectEngine.CreateCodeDocument(source, fileKind);
+
+ foreach (var phase in _projectEngine.Engine.Phases)
+ {
+ if (phase is DefaultRazorTagHelperContextDiscoveryPhase)
+ {
+ break;
+ }
+
+ codeDocument = phase.Execute(codeDocument);
+ }
+
+ return codeDocument.GetRequiredDocumentNode();
+ }
+
+ // Serializes the full state of a node tree: every readable data property, then each deep-cloned side
+ // reference, then Children. DeclDocumentNode is shared by reference (asserted separately), so it is not
+ // recursed.
+ private static string Dump(IntermediateNode root)
+ {
+ var builder = new StringBuilder();
+ DumpNode(root, builder, depth: 0, activePath: new HashSet());
+ return builder.ToString();
+ }
+
+ // `activePath` tracks the current recursion stack so a true cycle is broken, while a node reachable from
+ // two places (incidental sharing) is still dumped fully at each site. That keeps the dump symmetric
+ // between the original (which may share an instance) and its clone (which deep-copies each reference).
+ private static void DumpNode(IntermediateNode node, StringBuilder builder, int depth, HashSet activePath)
+ {
+ if (!activePath.Add(node))
+ {
+ builder.Append(' ', depth * 2).AppendLine("");
+ return;
+ }
+
+ builder.Append(' ', depth * 2).Append(node.GetType().Name);
+
+ var sideReferences = new List<(string Name, IntermediateNode Node)>();
+
+ foreach (var property in node.GetType()
+ .GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
+ .Where(p => p.CanRead && p.GetIndexParameters().Length == 0)
+ .OrderBy(p => p.Name, StringComparer.Ordinal))
+ {
+ if (property.Name is "Children" or "Parent" or "DeclDocumentNode" or "IsLazy")
+ {
+ // IsLazy is a content-storage detail: cloning a token materializes its lazy content into an
+ // eager string, so IsLazy legitimately differs while Content is identical.
+ continue;
+ }
+
+ object? value;
+ try
+ {
+ value = property.GetValue(node);
+ }
+ catch
+ {
+ continue;
+ }
+
+ if (value is IntermediateNode childNode)
+ {
+ sideReferences.Add((property.Name, childNode));
+ }
+ else
+ {
+ builder.Append(' ').Append(property.Name).Append('=').Append(Format(value));
+ }
+ }
+
+ builder.AppendLine();
+
+ foreach (var (name, child) in sideReferences)
+ {
+ builder.Append(' ', (depth + 1) * 2).Append('.').Append(name).Append(':').AppendLine();
+ DumpNode(child, builder, depth + 2, activePath);
+ }
+
+ foreach (var child in node.Children)
+ {
+ DumpNode(child, builder, depth + 1, activePath);
+ }
+
+ activePath.Remove(node);
+ }
+
+ private static string Format(object? value)
+ {
+ switch (value)
+ {
+ case null:
+ return "null";
+ case string s:
+ return "\"" + s + "\"";
+ case IEnumerable enumerable:
+ var items = enumerable.Cast().Select(Format);
+ return "[" + string.Join(", ", items) + "]";
+ default:
+ return value.ToString() ?? "null";
+ }
+ }
+}
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentInjectIntermediateNode.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentInjectIntermediateNode.cs
index 377f8a50b42cc..e93738d6fb17e 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentInjectIntermediateNode.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentInjectIntermediateNode.cs
@@ -47,6 +47,16 @@ public override void Accept(IntermediateNodeVisitor visitor)
AcceptExtensionNode(this, visitor);
}
+ protected override IntermediateNode CloneNode()
+ {
+ var clone = new ComponentInjectIntermediateNode(TypeName, MemberName, TypeSpan, MemberSpan)
+ {
+ IsSynthesizedHelper = IsSynthesizedHelper,
+ };
+
+ return clone;
+ }
+
public override void WriteNode(CodeTarget target, CodeRenderingContext context)
{
if (target == null)
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/RouteAttributeExtensionNode.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/RouteAttributeExtensionNode.cs
index 75a5d77184000..8f49e4127c513 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/RouteAttributeExtensionNode.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/RouteAttributeExtensionNode.cs
@@ -14,6 +14,16 @@ internal sealed class RouteAttributeExtensionNode(string template) : ExtensionIn
public override void Accept(IntermediateNodeVisitor visitor) => AcceptExtensionNode(this, visitor);
+ protected override IntermediateNode CloneNode()
+ {
+ var clone = new RouteAttributeExtensionNode(Template)
+ {
+ IsSynthesizedHelper = IsSynthesizedHelper,
+ };
+
+ return clone;
+ }
+
public override void WriteNode(CodeTarget target, CodeRenderingContext context)
{
context.CodeWriter.Write("[global::");
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/RazorCompiledItemMetadataAttributeIntermediateNode.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/RazorCompiledItemMetadataAttributeIntermediateNode.cs
index f57186e3fdd72..664dec2185389 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/RazorCompiledItemMetadataAttributeIntermediateNode.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/RazorCompiledItemMetadataAttributeIntermediateNode.cs
@@ -41,6 +41,19 @@ public override void Accept(IntermediateNodeVisitor visitor)
AcceptExtensionNode(this, visitor);
}
+ protected override IntermediateNode CloneNode()
+ {
+ var clone = new RazorCompiledItemMetadataAttributeIntermediateNode
+ {
+ Key = Key,
+ Value = Value,
+ ValueStringSyntax = ValueStringSyntax,
+ IsSynthesizedHelper = IsSynthesizedHelper,
+ };
+
+ return clone;
+ }
+
public override void WriteNode(CodeTarget target, CodeRenderingContext context)
{
if (target == null)
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/SectionIntermediateNode.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/SectionIntermediateNode.cs
index f42992eb84dee..9e83fd9f5e4da 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/SectionIntermediateNode.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/SectionIntermediateNode.cs
@@ -25,6 +25,17 @@ public override void Accept(IntermediateNodeVisitor visitor)
AcceptExtensionNode(this, visitor);
}
+ protected override IntermediateNode CloneNode()
+ {
+ var clone = new SectionIntermediateNode
+ {
+ SectionName = SectionName,
+ IsSynthesizedHelper = IsSynthesizedHelper,
+ };
+
+ return clone;
+ }
+
public override void WriteNode(CodeTarget target, CodeRenderingContext context)
{
if (target == null)
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/TemplateIntermediateNode.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/TemplateIntermediateNode.cs
index 1cd4247cf4f0f..d5f3a3b0a8cd2 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/TemplateIntermediateNode.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Extensions/TemplateIntermediateNode.cs
@@ -23,6 +23,16 @@ public override void Accept(IntermediateNodeVisitor visitor)
AcceptExtensionNode(this, visitor);
}
+ protected override IntermediateNode CloneNode()
+ {
+ var clone = new TemplateIntermediateNode
+ {
+ IsSynthesizedHelper = IsSynthesizedHelper,
+ };
+
+ return clone;
+ }
+
public override void WriteNode(CodeTarget target, CodeRenderingContext context)
{
if (target == null)
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/CSharpCodeAttributeValueIntermediateNode.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/CSharpCodeAttributeValueIntermediateNode.cs
index 70c7fa4bcd20b..dd1820396ede7 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/CSharpCodeAttributeValueIntermediateNode.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/CSharpCodeAttributeValueIntermediateNode.cs
@@ -23,6 +23,17 @@ public override void Accept(IntermediateNodeVisitor visitor)
visitor.VisitCSharpCodeAttributeValue(this);
}
+ protected override IntermediateNode CloneNode()
+ {
+ var clone = new CSharpCodeAttributeValueIntermediateNode
+ {
+ Prefix = Prefix,
+ IsSynthesizedHelper = IsSynthesizedHelper,
+ };
+
+ return clone;
+ }
+
public override void FormatNode(IntermediateNodeFormatter formatter)
{
formatter.WriteChildren(Children);
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/CSharpCodeIntermediateNode.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/CSharpCodeIntermediateNode.cs
index f70bacb7f53bb..bc16c17dc2a50 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/CSharpCodeIntermediateNode.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/CSharpCodeIntermediateNode.cs
@@ -21,6 +21,16 @@ public override void Accept(IntermediateNodeVisitor visitor)
visitor.VisitCSharpCode(this);
}
+ protected override IntermediateNode CloneNode()
+ {
+ var clone = new CSharpCodeIntermediateNode
+ {
+ IsSynthesizedHelper = IsSynthesizedHelper,
+ };
+
+ return clone;
+ }
+
public override void FormatNode(IntermediateNodeFormatter formatter)
{
formatter.WriteChildren(Children);
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/CSharpExpressionAttributeValueIntermediateNode.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/CSharpExpressionAttributeValueIntermediateNode.cs
index e76eac2c03ceb..52e4aa607903f 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/CSharpExpressionAttributeValueIntermediateNode.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/CSharpExpressionAttributeValueIntermediateNode.cs
@@ -23,6 +23,17 @@ public override void Accept(IntermediateNodeVisitor visitor)
visitor.VisitCSharpExpressionAttributeValue(this);
}
+ protected override IntermediateNode CloneNode()
+ {
+ var clone = new CSharpExpressionAttributeValueIntermediateNode
+ {
+ Prefix = Prefix,
+ IsSynthesizedHelper = IsSynthesizedHelper,
+ };
+
+ return clone;
+ }
+
public override void FormatNode(IntermediateNodeFormatter formatter)
{
formatter.WriteChildren(Children);
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/CSharpExpressionIntermediateNode.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/CSharpExpressionIntermediateNode.cs
index 9111cc48383f2..ac2631aecbd47 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/CSharpExpressionIntermediateNode.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/CSharpExpressionIntermediateNode.cs
@@ -21,6 +21,16 @@ public override void Accept(IntermediateNodeVisitor visitor)
visitor.VisitCSharpExpression(this);
}
+ protected override IntermediateNode CloneNode()
+ {
+ var clone = new CSharpExpressionIntermediateNode
+ {
+ IsSynthesizedHelper = IsSynthesizedHelper,
+ };
+
+ return clone;
+ }
+
public override void FormatNode(IntermediateNodeFormatter formatter)
{
formatter.WriteChildren(Children);
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/CSharpIntermediateToken.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/CSharpIntermediateToken.cs
index b9798fb20d77d..bf4f2ea02c5b9 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/CSharpIntermediateToken.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/CSharpIntermediateToken.cs
@@ -14,4 +14,10 @@ internal CSharpIntermediateToken(LazyContent content, SourceSpan? source)
: base(content, source)
{
}
+
+ protected override IntermediateNode CloneNode()
+ => new CSharpIntermediateToken(Content, Source)
+ {
+ IsSynthesizedHelper = IsSynthesizedHelper,
+ };
}
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/ClassDeclarationIntermediateNode.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/ClassDeclarationIntermediateNode.cs
index 06f7944cd45c7..b2fb00f6b88c3 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/ClassDeclarationIntermediateNode.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/ClassDeclarationIntermediateNode.cs
@@ -24,6 +24,23 @@ public sealed class ClassDeclarationIntermediateNode : MemberDeclarationIntermed
public override void Accept(IntermediateNodeVisitor visitor)
=> visitor.VisitClassDeclaration(this);
+ protected override IntermediateNode CloneNode()
+ {
+ var clone = new ClassDeclarationIntermediateNode
+ {
+ Name = Name,
+ BaseType = BaseType,
+ Modifiers = Modifiers,
+ Interfaces = Interfaces,
+ TypeParameters = TypeParameters,
+ IsPrimaryClass = IsPrimaryClass,
+ NullableContext = NullableContext,
+ IsSynthesizedHelper = IsSynthesizedHelper,
+ };
+
+ return clone;
+ }
+
public override void FormatNode(IntermediateNodeFormatter formatter)
{
formatter.WriteContent(Name);
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/DirectiveIntermediateNode.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/DirectiveIntermediateNode.cs
index 4cd4fb2999194..55db16b8ca4f7 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/DirectiveIntermediateNode.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/DirectiveIntermediateNode.cs
@@ -23,6 +23,18 @@ public override void Accept(IntermediateNodeVisitor visitor)
visitor.VisitDirective(this);
}
+ protected override IntermediateNode CloneNode()
+ {
+ var clone = new DirectiveIntermediateNode
+ {
+ DirectiveName = DirectiveName,
+ Directive = Directive,
+ IsSynthesizedHelper = IsSynthesizedHelper,
+ };
+
+ return clone;
+ }
+
public override void FormatNode(IntermediateNodeFormatter formatter)
{
formatter.WriteContent(DirectiveName);
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/DirectiveTokenIntermediateNode.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/DirectiveTokenIntermediateNode.cs
index 3d252fd07aeb2..1a709a0ce4d0a 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/DirectiveTokenIntermediateNode.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/DirectiveTokenIntermediateNode.cs
@@ -20,6 +20,14 @@ public override void Accept(IntermediateNodeVisitor visitor)
visitor.VisitDirectiveToken(this);
}
+ protected override IntermediateNode CloneNode()
+ => new DirectiveTokenIntermediateNode
+ {
+ Content = Content,
+ DirectiveToken = DirectiveToken,
+ IsSynthesizedHelper = IsSynthesizedHelper,
+ };
+
public override void FormatNode(IntermediateNodeFormatter formatter)
{
formatter.WriteContent(Content);
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 6dc75af7664e8..e629b5ad29d1a 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
@@ -57,4 +57,19 @@ public override void FormatNode(IntermediateNodeFormatter formatter)
formatter.WriteProperty(nameof(DocumentKind), DocumentKind);
}
+
+ protected override IntermediateNode CloneNode()
+ {
+ // The declaration subtree is already lowered and inert during replay, so it is shared by reference.
+ var clone = new DocumentIntermediateNode
+ {
+ DocumentKind = DocumentKind,
+ DeclDocumentNode = DeclDocumentNode,
+ FallbackComponentTypeName = FallbackComponentTypeName,
+ Options = Options,
+ Target = Target,
+ };
+
+ return clone;
+ }
}
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/FieldDeclarationIntermediateNode.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/FieldDeclarationIntermediateNode.cs
index b817c84f803c6..046ffd039bef3 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/FieldDeclarationIntermediateNode.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/FieldDeclarationIntermediateNode.cs
@@ -20,6 +20,21 @@ public sealed class FieldDeclarationIntermediateNode : MemberDeclarationIntermed
public override void Accept(IntermediateNodeVisitor visitor)
=> visitor.VisitFieldDeclaration(this);
+ protected override IntermediateNode CloneNode()
+ {
+ var clone = new FieldDeclarationIntermediateNode
+ {
+ Name = Name,
+ Type = Type,
+ Modifiers = Modifiers,
+ SuppressWarnings = SuppressWarnings,
+ IsTagHelperField = IsTagHelperField,
+ IsSynthesizedHelper = IsSynthesizedHelper,
+ };
+
+ return clone;
+ }
+
public override void FormatNode(IntermediateNodeFormatter formatter)
{
formatter.WriteContent(Name);
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/HtmlAttributeIntermediateNode.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/HtmlAttributeIntermediateNode.cs
index bb48cb55da2b9..3889ced93efad 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/HtmlAttributeIntermediateNode.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/HtmlAttributeIntermediateNode.cs
@@ -34,6 +34,22 @@ public override void Accept(IntermediateNodeVisitor visitor)
visitor.VisitHtmlAttribute(this);
}
+ protected override IntermediateNode CloneNode()
+ {
+ var clone = new HtmlAttributeIntermediateNode
+ {
+ AttributeName = AttributeName,
+ AttributeNameExpression = (CSharpExpressionIntermediateNode)AttributeNameExpression?.Clone(),
+ Prefix = Prefix,
+ Suffix = Suffix,
+ EventUpdatesAttributeName = EventUpdatesAttributeName,
+ OriginalAttributeName = OriginalAttributeName,
+ IsSynthesizedHelper = IsSynthesizedHelper,
+ };
+
+ return clone;
+ }
+
public override void FormatNode(IntermediateNodeFormatter formatter)
{
formatter.WriteContent(AttributeName);
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/HtmlAttributeValueIntermediateNode.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/HtmlAttributeValueIntermediateNode.cs
index 5ab2e95f133a9..c1dbcce7f5ed4 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/HtmlAttributeValueIntermediateNode.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/HtmlAttributeValueIntermediateNode.cs
@@ -23,6 +23,17 @@ public override void Accept(IntermediateNodeVisitor visitor)
visitor.VisitHtmlAttributeValue(this);
}
+ protected override IntermediateNode CloneNode()
+ {
+ var clone = new HtmlAttributeValueIntermediateNode
+ {
+ Prefix = Prefix,
+ IsSynthesizedHelper = IsSynthesizedHelper,
+ };
+
+ return clone;
+ }
+
public override void FormatNode(IntermediateNodeFormatter formatter)
{
formatter.WriteChildren(Children);
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/HtmlContentIntermediateNode.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/HtmlContentIntermediateNode.cs
index df88a369f15fb..a106b60a18179 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/HtmlContentIntermediateNode.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/HtmlContentIntermediateNode.cs
@@ -23,6 +23,17 @@ public override void Accept(IntermediateNodeVisitor visitor)
visitor.VisitHtml(this);
}
+ protected override IntermediateNode CloneNode()
+ {
+ var clone = new HtmlContentIntermediateNode
+ {
+ HasEncodedContent = HasEncodedContent,
+ IsSynthesizedHelper = IsSynthesizedHelper,
+ };
+
+ return clone;
+ }
+
public override void FormatNode(IntermediateNodeFormatter formatter)
{
formatter.WriteChildren(Children);
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/HtmlIntermediateToken.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/HtmlIntermediateToken.cs
index 6af9e1e8ca2d3..69f80df61cef8 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/HtmlIntermediateToken.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/HtmlIntermediateToken.cs
@@ -14,4 +14,10 @@ internal HtmlIntermediateToken(LazyContent content, SourceSpan? source)
: base(content, source)
{
}
+
+ protected override IntermediateNode CloneNode()
+ => new HtmlIntermediateToken(Content, Source)
+ {
+ IsSynthesizedHelper = IsSynthesizedHelper,
+ };
}
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/IntermediateNode.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/IntermediateNode.cs
index b5bffdab3f7dc..5e9dde9036d36 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/IntermediateNode.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/IntermediateNode.cs
@@ -1,6 +1,7 @@
// 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 System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
@@ -80,4 +81,36 @@ internal string GetDebuggerDisplay()
public virtual void FormatNode(IntermediateNodeFormatter formatter)
{
}
+
+ ///
+ /// Returns a deep copy of this node and its descendants. The node-specific state is produced by
+ /// ; this method copies the common state (source span, imported flag,
+ /// diagnostics) and deep-clones the children onto it. A node with a property that aliases one of
+ /// its overrides this to re-point that property at the cloned child.
+ ///
+ internal virtual IntermediateNode Clone()
+ {
+ var clone = CloneNode();
+
+ clone.Source = Source;
+ clone.IsImported = IsImported;
+ clone.AddDiagnosticsFromNode(this);
+
+ foreach (var child in Children)
+ {
+ clone.Children.Add(child.Clone());
+ }
+
+ return clone;
+ }
+
+ ///
+ /// Creates a copy of this node carrying only its own state -- the node-specific fields (including the
+ /// init-only ) and any child nodes held outside
+ /// (deep-cloned). The common state and the are copied by .
+ /// Overridden by every node kind that can appear in an unresolved tree; the base throws so an
+ /// unexpected kind fails loudly rather than silently producing an incomplete copy.
+ ///
+ protected virtual IntermediateNode CloneNode()
+ => throw new NotSupportedException($"{GetType().Name} does not support cloning.");
}
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/MalformedDirectiveIntermediateNode.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/MalformedDirectiveIntermediateNode.cs
index f7f8f4df5b4f0..e93787915ddc3 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/MalformedDirectiveIntermediateNode.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/MalformedDirectiveIntermediateNode.cs
@@ -23,6 +23,18 @@ public override void Accept(IntermediateNodeVisitor visitor)
visitor.VisitMalformedDirective(this);
}
+ protected override IntermediateNode CloneNode()
+ {
+ var clone = new MalformedDirectiveIntermediateNode
+ {
+ DirectiveName = DirectiveName,
+ Directive = Directive,
+ IsSynthesizedHelper = IsSynthesizedHelper,
+ };
+
+ return clone;
+ }
+
public override void FormatNode(IntermediateNodeFormatter formatter)
{
formatter.WriteContent(DirectiveName);
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/MarkupElementIntermediateNode.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/MarkupElementIntermediateNode.cs
index 0351f8073996d..072930cb4bf32 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/MarkupElementIntermediateNode.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/MarkupElementIntermediateNode.cs
@@ -42,6 +42,17 @@ public override void Accept(IntermediateNodeVisitor visitor)
visitor.VisitMarkupElement(this);
}
+ protected override IntermediateNode CloneNode()
+ {
+ var clone = new MarkupElementIntermediateNode
+ {
+ TagName = TagName,
+ IsSynthesizedHelper = IsSynthesizedHelper,
+ };
+
+ return clone;
+ }
+
public override void FormatNode(IntermediateNodeFormatter formatter)
{
if (formatter == null)
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/MethodDeclarationIntermediateNode.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/MethodDeclarationIntermediateNode.cs
index 0f0c43bdfb62a..8b274c7cd030f 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/MethodDeclarationIntermediateNode.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/MethodDeclarationIntermediateNode.cs
@@ -23,6 +23,21 @@ public sealed class MethodDeclarationIntermediateNode : MemberDeclarationInterme
public override void Accept(IntermediateNodeVisitor visitor)
=> visitor.VisitMethodDeclaration(this);
+ protected override IntermediateNode CloneNode()
+ {
+ var clone = new MethodDeclarationIntermediateNode
+ {
+ Name = Name,
+ ReturnType = ReturnType,
+ Modifiers = Modifiers,
+ Parameters = Parameters,
+ IsPrimaryMethod = IsPrimaryMethod,
+ IsSynthesizedHelper = IsSynthesizedHelper,
+ };
+
+ return clone;
+ }
+
public override void FormatNode(IntermediateNodeFormatter formatter)
{
formatter.WriteContent(Name);
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/NamespaceDeclarationIntermediateNode.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/NamespaceDeclarationIntermediateNode.cs
index 77f701e137a77..d4493b269671d 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/NamespaceDeclarationIntermediateNode.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/NamespaceDeclarationIntermediateNode.cs
@@ -20,4 +20,17 @@ public override void FormatNode(IntermediateNodeFormatter formatter)
formatter.WriteContent(Name);
formatter.WriteProperty(nameof(Name), Name);
}
+
+ protected override IntermediateNode CloneNode()
+ {
+ var clone = new NamespaceDeclarationIntermediateNode
+ {
+ Name = Name,
+ IsPrimaryNamespace = IsPrimaryNamespace,
+ IsGenericTyped = IsGenericTyped,
+ IsSynthesizedHelper = IsSynthesizedHelper,
+ };
+
+ return clone;
+ }
}
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/PropertyDeclarationIntermediateNode.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/PropertyDeclarationIntermediateNode.cs
index 20e4f0175eb1d..615b36409b406 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/PropertyDeclarationIntermediateNode.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/PropertyDeclarationIntermediateNode.cs
@@ -17,4 +17,18 @@ public sealed class PropertyDeclarationIntermediateNode : MemberDeclarationInter
public override void Accept(IntermediateNodeVisitor visitor)
=> visitor.VisitPropertyDeclaration(this);
+
+ protected override IntermediateNode CloneNode()
+ {
+ var clone = new PropertyDeclarationIntermediateNode
+ {
+ Name = Name,
+ Type = (IntermediateToken)Type.Clone(),
+ ExpressionBody = ExpressionBody,
+ Modifiers = Modifiers,
+ IsSynthesizedHelper = IsSynthesizedHelper,
+ };
+
+ return clone;
+ }
}
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/UnresolvedAttributeIntermediateNode.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/UnresolvedAttributeIntermediateNode.cs
index 86f7ccec576dc..34a27e6fc400e 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/UnresolvedAttributeIntermediateNode.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/UnresolvedAttributeIntermediateNode.cs
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Diagnostics;
+
namespace Microsoft.AspNetCore.Razor.Language.Intermediate;
///
@@ -71,6 +73,42 @@ public override void Accept(IntermediateNodeVisitor visitor)
visitor.VisitDefault(this);
}
+ protected override IntermediateNode CloneNode()
+ {
+ var clone = new UnresolvedAttributeIntermediateNode
+ {
+ AttributeName = AttributeName,
+ IsMinimized = IsMinimized,
+ ValueContent = ValueContent,
+ ValueSourceSpan = ValueSourceSpan,
+ AttributeStructure = AttributeStructure,
+ AttributeNameSpan = AttributeNameSpan,
+ AsTagHelperAttribute = AsTagHelperAttribute?.Clone(),
+ AsMarkupAttribute = AsMarkupAttribute?.Clone(),
+ IsSynthesizedHelper = IsSynthesizedHelper,
+ };
+
+ return clone;
+ }
+
+ internal override IntermediateNode Clone()
+ {
+ var clone = (UnresolvedAttributeIntermediateNode)base.Clone();
+
+ // HtmlAttributeNode aliases one of Children, so point the clone at its cloned child rather than
+ // an independent copy. Cloning it separately would leave the clone's HtmlAttributeNode and its
+ // Children entry as two divergent instances, so a phase that mutates one and walks the other
+ // would see stale state.
+ if (HtmlAttributeNode is { } htmlAttributeNode)
+ {
+ var index = Children.IndexOf(htmlAttributeNode);
+ Debug.Assert(index >= 0, "HtmlAttributeNode is expected to be one of Children.");
+ clone.HtmlAttributeNode = (HtmlAttributeIntermediateNode)clone.Children[index];
+ }
+
+ return clone;
+ }
+
public override void FormatNode(IntermediateNodeFormatter formatter)
{
formatter.WriteContent(AttributeName);
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/UnresolvedAttributeValueIntermediateNode.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/UnresolvedAttributeValueIntermediateNode.cs
index 3c477dbbe591a..a856050c0ee1d 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/UnresolvedAttributeValueIntermediateNode.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/UnresolvedAttributeValueIntermediateNode.cs
@@ -28,6 +28,17 @@ public override void Accept(IntermediateNodeVisitor visitor)
visitor.VisitDefault(this);
}
+ protected override IntermediateNode CloneNode()
+ {
+ var clone = new UnresolvedAttributeValueIntermediateNode
+ {
+ Prefix = Prefix,
+ IsSynthesizedHelper = IsSynthesizedHelper,
+ };
+
+ return clone;
+ }
+
public override void FormatNode(IntermediateNodeFormatter formatter)
{
formatter.WriteChildren(Children);
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/UnresolvedElementIntermediateNode.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/UnresolvedElementIntermediateNode.cs
index 4f14b549be8c3..4a06dbee5ab10 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/UnresolvedElementIntermediateNode.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/UnresolvedElementIntermediateNode.cs
@@ -73,6 +73,32 @@ public override void Accept(IntermediateNodeVisitor visitor)
visitor.VisitDefault(this);
}
+ protected override IntermediateNode CloneNode()
+ {
+ var clone = new UnresolvedElementIntermediateNode
+ {
+ TagName = TagName,
+ IsComponent = IsComponent,
+ IsEscaped = IsEscaped,
+ IsSelfClosing = IsSelfClosing,
+ HasEndTag = HasEndTag,
+ EndTagName = EndTagName,
+ EndTagSpan = EndTagSpan,
+ IsVoidElement = IsVoidElement,
+ StartTagNameSpan = StartTagNameSpan,
+ StartTagSpan = StartTagSpan,
+ AttributeData = AttributeData,
+ HasMissingCloseAngle = HasMissingCloseAngle,
+ HasDynamicExpressionChild = HasDynamicExpressionChild,
+ HasMissingEndCloseAngle = HasMissingEndCloseAngle,
+ StartTagEndIndex = StartTagEndIndex,
+ BodyEndIndex = BodyEndIndex,
+ IsSynthesizedHelper = IsSynthesizedHelper,
+ };
+
+ return clone;
+ }
+
public override void FormatNode(IntermediateNodeFormatter formatter)
{
formatter.WriteContent(TagName);
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/UnresolvedExpressionAttributeValueIntermediateNode.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/UnresolvedExpressionAttributeValueIntermediateNode.cs
index b824f460bdf0e..e249272d198b9 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/UnresolvedExpressionAttributeValueIntermediateNode.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/UnresolvedExpressionAttributeValueIntermediateNode.cs
@@ -35,6 +35,18 @@ public override void Accept(IntermediateNodeVisitor visitor)
visitor.VisitDefault(this);
}
+ protected override IntermediateNode CloneNode()
+ {
+ var clone = new UnresolvedExpressionAttributeValueIntermediateNode
+ {
+ Prefix = Prefix,
+ ContainsExpression = ContainsExpression,
+ IsSynthesizedHelper = IsSynthesizedHelper,
+ };
+
+ return clone;
+ }
+
public override void FormatNode(IntermediateNodeFormatter formatter)
{
formatter.WriteChildren(Children);
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/UsingDirectiveIntermediateNode.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/UsingDirectiveIntermediateNode.cs
index 7bbb413823b59..c692ba6a4f8db 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/UsingDirectiveIntermediateNode.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Intermediate/UsingDirectiveIntermediateNode.cs
@@ -27,6 +27,19 @@ public override void Accept(IntermediateNodeVisitor visitor)
visitor.VisitUsingDirective(this);
}
+ protected override IntermediateNode CloneNode()
+ {
+ var clone = new UsingDirectiveIntermediateNode
+ {
+ AppendLineDefaultAndHidden = AppendLineDefaultAndHidden,
+ Content = Content,
+ HasExplicitSemicolon = HasExplicitSemicolon,
+ IsSynthesizedHelper = IsSynthesizedHelper,
+ };
+
+ return clone;
+ }
+
public override void FormatNode(IntermediateNodeFormatter formatter)
{
formatter.WriteContent(Content);
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCodeDocument.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCodeDocument.cs
index 547156a8d5ed4..a07045af4979a 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCodeDocument.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCodeDocument.cs
@@ -36,6 +36,7 @@ public sealed partial class RazorCodeDocument
private readonly DocumentIntermediateNode? _documentNode;
private readonly RazorCSharpDocument? _csharpDocument;
private readonly RazorCSharpDocument? _declCSharpDocument;
+ private readonly DocumentIntermediateNode? _unresolvedDocumentNode;
private readonly ImmutableArray _directiveTagHelperContributions;
private RazorCodeDocument(
@@ -52,7 +53,8 @@ private RazorCodeDocument(
DocumentIntermediateNode? documentNode,
RazorCSharpDocument? csharpDocument,
RazorCSharpDocument? declCSharpDocument,
- ImmutableArray directiveTagHelperContributions)
+ ImmutableArray directiveTagHelperContributions,
+ DocumentIntermediateNode? unresolvedDocumentNode)
{
Source = source;
Imports = imports.NullToEmpty();
@@ -70,6 +72,7 @@ private RazorCodeDocument(
_csharpDocument = csharpDocument;
_declCSharpDocument = declCSharpDocument;
_directiveTagHelperContributions = directiveTagHelperContributions.NullToEmpty();
+ _unresolvedDocumentNode = unresolvedDocumentNode;
}
public static RazorCodeDocument Create(
@@ -100,7 +103,8 @@ public static RazorCodeDocument Create(
documentNode: null,
csharpDocument: null,
declCSharpDocument: null,
- directiveTagHelperContributions: default);
+ directiveTagHelperContributions: default,
+ unresolvedDocumentNode: null);
}
internal bool TryGetTagHelpers([NotNullWhen(true)] out TagHelperCollection? result)
@@ -121,7 +125,7 @@ internal RazorCodeDocument WithTagHelpers(TagHelperCollection? value)
{
return this;
}
- return new RazorCodeDocument(Source, Imports, ParserOptions, CodeGenerationOptions, value, _referencedTagHelpers, _syntaxTree, _tagHelperRewrittenSyntaxTree, _importSyntaxTrees, _tagHelperContext, _documentNode, _csharpDocument, _declCSharpDocument, _directiveTagHelperContributions);
+ return new RazorCodeDocument(Source, Imports, ParserOptions, CodeGenerationOptions, value, _referencedTagHelpers, _syntaxTree, _tagHelperRewrittenSyntaxTree, _importSyntaxTrees, _tagHelperContext, _documentNode, _csharpDocument, _declCSharpDocument, _directiveTagHelperContributions, _unresolvedDocumentNode);
}
internal bool TryGetReferencedTagHelpers([NotNullWhen(true)] out TagHelperCollection? result)
@@ -142,7 +146,7 @@ internal RazorCodeDocument WithReferencedTagHelpers(TagHelperCollection value)
{
return this;
}
- return new RazorCodeDocument(Source, Imports, ParserOptions, CodeGenerationOptions, _tagHelpers, value, _syntaxTree, _tagHelperRewrittenSyntaxTree, _importSyntaxTrees, _tagHelperContext, _documentNode, _csharpDocument, _declCSharpDocument, _directiveTagHelperContributions);
+ return new RazorCodeDocument(Source, Imports, ParserOptions, CodeGenerationOptions, _tagHelpers, value, _syntaxTree, _tagHelperRewrittenSyntaxTree, _importSyntaxTrees, _tagHelperContext, _documentNode, _csharpDocument, _declCSharpDocument, _directiveTagHelperContributions, _unresolvedDocumentNode);
}
internal bool TryGetSyntaxTree([NotNullWhen(true)] out RazorSyntaxTree? result)
@@ -164,7 +168,7 @@ internal RazorCodeDocument WithSyntaxTree(RazorSyntaxTree value)
{
return this;
}
- return new RazorCodeDocument(Source, Imports, ParserOptions, CodeGenerationOptions, _tagHelpers, _referencedTagHelpers, value, _tagHelperRewrittenSyntaxTree, _importSyntaxTrees, _tagHelperContext, _documentNode, _csharpDocument, _declCSharpDocument, _directiveTagHelperContributions);
+ return new RazorCodeDocument(Source, Imports, ParserOptions, CodeGenerationOptions, _tagHelpers, _referencedTagHelpers, value, _tagHelperRewrittenSyntaxTree, _importSyntaxTrees, _tagHelperContext, _documentNode, _csharpDocument, _declCSharpDocument, _directiveTagHelperContributions, _unresolvedDocumentNode);
}
internal bool TryGetTagHelperRewrittenSyntaxTree([NotNullWhen(true)] out RazorSyntaxTree? result)
@@ -186,7 +190,7 @@ internal RazorCodeDocument WithTagHelperRewrittenSyntaxTree(RazorSyntaxTree valu
{
return this;
}
- return new RazorCodeDocument(Source, Imports, ParserOptions, CodeGenerationOptions, _tagHelpers, _referencedTagHelpers, _syntaxTree, value, _importSyntaxTrees, _tagHelperContext, _documentNode, _csharpDocument, _declCSharpDocument, _directiveTagHelperContributions);
+ return new RazorCodeDocument(Source, Imports, ParserOptions, CodeGenerationOptions, _tagHelpers, _referencedTagHelpers, _syntaxTree, value, _importSyntaxTrees, _tagHelperContext, _documentNode, _csharpDocument, _declCSharpDocument, _directiveTagHelperContributions, _unresolvedDocumentNode);
}
internal bool TryGetImportSyntaxTrees(out ImmutableArray result)
@@ -213,7 +217,7 @@ internal RazorCodeDocument WithImportSyntaxTrees(ImmutableArray
{
return this;
}
- return new RazorCodeDocument(Source, Imports, ParserOptions, CodeGenerationOptions, _tagHelpers, _referencedTagHelpers, _syntaxTree, _tagHelperRewrittenSyntaxTree, value, _tagHelperContext, _documentNode, _csharpDocument, _declCSharpDocument, _directiveTagHelperContributions);
+ return new RazorCodeDocument(Source, Imports, ParserOptions, CodeGenerationOptions, _tagHelpers, _referencedTagHelpers, _syntaxTree, _tagHelperRewrittenSyntaxTree, value, _tagHelperContext, _documentNode, _csharpDocument, _declCSharpDocument, _directiveTagHelperContributions, _unresolvedDocumentNode);
}
internal bool TryGetTagHelperContext([NotNullWhen(true)] out TagHelperDocumentContext? result)
@@ -236,7 +240,7 @@ internal RazorCodeDocument WithTagHelperContext(TagHelperDocumentContext value)
{
return this;
}
- return new RazorCodeDocument(Source, Imports, ParserOptions, CodeGenerationOptions, _tagHelpers, _referencedTagHelpers, _syntaxTree, _tagHelperRewrittenSyntaxTree, _importSyntaxTrees, value, _documentNode, _csharpDocument, _declCSharpDocument, _directiveTagHelperContributions);
+ return new RazorCodeDocument(Source, Imports, ParserOptions, CodeGenerationOptions, _tagHelpers, _referencedTagHelpers, _syntaxTree, _tagHelperRewrittenSyntaxTree, _importSyntaxTrees, value, _documentNode, _csharpDocument, _declCSharpDocument, _directiveTagHelperContributions, _unresolvedDocumentNode);
}
internal bool TryGetDocumentNode([NotNullWhen(true)] out DocumentIntermediateNode? result)
@@ -258,7 +262,20 @@ internal RazorCodeDocument WithDocumentNode(DocumentIntermediateNode value)
{
return this;
}
- return new RazorCodeDocument(Source, Imports, ParserOptions, CodeGenerationOptions, _tagHelpers, _referencedTagHelpers, _syntaxTree, _tagHelperRewrittenSyntaxTree, _importSyntaxTrees, _tagHelperContext, value, _csharpDocument, _declCSharpDocument, _directiveTagHelperContributions);
+ return new RazorCodeDocument(Source, Imports, ParserOptions, CodeGenerationOptions, _tagHelpers, _referencedTagHelpers, _syntaxTree, _tagHelperRewrittenSyntaxTree, _importSyntaxTrees, _tagHelperContext, value, _csharpDocument, _declCSharpDocument, _directiveTagHelperContributions, _unresolvedDocumentNode);
+ }
+
+ internal DocumentIntermediateNode? GetUnresolvedDocumentNode()
+ => _unresolvedDocumentNode;
+
+ internal RazorCodeDocument WithUnresolvedDocumentNode(DocumentIntermediateNode value)
+ {
+ Debug.Assert(value is not null);
+ if (ReferenceEquals(value, _unresolvedDocumentNode))
+ {
+ return this;
+ }
+ return new RazorCodeDocument(Source, Imports, ParserOptions, CodeGenerationOptions, _tagHelpers, _referencedTagHelpers, _syntaxTree, _tagHelperRewrittenSyntaxTree, _importSyntaxTrees, _tagHelperContext, _documentNode, _csharpDocument, _declCSharpDocument, _directiveTagHelperContributions, value);
}
internal RazorCSharpDocument? GetCSharpDocument(bool declarationDocument)
@@ -295,7 +312,7 @@ internal RazorCodeDocument WithImplCSharpDocument(RazorCSharpDocument value)
{
return this;
}
- return new RazorCodeDocument(Source, Imports, ParserOptions, CodeGenerationOptions, _tagHelpers, _referencedTagHelpers, _syntaxTree, _tagHelperRewrittenSyntaxTree, _importSyntaxTrees, _tagHelperContext, _documentNode, value, _declCSharpDocument, _directiveTagHelperContributions);
+ return new RazorCodeDocument(Source, Imports, ParserOptions, CodeGenerationOptions, _tagHelpers, _referencedTagHelpers, _syntaxTree, _tagHelperRewrittenSyntaxTree, _importSyntaxTrees, _tagHelperContext, _documentNode, value, _declCSharpDocument, _directiveTagHelperContributions, _unresolvedDocumentNode);
}
#if SONICDEV
@@ -311,7 +328,7 @@ internal RazorCodeDocument WithDeclCSharpDocument(RazorCSharpDocument value)
{
return this;
}
- return new RazorCodeDocument(Source, Imports, ParserOptions, CodeGenerationOptions, _tagHelpers, _referencedTagHelpers, _syntaxTree, _tagHelperRewrittenSyntaxTree, _importSyntaxTrees, _tagHelperContext, _documentNode, _csharpDocument, value, _directiveTagHelperContributions);
+ return new RazorCodeDocument(Source, Imports, ParserOptions, CodeGenerationOptions, _tagHelpers, _referencedTagHelpers, _syntaxTree, _tagHelperRewrittenSyntaxTree, _importSyntaxTrees, _tagHelperContext, _documentNode, _csharpDocument, value, _directiveTagHelperContributions, _unresolvedDocumentNode);
}
internal ImmutableArray GetDirectiveTagHelperContributions()
@@ -324,7 +341,7 @@ internal RazorCodeDocument WithDirectiveTagHelperContributions(ImmutableArray(this, visitor);
}
+ protected override IntermediateNode CloneNode()
+ {
+ var clone = new InjectIntermediateNode
+ {
+ TypeName = TypeName,
+ TypeSource = TypeSource,
+ MemberName = MemberName,
+ MemberSource = MemberSource,
+ IsMalformed = IsMalformed,
+ IsSynthesizedHelper = IsSynthesizedHelper,
+ };
+
+ return clone;
+ }
+
public override void WriteNode(CodeTarget target, CodeRenderingContext context)
{
if (target == null)
diff --git a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/SourceGeneratorProjectEngine.cs b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/SourceGeneratorProjectEngine.cs
index 62bd47c71229f..25c38e05b0208 100644
--- a/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/SourceGeneratorProjectEngine.cs
+++ b/src/Razor/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/SourceGeneratorProjectEngine.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.AspNetCore.Razor.Language;
+using Microsoft.AspNetCore.Razor.Language.Intermediate;
using Microsoft.CodeAnalysis.Razor.Compiler.CSharp;
using System;
using System.Diagnostics;
@@ -14,8 +15,6 @@ internal sealed class SourceGeneratorProjectEngine
private readonly RazorProjectEngine _projectEngine;
private readonly IRazorEnginePhase _discoveryPhase;
- private readonly int _loweringPhaseIndex = -1;
- private readonly int _declLoweringPhaseIndex = -1;
private readonly int _discoveryPhaseIndex = -1;
private readonly int _rewritePhaseIndex = -1;
@@ -29,21 +28,13 @@ public SourceGeneratorProjectEngine(RazorProjectEngine projectEngine)
foreach (var phase in Phases)
{
- if (_loweringPhaseIndex >= 0 && _declLoweringPhaseIndex >= 0 && _discoveryPhaseIndex >= 0 && _rewritePhaseIndex >= 0)
+ if (_discoveryPhaseIndex >= 0 && _rewritePhaseIndex >= 0)
{
break;
}
switch (phase)
{
- case DefaultRazorIntermediateNodeLoweringPhase:
- _loweringPhaseIndex = index;
- break;
-
- case DefaultRazorDeclCSharpLoweringPhase:
- _declLoweringPhaseIndex = index;
- break;
-
case DefaultRazorTagHelperContextDiscoveryPhase:
_discoveryPhase = phase;
_discoveryPhaseIndex = index;
@@ -58,12 +49,8 @@ public SourceGeneratorProjectEngine(RazorProjectEngine projectEngine)
}
Debug.Assert(_discoveryPhase is not null);
- Debug.Assert(_loweringPhaseIndex >= 0);
- Debug.Assert(_declLoweringPhaseIndex >= 0);
Debug.Assert(_discoveryPhaseIndex >= 0);
Debug.Assert(_rewritePhaseIndex >= 0);
- Debug.Assert(_loweringPhaseIndex < _declLoweringPhaseIndex);
- Debug.Assert(_declLoweringPhaseIndex < _discoveryPhaseIndex);
Debug.Assert(_discoveryPhaseIndex < _rewritePhaseIndex);
}
@@ -73,11 +60,18 @@ public SourceGeneratorRazorCodeDocument ProcessInitialParse(RazorProjectItem pro
codeDocument = ExecutePhases(Phases[.._discoveryPhaseIndex], codeDocument, cancellationToken);
- // By this point, DefaultRazorParsingPhase has set the canonical syntax tree (_syntaxTree)
- // so that discovery and subsequent phases can read it via GetSyntaxTree().
return new SourceGeneratorRazorCodeDocument(codeDocument);
}
+ ///
+ /// Runs tag-helper discovery, resolution and rewrite for a document. The generator calls this twice per
+ /// document: first with to do the initial
+ /// pass, then again with . The second call is an incremental gate -- its inputs
+ /// include the project-wide tag-helper set, so it re-runs whenever that set changes, but it returns the
+ /// document unchanged unless the change actually affects this document (a used descriptor was added or
+ /// removed). Only then does it replay resolution. The first call's inputs deliberately exclude the
+ /// tag-helper set, so it re-runs only when the document itself changes.
+ ///
public SourceGeneratorRazorCodeDocument ProcessTagHelpers(
SourceGeneratorRazorCodeDocument sgDocument,
TagHelperCollection tagHelpers,
@@ -114,39 +108,32 @@ public SourceGeneratorRazorCodeDocument ProcessTagHelpers(
return sgDocument;
}
- // Re-process the document with the updated tag helpers, starting from IR lowering. Resolution
- // binds unresolved nodes to their tag helpers by mutating the IR in place, so replaying from
- // resolution over an already-resolved tree finds nothing left to bind. Re-lowering rebuilds
- // fresh unresolved IR from the syntax tree; classification and the markup split then re-run over
- // it -- the split is what carves the working IR into the impl half that resolution and rewrite
- // operate on, so it cannot be skipped.
- //
- // Two phases in that range are skipped:
- // * Discovery: the gate discovery above already computed the in-scope tag-helper context for the
- // updated set. It walks the syntax tree (not the IR), so re-lowering does not invalidate it,
- // and re-running it would recompute the identical context.
- // * Decl C# lowering: it lowers the markup-free declaration half, whose text depends only on the
- // document's own source. A tag-helper change elsewhere cannot alter it, so the declaration
- // document from the initial parse is still valid. It is captured here and restored afterward
- // because nothing in the replayed range regenerates it, and it feeds the generator's output
- // cache comparison and declaration-diagnostic reporting.
- var declCSharpDocument = codeDocument.GetDeclCSharpDocument();
-
- codeDocument = ExecutePhases(Phases[_loweringPhaseIndex.._declLoweringPhaseIndex], codeDocument, cancellationToken);
- codeDocument = ExecutePhases(Phases[(_discoveryPhaseIndex + 1)..(_rewritePhaseIndex + 1)], codeDocument, cancellationToken);
-
- if (declCSharpDocument is not null)
- {
- codeDocument = codeDocument.WithDeclCSharpDocument(declCSharpDocument);
- }
+ codeDocument = ResolveTagHelpers(codeDocument);
return new SourceGeneratorRazorCodeDocument(codeDocument);
}
codeDocument = codeDocument.WithTagHelpers(tagHelpers);
- codeDocument = ExecutePhases(Phases[_discoveryPhaseIndex..(_rewritePhaseIndex + 1)], codeDocument, cancellationToken);
+ codeDocument = _discoveryPhase.Execute(codeDocument, cancellationToken);
+ codeDocument = ResolveTagHelpers(codeDocument);
return new SourceGeneratorRazorCodeDocument(codeDocument);
+
+ RazorCodeDocument ResolveTagHelpers(RazorCodeDocument codeDocument)
+ {
+ // Capture the unresolved node on the first pass (discovery doesn't mutate it), then resolve a
+ // clone of it every time. Resolution binds tag helpers by mutating the IR in place, so cloning
+ // keeps the stored node pristine for later replays -- which start from it instead of re-lowering.
+ var unresolvedDocumentNode = codeDocument.GetUnresolvedDocumentNode();
+ if (unresolvedDocumentNode is null)
+ {
+ unresolvedDocumentNode = codeDocument.GetRequiredDocumentNode();
+ codeDocument = codeDocument.WithUnresolvedDocumentNode(unresolvedDocumentNode);
+ }
+
+ codeDocument = codeDocument.WithDocumentNode((DocumentIntermediateNode)unresolvedDocumentNode.Clone());
+ return ExecutePhases(Phases[(_discoveryPhaseIndex + 1)..(_rewritePhaseIndex + 1)], codeDocument, cancellationToken);
+ }
}
private static bool RequiresRewrite(
diff --git a/src/Razor/src/Compiler/perf/Microsoft.AspNetCore.Razor.Microbenchmarks.Generator/RazorBenchmarks.cs b/src/Razor/src/Compiler/perf/Microsoft.AspNetCore.Razor.Microbenchmarks.Generator/RazorBenchmarks.cs
index 1246f142de829..b2cbd023db80d 100644
--- a/src/Razor/src/Compiler/perf/Microsoft.AspNetCore.Razor.Microbenchmarks.Generator/RazorBenchmarks.cs
+++ b/src/Razor/src/Compiler/perf/Microsoft.AspNetCore.Razor.Microbenchmarks.Generator/RazorBenchmarks.cs
@@ -15,6 +15,9 @@ public class RazorBenchmarks : AbstractBenchmark
[Benchmark]
public GeneratorDriver Razor_Edit_Independent() => RunRazorBenchmark(Independent, "\\0.razor");
+ [Benchmark]
+ public GeneratorDriver Razor_Edit_IndependentIgnorable() => RunRazorBenchmark(IndependentIgnorable, "\\0.razor");
+
[Benchmark]
public GeneratorDriver Razor_Remove_Independent() => RunRazorBenchmark(null, "\\0.razor");
@@ -55,8 +58,17 @@ private GeneratorDriver RunRazorBenchmark(string? AddedFileContent, string FileP
});
+ // Replacing the file body without an @page directive drops the route from the component's declaration,
+ // so this is a public-surface (signature) change: it invalidates whole-compilation tag-helper discovery.
private const string Independent = "Independent file ";
+ // Keeps the file's @page route (its declaration/public surface), changing only the markup body, so the
+ // decl stays byte-identical and discovery is not re-run -- the common "edit a leaf's body" case.
+ private const string IndependentIgnorable = """
+ @page "/0"
+ Independent file
+ """;
+
private const string DependentIgnorable = """
@page "/counter"
diff --git a/src/Razor/src/Compiler/perf/Microsoft.AspNetCore.Razor.Microbenchmarks.Generator/RazorTests.cs b/src/Razor/src/Compiler/perf/Microsoft.AspNetCore.Razor.Microbenchmarks.Generator/RazorTests.cs
index f16de93e7ac78..8f5f45c052c54 100644
--- a/src/Razor/src/Compiler/perf/Microsoft.AspNetCore.Razor.Microbenchmarks.Generator/RazorTests.cs
+++ b/src/Razor/src/Compiler/perf/Microsoft.AspNetCore.Razor.Microbenchmarks.Generator/RazorTests.cs
@@ -86,6 +86,28 @@ public void Razor_Edit_Independent()
Assert.Contains("Independent file ", results.Results[0].GeneratedSources.Single(r => r.HintName == "Pages_Generated_0_razor.g.cs").SourceText.ToString());
}
+ [Fact(Skip = "https://github.com/dotnet/razor/issues/7982")]
+ public void Razor_Edit_IndependentIgnorable()
+ {
+ // arrange
+ var razorBenchmarks = new RazorBenchmarks();
+ razorBenchmarks.Setup();
+
+ // check the contents of the generated 0 page
+ var initialResults = razorBenchmarks.Project!.GeneratorDriver.GetRunResult();
+ Assert.Contains("Page 0 ", initialResults.Results[0].GeneratedSources.Single(r => r.HintName == "Pages_Generated_0_razor.g.cs").SourceText.ToString());
+
+ // act
+ var driver = razorBenchmarks.Razor_Edit_IndependentIgnorable();
+
+ // assert: the markup body changed, but the @page route (declaration) is preserved, so this is an
+ // impl-only edit -- the counterpart to Razor_Edit_DependentIgnorable.
+ var results = driver.GetRunResult();
+ Assert.Empty(results.Diagnostics);
+ var page = results.Results[0].GeneratedSources.Single(r => r.HintName == "Pages_Generated_0_razor.g.cs").SourceText.ToString();
+ Assert.Contains("Independent file ", page);
+ }
+
[Fact(Skip = "https://github.com/dotnet/razor/issues/7982")]
public void Razor_Remove_Independent()
{