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

+
+

Line1

+ child body +
+ + @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() {