Make temporary "layout" and "implements" syntax work with _ViewImports hierarchies

This commit is contained in:
Steve Sanderson 2018-02-19 12:48:08 +00:00
parent f649de2976
commit 1e0836167d
8 changed files with 304 additions and 229 deletions

View File

@ -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<string>
{
private RazorSourceDocument _source;
public SourceLinesEnumerable(RazorSourceDocument source)
=> _source = source;
public IEnumerator<string> GetEnumerator()
=> new SourceLinesEnumerator(_source);
IEnumerator IEnumerable.GetEnumerator()
=> new SourceLinesEnumerator(_source);
private class SourceLinesEnumerator : IEnumerator<string>
{
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;
}
}
}
}

View File

@ -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
{
/// <summary>
/// Visits each line in the document's imports (in order), followed by
/// each line in the document's primary syntax tree.
/// </summary>
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);
}
}
}
}

View File

@ -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<string> 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<string> _matchedContent = new List<string>();
public IEnumerable<string> 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);
}
}
}
}
}

View File

@ -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
{
/// <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 : TemporaryFakeDirectivePass
{
// Example: "Implements<MyApp.Namespace.ISomeType<T1, T2>>()"
// Captures: MyApp.Namespace.ISomeType<T1, T2>
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<string> matchedContent)
{
var classNode = codeDocument.GetDocumentIntermediateNode().FindPrimaryClass();
if (classNode.Interfaces == null)
{
classNode.Interfaces = new List<string>();
}
foreach (var implementsType in matchedContent)
{
classNode.Interfaces.Add(implementsType);
}
}
}
}

View File

@ -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
{
/// <summary>
/// This code is temporary. It finds source code lines of the form
/// @Layout<SomeType>()
/// ... 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.
/// </summary>
internal class TemporaryLayoutPass : TemporaryFakeDirectivePass
{
// Example: "Layout<MyApp.Namespace.SomeType<T1, T2>>()"
// Captures: MyApp.Namespace.SomeType<T1, T2>
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<string> 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);
}
}
}

View File

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

@ -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
{
/// <summary>
/// This code is temporary. It finds top-level expressions of the form
/// @Layout<SomeType>()
/// ... 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.
/// </summary>
internal class TemporaryLayoutPass : IntermediateNodePassBase, IRazorDirectiveClassifierPass
{
// Example: "Layout<MyApp.Namespace.SomeType<T1, T2>>()"
// Captures: MyApp.Namespace.SomeType<T1, T2>
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<CSharpExpressionIntermediateNode>();
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;
}
}
}
}
}

View File

@ -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<ITestInterface>(component);
Assert.Collection(frames,
frame => AssertFrame.Text(frame, "Hello"));
frame => AssertFrame.Text(frame, "\nHello"));
}
[Fact]