From 1e0836167da21bd1f028150e44099ee5a4229749 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Mon, 19 Feb 2018 12:48:08 +0000 Subject: [PATCH] Make temporary "layout" and "implements" syntax work with _ViewImports hierarchies --- .../Temporary/SourceLinesEnumerator.cs | 77 ++++++++++++ .../Temporary/SourceLinesVisitor.cs | 37 ++++++ .../Temporary/TemporaryFakeDirectivePass.cs | 82 ++++++++++++ .../Temporary/TemporaryImplementsPass.cs | 48 +++++++ .../Temporary/TemporaryLayoutPass.cs | 56 +++++++++ .../TemporaryImplementsPass.cs | 107 ---------------- .../TemporaryLayoutPass.cs | 118 ------------------ .../RazorCompilerTest.cs | 8 +- 8 files changed, 304 insertions(+), 229 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/SourceLinesEnumerator.cs create mode 100644 src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/SourceLinesVisitor.cs create mode 100644 src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/TemporaryFakeDirectivePass.cs create mode 100644 src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/TemporaryImplementsPass.cs create mode 100644 src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/TemporaryLayoutPass.cs delete mode 100644 src/Microsoft.AspNetCore.Blazor.Razor.Extensions/TemporaryImplementsPass.cs delete mode 100644 src/Microsoft.AspNetCore.Blazor.Razor.Extensions/TemporaryLayoutPass.cs diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/SourceLinesEnumerator.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/SourceLinesEnumerator.cs new file mode 100644 index 0000000000..a4d6a468b4 --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/SourceLinesEnumerator.cs @@ -0,0 +1,77 @@ +// 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 System; +using System.Collections; +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Blazor.Razor +{ + // This only exists to support SourceLinesVisitor and can be removed once + // we are able to implement proper Blazor-specific directives + internal class SourceLinesEnumerable : IEnumerable + { + private RazorSourceDocument _source; + + public SourceLinesEnumerable(RazorSourceDocument source) + => _source = source; + + public IEnumerator GetEnumerator() + => new SourceLinesEnumerator(_source); + + IEnumerator IEnumerable.GetEnumerator() + => new SourceLinesEnumerator(_source); + + private class SourceLinesEnumerator : IEnumerator + { + private readonly RazorSourceDocument _sourceDocument; + private readonly RazorSourceLineCollection _lines; + private int _currentLineIndex; + private int _cumulativeLengthOfPrecedingLines; + private char[] _currentLineBuffer = new char[200]; // Grows if needed + private string _currentLineText; + + public SourceLinesEnumerator(RazorSourceDocument sourceDocument) + { + _sourceDocument = sourceDocument ?? throw new ArgumentNullException(nameof(sourceDocument)); + _lines = _sourceDocument.Lines; + _currentLineIndex = -1; + } + + public string Current => _currentLineText; + + object IEnumerator.Current => _currentLineText; + + public void Dispose() + { + } + + public bool MoveNext() + { + _currentLineIndex++; + if (_currentLineIndex >= _lines.Count) + { + return false; + } + + var lineLength = _lines.GetLineLength(_currentLineIndex); + if (_currentLineBuffer.Length < lineLength) + { + _currentLineBuffer = new char[lineLength]; + } + + _sourceDocument.CopyTo(_cumulativeLengthOfPrecedingLines, _currentLineBuffer, 0, lineLength); + _currentLineText = new string(_currentLineBuffer, 0, lineLength); + _cumulativeLengthOfPrecedingLines += lineLength; + return true; + } + + public void Reset() + { + _currentLineIndex = -1; + _cumulativeLengthOfPrecedingLines = 0; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/SourceLinesVisitor.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/SourceLinesVisitor.cs new file mode 100644 index 0000000000..ae336c8f13 --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/SourceLinesVisitor.cs @@ -0,0 +1,37 @@ +// 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; + +namespace Microsoft.AspNetCore.Blazor.Razor +{ + // This only exists to support the temporary fake Blazor directives and can + // be removed once we are able to implement proper Blazor-specific directives + internal abstract class SourceLinesVisitor + { + /// + /// Visits each line in the document's imports (in order), followed by + /// each line in the document's primary syntax tree. + /// + public void Visit(RazorCodeDocument codeDocument) + { + foreach (var import in codeDocument.GetImportSyntaxTrees()) + { + VisitSyntaxTree(import); + } + + VisitSyntaxTree(codeDocument.GetSyntaxTree()); + } + + protected abstract void VisitLine(string line); + + private void VisitSyntaxTree(RazorSyntaxTree syntaxTree) + { + var sourceDocument = syntaxTree.Source; + foreach (var line in new SourceLinesEnumerable(sourceDocument)) + { + VisitLine(line); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/TemporaryFakeDirectivePass.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/TemporaryFakeDirectivePass.cs new file mode 100644 index 0000000000..6755234a26 --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/TemporaryFakeDirectivePass.cs @@ -0,0 +1,82 @@ +// 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 +{ + // Until we're able to add real directives, this implements a temporary mechanism whereby we + // search for lines of the form "@({regex})" (including in the imports sources) and do something + // with the regex matches. Also we remove the corresponding tokens from the intermediate + // representation to stop them from interfering with the compiled output on its own. + internal abstract class TemporaryFakeDirectivePass : IntermediateNodePassBase, IRazorDirectiveClassifierPass + { + private readonly Regex _sourceLineRegex; + private readonly Regex _tokenRegex; + + protected TemporaryFakeDirectivePass(string syntaxRegexPattern) + { + _sourceLineRegex = new Regex($@"^\s*@\({syntaxRegexPattern}\)\s*$"); + _tokenRegex = new Regex($@"^{syntaxRegexPattern}$"); + } + + protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) + { + // First, remove any matching lines from the intermediate representation + // in the primary document. Don't need to remove them from imports as they + // have no effect there anyway. + var methodNode = documentNode.FindPrimaryMethod(); + var methodNodeChildren = methodNode.Children.ToList(); + foreach (var node in methodNodeChildren) + { + if (IsMatchingNode(node)) + { + methodNode.Children.Remove(node); + } + } + + // Now find the matching lines in the source code (including imports) + // Need to do this on source, because the imports aren't in the intermediate representation + var linesVisitor = new RegexSourceLinesVisitor(_sourceLineRegex); + linesVisitor.Visit(codeDocument); + if (linesVisitor.MatchedContent.Any()) + { + HandleMatchedContent(codeDocument, linesVisitor.MatchedContent); + } + } + + protected abstract void HandleMatchedContent(RazorCodeDocument codeDocument, IEnumerable matchedContent); + + private bool IsMatchingNode(IntermediateNode node) + => node.Children.Count == 1 + && node.Children[0] is IntermediateToken intermediateToken + && _tokenRegex.IsMatch(intermediateToken.Content); + + private class RegexSourceLinesVisitor : SourceLinesVisitor + { + private Regex _searchRegex; + private readonly List _matchedContent = new List(); + + public IEnumerable MatchedContent => _matchedContent; + + public RegexSourceLinesVisitor(Regex searchRegex) + { + _searchRegex = searchRegex; + } + + protected override void VisitLine(string line) + { + // Pick the most specific by looking for the final one in the sources + var match = _searchRegex.Match(line); + if (match.Success) + { + _matchedContent.Add(match.Groups[1].Value); + } + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/TemporaryImplementsPass.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/TemporaryImplementsPass.cs new file mode 100644 index 0000000000..8b1e50fb19 --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/TemporaryImplementsPass.cs @@ -0,0 +1,48 @@ +// 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 : TemporaryFakeDirectivePass + { + // Example: "Implements>()" + // Captures: MyApp.Namespace.ISomeType + private const string ImplementsTokenPattern = @"\s*Implements\s*<(.+)\>\s*\(\s*\)\s*"; + + public static void Register(IRazorEngineBuilder configuration) + { + configuration.Features.Add(new TemporaryImplementsPass()); + } + + private TemporaryImplementsPass() : base(ImplementsTokenPattern) + { + } + + protected override void HandleMatchedContent(RazorCodeDocument codeDocument, IEnumerable matchedContent) + { + var classNode = codeDocument.GetDocumentIntermediateNode().FindPrimaryClass(); + if (classNode.Interfaces == null) + { + classNode.Interfaces = new List(); + } + + foreach (var implementsType in matchedContent) + { + classNode.Interfaces.Add(implementsType); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/TemporaryLayoutPass.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/TemporaryLayoutPass.cs new file mode 100644 index 0000000000..bfc7f89c09 --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/Temporary/TemporaryLayoutPass.cs @@ -0,0 +1,56 @@ +// 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; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.AspNetCore.Blazor.Razor +{ + /// + /// This code is temporary. It finds source code lines of the form + /// @Layout() + /// ... and converts them into [Layout(typeof(SomeType))] attributes 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 "@Layout SomeType" directive. + /// + internal class TemporaryLayoutPass : TemporaryFakeDirectivePass + { + // Example: "Layout>()" + // Captures: MyApp.Namespace.SomeType + private const string LayoutTokenPattern = @"\s*Layout\s*<(.+)\>\s*\(\s*\)\s*"; + + private const string LayoutAttributeTypeName + = "Microsoft.AspNetCore.Blazor.Layouts.LayoutAttribute"; + + public static void Register(IRazorEngineBuilder configuration) + { + configuration.Features.Add(new TemporaryLayoutPass()); + } + + private TemporaryLayoutPass() : base(LayoutTokenPattern) + { + } + + protected override void HandleMatchedContent(RazorCodeDocument codeDocument, IEnumerable matchedContent) + { + var chosenLayoutType = matchedContent.Last(); + var attributeNode = new CSharpCodeIntermediateNode(); + attributeNode.Children.Add(new IntermediateToken() + { + Kind = TokenKind.CSharp, + Content = $"[{LayoutAttributeTypeName}(typeof ({chosenLayoutType}))]" + Environment.NewLine, + }); + + var docNode = codeDocument.GetDocumentIntermediateNode(); + var namespaceNode = docNode.FindPrimaryNamespace(); + var classNode = docNode.FindPrimaryClass(); + var classNodeIndex = namespaceNode + .Children + .IndexOf(classNode); + namespaceNode.Children.Insert(classNodeIndex, attributeNode); + } + } +} diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/TemporaryImplementsPass.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/TemporaryImplementsPass.cs deleted file mode 100644 index 36ad28d7db..0000000000 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/TemporaryImplementsPass.cs +++ /dev/null @@ -1,107 +0,0 @@ -// 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.Razor.Extensions/TemporaryLayoutPass.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/TemporaryLayoutPass.cs deleted file mode 100644 index f18e7f6207..0000000000 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/TemporaryLayoutPass.cs +++ /dev/null @@ -1,118 +0,0 @@ -// 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; -using System.Linq; -using System.Text.RegularExpressions; - -namespace Microsoft.AspNetCore.Blazor.Razor -{ - /// - /// This code is temporary. It finds top-level expressions of the form - /// @Layout() - /// ... and converts them into [Layout(typeof(SomeType))] attributes 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 "@Layout SomeType" directive. - /// - internal class TemporaryLayoutPass : IntermediateNodePassBase, IRazorDirectiveClassifierPass - { - // Example: "Layout>()" - // Captures: MyApp.Namespace.SomeType - private static readonly Regex LayoutSourceRegex - = new Regex(@"^\s*Layout\s*<(.+)\>\s*\(\s*\)\s*$"); - private const string LayoutAttributeTypeName - = "Microsoft.AspNetCore.Blazor.Layouts.LayoutAttribute"; - - public static void Register(IRazorEngineBuilder configuration) - { - configuration.Features.Add(new TemporaryLayoutPass()); - } - - protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) - { - var visitor = new Visitor(); - visitor.Visit(documentNode); - - if (visitor.DidFindLayoutDeclaration) - { - visitor.MethodNode.Children.Remove(visitor.LayoutNode); - - var attributeNode = new CSharpCodeIntermediateNode(); - attributeNode.Children.Add(new IntermediateToken() - { - Kind = TokenKind.CSharp, - Content = $"[{LayoutAttributeTypeName}(typeof ({visitor.LayoutType}))]" + Environment.NewLine, - }); - - var classNodeIndex = visitor - .NamespaceNode - .Children - .IndexOf(visitor.ClassNode); - visitor.NamespaceNode.Children.Insert(classNodeIndex, attributeNode); - } - } - - private class Visitor : IntermediateNodeWalker - { - public bool DidFindLayoutDeclaration { get; private set; } - public NamespaceDeclarationIntermediateNode NamespaceNode { get; private set; } - public ClassDeclarationIntermediateNode ClassNode { get; private set; } - public MethodDeclarationIntermediateNode MethodNode { get; private set; } - public CSharpExpressionIntermediateNode LayoutNode { get; private set; } - public string LayoutType { get; private set; } - - public override void VisitNamespaceDeclaration(NamespaceDeclarationIntermediateNode node) - { - NamespaceNode = node; - base.VisitNamespaceDeclaration(node); - } - - public override void VisitClassDeclaration(ClassDeclarationIntermediateNode node) - { - ClassNode = node; - base.VisitClassDeclaration(node); - } - - public override void VisitMethodDeclaration(MethodDeclarationIntermediateNode 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 (TryGetLayoutType(intermediateToken.Content, out string layoutType)) - { - DidFindLayoutDeclaration = true; - MethodNode = methodNode; - LayoutNode = csharpExpression; - LayoutType = layoutType; - } - } - } - } - - base.VisitMethodDeclaration(methodNode); - } - - private bool TryGetLayoutType(string sourceCode, out string layoutType) - { - var match = LayoutSourceRegex.Match(sourceCode); - if (match.Success) - { - layoutType = match.Groups[1].Value; - return true; - } - else - { - layoutType = null; - return false; - } - } - } - } -} diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs index 7ae3467e5d..e02ec49362 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs @@ -377,7 +377,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test // Arrange/Act var testComponentTypeName = typeof(TestLayout).FullName.Replace('+', '.'); var component = CompileToComponent( - $"@(Layout<{testComponentTypeName}>())" + + $"@(Layout<{testComponentTypeName}>())\n" + $"Hello"); var frames = GetRenderTree(component); @@ -386,7 +386,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test Assert.NotNull(layoutAttribute); Assert.Equal(typeof(TestLayout), layoutAttribute.LayoutType); Assert.Collection(frames, - frame => AssertFrame.Text(frame, "Hello")); + frame => AssertFrame.Text(frame, "\nHello")); } [Fact] @@ -395,14 +395,14 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test // Arrange/Act var testInterfaceTypeName = typeof(ITestInterface).FullName.Replace('+', '.'); var component = CompileToComponent( - $"@(Implements<{testInterfaceTypeName}>())" + + $"@(Implements<{testInterfaceTypeName}>())\n" + $"Hello"); var frames = GetRenderTree(component); // Assert Assert.IsAssignableFrom(component); Assert.Collection(frames, - frame => AssertFrame.Text(frame, "Hello")); + frame => AssertFrame.Text(frame, "\nHello")); } [Fact]