From b341340d1f58c26b4c72c75d05016a5e3647be75 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Wed, 26 Oct 2016 18:05:38 -0700 Subject: [PATCH] Add skeleton of syntax phases --- .../DefaultRazorParsingPhase.cs | 16 ++++ .../DefaultRazorSyntaxTreePhase.cs | 30 ++++++ .../IRazorEnginePhase.cs | 2 +- .../IRazorParsingPhase.cs | 9 ++ .../IRazorSyntaxTreePass.cs | 13 +++ .../IRazorSyntaxTreePhase.cs | 10 ++ .../Properties/Resources.Designer.cs | 32 +++++++ .../RazorCodeDocumentExtensions.cs | 30 ++++++ .../RazorEngine.cs | 14 +++ .../RazorEnginePhaseBase.cs | 59 ++++++++++++ .../Resources.resx | 6 ++ .../DefaultRazorParsingPhaseTest.cs | 26 ++++++ .../DefaultRazorSyntaxTreePhaseTest.cs | 92 +++++++++++++++++++ .../IntegrationTest.cs | 2 +- .../RazorCodeDocumentExtensionsTest.cs | 41 +++++++++ .../RazorEngineTest.cs | 3 + 16 files changed, 383 insertions(+), 2 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorParsingPhase.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorSyntaxTreePhase.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/IRazorParsingPhase.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/IRazorSyntaxTreePass.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/IRazorSyntaxTreePhase.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/RazorCodeDocumentExtensions.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/RazorEnginePhaseBase.cs create mode 100644 test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultRazorParsingPhaseTest.cs create mode 100644 test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultRazorSyntaxTreePhaseTest.cs create mode 100644 test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorCodeDocumentExtensionsTest.cs diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorParsingPhase.cs b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorParsingPhase.cs new file mode 100644 index 0000000000..81caab0fd8 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorParsingPhase.cs @@ -0,0 +1,16 @@ +// 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; + +namespace Microsoft.AspNetCore.Razor.Evolution +{ + internal class DefaultRazorParsingPhase : RazorEnginePhaseBase, IRazorParsingPhase + { + protected override void ExecuteCore(RazorCodeDocument codeDocument) + { + var syntaxTree = RazorSyntaxTree.Parse(codeDocument.Source); + codeDocument.SetSyntaxTree(syntaxTree); + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorSyntaxTreePhase.cs b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorSyntaxTreePhase.cs new file mode 100644 index 0000000000..dfa9c365bc --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorSyntaxTreePhase.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 DefaultRazorSyntaxTreePhase : RazorEnginePhaseBase, IRazorSyntaxTreePhase + { + public IRazorSyntaxTreePass[] Passes { get; private set; } + + protected override void OnIntialized() + { + Passes = Engine.Features.OfType().OrderBy(p => p.Order).ToArray(); + } + + protected override void ExecuteCore(RazorCodeDocument codeDocument) + { + var syntaxTree = codeDocument.GetSyntaxTree(); + ThrowForMissingDependency(syntaxTree); + + foreach (var pass in Passes) + { + syntaxTree = pass.Execute(codeDocument, syntaxTree); + } + + codeDocument.SetSyntaxTree(syntaxTree); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/IRazorEnginePhase.cs b/src/Microsoft.AspNetCore.Razor.Evolution/IRazorEnginePhase.cs index 08dd34ba62..155bdcda72 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/IRazorEnginePhase.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/IRazorEnginePhase.cs @@ -7,6 +7,6 @@ namespace Microsoft.AspNetCore.Razor.Evolution { RazorEngine Engine { get; set; } - void Execute(RazorCodeDocument document); + void Execute(RazorCodeDocument codeDocument); } } diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/IRazorParsingPhase.cs b/src/Microsoft.AspNetCore.Razor.Evolution/IRazorParsingPhase.cs new file mode 100644 index 0000000000..3496b1f07b --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/IRazorParsingPhase.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 IRazorParsingPhase : IRazorEnginePhase + { + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/IRazorSyntaxTreePass.cs b/src/Microsoft.AspNetCore.Razor.Evolution/IRazorSyntaxTreePass.cs new file mode 100644 index 0000000000..80cbdc135c --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/IRazorSyntaxTreePass.cs @@ -0,0 +1,13 @@ +// 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 +{ + // Internal until we flesh out public RazorSyntaxTree API + internal interface IRazorSyntaxTreePass : IRazorEngineFeature + { + int Order { get; } + + RazorSyntaxTree Execute(RazorCodeDocument codeDocument, RazorSyntaxTree syntaxTree); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/IRazorSyntaxTreePhase.cs b/src/Microsoft.AspNetCore.Razor.Evolution/IRazorSyntaxTreePhase.cs new file mode 100644 index 0000000000..6dad08b352 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/IRazorSyntaxTreePhase.cs @@ -0,0 +1,10 @@ +// 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 +{ + // Internal until we flesh out public RazorSyntaxTree API. + internal interface IRazorSyntaxTreePhase : IRazorEnginePhase + { + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Properties/Resources.Designer.cs index 8dc9033588..4fbc0eb223 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Properties/Resources.Designer.cs @@ -10,6 +10,38 @@ namespace Microsoft.AspNetCore.Razor.Evolution private static readonly ResourceManager _resourceManager = new ResourceManager("Microsoft.AspNetCore.Razor.Evolution.Resources", typeof(Resources).GetTypeInfo().Assembly); + /// + /// The '{0}' phase requires a '{1}' provided by the '{2}'. + /// + internal static string PhaseDependencyMissing + { + get { return GetString("PhaseDependencyMissing"); } + } + + /// + /// The '{0}' phase requires a '{1}' provided by the '{2}'. + /// + internal static string FormatPhaseDependencyMissing(object p0, object p1, object p2) + { + return string.Format(CultureInfo.CurrentCulture, GetString("PhaseDependencyMissing"), p0, p1, p2); + } + + /// + /// The phase must be initialized by setting the '{0}' property. + /// + internal static string PhaseMustBeInitialized + { + get { return GetString("PhaseMustBeInitialized"); } + } + + /// + /// The phase must be initialized by setting the '{0}' property. + /// + internal static string FormatPhaseMustBeInitialized(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("PhaseMustBeInitialized"), p0); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/RazorCodeDocumentExtensions.cs b/src/Microsoft.AspNetCore.Razor.Evolution/RazorCodeDocumentExtensions.cs new file mode 100644 index 0000000000..4302036b3b --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/RazorCodeDocumentExtensions.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; + +namespace Microsoft.AspNetCore.Razor.Evolution +{ + public static class RazorCodeDocumentExtensions + { + public static RazorSyntaxTree GetSyntaxTree(this RazorCodeDocument document) + { + if (document == null) + { + throw new ArgumentNullException(nameof(document)); + } + + return document.Items[typeof(RazorSyntaxTree)] as RazorSyntaxTree; + } + + public static void SetSyntaxTree(this RazorCodeDocument document, RazorSyntaxTree syntaxTree) + { + if (document == null) + { + throw new ArgumentNullException(nameof(document)); + } + + document.Items[typeof(RazorSyntaxTree)] = syntaxTree; + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngine.cs b/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngine.cs index 0f26c3e27f..518467911d 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngine.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngine.cs @@ -16,10 +16,24 @@ namespace Microsoft.AspNetCore.Razor.Evolution public static RazorEngine Create(Action configure) { var builder = new DefaultRazorEngineBuilder(); + AddDefaults(builder); configure?.Invoke(builder); return builder.Build(); } + public static RazorEngine CreateEmpty(Action configure) + { + var builder = new DefaultRazorEngineBuilder(); + configure?.Invoke(builder); + return builder.Build(); + } + + internal static void AddDefaults(IRazorEngineBuilder builder) + { + builder.Phases.Add(new DefaultRazorParsingPhase()); + builder.Phases.Add(new DefaultRazorSyntaxTreePhase()); + } + public abstract IReadOnlyList Features { get; } public abstract IReadOnlyList Phases { get; } diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/RazorEnginePhaseBase.cs b/src/Microsoft.AspNetCore.Razor.Evolution/RazorEnginePhaseBase.cs new file mode 100644 index 0000000000..38fc33ca39 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/RazorEnginePhaseBase.cs @@ -0,0 +1,59 @@ +// 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; + +namespace Microsoft.AspNetCore.Razor.Evolution +{ + internal abstract class RazorEnginePhaseBase : IRazorEnginePhase + { + private RazorEngine _engine; + + public RazorEngine Engine + { + get { return _engine; } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _engine = value; + OnIntialized(); + } + } + + public void Execute(RazorCodeDocument codeDocument) + { + if (codeDocument == null) + { + throw new ArgumentNullException(nameof(codeDocument)); + } + + if (Engine == null) + { + throw new InvalidOperationException(Resources.FormatPhaseMustBeInitialized(nameof(Engine))); + } + + ExecuteCore(codeDocument); + } + + protected void ThrowForMissingDependency(T value) + { + if (value == null) + { + throw new InvalidOperationException(Resources.FormatPhaseDependencyMissing( + GetType().Name, + typeof(T).Name, + typeof(RazorCodeDocument).Name)); + } + } + + protected virtual void OnIntialized() + { + } + + protected abstract void ExecuteCore(RazorCodeDocument codeDocument); + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Resources.resx b/src/Microsoft.AspNetCore.Razor.Evolution/Resources.resx index 1af7de150c..55420a29df 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/Resources.resx +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Resources.resx @@ -117,4 +117,10 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + The '{0}' phase requires a '{1}' provided by the '{2}'. + + + The phase must be initialized by setting the '{0}' property. + \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultRazorParsingPhaseTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultRazorParsingPhaseTest.cs new file mode 100644 index 0000000000..39e505cabf --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultRazorParsingPhaseTest.cs @@ -0,0 +1,26 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution +{ + public class DefaultRazorParsingPhaseTest + { + [Fact] + public void Execute_AddsSyntaxTree() + { + // Arrange + var phase = new DefaultRazorParsingPhase(); + var engine = RazorEngine.CreateEmpty(b => b.Phases.Add(phase)); + + var codeDocument = TestRazorCodeDocument.CreateEmpty(); + + // Act + phase.Execute(codeDocument); + + // Assert + Assert.NotNull(codeDocument.GetSyntaxTree()); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultRazorSyntaxTreePhaseTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultRazorSyntaxTreePhaseTest.cs new file mode 100644 index 0000000000..91ae926372 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DefaultRazorSyntaxTreePhaseTest.cs @@ -0,0 +1,92 @@ +// 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.Testing; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution +{ + public class DefaultRazorSyntaxTreePhaseTest + { + [Fact] + public void OnInitialized_OrdersPassesInAscendingOrder() + { + // Arrange & Act + var phase = new DefaultRazorSyntaxTreePhase(); + + 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 DefaultRazorSyntaxTreePhase(); + + var engine = RazorEngine.CreateEmpty(b => b.Phases.Add(phase)); + + var codeDocument = TestRazorCodeDocument.CreateEmpty(); + + // Act & Assert + ExceptionAssert.Throws( + () => phase.Execute(codeDocument), + $"The '{nameof(DefaultRazorSyntaxTreePhase)}' phase requires a '{nameof(RazorSyntaxTree)}' " + + $"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 trees, we're just going to look at the identity via strict mocks. + var originalSyntaxTree = RazorSyntaxTree.Parse(codeDocument.Source); + var firstPassSyntaxTree = RazorSyntaxTree.Parse(codeDocument.Source); + var secondPassSyntaxTree = RazorSyntaxTree.Parse(codeDocument.Source); + + var firstPass = new Mock(MockBehavior.Strict); + firstPass.SetupGet(m => m.Order).Returns(0); + firstPass.Setup(m => m.Execute(codeDocument, originalSyntaxTree)).Returns(firstPassSyntaxTree); + + var secondPass = new Mock(MockBehavior.Strict); + secondPass.SetupGet(m => m.Order).Returns(0); + secondPass.Setup(m => m.Execute(codeDocument, firstPassSyntaxTree)).Returns(secondPassSyntaxTree); + + var phase = new DefaultRazorSyntaxTreePhase(); + + var engine = RazorEngine.CreateEmpty(b => + { + b.Phases.Add(phase); + + b.Features.Add(secondPass.Object); + b.Features.Add(firstPass.Object); + }); + + // Act + phase.Execute(codeDocument); + + // Assert + Assert.Same(secondPassSyntaxTree, codeDocument.GetSyntaxTree()); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/IntegrationTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/IntegrationTest.cs index 7fe96c1360..e85cc4b72b 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/IntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/IntegrationTest.cs @@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution engine.Process(document); // Assert - // (nothing to verify yet) + Assert.NotNull(document.GetSyntaxTree()); } } } diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorCodeDocumentExtensionsTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorCodeDocumentExtensionsTest.cs new file mode 100644 index 0000000000..16bb6b1c84 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorCodeDocumentExtensionsTest.cs @@ -0,0 +1,41 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution +{ + public class RazorCodeDocumentExtensionsTest + { + [Fact] + public void GetRazorSyntaxTree_ReturnsSyntaxTree() + { + // Arrange + var codeDocument = TestRazorCodeDocument.CreateEmpty(); + + var expected = RazorSyntaxTree.Parse(codeDocument.Source); + codeDocument.Items[typeof(RazorSyntaxTree)] = expected; + + // Act + var actual = codeDocument.GetSyntaxTree(); + + // Assert + Assert.Same(expected, actual); + } + + [Fact] + public void SetRazorSyntaxTree_SetsSyntaxTree() + { + // Arrange + var codeDocument = TestRazorCodeDocument.CreateEmpty(); + + var expected = RazorSyntaxTree.Parse(codeDocument.Source); + + // Act + codeDocument.SetSyntaxTree(expected); + + // Assert + Assert.Same(expected, codeDocument.Items[typeof(RazorSyntaxTree)]); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineTest.cs index e35f7d0874..70a207a6a2 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineTest.cs @@ -45,6 +45,9 @@ namespace Microsoft.AspNetCore.Razor.Evolution // Act var engine = RazorEngine.Create(builder => { + builder.Features.Clear(); + builder.Phases.Clear(); + builder.Features.Add(Mock.Of()); builder.Features.Add(Mock.Of());