From 46018f9512b385363b5021c7775e262bff57920c Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Tue, 29 Nov 2016 21:28:27 -0800 Subject: [PATCH] Add IR phase This change adds a phase which runs IR passes. Design and code are almost exactly the same as the existing SyntaxTree phase. However all of this is public because the IR is public API. --- .../DefaultRazorIRLoweringPhase.cs | 1 - .../DefaultRazorIRPhase.cs | 30 ++++++ .../IRazorIRPass.cs | 14 +++ .../IRazorIRPhase.cs | 9 ++ .../RazorEngine.cs | 1 + .../DefaultRazorIRPhaseTest.cs | 95 +++++++++++++++++++ .../RazorEngineTest.cs | 4 +- 7 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorIRPhase.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/IRazorIRPass.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/IRazorIRPhase.cs create mode 100644 test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultRazorIRPhaseTest.cs diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorIRLoweringPhase.cs b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorIRLoweringPhase.cs index b2b528dab0..98f4c5bf9e 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorIRLoweringPhase.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorIRLoweringPhase.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; -using System.Linq; using Microsoft.AspNetCore.Razor.Evolution.Intermediate; using Microsoft.AspNetCore.Razor.Evolution.Legacy; diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorIRPhase.cs b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorIRPhase.cs new file mode 100644 index 0000000000..6c3e8cf96c --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorIRPhase.cs @@ -0,0 +1,30 @@ +// 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.Linq; + +namespace Microsoft.AspNetCore.Razor.Evolution +{ + internal class DefaultRazorIRPhase : RazorEnginePhaseBase + { + public IRazorIRPass[] Passes { get; private set; } + + protected override void OnIntialized() + { + Passes = Engine.Features.OfType().OrderBy(p => p.Order).ToArray(); + } + + protected override void ExecuteCore(RazorCodeDocument codeDocument) + { + var irDocument = codeDocument.GetIRDocument(); + ThrowForMissingDependency(irDocument); + + foreach (var pass in Passes) + { + irDocument = pass.Execute(codeDocument, irDocument); + } + + codeDocument.SetIRDocument(irDocument); + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/IRazorIRPass.cs b/src/Microsoft.AspNetCore.Razor.Evolution/IRazorIRPass.cs new file mode 100644 index 0000000000..5b98c93b26 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/IRazorIRPass.cs @@ -0,0 +1,14 @@ +// 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 +{ + public interface IRazorIRPass : IRazorEngineFeature + { + int Order { get; } + + DocumentIRNode Execute(RazorCodeDocument codeDocument, DocumentIRNode irDocument); + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/IRazorIRPhase.cs b/src/Microsoft.AspNetCore.Razor.Evolution/IRazorIRPhase.cs new file mode 100644 index 0000000000..0351bbbc39 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/IRazorIRPhase.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 +{ + public interface IRazorIRPhase : IRazorEnginePhase + { + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngine.cs b/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngine.cs index b9b2a8750c..79bd6f541d 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngine.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngine.cs @@ -33,6 +33,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution builder.Phases.Add(new DefaultRazorParsingPhase()); builder.Phases.Add(new DefaultRazorSyntaxTreePhase()); builder.Phases.Add(new DefaultRazorIRLoweringPhase()); + builder.Phases.Add(new DefaultRazorIRPhase()); builder.Features.Add(new TagHelperBinderSyntaxTreePass()); builder.Features.Add(new HtmlNodeOptimizationPass()); diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultRazorIRPhaseTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultRazorIRPhaseTest.cs new file mode 100644 index 0000000000..cacd605b62 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultRazorIRPhaseTest.cs @@ -0,0 +1,95 @@ +// 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 Microsoft.AspNetCore.Razor.Evolution.Intermediate; +using Microsoft.AspNetCore.Testing; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution +{ + public class DefaultRazorIRPhaseTest + { + [Fact] + public void OnInitialized_OrdersPassesInAscendingOrder() + { + // Arrange & Act + var phase = new DefaultRazorIRPhase(); + + var first = Mock.Of(p => p.Order == 15); + var second = Mock.Of(p => p.Order == 17); + + var engine = RazorEngine.CreateEmpty(b => + { + b.Phases.Add(phase); + + b.Features.Add(second); + b.Features.Add(first); + }); + + // Assert + Assert.Collection( + phase.Passes, + p => Assert.Same(first, p), + p => Assert.Same(second, p)); + } + + [Fact] + public void Execute_ThrowsForMissingDependency() + { + // Arrange + var phase = new DefaultRazorIRPhase(); + + var engine = RazorEngine.CreateEmpty(b => b.Phases.Add(phase)); + + var codeDocument = TestRazorCodeDocument.CreateEmpty(); + + // Act & Assert + ExceptionAssert.Throws( + () => phase.Execute(codeDocument), + $"The '{nameof(DefaultRazorIRPhase)}' phase requires a '{nameof(DocumentIRNode)}' " + + $"provided by the '{nameof(RazorCodeDocument)}'."); + } + + [Fact] + public void Execute_ExecutesPhasesInOrder() + { + // Arrange + var codeDocument = TestRazorCodeDocument.CreateEmpty(); + + // We're going to set up mocks to simulate a sequence of passes. We don't care about + // what's in the nodes, we're just going to look at the identity via strict mocks. + var originalNode = new DocumentIRNode(); + var firstPassNode = new DocumentIRNode(); + var secondPassNode = new DocumentIRNode(); + codeDocument.SetIRDocument(originalNode); + + var firstPass = new Mock(MockBehavior.Strict); + firstPass.SetupGet(m => m.Order).Returns(0); + firstPass.SetupProperty(m => m.Engine); + firstPass.Setup(m => m.Execute(codeDocument, originalNode)).Returns(firstPassNode); + + var secondPass = new Mock(MockBehavior.Strict); + secondPass.SetupGet(m => m.Order).Returns(1); + secondPass.SetupProperty(m => m.Engine); + secondPass.Setup(m => m.Execute(codeDocument, firstPassNode)).Returns(secondPassNode); + + var phase = new DefaultRazorIRPhase(); + + var engine = RazorEngine.CreateEmpty(b => + { + b.Phases.Add(phase); + + b.Features.Add(firstPass.Object); + b.Features.Add(secondPass.Object); + }); + + // Act + phase.Execute(codeDocument); + + // Assert + Assert.Same(secondPassNode, codeDocument.GetIRDocument()); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineTest.cs index 7d2499bf34..567d20abe9 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineTest.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; -using Microsoft.AspNetCore.Razor.Evolution; using Moq; using Xunit; @@ -86,7 +85,8 @@ namespace Microsoft.AspNetCore.Razor.Evolution phases, phase => Assert.IsType(phase), phase => Assert.IsType(phase), - phase => Assert.IsType(phase)); + phase => Assert.IsType(phase), + phase => Assert.IsType(phase)); } } } \ No newline at end of file