From 7139cb70c580606cafa53e484185e3861a75e542 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Fri, 16 Feb 2018 12:39:38 +0000 Subject: [PATCH] Support temporary @(Implements()) syntax --- .../BlazorRazorEngine.cs | 1 + .../TemporaryImplementsPass.cs | 107 ++++++++++++++++++ .../Components/RazorToolingWorkaround.cs | 4 + .../RazorCompilerTest.cs | 18 +++ 4 files changed, 130 insertions(+) create mode 100644 src/Microsoft.AspNetCore.Blazor.Razor.Extensions/TemporaryImplementsPass.cs diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRazorEngine.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRazorEngine.cs index b93a95b7dd..cc7842a081 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRazorEngine.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRazorEngine.cs @@ -23,6 +23,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor { FunctionsDirective.Register(configure); TemporaryLayoutPass.Register(configure); + TemporaryImplementsPass.Register(configure); configure.SetBaseType(BlazorComponent.FullTypeName); diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/TemporaryImplementsPass.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/TemporaryImplementsPass.cs new file mode 100644 index 0000000000..36ad28d7db --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/TemporaryImplementsPass.cs @@ -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 +{ + /// + /// This code is temporary. It finds top-level expressions of the form + /// @Implements() + /// ... 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. + /// + internal class TemporaryImplementsPass : IntermediateNodePassBase, IRazorDirectiveClassifierPass + { + // Example: "Implements>()" + // Captures: MyApp.Namespace.ISomeType + 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(); + } + + 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 ImplementsNodes { get; private set; } + = new List(); + public List ImplementsTypes { get; private set; } + = new List(); + + public override void VisitClassDeclaration(ClassDeclarationIntermediateNode node) + { + ClassNode = node; + base.VisitClassDeclaration(node); + } + + public override void VisitMethodDeclaration(MethodDeclarationIntermediateNode methodNode) + { + MethodNode = methodNode; + + var topLevelExpressions = methodNode.Children.OfType(); + 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; + } + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Blazor/Components/RazorToolingWorkaround.cs b/src/Microsoft.AspNetCore.Blazor/Components/RazorToolingWorkaround.cs index 9f69c73d6e..53ea414131 100644 --- a/src/Microsoft.AspNetCore.Blazor/Components/RazorToolingWorkaround.cs +++ b/src/Microsoft.AspNetCore.Blazor/Components/RazorToolingWorkaround.cs @@ -30,6 +30,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor // It will be removed when we can add Blazor-specific directives. public object Layout() where TLayout : IComponent => throw new NotImplementedException(); + + // Similar temporary mechanism as above + public object Implements() + => throw new NotImplementedException(); } namespace Internal diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs index 59469f1159..9ec5eb25d7 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs @@ -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(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 { } } }