Add skeleton of syntax phases

This commit is contained in:
Ryan Nowak 2016-10-26 18:05:38 -07:00 committed by N. Taylor Mullen
parent 49d3574677
commit b341340d1f
16 changed files with 383 additions and 2 deletions

View File

@ -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);
}
}
}

View File

@ -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<IRazorSyntaxTreePass>().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);
}
}
}

View File

@ -7,6 +7,6 @@ namespace Microsoft.AspNetCore.Razor.Evolution
{
RazorEngine Engine { get; set; }
void Execute(RazorCodeDocument document);
void Execute(RazorCodeDocument codeDocument);
}
}

View File

@ -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
{
}
}

View File

@ -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);
}
}

View File

@ -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
{
}
}

View File

@ -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);
/// <summary>
/// The '{0}' phase requires a '{1}' provided by the '{2}'.
/// </summary>
internal static string PhaseDependencyMissing
{
get { return GetString("PhaseDependencyMissing"); }
}
/// <summary>
/// The '{0}' phase requires a '{1}' provided by the '{2}'.
/// </summary>
internal static string FormatPhaseDependencyMissing(object p0, object p1, object p2)
{
return string.Format(CultureInfo.CurrentCulture, GetString("PhaseDependencyMissing"), p0, p1, p2);
}
/// <summary>
/// The phase must be initialized by setting the '{0}' property.
/// </summary>
internal static string PhaseMustBeInitialized
{
get { return GetString("PhaseMustBeInitialized"); }
}
/// <summary>
/// The phase must be initialized by setting the '{0}' property.
/// </summary>
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);

View File

@ -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;
}
}
}

View File

@ -16,10 +16,24 @@ namespace Microsoft.AspNetCore.Razor.Evolution
public static RazorEngine Create(Action<IRazorEngineBuilder> configure)
{
var builder = new DefaultRazorEngineBuilder();
AddDefaults(builder);
configure?.Invoke(builder);
return builder.Build();
}
public static RazorEngine CreateEmpty(Action<IRazorEngineBuilder> 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<IRazorEngineFeature> Features { get; }
public abstract IReadOnlyList<IRazorEnginePhase> Phases { get; }

View File

@ -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>(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);
}
}

View File

@ -117,4 +117,10 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="PhaseDependencyMissing" xml:space="preserve">
<value>The '{0}' phase requires a '{1}' provided by the '{2}'.</value>
</data>
<data name="PhaseMustBeInitialized" xml:space="preserve">
<value>The phase must be initialized by setting the '{0}' property.</value>
</data>
</root>

View File

@ -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());
}
}
}

View File

@ -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<IRazorSyntaxTreePass>(p => p.Order == 15);
var second = Mock.Of<IRazorSyntaxTreePass>(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<InvalidOperationException>(
() => 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<IRazorSyntaxTreePass>(MockBehavior.Strict);
firstPass.SetupGet(m => m.Order).Returns(0);
firstPass.Setup(m => m.Execute(codeDocument, originalSyntaxTree)).Returns(firstPassSyntaxTree);
var secondPass = new Mock<IRazorSyntaxTreePass>(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());
}
}
}

View File

@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution
engine.Process(document);
// Assert
// (nothing to verify yet)
Assert.NotNull(document.GetSyntaxTree());
}
}
}

View File

@ -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)]);
}
}
}

View File

@ -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<IRazorEngineFeature>());
builder.Features.Add(Mock.Of<IRazorEngineFeature>());