diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Properties/ViewComponentResources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Properties/ViewComponentResources.Designer.cs index 6813fb723b..514af37554 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Properties/ViewComponentResources.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Properties/ViewComponentResources.Designer.cs @@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions internal static class ViewComponentResources { private static readonly ResourceManager _resourceManager - = new ResourceManager("Microsoft.CodeAnalysis.Razor.ViewComponentResources", typeof(ViewComponentResources).GetTypeInfo().Assembly); + = new ResourceManager("Microsoft.AspNetCore.Mvc.Razor.Extensions.ViewComponentResources", typeof(ViewComponentResources).GetTypeInfo().Assembly); /// /// View component '{0}' must have exactly one public method named '{1}' or '{2}'. diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ViewComponentTagHelperPass.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ViewComponentTagHelperPass.cs index 950306dd97..a166dcebef 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ViewComponentTagHelperPass.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ViewComponentTagHelperPass.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -9,7 +8,6 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Intermediate; using Microsoft.AspNetCore.Razor.Language.Legacy; -using Microsoft.CodeAnalysis.Razor; namespace Microsoft.AspNetCore.Mvc.Razor.Extensions { @@ -37,9 +35,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions } } - foreach (var (node, parent) in visitor.CreateTagHelpers) + foreach (var (parent, node) in visitor.CreateTagHelpers) { - RewriteCreateNode(visitor.Namespace, visitor.Class, node, parent); + RewriteCreateNode(visitor.Namespace, visitor.Class, (CreateTagHelperIRNode)node, parent); } } @@ -230,7 +228,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions public NamespaceDeclarationIRNode Namespace { get; private set; } - public List<(CreateTagHelperIRNode node, RazorIRNode parent)> CreateTagHelpers { get; } = new List<(CreateTagHelperIRNode node, RazorIRNode parent)>(); + public List CreateTagHelpers { get; } = new List(); public Dictionary TagHelpers { get; } = new Dictionary(); @@ -243,7 +241,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions var vcName = tagHelper.Metadata[ViewComponentTagHelperDescriptorConventions.ViewComponentNameKey]; TagHelpers[vcName] = tagHelper; - CreateTagHelpers.Add((node, Parent)); + CreateTagHelpers.Add(new RazorIRNodeReference(Parent, node)); } } diff --git a/src/Microsoft.AspNetCore.Razor.Language/DefaultDirectiveIRPass.cs b/src/Microsoft.AspNetCore.Razor.Language/DefaultDirectiveIRPass.cs index 4968b75f09..e55430dac6 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/DefaultDirectiveIRPass.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/DefaultDirectiveIRPass.cs @@ -16,25 +16,28 @@ namespace Microsoft.AspNetCore.Razor.Language var parserOptions = irDocument.Options; var designTime = parserOptions.DesignTime; - var walker = new DirectiveWalker(); - walker.VisitDocument(irDocument); - var classNode = walker.ClassNode; - foreach (var (node, parent) in walker.FunctionsDirectiveNodes) + var classNode = irDocument.FindPrimaryClass(); + if (classNode == null) { - parent.Children.Remove(node); + return; + } - foreach (var child in node.Children.Except(node.Tokens)) + foreach (var functions in irDocument.FindDirectiveReferences(CSharpCodeParser.FunctionsDirectiveDescriptor)) + { + functions.Remove(); + + for (var i =0; i < functions.Node.Children.Count; i++) { - classNode.Children.Add(child); + classNode.Children.Add(functions.Node.Children[i]); } } - foreach (var (node, parent) in walker.InheritsDirectiveNodes.Reverse()) + foreach (var inherits in irDocument.FindDirectiveReferences(CSharpCodeParser.InheritsDirectiveDescriptor).Reverse()) { - parent.Children.Remove(node); + inherits.Remove(); - var token = node.Tokens.FirstOrDefault(); + var token = ((DirectiveIRNode)inherits.Node).Tokens.FirstOrDefault(); if (token != null) { classNode.BaseType = token.Content; @@ -42,74 +45,30 @@ namespace Microsoft.AspNetCore.Razor.Language } } - foreach (var (node, parent) in walker.SectionDirectiveNodes) + foreach (var section in irDocument.FindDirectiveReferences(CSharpCodeParser.SectionDirectiveDescriptor)) { - var sectionIndex = parent.Children.IndexOf(node); - parent.Children.Remove(node); - - var defineSectionEndStatement = new CSharpCodeIRNode(); - RazorIRBuilder.Create(defineSectionEndStatement) - .Add(new RazorIRToken() - { - Kind = RazorIRToken.TokenKind.CSharp, - Content = "});" - }); - - parent.Children.Insert(sectionIndex, defineSectionEndStatement); - - foreach (var child in node.Children.Except(node.Tokens).Reverse()) - { - parent.Children.Insert(sectionIndex, child); - } - var lambdaContent = designTime ? "__razor_section_writer" : string.Empty; - var sectionName = node.Tokens.FirstOrDefault()?.Content; - var defineSectionStartStatement = new CSharpCodeIRNode(); - RazorIRBuilder.Create(defineSectionStartStatement) - .Add(new RazorIRToken() - { - Kind = RazorIRToken.TokenKind.CSharp, - Content = $"DefineSection(\"{sectionName}\", async ({lambdaContent}) => {{" - }); + var sectionName = ((DirectiveIRNode)section.Node).Tokens.FirstOrDefault()?.Content; - parent.Children.Insert(sectionIndex, defineSectionStartStatement); - } - } - - private class DirectiveWalker : RazorIRNodeWalker - { - public ClassDeclarationIRNode ClassNode { get; private set; } - - public IList<(DirectiveIRNode node, RazorIRNode parent)> FunctionsDirectiveNodes { get; } = new List<(DirectiveIRNode node, RazorIRNode parent)>(); - - public IList<(DirectiveIRNode node, RazorIRNode parent)> InheritsDirectiveNodes { get; } = new List<(DirectiveIRNode node, RazorIRNode parent)>(); - - public IList<(DirectiveIRNode node, RazorIRNode parent)> SectionDirectiveNodes { get; } = new List<(DirectiveIRNode node, RazorIRNode parent)>(); - - public override void VisitClassDeclaration(ClassDeclarationIRNode node) - { - if (ClassNode == null) + var builder = RazorIRBuilder.Create(new CSharpCodeIRNode()); + builder.Add(new RazorIRToken() { - ClassNode = node; - } + Kind = RazorIRToken.TokenKind.CSharp, + Content = $"DefineSection(\"{sectionName}\", async ({lambdaContent}) => {{" + }); + section.InsertBefore(builder.Build()); + + section.InsertBefore(section.Node.Children.Except(((DirectiveIRNode)section.Node).Tokens)); - VisitDefault(node); - } + builder = RazorIRBuilder.Create(new CSharpCodeIRNode()); + builder.Add(new RazorIRToken() + { + Kind = RazorIRToken.TokenKind.CSharp, + Content = "});" + }); + section.InsertAfter(builder.Build()); - public override void VisitDirective(DirectiveIRNode node) - { - if (string.Equals(node.Name, CSharpCodeParser.FunctionsDirectiveDescriptor.Directive, StringComparison.Ordinal)) - { - FunctionsDirectiveNodes.Add((node, Parent)); - } - else if (string.Equals(node.Name, CSharpCodeParser.InheritsDirectiveDescriptor.Directive, StringComparison.Ordinal)) - { - InheritsDirectiveNodes.Add((node, Parent)); - } - else if (string.Equals(node.Name, CSharpCodeParser.SectionDirectiveDescriptor.Directive, StringComparison.Ordinal)) - { - SectionDirectiveNodes.Add((node, Parent)); - } + section.Remove(); } } } diff --git a/src/Microsoft.AspNetCore.Razor.Language/Intermediate/DocumentIRNodeExtensions.cs b/src/Microsoft.AspNetCore.Razor.Language/Intermediate/DocumentIRNodeExtensions.cs index 4bb770ec2e..b57a510bcc 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Intermediate/DocumentIRNodeExtensions.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Intermediate/DocumentIRNodeExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; namespace Microsoft.AspNetCore.Razor.Language.Intermediate { @@ -37,6 +38,23 @@ namespace Microsoft.AspNetCore.Razor.Language.Intermediate return FindWithAnnotation(node, CommonAnnotations.PrimaryNamespace); } + public static IReadOnlyList FindDirectiveReferences(this DocumentIRNode node, DirectiveDescriptor directive) + { + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + if (directive == null) + { + throw new ArgumentNullException(nameof(directive)); + } + + var visitor = new DirectiveVisitor(directive); + visitor.Visit(node); + return visitor.Directives; + } + private static T FindWithAnnotation(RazorIRNode node, object annotation) where T : RazorIRNode { if (node is T target && object.ReferenceEquals(target.Annotations[annotation], annotation)) @@ -55,5 +73,27 @@ namespace Microsoft.AspNetCore.Razor.Language.Intermediate return null; } + + private class DirectiveVisitor : RazorIRNodeWalker + { + private readonly DirectiveDescriptor _directive; + + public DirectiveVisitor(DirectiveDescriptor directive) + { + _directive = directive; + } + + public List Directives = new List(); + + public override void VisitDirective(DirectiveIRNode node) + { + if (_directive == node.Descriptor) + { + Directives.Add(new RazorIRNodeReference(Parent, node)); + } + + base.VisitDirective(node); + } + } } } diff --git a/src/Microsoft.AspNetCore.Razor.Language/Intermediate/RazorIRNodeReference.cs b/src/Microsoft.AspNetCore.Razor.Language/Intermediate/RazorIRNodeReference.cs new file mode 100644 index 0000000000..7bf4e04a50 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Language/Intermediate/RazorIRNodeReference.cs @@ -0,0 +1,209 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Razor.Language.Intermediate +{ + public struct RazorIRNodeReference + { + public RazorIRNodeReference(RazorIRNode parent, RazorIRNode node) + { + if (parent == null) + { + throw new ArgumentNullException(nameof(parent)); + } + + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + Parent = parent; + Node = node; + } + + public void Deconstruct(out RazorIRNode parent, out RazorIRNode node) + { + parent = Parent; + node = Node; + } + + public RazorIRNode Node { get; } + + public RazorIRNode Parent { get; } + + public RazorIRNodeReference InsertAfter(RazorIRNode node) + { + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + if (Parent == null) + { + throw new InvalidOperationException(Resources.IRNodeReference_NotInitialized); + } + + if (Parent.Children.IsReadOnly) + { + throw new InvalidOperationException(Resources.FormatIRNodeReference_CollectionIsReadOnly(Parent)); + } + + var index = Parent.Children.IndexOf(Node); + if (index == -1) + { + throw new InvalidOperationException(Resources.FormatIRNodeReference_NodeNotFound( + Node, + Parent)); + } + + Parent.Children.Insert(index + 1, node); + return new RazorIRNodeReference(Parent, node); + } + + public void InsertAfter(IEnumerable nodes) + { + if (nodes == null) + { + throw new ArgumentNullException(nameof(nodes)); + } + + if (Parent == null) + { + throw new InvalidOperationException(Resources.IRNodeReference_NotInitialized); + } + + if (Parent.Children.IsReadOnly) + { + throw new InvalidOperationException(Resources.FormatIRNodeReference_CollectionIsReadOnly(Parent)); + } + + var index = Parent.Children.IndexOf(Node); + if (index == -1) + { + throw new InvalidOperationException(Resources.FormatIRNodeReference_NodeNotFound( + Node, + Parent)); + } + + foreach (var node in nodes) + { + Parent.Children.Insert(++index, node); + } + } + + public RazorIRNodeReference InsertBefore(RazorIRNode node) + { + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + if (Parent == null) + { + throw new InvalidOperationException(Resources.IRNodeReference_NotInitialized); + } + + if (Parent.Children.IsReadOnly) + { + throw new InvalidOperationException(Resources.FormatIRNodeReference_CollectionIsReadOnly(Parent)); + } + + var index = Parent.Children.IndexOf(Node); + if (index == -1) + { + throw new InvalidOperationException(Resources.FormatIRNodeReference_NodeNotFound( + Node, + Parent)); + } + + Parent.Children.Insert(index, node); + return new RazorIRNodeReference(Parent, node); + } + + public void InsertBefore(IEnumerable nodes) + { + if (nodes == null) + { + throw new ArgumentNullException(nameof(nodes)); + } + + if (Parent == null) + { + throw new InvalidOperationException(Resources.IRNodeReference_NotInitialized); + } + + if (Parent.Children.IsReadOnly) + { + throw new InvalidOperationException(Resources.FormatIRNodeReference_CollectionIsReadOnly(Parent)); + } + + var index = Parent.Children.IndexOf(Node); + if (index == -1) + { + throw new InvalidOperationException(Resources.FormatIRNodeReference_NodeNotFound( + Node, + Parent)); + } + + foreach (var node in nodes) + { + Parent.Children.Insert(index++, node); + } + } + + public void Remove() + { + if (Parent == null) + { + throw new InvalidOperationException(Resources.IRNodeReference_NotInitialized); + } + + if (Parent.Children.IsReadOnly) + { + throw new InvalidOperationException(Resources.FormatIRNodeReference_CollectionIsReadOnly(Parent)); + } + + var index = Parent.Children.IndexOf(Node); + if (index == -1) + { + throw new InvalidOperationException(Resources.FormatIRNodeReference_NodeNotFound( + Node, + Parent)); + } + + Parent.Children.RemoveAt(index); + } + + public RazorIRNodeReference Replace(RazorIRNode node) + { + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + if (Parent == null) + { + throw new InvalidOperationException(Resources.IRNodeReference_NotInitialized); + } + + if (Parent.Children.IsReadOnly) + { + throw new InvalidOperationException(Resources.FormatIRNodeReference_CollectionIsReadOnly(Parent)); + } + + var index = Parent.Children.IndexOf(Node); + if (index == -1) + { + throw new InvalidOperationException(Resources.FormatIRNodeReference_NodeNotFound( + Node, + Parent)); + } + + Parent.Children[index] = node; + return new RazorIRNodeReference(Parent, node); + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs index 28940691d1..907fec1eeb 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs @@ -458,6 +458,48 @@ namespace Microsoft.AspNetCore.Razor.Language internal static string FormatKeyMustNotBeNull() => GetString("KeyMustNotBeNull"); + /// + /// The reference is invalid. The node '{0}' could not be found as a child of '{1}'. + /// + internal static string IRNodeReference_NodeNotFound + { + get => GetString("IRNodeReference_NodeNotFound"); + } + + /// + /// The reference is invalid. The node '{0}' could not be found as a child of '{1}'. + /// + internal static string FormatIRNodeReference_NodeNotFound(object p0, object p1) + => string.Format(CultureInfo.CurrentCulture, GetString("IRNodeReference_NodeNotFound"), p0, p1); + + /// + /// The reference is invalid. References initialized with the default constructor cannot modify nodes. + /// + internal static string IRNodeReference_NotInitialized + { + get => GetString("IRNodeReference_NotInitialized"); + } + + /// + /// The reference is invalid. References initialized with the default constructor cannot modify nodes. + /// + internal static string FormatIRNodeReference_NotInitialized() + => GetString("IRNodeReference_NotInitialized"); + + /// + /// The node '{0}' has a read-only child collection and cannot be modified. + /// + internal static string IRNodeReference_CollectionIsReadOnly + { + get => GetString("IRNodeReference_CollectionIsReadOnly"); + } + + /// + /// The node '{0}' has a read-only child collection and cannot be modified. + /// + internal static string FormatIRNodeReference_CollectionIsReadOnly(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("IRNodeReference_CollectionIsReadOnly"), p0); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Razor.Language/Resources.resx b/src/Microsoft.AspNetCore.Razor.Language/Resources.resx index 610215c0f5..3f490ae2f9 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Resources.resx +++ b/src/Microsoft.AspNetCore.Razor.Language/Resources.resx @@ -213,4 +213,13 @@ The key must not be null. + + The reference is invalid. The node '{0}' could not be found as a child of '{1}'. + + + The reference is invalid. References initialized with the default constructor cannot modify nodes. + + + The node '{0}' has a read-only child collection and cannot be modified. + \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Intermediate/DocumentIRNodeExtensionsTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/Intermediate/DocumentIRNodeExtensionsTest.cs index 04ae620a49..70e2d35c95 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/Intermediate/DocumentIRNodeExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/Intermediate/DocumentIRNodeExtensionsTest.cs @@ -60,5 +60,54 @@ namespace Microsoft.AspNetCore.Razor.Language.Intermediate // Assert Assert.Same(@namespace, result); } + + [Fact] + public void FindDirectiveReferences_FindsMatchingDirectives() + { + // Arrange + var directive = DirectiveDescriptor.CreateSingleLineDirective("test"); + var directive2 = DirectiveDescriptor.CreateSingleLineDirective("test"); + + var document = new DocumentIRNode(); + var @namespace = new NamespaceDeclarationIRNode(); + + var builder = RazorIRBuilder.Create(document); + builder.Push(@namespace); + + var match1 = new DirectiveIRNode() + { + Descriptor = directive, + }; + builder.Add(match1); + + var nonMatch = new DirectiveIRNode() + { + Descriptor = directive2, + }; + builder.Add(nonMatch); + + var match2 = new DirectiveIRNode() + { + Descriptor = directive, + }; + builder.Add(match2); + + // Act + var results = document.FindDirectiveReferences(directive); + + // Assert + Assert.Collection( + results, + r => + { + Assert.Same(@namespace, r.Parent); + Assert.Same(match1, r.Node); + }, + r => + { + Assert.Same(@namespace, r.Parent); + Assert.Same(match2, r.Node); + }); + } } } diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Intermediate/RazorIRNodeReferenceTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/Intermediate/RazorIRNodeReferenceTest.cs new file mode 100644 index 0000000000..997bca8ffa --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/Intermediate/RazorIRNodeReferenceTest.cs @@ -0,0 +1,514 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Language.Intermediate +{ + public class RazorIRNodeReferenceTest + { + [Fact] + public void InsertAfter_SingleNode_AddsNodeAfterNode() + { + // Arrange + var parent = new BasicIRNode("Parent"); + + var node1 = new BasicIRNode("Node1"); + var node2 = new BasicIRNode("Node2"); + var node3 = new BasicIRNode("Node3"); + + parent.Children.Add(node1); + parent.Children.Add(node3); + + var reference = new RazorIRNodeReference(parent, node1); + + // Act + reference.InsertAfter(node2); + + // Assert + Assert.Equal(new[] { node1, node2, node3, }, parent.Children); + } + + [Fact] + public void InsertAfter_SingleNode_AddsNodeAfterNode_AtEnd() + { + // Arrange + var parent = new BasicIRNode("Parent"); + + var node1 = new BasicIRNode("Node1"); + var node2 = new BasicIRNode("Node2"); + var node3 = new BasicIRNode("Node3"); + + parent.Children.Add(node1); + parent.Children.Add(node2); + + var reference = new RazorIRNodeReference(parent, node2); + + // Act + reference.InsertAfter(node3); + + // Assert + Assert.Equal(new[] { node1, node2, node3, }, parent.Children); + } + + [Fact] + public void InsertAfter_MultipleNodes_AddsNodesAfterNode() + { + // Arrange + var parent = new BasicIRNode("Parent"); + + var node1 = new BasicIRNode("Node1"); + var node2 = new BasicIRNode("Node2"); + var node3 = new BasicIRNode("Node3"); + var node4 = new BasicIRNode("Node4"); + + parent.Children.Add(node1); + parent.Children.Add(node4); + + var reference = new RazorIRNodeReference(parent, node1); + + // Act + reference.InsertAfter(new[] { node2, node3 }); + + // Assert + Assert.Equal(new[] { node1, node2, node3, node4, }, parent.Children); + } + + [Fact] + public void InsertAfter_MultipleNodes_AddsNodesAfterNode_AtEnd() + { + // Arrange + var parent = new BasicIRNode("Parent"); + + var node1 = new BasicIRNode("Node1"); + var node2 = new BasicIRNode("Node2"); + var node3 = new BasicIRNode("Node3"); + var node4 = new BasicIRNode("Node4"); + + parent.Children.Add(node1); + parent.Children.Add(node2); + + var reference = new RazorIRNodeReference(parent, node2); + + // Act + reference.InsertAfter(new[] { node3, node4 }); + + // Assert + Assert.Equal(new[] { node1, node2, node3, node4, }, parent.Children); + } + + [Fact] + public void InsertBefore_SingleNode_AddsNodeBeforeNode() + { + // Arrange + var parent = new BasicIRNode("Parent"); + + var node1 = new BasicIRNode("Node1"); + var node2 = new BasicIRNode("Node2"); + var node3 = new BasicIRNode("Node3"); + + parent.Children.Add(node1); + parent.Children.Add(node3); + + var reference = new RazorIRNodeReference(parent, node3); + + // Act + reference.InsertBefore(node2); + + // Assert + Assert.Equal(new[] { node1, node2, node3, }, parent.Children); + } + + [Fact] + public void InsertBefore_SingleNode_AddsNodeBeforeNode_AtBeginning() + { + // Arrange + var parent = new BasicIRNode("Parent"); + + var node1 = new BasicIRNode("Node1"); + var node2 = new BasicIRNode("Node2"); + var node3 = new BasicIRNode("Node3"); + + parent.Children.Add(node2); + parent.Children.Add(node3); + + var reference = new RazorIRNodeReference(parent, node2); + + // Act + reference.InsertBefore(node1); + + // Assert + Assert.Equal(new[] { node1, node2, node3, }, parent.Children); + } + + [Fact] + public void InsertBefore_MultipleNodes_AddsNodesBeforeNode() + { + // Arrange + var parent = new BasicIRNode("Parent"); + + var node1 = new BasicIRNode("Node1"); + var node2 = new BasicIRNode("Node2"); + var node3 = new BasicIRNode("Node3"); + var node4 = new BasicIRNode("Node4"); + + parent.Children.Add(node1); + parent.Children.Add(node4); + + var reference = new RazorIRNodeReference(parent, node4); + + // Act + reference.InsertBefore(new[] { node2, node3 }); + + // Assert + Assert.Equal(new[] { node1, node2, node3, node4, }, parent.Children); + } + + [Fact] + public void InsertAfter_MultipleNodes_AddsNodesBeforeNode_AtBeginning() + { + // Arrange + var parent = new BasicIRNode("Parent"); + + var node1 = new BasicIRNode("Node1"); + var node2 = new BasicIRNode("Node2"); + var node3 = new BasicIRNode("Node3"); + var node4 = new BasicIRNode("Node4"); + + parent.Children.Add(node3); + parent.Children.Add(node4); + + var reference = new RazorIRNodeReference(parent, node3); + + // Act + reference.InsertBefore(new[] { node1, node2 }); + + // Assert + Assert.Equal(new[] { node1, node2, node3, node4, }, parent.Children); + } + + [Fact] + public void Remove_RemovesNode() + { + // Arrange + var parent = new BasicIRNode("Parent"); + + var node1 = new BasicIRNode("Node1"); + var node2 = new BasicIRNode("Node2"); + var node3 = new BasicIRNode("Node3"); + + parent.Children.Add(node1); + parent.Children.Add(node3); + parent.Children.Add(node2); + + var reference = new RazorIRNodeReference(parent, node3); + + // Act + reference.Remove(); + + // Assert + Assert.Equal(new[] { node1, node2,}, parent.Children); + } + + [Fact] + public void Replace_ReplacesNode() + { + // Arrange + var parent = new BasicIRNode("Parent"); + + var node1 = new BasicIRNode("Node1"); + var node2 = new BasicIRNode("Node2"); + var node3 = new BasicIRNode("Node3"); + var node4 = new BasicIRNode("Node4"); + + parent.Children.Add(node1); + parent.Children.Add(node4); + parent.Children.Add(node3); + + var reference = new RazorIRNodeReference(parent, node4); + + // Act + reference.Replace(node2); + + // Assert + Assert.Equal(new[] { node1, node2, node3, }, parent.Children); + } + + [Fact] + public void InsertAfter_SingleNode_ThrowsForReferenceNotInitialized() + { + // Arrange + var reference = new RazorIRNodeReference(); + + // Act & Assert + var exception = Assert.Throws(() => reference.InsertAfter(new BasicIRNode("_"))); + Assert.Equal("The reference is invalid. References initialized with the default constructor cannot modify nodes.", exception.Message); + } + + [Fact] + public void InsertAfter_MulipleNodes_ThrowsForReferenceNotInitialized() + { + // Arrange + var reference = new RazorIRNodeReference(); + + // Act & Assert + var exception = Assert.Throws(() => reference.InsertAfter(new[] { new BasicIRNode("_") })); + Assert.Equal("The reference is invalid. References initialized with the default constructor cannot modify nodes.", exception.Message); + } + + [Fact] + public void InsertBefore_SingleNode_ThrowsForReferenceNotInitialized() + { + // Arrange + var reference = new RazorIRNodeReference(); + + // Act & Assert + var exception = Assert.Throws(() => reference.InsertBefore(new BasicIRNode("_"))); + Assert.Equal("The reference is invalid. References initialized with the default constructor cannot modify nodes.", exception.Message); + } + + [Fact] + public void InsertBefore_MulipleNodes_ThrowsForReferenceNotInitialized() + { + // Arrange + var reference = new RazorIRNodeReference(); + + // Act & Assert + var exception = Assert.Throws(() => reference.InsertBefore(new[] { new BasicIRNode("_") })); + Assert.Equal("The reference is invalid. References initialized with the default constructor cannot modify nodes.", exception.Message); + } + + [Fact] + public void Remove_ThrowsForReferenceNotInitialized() + { + // Arrange + var reference = new RazorIRNodeReference(); + + // Act & Assert + var exception = Assert.Throws(() => reference.Remove()); + Assert.Equal("The reference is invalid. References initialized with the default constructor cannot modify nodes.", exception.Message); + } + + [Fact] + public void Replace_ThrowsForReferenceNotInitialized() + { + // Arrange + var reference = new RazorIRNodeReference(); + + // Act & Assert + var exception = Assert.Throws(() => reference.Replace(new BasicIRNode("_"))); + Assert.Equal("The reference is invalid. References initialized with the default constructor cannot modify nodes.", exception.Message); + } + + [Fact] + public void InsertAfter_SingleNode_ThrowsForReadOnlyCollection() + { + // Arrange + var parent = new BasicIRNode("Parent", ReadOnlyIRNodeCollection.Instance); + + var node1 = new BasicIRNode("Node1"); + + var reference = new RazorIRNodeReference(parent, node1); + + // Act & Assert + var exception = Assert.Throws(() => reference.InsertAfter(new BasicIRNode("_"))); + Assert.Equal("The node 'Parent' has a read-only child collection and cannot be modified.", exception.Message); + } + + [Fact] + public void InsertAfter_MulipleNodes_ThrowsForReadOnlyCollection() + { + // Arrange + var parent = new BasicIRNode("Parent", ReadOnlyIRNodeCollection.Instance); + + var node1 = new BasicIRNode("Node1"); + + var reference = new RazorIRNodeReference(parent, node1); + + // Act & Assert + var exception = Assert.Throws(() => reference.InsertAfter(new[] { new BasicIRNode("_") })); + Assert.Equal("The node 'Parent' has a read-only child collection and cannot be modified.", exception.Message); + } + + [Fact] + public void InsertBefore_SingleNode_ThrowsForReadOnlyCollection() + { + // Arrange + var parent = new BasicIRNode("Parent", ReadOnlyIRNodeCollection.Instance); + + var node1 = new BasicIRNode("Node1"); + + var reference = new RazorIRNodeReference(parent, node1); + + // Act & Assert + var exception = Assert.Throws(() => reference.InsertBefore(new BasicIRNode("_"))); + Assert.Equal("The node 'Parent' has a read-only child collection and cannot be modified.", exception.Message); + } + + [Fact] + public void InsertBefore_MulipleNodes_ThrowsForReadOnlyCollection() + { + // Arrange + var parent = new BasicIRNode("Parent", ReadOnlyIRNodeCollection.Instance); + + var node1 = new BasicIRNode("Node1"); + + var reference = new RazorIRNodeReference(parent, node1); + + // Act & Assert + var exception = Assert.Throws(() => reference.InsertBefore(new[] { new BasicIRNode("_") })); + Assert.Equal("The node 'Parent' has a read-only child collection and cannot be modified.", exception.Message); + } + + [Fact] + public void Remove_ThrowsForReadOnlyCollection() + { + // Arrange + var parent = new BasicIRNode("Parent", ReadOnlyIRNodeCollection.Instance); + + var node1 = new BasicIRNode("Node1"); + + var reference = new RazorIRNodeReference(parent, node1); + + // Act & Assert + var exception = Assert.Throws(() => reference.Remove()); + Assert.Equal("The node 'Parent' has a read-only child collection and cannot be modified.", exception.Message); + } + + [Fact] + public void Replace_ThrowsForReadOnlyCollection() + { + // Arrange + var parent = new BasicIRNode("Parent", ReadOnlyIRNodeCollection.Instance); + + var node1 = new BasicIRNode("Node1"); + + var reference = new RazorIRNodeReference(parent, node1); + + // Act & Assert + var exception = Assert.Throws(() => reference.Replace(new BasicIRNode("_"))); + Assert.Equal("The node 'Parent' has a read-only child collection and cannot be modified.", exception.Message); + } + + [Fact] + public void InsertAfter_SingleNode_ThrowsForNodeNotFound() + { + // Arrange + var parent = new BasicIRNode("Parent"); + + var node1 = new BasicIRNode("Node1"); + + var reference = new RazorIRNodeReference(parent, node1); + + // Act & Assert + var exception = Assert.Throws(() => reference.InsertAfter(new BasicIRNode("_"))); + Assert.Equal("The reference is invalid. The node 'Node1' could not be found as a child of 'Parent'.", exception.Message); + } + + [Fact] + public void InsertAfter_MulipleNodes_ThrowsForNodeNotFound() + { + // Arrange + var parent = new BasicIRNode("Parent"); + + var node1 = new BasicIRNode("Node1"); + + var reference = new RazorIRNodeReference(parent, node1); + + // Act & Assert + var exception = Assert.Throws(() => reference.InsertAfter(new[] { new BasicIRNode("_") })); + Assert.Equal("The reference is invalid. The node 'Node1' could not be found as a child of 'Parent'.", exception.Message); + } + + [Fact] + public void InsertBefore_SingleNode_ThrowsForNodeNotFound() + { + // Arrange + var parent = new BasicIRNode("Parent"); + + var node1 = new BasicIRNode("Node1"); + + var reference = new RazorIRNodeReference(parent, node1); + + // Act & Assert + var exception = Assert.Throws(() => reference.InsertBefore(new BasicIRNode("_"))); + Assert.Equal("The reference is invalid. The node 'Node1' could not be found as a child of 'Parent'.", exception.Message); + } + + [Fact] + public void InsertBefore_MulipleNodes_ThrowsForNodeNotFound() + { + // Arrange + var parent = new BasicIRNode("Parent"); + + var node1 = new BasicIRNode("Node1"); + + var reference = new RazorIRNodeReference(parent, node1); + + // Act & Assert + var exception = Assert.Throws(() => reference.InsertBefore(new[] { new BasicIRNode("_") })); + Assert.Equal("The reference is invalid. The node 'Node1' could not be found as a child of 'Parent'.", exception.Message); + } + + [Fact] + public void Remove_ThrowsForNodeNotFound() + { + // Arrange + var parent = new BasicIRNode("Parent"); + + var node1 = new BasicIRNode("Node1"); + + var reference = new RazorIRNodeReference(parent, node1); + + // Act & Assert + var exception = Assert.Throws(() => reference.Remove()); + Assert.Equal("The reference is invalid. The node 'Node1' could not be found as a child of 'Parent'.", exception.Message); + } + + [Fact] + public void Replace_ThrowsForNodeNotFound() + { + // Arrange + var parent = new BasicIRNode("Parent"); + + var node1 = new BasicIRNode("Node1"); + + var reference = new RazorIRNodeReference(parent, node1); + + // Act & Assert + var exception = Assert.Throws(() => reference.Replace(new BasicIRNode("_"))); + Assert.Equal("The reference is invalid. The node 'Node1' could not be found as a child of 'Parent'.", exception.Message); + } + + private class BasicIRNode : RazorIRNode + { + public BasicIRNode(string name) + : this(name, new DefaultIRNodeCollection()) + { + Name = name; + } + + public BasicIRNode(string name, RazorIRNodeCollection children) + { + Name = name; + Children = children; + } + + public string Name { get; } + + public override ItemCollection Annotations { get; } = new DefaultItemCollection(); + + public override RazorIRNodeCollection Children { get; } + + public override SourceSpan? Source { get; set; } + + public override void Accept(RazorIRNodeVisitor visitor) + { + throw new System.NotImplementedException(); + } + + public override string ToString() => Name; + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Intermediate/RazorIRNodeWalkerTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/Intermediate/RazorIRNodeWalkerTest.cs index 3d143571da..4691cf55e0 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/Intermediate/RazorIRNodeWalkerTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/Intermediate/RazorIRNodeWalkerTest.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using Xunit; namespace Microsoft.AspNetCore.Razor.Language.Intermediate @@ -43,14 +44,68 @@ namespace Microsoft.AspNetCore.Razor.Language.Intermediate Assert.Equal(nodes, walker.Visited.ToArray()); } + [Fact] + public void IRNodeWalker_Visit_SetsParentAndAncestors() + { + // Arrange + var walker = new DerivedIRNodeWalker(); + + var nodes = new RazorIRNode[] + { + new BasicIRNode("Root"), + new BasicIRNode("Root->A"), + new BasicIRNode("Root->B"), + new BasicIRNode("Root->B->1"), + new BasicIRNode("Root->B->2"), + new BasicIRNode("Root->C"), + }; + + var ancestors = new Dictionary() + { + { "Root", new string[]{ } }, + { "Root->A", new string[] { "Root" } }, + { "Root->B", new string[] { "Root" } }, + { "Root->B->1", new string[] { "Root->B", "Root" } }, + { "Root->B->2", new string[] { "Root->B", "Root" } }, + { "Root->C", new string[] { "Root" } }, + }; + + walker.OnVisiting = (n) => + { + Assert.Equal(ancestors[((BasicIRNode)n).Name], walker.Ancestors.Cast().Select(b => b.Name)); + Assert.Equal(ancestors[((BasicIRNode)n).Name].FirstOrDefault(), ((BasicIRNode)walker.Parent)?.Name); + }; + + var builder = new DefaultRazorIRBuilder(); + builder.Push(nodes[0]); + builder.Add(nodes[1]); + builder.Push(nodes[2]); + builder.Add(nodes[3]); + builder.Add(nodes[4]); + builder.Pop(); + builder.Add(nodes[5]); + + var root = builder.Pop(); + + // Act & Assert + walker.Visit(root); + } + private class DerivedIRNodeWalker : RazorIRNodeWalker { + public new IEnumerable Ancestors => base.Ancestors; + + public new RazorIRNode Parent => base.Parent; + public List Visited { get; } = new List(); + public Action OnVisiting { get; set; } + public override void VisitDefault(RazorIRNode node) { Visited.Add(node); + OnVisiting?.Invoke(node); base.VisitDefault(node); } @@ -60,7 +115,6 @@ namespace Microsoft.AspNetCore.Razor.Language.Intermediate } } - private class BasicIRNode : RazorIRNode { public BasicIRNode(string name)