From e84bc667003da89ff8f137387589ffe42157889d Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Thu, 29 Dec 2016 22:15:28 -0800 Subject: [PATCH] First cut of adding API sets This change defines stages for IR processing. The comments in RazorIRPass really explain the details. I've also made the preliminary changes to the stuff we've built so far to follow the new conventions. This is building towards multitargeting for Razor, being able to target both Razor Pages and Razor MVC Views from the same engine, being able to target different codegen and methods from within the same engine. --- .../DefaultDirectiveIRPass.cs | 13 +- .../DefaultDocumentClassifier.cs | 123 +++++++++++++++ .../DefaultRazorIRLoweringPhase.cs | 96 ++++------- .../Intermediate/DefaultRazorIRBuilder.cs | 23 ++- .../Intermediate/DocumentIRNode.cs | 2 + .../Intermediate/RazorIRBuilder.cs | 2 + .../Intermediate/RazorIRBuilderExtensions.cs | 25 +++ .../RazorDesignTimeIRPass.cs | 4 +- .../RazorEngine.cs | 1 + .../RazorIRPass.cs | 87 ++++++++++ .../RazorIRPassBase.cs | 27 +++- .../DefaultDirectiveIRPassTest.cs | 5 +- .../DefaultDocumentClassifierTest.cs | 149 ++++++++++++++++++ .../CodeGenerationIntegrationTest.cs | 4 +- .../PageDocumentIntegrationTest.cs | 9 ++ .../Intermediate/DefaultRazorIRBuilderTest.cs | 68 +++++++- ...aultRazorIRLoweringPhaseIntegrationTest.cs | 138 ++++++---------- .../RazorIRBuilderExtensionsTest.cs | 132 ++++++++++++++++ .../RazorEngineTest.cs | 2 + .../Imports_Runtime.codegen.cs | 3 +- 20 files changed, 737 insertions(+), 176 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/DefaultDocumentClassifier.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/RazorIRBuilderExtensions.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/RazorIRPass.cs create mode 100644 test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultDocumentClassifierTest.cs create mode 100644 test/Microsoft.AspNetCore.Razor.Evolution.Test/IntegrationTests/PageDocumentIntegrationTest.cs create mode 100644 test/Microsoft.AspNetCore.Razor.Evolution.Test/Intermediate/RazorIRBuilderExtensionsTest.cs 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);