Support temporary @(Implements<MyInterfaceType>()) syntax

This commit is contained in:
Steve Sanderson 2018-02-16 12:39:38 +00:00
parent a6cd139f9c
commit 7139cb70c5
4 changed files with 130 additions and 0 deletions

View File

@ -23,6 +23,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
{
FunctionsDirective.Register(configure);
TemporaryLayoutPass.Register(configure);
TemporaryImplementsPass.Register(configure);
configure.SetBaseType(BlazorComponent.FullTypeName);

View File

@ -0,0 +1,107 @@
// 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.Language;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace Microsoft.AspNetCore.Blazor.Razor
{
/// <summary>
/// This code is temporary. It finds top-level expressions of the form
/// @Implements<SomeInterfaceType>()
/// ... and converts them into interface declarations on the class.
/// Once we're able to add Blazor-specific directives and have them show up in tooling,
/// we'll replace this with a simpler and cleaner "@implements SomeInterfaceType" directive.
/// </summary>
internal class TemporaryImplementsPass : IntermediateNodePassBase, IRazorDirectiveClassifierPass
{
// Example: "Implements<MyApp.Namespace.ISomeType<T1, T2>>()"
// Captures: MyApp.Namespace.ISomeType<T1, T2>
private static readonly Regex ImplementsSourceRegex
= new Regex(@"^\s*Implements\s*<(.+)\>\s*\(\s*\)\s*$");
public static void Register(IRazorEngineBuilder configuration)
{
configuration.Features.Add(new TemporaryImplementsPass());
}
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
{
var visitor = new Visitor();
visitor.Visit(documentNode);
foreach (var implementsNode in visitor.ImplementsNodes)
{
visitor.MethodNode.Children.Remove(implementsNode);
}
if (visitor.ClassNode.Interfaces == null)
{
visitor.ClassNode.Interfaces = new List<string>();
}
foreach (var implementsType in visitor.ImplementsTypes)
{
visitor.ClassNode.Interfaces.Add(implementsType);
}
}
private class Visitor : IntermediateNodeWalker
{
public ClassDeclarationIntermediateNode ClassNode { get; private set; }
public MethodDeclarationIntermediateNode MethodNode { get; private set; }
public List<CSharpExpressionIntermediateNode> ImplementsNodes { get; private set; }
= new List<CSharpExpressionIntermediateNode>();
public List<string> ImplementsTypes { get; private set; }
= new List<string>();
public override void VisitClassDeclaration(ClassDeclarationIntermediateNode node)
{
ClassNode = node;
base.VisitClassDeclaration(node);
}
public override void VisitMethodDeclaration(MethodDeclarationIntermediateNode methodNode)
{
MethodNode = methodNode;
var topLevelExpressions = methodNode.Children.OfType<CSharpExpressionIntermediateNode>();
foreach (var csharpExpression in topLevelExpressions)
{
if (csharpExpression.Children.Count == 1)
{
var child = csharpExpression.Children[0];
if (child is IntermediateToken intermediateToken)
{
if (TryGetImplementsType(intermediateToken.Content, out string implementsType))
{
ImplementsNodes.Add(csharpExpression);
ImplementsTypes.Add(implementsType);
}
}
}
}
base.VisitMethodDeclaration(methodNode);
}
private bool TryGetImplementsType(string sourceCode, out string implementsType)
{
var match = ImplementsSourceRegex.Match(sourceCode);
if (match.Success)
{
implementsType = match.Groups[1].Value;
return true;
}
else
{
implementsType = null;
return false;
}
}
}
}
}

View File

@ -30,6 +30,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor
// It will be removed when we can add Blazor-specific directives.
public object Layout<TLayout>() where TLayout : IComponent
=> throw new NotImplementedException();
// Similar temporary mechanism as above
public object Implements<TInterface>()
=> throw new NotImplementedException();
}
namespace Internal

View File

@ -389,6 +389,22 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
frame => AssertFrame.Text(frame, "Hello"));
}
[Fact]
public void SupportsImplementsDeclarationsViaTemporarySyntax()
{
// Arrange/Act
var testInterfaceTypeName = typeof(ITestInterface).FullName.Replace('+', '.');
var component = CompileToComponent(
$"@(Implements<{testInterfaceTypeName}>())" +
$"Hello");
var frames = GetRenderTree(component);
// Assert
Assert.IsAssignableFrom<ITestInterface>(component);
Assert.Collection(frames,
frame => AssertFrame.Text(frame, "Hello"));
}
private static RenderTreeFrame[] GetRenderTree(IComponent component)
{
var renderer = new TestRenderer();
@ -523,5 +539,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
{
}
}
public interface ITestInterface { }
}
}