diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/DefaultDirectiveIRPass.cs b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultDirectiveIRPass.cs index ee2e6540b2..0c3b99fb19 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/DefaultDirectiveIRPass.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultDirectiveIRPass.cs @@ -10,21 +10,16 @@ namespace Microsoft.AspNetCore.Razor.Evolution { internal class DefaultDirectiveIRPass : RazorIRPassBase { - RazorParserOptions _parserOptions; + public override int Order => RazorIRPass.DefaultDirectiveClassifierOrder; - public override int Order => 150; - - protected override void OnIntialized(RazorCodeDocument codeDocument) + public override DocumentIRNode ExecuteCore(RazorCodeDocument codeDocument, DocumentIRNode irDocument) { var syntaxTree = codeDocument.GetSyntaxTree(); ThrowForMissingDocumentDependency(syntaxTree); - _parserOptions = syntaxTree.Options; - } + var parserOptions = syntaxTree.Options; - public override DocumentIRNode ExecuteCore(DocumentIRNode irDocument) - { - var designTime = _parserOptions.DesignTimeMode; + var designTime = parserOptions.DesignTimeMode; var walker = new DirectiveWalker(designTime); walker.VisitDocument(irDocument); diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/DefaultDocumentClassifier.cs b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultDocumentClassifier.cs new file mode 100644 index 0000000000..5e0118cec4 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultDocumentClassifier.cs @@ -0,0 +1,123 @@ +// 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.Collections.Generic; +using Microsoft.AspNetCore.Razor.Evolution.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Evolution +{ + internal class DefaultDocumentClassifier : RazorIRPassBase + { + public override int Order => RazorIRPass.DefaultDocumentClassifierOrder; + + public static string DocumentKind = "default"; + + public override DocumentIRNode ExecuteCore(RazorCodeDocument codeDocument, DocumentIRNode irDocument) + { + if (irDocument.DocumentKind != null) + { + return irDocument; + } + + irDocument.DocumentKind = DocumentKind; + + // Rewrite a use default namespace and class declaration. + var children = new List(irDocument.Children); + irDocument.Children.Clear(); + + var @namespace = new NamespaceDeclarationIRNode() + { + //Content = "GeneratedNamespace", + }; + + var @class = new ClassDeclarationIRNode() + { + //AccessModifier = "public", + //Name = "GeneratedClass", + }; + + var method = new RazorMethodDeclarationIRNode() + { + //AccessModifier = "public", + // Modifiers = new List() { "async" }, + //Name = "Execute", + //ReturnType = "Task", + }; + + var documentBuilder = RazorIRBuilder.Create(irDocument); + + var namespaceBuilder = RazorIRBuilder.Create(documentBuilder.Current); + namespaceBuilder.Push(@namespace); + + var classBuilder = RazorIRBuilder.Create(namespaceBuilder.Current); + classBuilder.Push(@class); + + var methodBuilder = RazorIRBuilder.Create(classBuilder.Current); + methodBuilder.Push(method); + + var visitor = new Visitor(documentBuilder, namespaceBuilder, classBuilder, methodBuilder); + + for (var i = 0; i < children.Count; i++) + { + visitor.Visit(children[i]); + } + + return irDocument; + } + + private class Visitor : RazorIRNodeVisitor + { + private readonly RazorIRBuilder _document; + private readonly RazorIRBuilder _namespace; + private readonly RazorIRBuilder _class; + private readonly RazorIRBuilder _method; + + public Visitor(RazorIRBuilder document, RazorIRBuilder @namespace, RazorIRBuilder @class, RazorIRBuilder method) + { + _document = document; + _namespace = @namespace; + _class = @class; + _method = method; + } + + public override void VisitChecksum(ChecksumIRNode node) + { + _document.Insert(0, node); + } + + public override void VisitUsingStatement(UsingStatementIRNode node) + { + _namespace.AddAfter(node); + } + + internal override void VisitDeclareTagHelperFields(DeclareTagHelperFieldsIRNode node) + { + _class.Insert(0, node); + } + + public override void VisitDefault(RazorIRNode node) + { + _method.Add(node); + } + } + + public void Foo() + { + //// For prettiness, let's insert the usings before the class declaration. + //var i = 0; + //for (; i < Namespace.Children.Count; i++) + //{ + // if (Namespace.Children[i] is ClassDeclarationIRNode) + // { + // break; + // } + //} + + //var @using = new UsingStatementIRNode() + //{ + // Content = namespaceImport, + // SourceRange = BuildSourceRangeFromNode(span), + //}; + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorIRLoweringPhase.cs b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorIRLoweringPhase.cs index 4ba64a7aaf..3af0b9543b 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorIRLoweringPhase.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorIRLoweringPhase.cs @@ -18,57 +18,51 @@ namespace Microsoft.AspNetCore.Razor.Evolution var visitor = new Visitor(codeDocument, syntaxTree.Options); + var i = 0; + var builder = visitor.Builder; + foreach (var namespaceImport in syntaxTree.Options.NamespaceImports) + { + if (visitor.Namespaces.Add(namespaceImport)) + { + var @using = new UsingStatementIRNode() + { + Content = namespaceImport, + }; + + builder.Insert(i++, @using); + } + } + + var checksum = ChecksumIRNode.Create(codeDocument.Source); + visitor.Builder.Insert(0, checksum); + visitor.VisitBlock(syntaxTree.Root); + var irDocument = (DocumentIRNode)visitor.Builder.Build(); codeDocument.SetIRDocument(irDocument); } private class Visitor : ParserVisitor { - private readonly Stack _builders; private readonly RazorParserOptions _options; private readonly RazorCodeDocument _codeDocument; + private DeclareTagHelperFieldsIRNode _tagHelperFields; + public Visitor(RazorCodeDocument codeDocument, RazorParserOptions options) { _codeDocument = codeDocument; _options = options; - _builders = new Stack(); - var document = RazorIRBuilder.Document(); - _builders.Push(document); - var checksum = ChecksumIRNode.Create(codeDocument.Source); - Builder.Add(checksum); + Namespaces = new HashSet(); - Namespace = new NamespaceDeclarationIRNode(); - Builder.Push(Namespace); - - foreach (var namespaceImport in options.NamespaceImports) - { - var @using = new UsingStatementIRNode() - { - Content = namespaceImport, - Parent = Namespace, - }; - - Builder.Add(@using); - } - - Class = new ClassDeclarationIRNode(); - Builder.Push(Class); - - Method = new RazorMethodDeclarationIRNode(); - Builder.Push(Method); + Builder = RazorIRBuilder.Document(); } - public RazorIRBuilder Builder => _builders.Peek(); + public RazorIRBuilder Builder { get; } - public NamespaceDeclarationIRNode Namespace { get; } - - public ClassDeclarationIRNode Class { get; } - - public RazorMethodDeclarationIRNode Method { get; } + public HashSet Namespaces { get; } // Example // @@ -254,31 +248,15 @@ namespace Microsoft.AspNetCore.Razor.Evolution { var namespaceImport = chunkGenerator.Namespace.Trim(); - if (_options.NamespaceImports.Contains(namespaceImport, StringComparer.Ordinal)) + // Track seen namespaces so we don't add duplicates from options. + if (Namespaces.Add(namespaceImport)) { - // Already added by default - - return; - } - - // For prettiness, let's insert the usings before the class declaration. - var i = 0; - for (; i < Namespace.Children.Count; i++) - { - if (Namespace.Children[i] is ClassDeclarationIRNode) + Builder.Add(new UsingStatementIRNode() { - break; - } + Content = namespaceImport, + SourceRange = BuildSourceRangeFromNode(span), + }); } - - var @using = new UsingStatementIRNode() - { - Content = namespaceImport, - Parent = Namespace, - SourceRange = BuildSourceRangeFromNode(span), - }; - - Namespace.Children.Insert(i, @using); } public override void VisitDirectiveToken(DirectiveTokenChunkGenerator chunkGenerator, Span span) @@ -355,19 +333,15 @@ namespace Microsoft.AspNetCore.Razor.Evolution private void DeclareTagHelperFields(TagHelperBlock block) { - var declareFieldsNode = Class.Children.OfType().SingleOrDefault(); - if (declareFieldsNode == null) + if (_tagHelperFields == null) { - declareFieldsNode = new DeclareTagHelperFieldsIRNode(); - declareFieldsNode.Parent = Class; - - var methodIndex = Class.Children.IndexOf(Method); - Class.Children.Insert(methodIndex, declareFieldsNode); + _tagHelperFields = new DeclareTagHelperFieldsIRNode(); + Builder.Add(_tagHelperFields); } foreach (var descriptor in block.Descriptors) { - declareFieldsNode.UsedTagHelperTypeNames.Add(descriptor.TypeName); + _tagHelperFields.UsedTagHelperTypeNames.Add(descriptor.TypeName); } } diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/DefaultRazorIRBuilder.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/DefaultRazorIRBuilder.cs index 05a8f332cd..f6658c2b06 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/DefaultRazorIRBuilder.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/DefaultRazorIRBuilder.cs @@ -31,8 +31,27 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate throw new ArgumentNullException(nameof(node)); } - Push(node); - Pop(); + node.Parent = Current; + Current.Children.Add(node); + } + + public override void Insert(int index, RazorIRNode node) + { + if (index < 0 || index - Current.Children.Count > 0) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + node.Parent = Current; + if (index == Current.Children.Count) + { + // Allow inserting at 'Children.Count' to be friendlier than List<> typically is. + Current.Children.Add(node); + } + else + { + Current.Children.Insert(index, node); + } } public override RazorIRNode Build() diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/DocumentIRNode.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/DocumentIRNode.cs index f4d9f72965..1ca50c814f 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/DocumentIRNode.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/DocumentIRNode.cs @@ -11,6 +11,8 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate { public override IList Children { get; } = new List(); + public string DocumentKind { get; set; } + public override RazorIRNode Parent { get; set; } internal override MappingLocation SourceRange { get; set; } diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/RazorIRBuilder.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/RazorIRBuilder.cs index 14cf934d04..2fd75eb23d 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/RazorIRBuilder.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/RazorIRBuilder.cs @@ -28,6 +28,8 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate public abstract void Add(RazorIRNode node); + public abstract void Insert(int index, RazorIRNode node); + public abstract RazorIRNode Build(); public abstract void Push(RazorIRNode node); diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/RazorIRBuilderExtensions.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/RazorIRBuilderExtensions.cs new file mode 100644 index 0000000000..4e26e209ec --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/RazorIRBuilderExtensions.cs @@ -0,0 +1,25 @@ +// 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. + +namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate +{ + public static class RazorIRBuilderExtensions + { + public static void AddAfter(this RazorIRBuilder builder, RazorIRNode node) + where TNode : RazorIRNode + { + var children = builder.Current.Children; + var i = children.Count - 1; + for (; i >= 0; i--) + { + var child = children[i]; + if (child is TNode || child.GetType() == node.GetType()) + { + break; + } + } + + builder.Insert(i + 1, node); + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/RazorDesignTimeIRPass.cs b/src/Microsoft.AspNetCore.Razor.Evolution/RazorDesignTimeIRPass.cs index edb0de9e12..ab14217498 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/RazorDesignTimeIRPass.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/RazorDesignTimeIRPass.cs @@ -12,9 +12,9 @@ namespace Microsoft.AspNetCore.Razor.Evolution { internal const string DesignTimeVariable = "__o"; - public override int Order => 25; + public override int Order => RazorIRPass.DirectiveClassifierOrder; - public override DocumentIRNode ExecuteCore(DocumentIRNode irDocument) + public override DocumentIRNode ExecuteCore(RazorCodeDocument codeDocument, DocumentIRNode irDocument) { var walker = new DesignTimeHelperWalker(); walker.VisitDocument(irDocument); diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngine.cs b/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngine.cs index 65577123b8..852713750a 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngine.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngine.cs @@ -59,6 +59,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution builder.Features.Add(new TagHelperBinderSyntaxTreePass()); // IR Passes + builder.Features.Add(new DefaultDocumentClassifier()); builder.Features.Add(new DefaultDirectiveIRPass()); } diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/RazorIRPass.cs b/src/Microsoft.AspNetCore.Razor.Evolution/RazorIRPass.cs new file mode 100644 index 0000000000..cd35d6ea92 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/RazorIRPass.cs @@ -0,0 +1,87 @@ +// 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 Microsoft.AspNetCore.Razor.Evolution.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Evolution +{ + /// + /// Provides constants for ordering of objects. When implementing an + /// , choose a value for according to + /// the logical task that must be performed. + /// + /// + /// + /// objects are executed according to an ascending ordering of the + /// property. The default configuration of + /// prescribes a logical ordering of specific phases of IR processing. + /// + /// + /// The IR document is first produced by . At this point no IR passes have + /// been executed. The default will perform a mechanical transformation + /// of the syntax tree to IR resulting in a mostly flat structure. It is up to later phases to give the document + /// structure and semantics according to a document kind. The default is + /// also responsible for synthesizing IR nodes for global cross-current concerns such as checksums or global settings. + /// + /// + /// The first phase of IR procesing is document classification. IR passes in this phase should classify the + /// document according to any relevant criteria (project configuration, file extension, directive) and modify + /// the IR tree to suit the desired document shape. Document classifiers should also set + /// to prevent other classifiers from running. If no classifier + /// matches the document, then it will be classified as "generic" and processed according to set + /// of reasonable defaults. + /// + /// + /// The second phase of IR processing is directive classification. IR passes in this phase should interpret + /// directives and processing them accordingly by transforming IR nodes or adding diagnostics to the IR. At + /// this time the document kind has been identified, so any directive that can't be applied should trigger + /// errors. If implementing a document kind that diverges from the standard structure of Razor documents + /// it may be necessary to reimplement processing of default directives. + /// + /// + /// The last phase of IR processing is lowering. IR passes in this phase perform some kind of transformation + /// on the IR that optimizes the generated code. The key distinction here is that information may be discarded + /// during this phase. + /// + /// + /// Finally, the transforms the IR document into generated C# code. + /// At this time any directives or IR constructs that cannot be understood by code generation will result + /// in an error. + /// + /// + public static class RazorIRPass + { + /// + /// An that implements a document classifier should use this value as its + /// . + /// + public static readonly int DocumentClassifierOrder = 1100; + + /// + /// value used by the default document classifier. + /// + public static readonly int DefaultDocumentClassifierOrder = 1900; + + /// + /// An that implements a directive classifier should use this value as its + /// . + /// + public static readonly int DirectiveClassifierOrder = 2100; + + /// + /// value used by the default directive classifier. + /// + public static readonly int DefaultDirectiveClassifierOrder = 2900; + + /// + /// An that implements a lowering phase should use this value as its + /// . + /// + public static readonly int LoweringOrder = 4100; + + /// + /// value used by the default lowering phase. + /// + public static readonly int DefaultLoweringOrder = 4900; + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/RazorIRPassBase.cs b/src/Microsoft.AspNetCore.Razor.Evolution/RazorIRPassBase.cs index aafc76dc18..6213773490 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/RazorIRPassBase.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/RazorIRPassBase.cs @@ -8,9 +8,24 @@ namespace Microsoft.AspNetCore.Razor.Evolution { internal abstract class RazorIRPassBase : IRazorIRPass { - public RazorEngine Engine { get; set; } + private RazorEngine _engine; - public virtual int Order => 0; + public RazorEngine Engine + { + get { return _engine; } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _engine = value; + OnIntialized(); + } + } + + public abstract int Order { get; } protected void ThrowForMissingDocumentDependency(TDocumentDependency value) { @@ -36,7 +51,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution } } - protected virtual void OnIntialized(RazorCodeDocument codeDocument) + protected virtual void OnIntialized() { } @@ -57,11 +72,9 @@ namespace Microsoft.AspNetCore.Razor.Evolution throw new InvalidOperationException(Resources.FormatPhaseMustBeInitialized(nameof(Engine))); } - OnIntialized(codeDocument); - - return ExecuteCore(irDocument); + return ExecuteCore(codeDocument, irDocument); } - public abstract DocumentIRNode ExecuteCore(DocumentIRNode irDocument); + public abstract DocumentIRNode ExecuteCore(RazorCodeDocument codeDocument, DocumentIRNode irDocument); } } diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultDirectiveIRPassTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultDirectiveIRPassTest.cs index cbdc895377..54456f8436 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultDirectiveIRPassTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultDirectiveIRPassTest.cs @@ -180,7 +180,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution { var phase = engine.Phases[i]; phase.Execute(codeDocument); - + if (phase is IRazorIRLoweringPhase) { break; @@ -189,6 +189,9 @@ namespace Microsoft.AspNetCore.Razor.Evolution var irDocument = codeDocument.GetIRDocument(); Assert.NotNull(irDocument); + + // These tests depend on the document->namespace->class structure. + irDocument = new DefaultDocumentClassifier() { Engine = engine, }.Execute(codeDocument, irDocument); return irDocument; } } diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultDocumentClassifierTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultDocumentClassifierTest.cs new file mode 100644 index 0000000000..ceb146320b --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultDocumentClassifierTest.cs @@ -0,0 +1,149 @@ +// 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 Microsoft.AspNetCore.Razor.Evolution.Intermediate; +using Xunit; +using static Microsoft.AspNetCore.Razor.Evolution.Intermediate.RazorIRAssert; + +namespace Microsoft.AspNetCore.Razor.Evolution +{ + public class DefaultDocumentClassifierTest + { + [Fact] + public void Execute_IgnoresDocumentsWithDocumentKind() + { + // Arrange + var irDocument = new DocumentIRNode() + { + DocumentKind = "ignore", + }; + + var pass = new DefaultDocumentClassifier(); + pass.Engine = RazorEngine.CreateEmpty(b => { }); + + // Act + pass.Execute(TestRazorCodeDocument.CreateEmpty(), irDocument); + + // Assert + Assert.Equal("ignore", irDocument.DocumentKind); + NoChildren(irDocument); + } + + [Fact] + public void Execute_CreatesClassStructure() + { + // Arrange + var irDocument = new DocumentIRNode(); + + var pass = new DefaultDocumentClassifier(); + pass.Engine = RazorEngine.CreateEmpty(b =>{ }); + + // Act + pass.Execute(TestRazorCodeDocument.CreateEmpty(), irDocument); + + // Assert + Assert.Equal(DefaultDocumentClassifier.DocumentKind, irDocument.DocumentKind); + + var @namespace = SingleChild(irDocument); + var @class = SingleChild(@namespace); + var method = SingleChild(@class); + NoChildren(method); + } + + [Fact] + public void Execute_AddsCheckumFirstToDocument() + { + // Arrange + var irDocument = new DocumentIRNode(); + + var builder = RazorIRBuilder.Create(irDocument); + builder.Add(new ChecksumIRNode()); + + var pass = new DefaultDocumentClassifier(); + pass.Engine = RazorEngine.CreateEmpty(b => { }); + + // Act + pass.Execute(TestRazorCodeDocument.CreateEmpty(), irDocument); + + // Assert + Children( + irDocument, + n => Assert.IsType(n), + n => Assert.IsType(n)); + } + + [Fact] + public void Execute_AddsUsingsToNamespace() + { + // Arrange + var irDocument = new DocumentIRNode(); + + var builder = RazorIRBuilder.Create(irDocument); + builder.Add(new UsingStatementIRNode()); + + var pass = new DefaultDocumentClassifier(); + pass.Engine = RazorEngine.CreateEmpty(b => { }); + + // Act + pass.Execute(TestRazorCodeDocument.CreateEmpty(), irDocument); + + // Assert + var @namespace = SingleChild(irDocument); + Children( + @namespace, + n => Assert.IsType(n), + n => Assert.IsType(n)); + } + + [Fact] + public void Execute_AddsTagHelperFieldsToClass() + { + // Arrange + var irDocument = new DocumentIRNode(); + + var builder = RazorIRBuilder.Create(irDocument); + builder.Add(new DeclareTagHelperFieldsIRNode()); + + var pass = new DefaultDocumentClassifier(); + pass.Engine = RazorEngine.CreateEmpty(b => { }); + + // Act + pass.Execute(TestRazorCodeDocument.CreateEmpty(), irDocument); + + // Assert + var @namespace = SingleChild(irDocument); + var @class = SingleChild(@namespace); + Children( + @class, + n => Assert.IsType(n), + n => Assert.IsType(n)); + } + + [Fact] + public void Execute_AddsTheRestToMethod() + { + // Arrange + var irDocument = new DocumentIRNode(); + + var builder = RazorIRBuilder.Create(irDocument); + builder.Add(new HtmlContentIRNode()); + builder.Add(new CSharpStatementIRNode()); + + var pass = new DefaultDocumentClassifier(); + pass.Engine = RazorEngine.CreateEmpty(b => { }); + + // Act + pass.Execute(TestRazorCodeDocument.CreateEmpty(), irDocument); + + // Assert + var @namespace = SingleChild(irDocument); + var @class = SingleChild(@namespace); + var method = SingleChild(@class); + Children( + method, + n => Assert.IsType(n), + n => Assert.IsType(n)); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/IntegrationTests/CodeGenerationIntegrationTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/IntegrationTests/CodeGenerationIntegrationTest.cs index bfc7389e00..6b18090638 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/IntegrationTests/CodeGenerationIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/IntegrationTests/CodeGenerationIntegrationTest.cs @@ -1466,7 +1466,9 @@ namespace Microsoft.AspNetCore.Razor.Evolution.IntegrationTests private class ApiSetsIRTestAdapter : RazorIRPassBase { - public override DocumentIRNode ExecuteCore(DocumentIRNode irDocument) + public override int Order => RazorIRPass.LoweringOrder; + + public override DocumentIRNode ExecuteCore(RazorCodeDocument codeDocument, DocumentIRNode irDocument) { var walker = new ApiSetsIRWalker(); walker.Visit(irDocument); diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/IntegrationTests/PageDocumentIntegrationTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/IntegrationTests/PageDocumentIntegrationTest.cs new file mode 100644 index 0000000000..4fa0feac51 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/IntegrationTests/PageDocumentIntegrationTest.cs @@ -0,0 +1,9 @@ +// 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. + +namespace Microsoft.AspNetCore.Razor.Evolution.IntegrationTests +{ + public class PageDocumentIntegrationTest + { + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Intermediate/DefaultRazorIRBuilderTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Intermediate/DefaultRazorIRBuilderTest.cs index 9c9333f7b0..626a15557d 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Intermediate/DefaultRazorIRBuilderTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Intermediate/DefaultRazorIRBuilderTest.cs @@ -107,7 +107,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate } [Fact] - public void Add_DoesPushAndPop() + public void Add_AddsToChildrenAndSetsParent() { // Arrange var builder = new DefaultRazorIRBuilder(); @@ -126,6 +126,72 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate Assert.Collection(parent.Children, n => Assert.Same(node, n)); } + [Fact] + public void Insert_AddsToChildrenAndSetsParent_EmptyCollection() + { + // Arrange + var builder = new DefaultRazorIRBuilder(); + + var parent = new BasicIRNode(); + builder.Push(parent); + + var node = new BasicIRNode(); + + // Act + builder.Insert(0, node); + + // Assert + Assert.Same(parent, builder.Current); + Assert.Same(parent, node.Parent); + Assert.Collection(parent.Children, n => Assert.Same(node, n)); + } + + [Fact] + public void Insert_AddsToChildrenAndSetsParent_NonEmpyCollection() + { + // Arrange + var builder = new DefaultRazorIRBuilder(); + + var parent = new BasicIRNode(); + builder.Push(parent); + + var child = new BasicIRNode(); + builder.Add(child); + + var node = new BasicIRNode(); + + // Act + builder.Insert(0, node); + + // Assert + Assert.Same(parent, builder.Current); + Assert.Same(parent, node.Parent); + Assert.Collection(parent.Children, n => Assert.Same(node, n), n => Assert.Same(child, n)); + } + + [Fact] + public void Insert_AddsToChildrenAndSetsParent_NonEmpyCollection_AtEnd() + { + // Arrange + var builder = new DefaultRazorIRBuilder(); + + var parent = new BasicIRNode(); + builder.Push(parent); + + var child = new BasicIRNode(); + builder.Add(child); + + var node = new BasicIRNode(); + + // Act + builder.Insert(1, node); + + // Assert + Assert.Same(parent, builder.Current); + Assert.Same(parent, node.Parent); + Assert.Collection(parent.Children, n => Assert.Same(child, n), n => Assert.Same(node, n)); + } + [Fact] public void Build_PopsMultipleLevels() { diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Intermediate/DefaultRazorIRLoweringPhaseIntegrationTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Intermediate/DefaultRazorIRLoweringPhaseIntegrationTest.cs index bdeb979369..568a3b648c 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Intermediate/DefaultRazorIRLoweringPhaseIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Intermediate/DefaultRazorIRLoweringPhaseIntegrationTest.cs @@ -1,20 +1,20 @@ // 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 static Microsoft.AspNetCore.Razor.Evolution.Intermediate.RazorIRAssert; -using Xunit; using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Evolution.Legacy; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Evolution.Legacy; +using Xunit; +using static Microsoft.AspNetCore.Razor.Evolution.Intermediate.RazorIRAssert; namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate { public class DefaultRazorIRLoweringPhaseIntegrationTest { [Fact] - public void Lower_EmptyDocument() + public void Lower_EmptyDocument_AddsGlobalUsingsAndNamespace() { // Arrange var codeDocument = TestRazorCodeDocument.CreateEmpty(); @@ -25,15 +25,8 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate // Assert Children(irDocument, n => Assert.IsType(n), - n => Assert.IsType(n)); - var @namespace = irDocument.Children[1]; - Children(@namespace, - n => Assert.IsType(n), - n => Assert.IsType(n), - n => Assert.IsType(n)); - var @class = @namespace.Children[2]; - var method = SingleChild(@class); - Assert.Empty(method.Children); + n => Using("System", n), + n => Using("System.Threading.Tasks", n)); } [Fact] @@ -48,17 +41,9 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate // Assert Children(irDocument, n => Assert.IsType(n), - n => Assert.IsType(n)); - var @namespace = irDocument.Children[1]; - Children(@namespace, n => Assert.IsType(n), n => Assert.IsType(n), - n => Assert.IsType(n)); - var @class = @namespace.Children[2]; - var method = SingleChild(@class); - var html = SingleChild(method); - - Assert.Equal("Hello, World!", html.Content); + n => Html("Hello, World!", n)); } [Fact] @@ -78,15 +63,8 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate // Assert Children(irDocument, n => Assert.IsType(n), - n => Assert.IsType(n)); - var @namespace = irDocument.Children[1]; - Children(@namespace, n => Assert.IsType(n), n => Assert.IsType(n), - n => Assert.IsType(n)); - var @class = @namespace.Children[2]; - var method = SingleChild(@class); - Children(method, n => Html( @" @@ -115,15 +93,8 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate // Assert Children(irDocument, n => Assert.IsType(n), - n => Assert.IsType(n)); - var @namespace = irDocument.Children[1]; - Children(@namespace, n => Assert.IsType(n), n => Assert.IsType(n), - n => Assert.IsType(n)); - var @class = @namespace.Children[2]; - var method = SingleChild(@class); - Children(method, n => Html( @" @@ -157,16 +128,12 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate // Assert Children(irDocument, n => Assert.IsType(n), - n => Assert.IsType(n)); - var @namespace = irDocument.Children[1]; - Children(@namespace, n => Assert.IsType(n), n => Assert.IsType(n), - n => Assert.IsType(n)); - var @class = @namespace.Children[2]; - Children(@class, - n => Assert.IsType(n), - n => Assert.IsType(n)); + n => Directive( + "functions", + n, + c => Assert.IsType(c))); } [Fact] @@ -181,12 +148,8 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate // Assert Children(irDocument, n => Assert.IsType(n), - n => Assert.IsType(n)); - var @namespace = irDocument.Children[1]; - Children(@namespace, n => Using("System", n), - n => Using(typeof(Task).Namespace, n), - n => Assert.IsType(n)); + n => Using(typeof(Task).Namespace, n)); } [Fact] @@ -209,28 +172,23 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate // Assert Children(irDocument, n => Assert.IsType(n), - n => Assert.IsType(n)); - var @namespace = irDocument.Children[1]; - Children(@namespace, n => Using("System", n), n => Using(typeof(Task).Namespace, n), - n => Assert.IsType(n)); - var @class = @namespace.Children[2]; - Children(@class, n => TagHelperFieldDeclaration(n, "SpanTagHelper"), - n => Assert.IsType(n)); - var method = @class.Children[1]; - var tagHelperNode = SingleChild(method); - Children(tagHelperNode, - n => TagHelperStructure("span", TagMode.StartTagAndEndTag, n), - n => Assert.IsType(n), - n => TagHelperHtmlAttribute( - "val", - HtmlAttributeValueStyle.DoubleQuotes, - n, - v => CSharpAttributeValue(string.Empty, "Hello", v), - v => LiteralAttributeValue(" ", "World", v)), - n => Assert.IsType(n)); + n => + { + var tagHelperNode = Assert.IsType(n); + Children(tagHelperNode, + c => TagHelperStructure("span", TagMode.StartTagAndEndTag, c), + c => Assert.IsType(c), + c => TagHelperHtmlAttribute( + "val", + HtmlAttributeValueStyle.DoubleQuotes, + c, + v => CSharpAttributeValue(string.Empty, "Hello", v), + v => LiteralAttributeValue(" ", "World", v)), + c => Assert.IsType(c)); + }); } [Fact] @@ -256,28 +214,23 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate // Assert Children(irDocument, n => Assert.IsType(n), - n => Assert.IsType(n)); - var @namespace = irDocument.Children[1]; - Children(@namespace, n => Using("System", n), n => Using(typeof(Task).Namespace, n), - n => Assert.IsType(n)); - var @class = @namespace.Children[2]; - Children(@class, n => TagHelperFieldDeclaration(n, "InputTagHelper"), - n => Assert.IsType(n)); - var method = @class.Children[1]; - var tagHelperNode = SingleChild(method); - Children(tagHelperNode, - n => TagHelperStructure("input", TagMode.SelfClosing, n), - n => Assert.IsType(n), - n => SetTagHelperProperty( - "bound", - "FooProp", - HtmlAttributeValueStyle.SingleQuotes, - n, - v => Html("foo", v)), - n => Assert.IsType(n)); + n => + { + var tagHelperNode = Assert.IsType(n); + Children(tagHelperNode, + c => TagHelperStructure("input", TagMode.SelfClosing, c), + c => Assert.IsType(c), + c => SetTagHelperProperty( + "bound", + "FooProp", + HtmlAttributeValueStyle.SingleQuotes, + c, + v => Html("foo", v)), + c => Assert.IsType(c)); + }); } private DocumentIRNode Lower(RazorCodeDocument codeDocument) @@ -287,15 +240,17 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate private DocumentIRNode Lower(RazorCodeDocument codeDocument, IEnumerable descriptors) { - var engine = RazorEngine.Create( - builder => builder.Features.Add(new TagHelperFeature(new TestTagHelperDescriptorResolver(descriptors)))); + var engine = RazorEngine.Create(builder => + { + builder.Features.Add(new TagHelperFeature(new TestTagHelperDescriptorResolver(descriptors))); + }); for (var i = 0; i < engine.Phases.Count; i++) { var phase = engine.Phases[i]; phase.Execute(codeDocument); - if (phase is IRazorIRPhase) + if (phase is IRazorIRLoweringPhase) { break; } @@ -303,6 +258,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate var irDocument = codeDocument.GetIRDocument(); Assert.NotNull(irDocument); + return irDocument; } diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Intermediate/RazorIRBuilderExtensionsTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Intermediate/RazorIRBuilderExtensionsTest.cs new file mode 100644 index 0000000000..617c5d96ab --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Intermediate/RazorIRBuilderExtensionsTest.cs @@ -0,0 +1,132 @@ +// 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 Microsoft.AspNetCore.Razor.Evolution.Legacy; +using Microsoft.AspNetCore.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate +{ + public class RazorIRBuilderExtensionsTest + { + [Fact] + public void AddAfter_EmptyList() + { + // Arrange + var builder = RazorIRBuilder.Document(); + + var node = new BasicIRNode3(); + + // Act + builder.AddAfter(node); + + // Assert + Assert.Collection(builder.Current.Children, n => Assert.Same(node, n)); + } + + [Fact] + public void AddAfter_AfterMatch() + { + // Arrange + var builder = RazorIRBuilder.Document(); + builder.Add(new BasicIRNode()); + builder.Add(new BasicIRNode()); + builder.Add(new BasicIRNode3()); + + var node = new BasicIRNode3(); + + // Act + builder.AddAfter(node); + + // Assert + Assert.Collection( + builder.Current.Children, + n => Assert.IsType(n), + n => Assert.IsType(n), + n => Assert.IsType(n), + n => Assert.Same(node, n)); + } + + [Fact] + public void AddAfter_AfterMatch_Noncontinuous() + { + // Arrange + var builder = RazorIRBuilder.Document(); + builder.Add(new BasicIRNode()); + builder.Add(new BasicIRNode2()); + builder.Add(new BasicIRNode()); + + var node = new BasicIRNode3(); + + // Act + builder.AddAfter(node); + + // Assert + Assert.Collection( + builder.Current.Children, + n => Assert.IsType(n), + n => Assert.IsType(n), + n => Assert.IsType(n), + n => Assert.Same(node, n)); + } + + private class BasicIRNode : RazorIRNode + { + public override IList Children { get; } = new List(); + + public override RazorIRNode Parent { get; set; } + + internal override MappingLocation SourceRange { get; set; } + + public override void Accept(RazorIRNodeVisitor visitor) + { + throw new NotImplementedException(); + } + + public override TResult Accept(RazorIRNodeVisitor visitor) + { + throw new NotImplementedException(); + } + } + + private class BasicIRNode2 : RazorIRNode + { + public override IList Children { get; } = new List(); + + public override RazorIRNode Parent { get; set; } + + internal override MappingLocation SourceRange { get; set; } + + public override void Accept(RazorIRNodeVisitor visitor) + { + throw new NotImplementedException(); + } + + public override TResult Accept(RazorIRNodeVisitor visitor) + { + throw new NotImplementedException(); + } + } + + private class BasicIRNode3 : RazorIRNode + { + public override IList Children { get; } = new List(); + + public override RazorIRNode Parent { get; set; } + + internal override MappingLocation SourceRange { get; set; } + + public override void Accept(RazorIRNodeVisitor visitor) + { + throw new NotImplementedException(); + } + + public override TResult Accept(RazorIRNodeVisitor visitor) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineTest.cs index 342efc7172..9de42b74ff 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineTest.cs @@ -139,6 +139,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution feature => Assert.IsType(feature), feature => Assert.IsType(feature), feature => Assert.IsType(feature), + feature => Assert.IsType(feature), feature => Assert.IsType(feature)); } @@ -160,6 +161,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution feature => Assert.IsType(feature), feature => Assert.IsType(feature), feature => Assert.IsType(feature), + feature => Assert.IsType(feature), feature => Assert.IsType(feature), feature => Assert.IsType(feature), feature => Assert.IsType(feature)); diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Imports_Runtime.codegen.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Imports_Runtime.codegen.cs index 6d1426d2ff..51e246f067 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Imports_Runtime.codegen.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Imports_Runtime.codegen.cs @@ -14,7 +14,8 @@ namespace Microsoft.AspNetCore.Razor.Evolution.IntegrationTests.TestFiles #pragma warning disable 1998 public async System.Threading.Tasks.Task ExecuteAsync() { - WriteLiteral("\r\n\r\n

Path\'s full type name is "); + WriteLiteral("\r\n"); + WriteLiteral("\r\n

Path\'s full type name is "); #line 9 "TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Imports.cshtml" Write(typeof(Path).FullName);