Add extensible directive abstractions

- Based generic directive implementation off of descriptors.
- Added parsing logic to consume descriptors and parse content that's expected.
- Added parsing errors to automagically detect unexpected directive pieces.
- Updated visitor implementations to understand the directive bits.
- Added a builder abstraction to easily create descriptors. Had to maintain the ability to manually construct a descriptor to enable convenient serialization/deserialization.
- Added tests/comparers to verify correctness of parsing.

#853
This commit is contained in:
N. Taylor Mullen 2016-11-22 16:42:45 -08:00
parent 522f6e969d
commit 518378f499
22 changed files with 1222 additions and 3 deletions

View File

@ -206,6 +206,30 @@ namespace Microsoft.AspNetCore.Razor.Evolution
Namespace.Children.Insert(i, @using);
}
public override void VisitDirectiveToken(DirectiveTokenChunkGenerator chunkGenerator, Span span)
{
Builder.Add(new DirectiveTokenIRNode()
{
Content = span.Content,
Descriptor = chunkGenerator.Descriptor,
SourceLocation = span.Start,
});
}
public override void VisitStartDirectiveBlock(DirectiveChunkGenerator chunkGenerator, Block block)
{
Builder.Push(new DirectiveIRNode()
{
Name = chunkGenerator.Descriptor.Name,
Descriptor = chunkGenerator.Descriptor,
});
}
public override void VisitEndDirectiveBlock(DirectiveChunkGenerator chunkGenerator, Block block)
{
Builder.Pop();
}
private class ContainerRazorIRNode : RazorIRNode
{
private SourceLocation? _location;

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.Collections.Generic;
namespace Microsoft.AspNetCore.Razor.Evolution
{
public class DirectiveDescriptor
{
public string Name { get; set; }
public DirectiveDescriptorKind Kind { get; set; }
public IReadOnlyList<DirectiveTokenDescriptor> Tokens { get; set; }
}
}

View File

@ -0,0 +1,96 @@
// 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.Collections.Generic;
namespace Microsoft.AspNetCore.Razor.Evolution
{
public static class DirectiveDescriptorBuilder
{
public static IDirectiveDescriptorBuilder Create(string name)
{
return new DefaultDirectiveDescriptorBuilder(name, DirectiveDescriptorKind.SingleLine);
}
public static IDirectiveDescriptorBuilder CreateRazorBlock(string name)
{
return new DefaultDirectiveDescriptorBuilder(name, DirectiveDescriptorKind.RazorBlock);
}
public static IDirectiveDescriptorBuilder CreateCodeBlock(string name)
{
return new DefaultDirectiveDescriptorBuilder(name, DirectiveDescriptorKind.CodeBlock);
}
private class DefaultDirectiveDescriptorBuilder : IDirectiveDescriptorBuilder
{
private readonly List<DirectiveTokenDescriptor> _tokenDescriptors;
private readonly string _name;
private readonly DirectiveDescriptorKind _type;
public DefaultDirectiveDescriptorBuilder(string name, DirectiveDescriptorKind type)
{
_name = name;
_type = type;
_tokenDescriptors = new List<DirectiveTokenDescriptor>();
}
public IDirectiveDescriptorBuilder AddType()
{
var descriptor = new DirectiveTokenDescriptor()
{
Kind = DirectiveTokenKind.Type
};
_tokenDescriptors.Add(descriptor);
return this;
}
public IDirectiveDescriptorBuilder AddMember()
{
var descriptor = new DirectiveTokenDescriptor()
{
Kind = DirectiveTokenKind.Member
};
_tokenDescriptors.Add(descriptor);
return this;
}
public IDirectiveDescriptorBuilder AddString()
{
var descriptor = new DirectiveTokenDescriptor()
{
Kind = DirectiveTokenKind.String
};
_tokenDescriptors.Add(descriptor);
return this;
}
public IDirectiveDescriptorBuilder AddLiteral(string literal)
{
var descriptor = new DirectiveTokenDescriptor()
{
Kind = DirectiveTokenKind.Literal,
Value = literal,
};
_tokenDescriptors.Add(descriptor);
return this;
}
public DirectiveDescriptor Build()
{
var descriptor = new DirectiveDescriptor
{
Name = _name,
Kind = _type,
Tokens = _tokenDescriptors,
};
return descriptor;
}
}
}
}

View File

@ -0,0 +1,49 @@
// 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 System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Razor.Evolution
{
internal class DirectiveDescriptorComparer : IEqualityComparer<DirectiveDescriptor>
{
public static readonly DirectiveDescriptorComparer Default = new DirectiveDescriptorComparer();
protected DirectiveDescriptorComparer()
{
}
public bool Equals(DirectiveDescriptor descriptorX, DirectiveDescriptor descriptorY)
{
if (descriptorX == descriptorY)
{
return true;
}
return descriptorX != null &&
string.Equals(descriptorX.Name, descriptorY.Name, StringComparison.Ordinal) &&
descriptorX.Kind == descriptorY.Kind &&
Enumerable.SequenceEqual(
descriptorX.Tokens,
descriptorY.Tokens,
DirectiveTokenDescriptorComparer.Default);
}
public int GetHashCode(DirectiveDescriptor descriptor)
{
if (descriptor == null)
{
throw new ArgumentNullException(nameof(descriptor));
}
var hashCodeCombiner = HashCodeCombiner.Start();
hashCodeCombiner.Add(descriptor.Name, StringComparer.Ordinal);
hashCodeCombiner.Add(descriptor.Kind);
return hashCodeCombiner.CombinedHash;
}
}
}

View File

@ -0,0 +1,12 @@
// 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 enum DirectiveDescriptorKind
{
SingleLine,
RazorBlock,
CodeBlock
}
}

View File

@ -0,0 +1,12 @@
// 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 class DirectiveTokenDescriptor
{
public DirectiveTokenKind Kind { get; set; }
public string Value { get; set; }
}
}

View File

@ -0,0 +1,44 @@
// 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 System.Collections.Generic;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Razor.Evolution
{
internal class DirectiveTokenDescriptorComparer : IEqualityComparer<DirectiveTokenDescriptor>
{
public static readonly DirectiveTokenDescriptorComparer Default = new DirectiveTokenDescriptorComparer();
protected DirectiveTokenDescriptorComparer()
{
}
public bool Equals(DirectiveTokenDescriptor descriptorX, DirectiveTokenDescriptor descriptorY)
{
if (descriptorX == descriptorY)
{
return true;
}
return descriptorX != null &&
string.Equals(descriptorX.Value, descriptorY.Value, StringComparison.Ordinal) &&
descriptorX.Kind == descriptorY.Kind;
}
public int GetHashCode(DirectiveTokenDescriptor descriptor)
{
if (descriptor == null)
{
throw new ArgumentNullException(nameof(descriptor));
}
var hashCodeCombiner = HashCodeCombiner.Start();
hashCodeCombiner.Add(descriptor.Value, StringComparer.Ordinal);
hashCodeCombiner.Add(descriptor.Kind);
return hashCodeCombiner.CombinedHash;
}
}
}

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
{
public enum DirectiveTokenKind
{
Type,
Member,
String,
Literal
}
}

View File

@ -0,0 +1,18 @@
// 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 IDirectiveDescriptorBuilder
{
IDirectiveDescriptorBuilder AddType();
IDirectiveDescriptorBuilder AddMember();
IDirectiveDescriptorBuilder AddString();
IDirectiveDescriptorBuilder AddLiteral(string literal);
DirectiveDescriptor Build();
}
}

View File

@ -0,0 +1,34 @@
// 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.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Razor.Evolution.Legacy;
namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate
{
public class DirectiveIRNode : RazorIRNode
{
public override IList<RazorIRNode> Children { get; } = new List<RazorIRNode>();
public override RazorIRNode Parent { get; set; }
internal override SourceLocation SourceLocation { get; set; }
public string Name { get; set; }
public IEnumerable<DirectiveTokenIRNode> Tokens => Children.OfType<DirectiveTokenIRNode>();
public DirectiveDescriptor Descriptor { get; set; }
public override void Accept(RazorIRNodeVisitor visitor)
{
visitor.VisitDirective(this);
}
public override TResult Accept<TResult>(RazorIRNodeVisitor<TResult> visitor)
{
return visitor.VisitDirective(this);
}
}
}

View File

@ -0,0 +1,31 @@
// 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.Collections.Generic;
using Microsoft.AspNetCore.Razor.Evolution.Legacy;
namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate
{
public class DirectiveTokenIRNode : RazorIRNode
{
public override IList<RazorIRNode> Children { get; } = EmptyArray;
public override RazorIRNode Parent { get; set; }
internal override SourceLocation SourceLocation { get; set; }
public string Content { get; set; }
public DirectiveTokenDescriptor Descriptor { get; set; }
public override void Accept(RazorIRNodeVisitor visitor)
{
visitor.VisitDirectiveToken(this);
}
public override TResult Accept<TResult>(RazorIRNodeVisitor<TResult> visitor)
{
return visitor.VisitDirectiveToken(this);
}
}
}

View File

@ -14,6 +14,16 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate
{
}
public virtual void VisitDirectiveToken(DirectiveTokenIRNode node)
{
VisitDefault(node);
}
public virtual void VisitDirective(DirectiveIRNode node)
{
VisitDefault(node);
}
public virtual void VisitTemplate(TemplateIRNode node)
{
VisitDefault(node);

View File

@ -15,6 +15,16 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate
return default(TResult);
}
public virtual TResult VisitDirectiveToken(DirectiveTokenIRNode node)
{
return VisitDefault(node);
}
public virtual TResult VisitDirective(DirectiveIRNode node)
{
return VisitDefault(node);
}
public virtual TResult VisitTemplate(TemplateIRNode node)
{
return VisitDefault(node);

View File

@ -39,11 +39,16 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
private Dictionary<CSharpKeyword, Action<bool>> _keywordParsers = new Dictionary<CSharpKeyword, Action<bool>>();
public CSharpCodeParser(ParserContext context)
: this (directiveDescriptors: Enumerable.Empty<DirectiveDescriptor>(), context: context)
{
}
public CSharpCodeParser(IEnumerable<DirectiveDescriptor> directiveDescriptors, ParserContext context)
: base(CSharpLanguageCharacteristics.Instance, context)
{
Keywords = new HashSet<string>();
SetUpKeywords();
SetupDirectives();
SetupDirectives(directiveDescriptors);
SetUpExpressions();
}
@ -1404,8 +1409,13 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
}
}
private void SetupDirectives()
private void SetupDirectives(IEnumerable<DirectiveDescriptor> directiveDescriptors)
{
foreach (var directiveDescriptor in directiveDescriptors)
{
MapDirectives(() => HandleDirective(directiveDescriptor), directiveDescriptor.Name);
}
MapDirectives(TagHelperPrefixDirective, SyntaxConstants.CSharp.TagHelperPrefixKeyword);
MapDirectives(AddTagHelperDirective, SyntaxConstants.CSharp.AddTagHelperKeyword);
MapDirectives(RemoveTagHelperDirective, SyntaxConstants.CSharp.RemoveTagHelperKeyword);
@ -1414,6 +1424,183 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
MapDirectives(SectionDirective, SyntaxConstants.CSharp.SectionKeyword);
}
private void HandleDirective(DirectiveDescriptor descriptor)
{
Context.Builder.CurrentBlock.Type = BlockType.Directive;
Context.Builder.CurrentBlock.ChunkGenerator = new DirectiveChunkGenerator(descriptor);
AssertDirective(descriptor.Name);
AcceptAndMoveNext();
Output(SpanKind.MetaCode, AcceptedCharacters.None);
for (var i = 0; i < descriptor.Tokens.Count; i++)
{
var tokenDescriptor = descriptor.Tokens[i];
AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true));
if (tokenDescriptor.Kind == DirectiveTokenKind.Member || tokenDescriptor.Kind == DirectiveTokenKind.Type)
{
Span.ChunkGenerator = SpanChunkGenerator.Null;
Output(SpanKind.Code, AcceptedCharacters.WhiteSpace);
}
else
{
Span.ChunkGenerator = SpanChunkGenerator.Null;
Output(SpanKind.Markup, AcceptedCharacters.WhiteSpace);
}
var outputKind = SpanKind.Markup;
switch (tokenDescriptor.Kind)
{
case DirectiveTokenKind.Type:
if (!NamespaceOrTypeName())
{
// Error logged for invalid type name, continue onto next piece.
continue;
}
outputKind = SpanKind.Code;
break;
case DirectiveTokenKind.Member:
if (At(CSharpSymbolType.Identifier))
{
AcceptAndMoveNext();
}
else
{
Context.ErrorSink.OnError(
CurrentLocation,
LegacyResources.FormatDirectiveExpectsIdentifier(descriptor.Name),
CurrentSymbol.Content.Length);
return;
}
outputKind = SpanKind.Code;
break;
case DirectiveTokenKind.String:
AcceptAndMoveNext();
break;
case DirectiveTokenKind.Literal:
if (string.Equals(CurrentSymbol.Content, tokenDescriptor.Value, StringComparison.Ordinal))
{
AcceptAndMoveNext();
}
else
{
Context.ErrorSink.OnError(
CurrentLocation,
LegacyResources.FormatUnexpectedDirectiveLiteral(descriptor.Name, tokenDescriptor.Value),
CurrentSymbol.Content.Length);
return;
}
break;
}
Span.ChunkGenerator = new DirectiveTokenChunkGenerator(tokenDescriptor);
Output(outputKind, AcceptedCharacters.NonWhiteSpace);
}
AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true));
Span.ChunkGenerator = SpanChunkGenerator.Null;
switch (descriptor.Kind)
{
case DirectiveDescriptorKind.SingleLine:
Optional(CSharpSymbolType.Semicolon);
if (At(CSharpSymbolType.NewLine))
{
AcceptAndMoveNext();
}
else if (!EndOfFile)
{
Context.ErrorSink.OnError(
CurrentLocation,
LegacyResources.FormatUnexpectedDirectiveLiteral(descriptor.Name, Environment.NewLine),
CurrentSymbol.Content.Length);
}
Output(SpanKind.Markup, AcceptedCharacters.AllWhiteSpace);
break;
case DirectiveDescriptorKind.RazorBlock:
Output(SpanKind.Markup, AcceptedCharacters.WhiteSpace);
ParseDirectiveBlock(descriptor, parseChildren: (startingBraceLocation) =>
{
// When transitioning to the HTML parser we no longer want to act as if we're in a nested C# state.
// For instance, if <div>@hello.</div> is in a nested C# block we don't want the trailing '.' to be handled
// as C#; it should be handled as a period because it's wrapped in markup.
var wasNested = IsNested;
IsNested = false;
using (PushSpanConfig())
{
HtmlParser.ParseSection(Tuple.Create("{", "}"), caseSensitive: true);
}
Initialize(Span);
IsNested = wasNested;
NextToken();
});
break;
case DirectiveDescriptorKind.CodeBlock:
Output(SpanKind.Markup, AcceptedCharacters.WhiteSpace);
ParseDirectiveBlock(descriptor, parseChildren: (startingBraceLocation) =>
{
NextToken();
Balance(BalancingModes.NoErrorOnFailure, CSharpSymbolType.LeftBrace, CSharpSymbolType.RightBrace, startingBraceLocation);
Span.ChunkGenerator = new StatementChunkGenerator();
Output(SpanKind.Code);
});
break;
}
}
private void ParseDirectiveBlock(DirectiveDescriptor descriptor, Action<SourceLocation> parseChildren)
{
if (EndOfFile)
{
Context.ErrorSink.OnError(
CurrentLocation,
LegacyResources.FormatUnexpectedEOFAfterDirective(descriptor.Name, "{"),
length: 1 /* { */);
}
else if (!At(CSharpSymbolType.LeftBrace))
{
Context.ErrorSink.OnError(
CurrentLocation,
LegacyResources.FormatUnexpectedDirectiveLiteral(descriptor.Name, "{"),
CurrentSymbol.Content.Length);
}
else
{
var editHandler = new AutoCompleteEditHandler(Language.TokenizeString, autoCompleteAtEndOfSpan: true);
Span.EditHandler = editHandler;
var startingBraceLocation = CurrentLocation;
Accept(CurrentSymbol);
Span.ChunkGenerator = SpanChunkGenerator.Null;
Output(SpanKind.MetaCode, AcceptedCharacters.None);
parseChildren(startingBraceLocation);
Span.ChunkGenerator = SpanChunkGenerator.Null;
if (!Optional(CSharpSymbolType.RightBrace))
{
editHandler.AutoCompleteString = "}";
Context.ErrorSink.OnError(
startingBraceLocation,
LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF(descriptor.Name, "{", "}"),
length: 1 /* } */);
}
else
{
Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
}
CompleteBlock(insertMarkerIfNecessary: false, captureWhitespaceToEndOfLine: true);
Span.ChunkGenerator = SpanChunkGenerator.Null;
Output(SpanKind.MetaCode, AcceptedCharacters.None);
}
}
protected virtual void TagHelperPrefixDirective()
{
TagHelperDirective(

View File

@ -0,0 +1,45 @@
// 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.Extensions.Internal;
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
internal class DirectiveChunkGenerator : ParentChunkGenerator
{
private static readonly Type Type = typeof(DirectiveChunkGenerator);
public DirectiveChunkGenerator(DirectiveDescriptor descriptor)
{
Descriptor = descriptor;
}
public DirectiveDescriptor Descriptor { get; }
public override void AcceptStart(ParserVisitor visitor, Block block)
{
visitor.VisitStartDirectiveBlock(this, block);
}
public override void AcceptEnd(ParserVisitor visitor, Block block)
{
visitor.VisitEndDirectiveBlock(this, block);
}
public override bool Equals(object obj)
{
var other = obj as DirectiveChunkGenerator;
return base.Equals(other) &&
DirectiveDescriptorComparer.Default.Equals(Descriptor, other.Descriptor);
}
public override int GetHashCode()
{
var combiner = HashCodeCombiner.Start();
combiner.Add(base.GetHashCode());
combiner.Add(Type);
return combiner.CombinedHash;
}
}
}

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 System;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
internal class DirectiveTokenChunkGenerator : SpanChunkGenerator
{
private static readonly Type Type = typeof(DirectiveTokenChunkGenerator);
public DirectiveTokenChunkGenerator(DirectiveTokenDescriptor tokenDescriptor)
{
Descriptor = tokenDescriptor;
}
public DirectiveTokenDescriptor Descriptor { get; set; }
public override void Accept(ParserVisitor visitor, Span span)
{
visitor.VisitDirectiveToken(this, span);
}
public override bool Equals(object obj)
{
var other = obj as DirectiveTokenChunkGenerator;
return base.Equals(other) &&
DirectiveTokenDescriptorComparer.Default.Equals(Descriptor, other.Descriptor);
}
public override int GetHashCode()
{
var combiner = HashCodeCombiner.Start();
combiner.Add(base.GetHashCode());
combiner.Add(Type);
return combiner.CombinedHash;
}
}
}

View File

@ -89,10 +89,22 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
}
public virtual void VisitDirectiveToken(DirectiveTokenChunkGenerator chunkGenerator, Span block)
{
}
public virtual void VisitEndTemplateBlock(TemplateBlockChunkGenerator chunkGenerator, Block block)
{
}
public virtual void VisitStartDirectiveBlock(DirectiveChunkGenerator chunkGenerator, Block block)
{
}
public virtual void VisitEndDirectiveBlock(DirectiveChunkGenerator chunkGenerator, Block block)
{
}
public virtual void VisitStartTemplateBlock(TemplateBlockChunkGenerator chunkGenerator, Block block)
{
}

View File

@ -179,6 +179,9 @@
<data name="CSharpSymbol_Whitespace" xml:space="preserve">
<value>&lt;&lt;white space&gt;&gt;</value>
</data>
<data name="DirectiveExpectsIdentifier" xml:space="preserve">
<value>The '{0}' directive expects an identifier.</value>
</data>
<data name="EndBlock_Called_Without_Matching_StartBlock" xml:space="preserve">
<value>"EndBlock" was called without a matching call to "StartBlock".</value>
</data>
@ -381,4 +384,10 @@ Instead, wrap the contents of the block in "{{}}":
<data name="TokenizerView_CannotPutBack" xml:space="preserve">
<value>In order to put a symbol back, it must have been the symbol which ended at the current position. The specified symbol ends at {0}, but the current position is {1}</value>
</data>
<data name="UnexpectedDirectiveLiteral" xml:space="preserve">
<value>Unexpected literal following the '{0}' directive. Expected '{1}'.</value>
</data>
<data name="UnexpectedEOFAfterDirective" xml:space="preserve">
<value>Unexpected end of file following the '{0}' directive. Expected '{1}'.</value>
</data>
</root>

View File

@ -330,6 +330,22 @@ namespace Microsoft.AspNetCore.Razor.Evolution
return GetString("CSharpSymbol_Whitespace");
}
/// <summary>
/// The '{0}' directive expects an identifier.
/// </summary>
internal static string DirectiveExpectsIdentifier
{
get { return GetString("DirectiveExpectsIdentifier"); }
}
/// <summary>
/// The '{0}' directive expects an identifier.
/// </summary>
internal static string FormatDirectiveExpectsIdentifier(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("DirectiveExpectsIdentifier"), p0);
}
/// <summary>
/// "EndBlock" was called without a matching call to "StartBlock".
/// </summary>
@ -1318,6 +1334,38 @@ namespace Microsoft.AspNetCore.Razor.Evolution
return string.Format(CultureInfo.CurrentCulture, GetString("TokenizerView_CannotPutBack"), p0, p1);
}
/// <summary>
/// Unexpected literal following the '{0}' directive. Expected '{1}'.
/// </summary>
internal static string UnexpectedDirectiveLiteral
{
get { return GetString("UnexpectedDirectiveLiteral"); }
}
/// <summary>
/// Unexpected literal following the '{0}' directive. Expected '{1}'.
/// </summary>
internal static string FormatUnexpectedDirectiveLiteral(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("UnexpectedDirectiveLiteral"), p0, p1);
}
/// <summary>
/// Unexpected end of file following the '{0}' directive. Expected '{1}'.
/// </summary>
internal static string UnexpectedEOFAfterDirective
{
get { return GetString("UnexpectedEOFAfterDirective"); }
}
/// <summary>
/// Unexpected end of file following the '{0}' directive. Expected '{1}'.
/// </summary>
internal static string FormatUnexpectedEOFAfterDirective(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("UnexpectedEOFAfterDirective"), p0, p1);
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -0,0 +1,123 @@
// 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 DirectiveDescriptorBuilderTest
{
[Fact]
public void Create_BuildsSingleLineDirectiveDescriptor()
{
// Act
var descriptor = DirectiveDescriptorBuilder.Create("custom").Build();
// Assert
Assert.Equal(DirectiveDescriptorKind.SingleLine, descriptor.Kind);
}
[Fact]
public void CreateRazorBlock_BuildsRazorBlockDirectiveDescriptor()
{
// Act
var descriptor = DirectiveDescriptorBuilder.CreateRazorBlock("custom").Build();
// Assert
Assert.Equal(DirectiveDescriptorKind.RazorBlock, descriptor.Kind);
}
[Fact]
public void CreateCodeBlock_BuildsCodeBlockDirectiveDescriptor()
{
// Act
var descriptor = DirectiveDescriptorBuilder.CreateCodeBlock("custom").Build();
// Assert
Assert.Equal(DirectiveDescriptorKind.CodeBlock, descriptor.Kind);
}
[Fact]
public void AddType_AddsToken()
{
// Arrange
var builder = DirectiveDescriptorBuilder.Create("custom");
// Act
var descriptor = builder.AddType().Build();
// Assert
var token = Assert.Single(descriptor.Tokens);
Assert.Equal(DirectiveTokenKind.Type, token.Kind);
}
[Fact]
public void AddMember_AddsToken()
{
// Arrange
var builder = DirectiveDescriptorBuilder.Create("custom");
// Act
var descriptor = builder.AddMember().Build();
// Assert
var token = Assert.Single(descriptor.Tokens);
Assert.Equal(DirectiveTokenKind.Member, token.Kind);
}
[Fact]
public void AddString_AddsToken()
{
// Arrange
var builder = DirectiveDescriptorBuilder.Create("custom");
// Act
var descriptor = builder.AddString().Build();
// Assert
var token = Assert.Single(descriptor.Tokens);
Assert.Equal(DirectiveTokenKind.String, token.Kind);
}
[Fact]
public void AddLiteral_AddsToken()
{
// Arrange
var builder = DirectiveDescriptorBuilder.Create("custom");
// Act
var descriptor = builder.AddLiteral(",").Build();
// Assert
var token = Assert.Single(descriptor.Tokens);
Assert.Equal(DirectiveTokenKind.Literal, token.Kind);
Assert.Equal(",", token.Value);
}
[Fact]
public void AddX_MaintainsMultipleTokens()
{
// Arrange
var builder = DirectiveDescriptorBuilder.Create("custom");
// Act
var descriptor = builder
.AddType()
.AddMember()
.AddString()
.AddLiteral(",")
.Build();
// Assert
Assert.Collection(descriptor.Tokens,
token => Assert.Equal(DirectiveTokenKind.Type, token.Kind),
token => Assert.Equal(DirectiveTokenKind.Member, token.Kind),
token => Assert.Equal(DirectiveTokenKind.String, token.Kind),
token =>
{
Assert.Equal(DirectiveTokenKind.Literal, token.Kind);
Assert.Equal(",", token.Value);
});
}
}
}

View File

@ -1,12 +1,378 @@
// 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 System.Collections.Generic;
using Xunit;
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
{
public class CSharpDirectivesTest : CsHtmlCodeParserTestBase
{
[Fact]
public void DirectiveDescriptor_UnderstandsTypeTokens()
{
// Arrange
var descriptor = DirectiveDescriptorBuilder.Create("custom").AddType().Build();
// Act & Assert
ParseCodeBlockTest(
"@custom System.Text.Encoding.ASCIIEncoding",
new[] { descriptor },
new DirectiveBlock(
new DirectiveChunkGenerator(descriptor),
Factory.CodeTransition(),
Factory.MetaCode("custom").Accepts(AcceptedCharacters.None),
Factory.Span(SpanKind.Code, " ", markup: false).Accepts(AcceptedCharacters.WhiteSpace),
Factory.Span(SpanKind.Code, "System.Text.Encoding.ASCIIEncoding", markup: false)
.With(new DirectiveTokenChunkGenerator(descriptor.Tokens[0]))
.Accepts(AcceptedCharacters.NonWhiteSpace)));
}
[Fact]
public void DirectiveDescriptor_UnderstandsMemberTokens()
{
// Arrange
var descriptor = DirectiveDescriptorBuilder.Create("custom").AddMember().Build();
// Act & Assert
ParseCodeBlockTest(
"@custom Some_Member",
new[] { descriptor },
new DirectiveBlock(
new DirectiveChunkGenerator(descriptor),
Factory.CodeTransition(),
Factory.MetaCode("custom").Accepts(AcceptedCharacters.None),
Factory.Span(SpanKind.Code, " ", markup: false).Accepts(AcceptedCharacters.WhiteSpace),
Factory.Span(SpanKind.Code, "Some_Member", markup: false)
.With(new DirectiveTokenChunkGenerator(descriptor.Tokens[0]))
.Accepts(AcceptedCharacters.NonWhiteSpace)));
}
[Fact]
public void DirectiveDescriptor_UnderstandsStringTokens()
{
// Arrange
var descriptor = DirectiveDescriptorBuilder.Create("custom").AddString().Build();
// Act & Assert
ParseCodeBlockTest(
"@custom AString",
new[] { descriptor },
new DirectiveBlock(
new DirectiveChunkGenerator(descriptor),
Factory.CodeTransition(),
Factory.MetaCode("custom").Accepts(AcceptedCharacters.None),
Factory.Span(SpanKind.Markup, " ", markup: false).Accepts(AcceptedCharacters.WhiteSpace),
Factory.Span(SpanKind.Markup, "AString", markup: false)
.With(new DirectiveTokenChunkGenerator(descriptor.Tokens[0]))
.Accepts(AcceptedCharacters.NonWhiteSpace)));
}
[Fact]
public void DirectiveDescriptor_UnderstandsLiteralTokens()
{
// Arrange
var descriptor = DirectiveDescriptorBuilder.Create("custom").AddLiteral("!").Build();
// Act & Assert
ParseCodeBlockTest(
"@custom !",
new[] { descriptor },
new DirectiveBlock(
new DirectiveChunkGenerator(descriptor),
Factory.CodeTransition(),
Factory.MetaCode("custom").Accepts(AcceptedCharacters.None),
Factory.Span(SpanKind.Markup, " ", markup: false).Accepts(AcceptedCharacters.WhiteSpace),
Factory.Span(SpanKind.Markup, "!", markup: false)
.With(new DirectiveTokenChunkGenerator(descriptor.Tokens[0]))
.Accepts(AcceptedCharacters.NonWhiteSpace)));
}
[Fact]
public void DirectiveDescriptor_UnderstandsMultipleTokens()
{
// Arrange
var descriptor = DirectiveDescriptorBuilder.Create("custom")
.AddType()
.AddMember()
.AddString()
.AddLiteral("!")
.Build();
// Act & Assert
ParseCodeBlockTest(
"@custom System.Text.Encoding.ASCIIEncoding Some_Member AString !",
new[] { descriptor },
new DirectiveBlock(
new DirectiveChunkGenerator(descriptor),
Factory.CodeTransition(),
Factory.MetaCode("custom").Accepts(AcceptedCharacters.None),
Factory.Span(SpanKind.Code, " ", markup: false).Accepts(AcceptedCharacters.WhiteSpace),
Factory.Span(SpanKind.Code, "System.Text.Encoding.ASCIIEncoding", markup: false)
.With(new DirectiveTokenChunkGenerator(descriptor.Tokens[0]))
.Accepts(AcceptedCharacters.NonWhiteSpace),
Factory.Span(SpanKind.Code, " ", markup: false).Accepts(AcceptedCharacters.WhiteSpace),
Factory.Span(SpanKind.Code, "Some_Member", markup: false)
.With(new DirectiveTokenChunkGenerator(descriptor.Tokens[1]))
.Accepts(AcceptedCharacters.NonWhiteSpace),
Factory.Span(SpanKind.Markup, " ", markup: false).Accepts(AcceptedCharacters.WhiteSpace),
Factory.Span(SpanKind.Markup, "AString", markup: false)
.With(new DirectiveTokenChunkGenerator(descriptor.Tokens[2]))
.Accepts(AcceptedCharacters.NonWhiteSpace),
Factory.Span(SpanKind.Markup, " ", markup: false).Accepts(AcceptedCharacters.WhiteSpace),
Factory.Span(SpanKind.Markup, "!", markup: false)
.With(new DirectiveTokenChunkGenerator(descriptor.Tokens[3]))
.Accepts(AcceptedCharacters.NonWhiteSpace)));
}
[Fact]
public void DirectiveDescriptor_UnderstandsRazorBlocks()
{
// Arrange
var descriptor = DirectiveDescriptorBuilder.CreateRazorBlock("custom").AddString().Build();
// Act & Assert
ParseCodeBlockTest(
"@custom Header { <p>F{o}o</p> }",
new[] { descriptor },
new DirectiveBlock(
new DirectiveChunkGenerator(descriptor),
Factory.CodeTransition(),
Factory.MetaCode("custom").Accepts(AcceptedCharacters.None),
Factory.Span(SpanKind.Markup, " ", markup: false).Accepts(AcceptedCharacters.WhiteSpace),
Factory.Span(SpanKind.Markup, "Header", markup: false)
.With(new DirectiveTokenChunkGenerator(descriptor.Tokens[0]))
.Accepts(AcceptedCharacters.NonWhiteSpace),
Factory.Span(SpanKind.Markup, " ", markup: false).Accepts(AcceptedCharacters.WhiteSpace),
Factory.MetaCode("{")
.AutoCompleteWith(null, atEndOfSpan: true)
.Accepts(AcceptedCharacters.None),
new MarkupBlock(
Factory.Markup(" "),
new MarkupTagBlock(
Factory.Markup("<p>")),
Factory.Markup("F", "{", "o", "}", "o"),
new MarkupTagBlock(
Factory.Markup("</p>")),
Factory.Markup(" ")),
Factory.MetaCode("}").Accepts(AcceptedCharacters.None)));
}
[Fact]
public void DirectiveDescriptor_UnderstandsCodeBlocks()
{
// Arrange
var descriptor = DirectiveDescriptorBuilder.CreateCodeBlock("custom").AddString().Build();
// Act & Assert
ParseCodeBlockTest(
"@custom Name { foo(); bar(); }",
new[] { descriptor },
new DirectiveBlock(
new DirectiveChunkGenerator(descriptor),
Factory.CodeTransition(),
Factory.MetaCode("custom").Accepts(AcceptedCharacters.None),
Factory.Span(SpanKind.Markup, " ", markup: false).Accepts(AcceptedCharacters.WhiteSpace),
Factory.Span(SpanKind.Markup, "Name", markup: false)
.With(new DirectiveTokenChunkGenerator(descriptor.Tokens[0]))
.Accepts(AcceptedCharacters.NonWhiteSpace),
Factory.Span(SpanKind.Markup, " ", markup: false).Accepts(AcceptedCharacters.WhiteSpace),
Factory.MetaCode("{")
.AutoCompleteWith(null, atEndOfSpan: true)
.Accepts(AcceptedCharacters.None),
Factory.Code(" foo(); bar(); ").AsStatement(),
Factory.MetaCode("}").Accepts(AcceptedCharacters.None)));
}
[Fact]
public void DirectiveDescriptor_AllowsWhiteSpaceAroundTokens()
{
// Arrange
var descriptor = DirectiveDescriptorBuilder.Create("custom")
.AddType()
.AddMember()
.Build();
// Act & Assert
ParseCodeBlockTest(
"@custom System.Text.Encoding.ASCIIEncoding Some_Member ",
new[] { descriptor },
new DirectiveBlock(
new DirectiveChunkGenerator(descriptor),
Factory.CodeTransition(),
Factory.MetaCode("custom").Accepts(AcceptedCharacters.None),
Factory.Span(SpanKind.Code, " ", markup: false).Accepts(AcceptedCharacters.WhiteSpace),
Factory.Span(SpanKind.Code, "System.Text.Encoding.ASCIIEncoding", markup: false)
.With(new DirectiveTokenChunkGenerator(descriptor.Tokens[0]))
.Accepts(AcceptedCharacters.NonWhiteSpace),
Factory.Span(SpanKind.Code, " ", markup: false).Accepts(AcceptedCharacters.WhiteSpace),
Factory.Span(SpanKind.Code, "Some_Member", markup: false)
.With(new DirectiveTokenChunkGenerator(descriptor.Tokens[1]))
.Accepts(AcceptedCharacters.NonWhiteSpace),
Factory.Span(SpanKind.Markup, " ", markup: false)
.Accepts(AcceptedCharacters.AllWhiteSpace)));
}
[Fact]
public void DirectiveDescriptor_ErrorsForInvalidMemberTokens()
{
// Arrange
var descriptor = DirectiveDescriptorBuilder.Create("custom").AddMember().Build();
var expectedErorr = new RazorError(
LegacyResources.FormatDirectiveExpectsIdentifier("custom"),
new SourceLocation(8, 0, 8),
length: 1);
// Act & Assert
ParseCodeBlockTest(
"@custom -Some_Member",
new[] { descriptor },
new DirectiveBlock(
new DirectiveChunkGenerator(descriptor),
Factory.CodeTransition(),
Factory.MetaCode("custom").Accepts(AcceptedCharacters.None),
Factory.Span(SpanKind.Code, " ", markup: false).Accepts(AcceptedCharacters.WhiteSpace)),
expectedErorr);
}
[Fact]
public void DirectiveDescriptor_ErrorsForUnmatchedLiteralTokens()
{
// Arrange
var descriptor = DirectiveDescriptorBuilder.Create("custom").AddLiteral("!").Build();
var expectedErorr = new RazorError(
LegacyResources.FormatUnexpectedDirectiveLiteral("custom", "!"),
new SourceLocation(8, 0, 8),
length: 2);
// Act & Assert
ParseCodeBlockTest(
"@custom hi",
new[] { descriptor },
new DirectiveBlock(
new DirectiveChunkGenerator(descriptor),
Factory.CodeTransition(),
Factory.MetaCode("custom").Accepts(AcceptedCharacters.None),
Factory.Span(SpanKind.Markup, " ", markup: false).Accepts(AcceptedCharacters.WhiteSpace)),
expectedErorr);
}
[Fact]
public void DirectiveDescriptor_ErrorsExtraContentAfterDirective()
{
// Arrange
var descriptor = DirectiveDescriptorBuilder.Create("custom").AddString().Build();
var expectedErorr = new RazorError(
LegacyResources.FormatUnexpectedDirectiveLiteral("custom", Environment.NewLine),
new SourceLocation(14, 0, 14),
length: 5);
// Act & Assert
ParseCodeBlockTest(
"@custom hello world",
new[] { descriptor },
new DirectiveBlock(
new DirectiveChunkGenerator(descriptor),
Factory.CodeTransition(),
Factory.MetaCode("custom").Accepts(AcceptedCharacters.None),
Factory.Span(SpanKind.Markup, " ", markup: false).Accepts(AcceptedCharacters.WhiteSpace),
Factory.Span(SpanKind.Markup, "hello", markup: false)
.With(new DirectiveTokenChunkGenerator(descriptor.Tokens[0]))
.Accepts(AcceptedCharacters.NonWhiteSpace),
Factory.Span(SpanKind.Markup, " ", markup: false).Accepts(AcceptedCharacters.AllWhiteSpace)),
expectedErorr);
}
[Fact]
public void DirectiveDescriptor_ErrorsWhenExtraContentBeforeBlockStart()
{
// Arrange
var descriptor = DirectiveDescriptorBuilder.CreateCodeBlock("custom").AddString().Build();
var expectedErorr = new RazorError(
LegacyResources.FormatUnexpectedDirectiveLiteral("custom", "{"),
new SourceLocation(14, 0, 14),
length: 5);
// Act & Assert
ParseCodeBlockTest(
"@custom Hello World { foo(); bar(); }",
new[] { descriptor },
new DirectiveBlock(
new DirectiveChunkGenerator(descriptor),
Factory.CodeTransition(),
Factory.MetaCode("custom").Accepts(AcceptedCharacters.None),
Factory.Span(SpanKind.Markup, " ", markup: false).Accepts(AcceptedCharacters.WhiteSpace),
Factory.Span(SpanKind.Markup, "Hello", markup: false)
.With(new DirectiveTokenChunkGenerator(descriptor.Tokens[0]))
.Accepts(AcceptedCharacters.NonWhiteSpace),
Factory.Span(SpanKind.Markup, " ", markup: false).Accepts(AcceptedCharacters.WhiteSpace)),
expectedErorr);
}
[Fact]
public void DirectiveDescriptor_ErrorsWhenEOFBeforeDirectiveBlockStart()
{
// Arrange
var descriptor = DirectiveDescriptorBuilder.CreateCodeBlock("custom").AddString().Build();
var expectedErorr = new RazorError(
LegacyResources.FormatUnexpectedEOFAfterDirective("custom", "{"),
new SourceLocation(13, 0, 13),
length: 1);
// Act & Assert
ParseCodeBlockTest(
"@custom Hello",
new[] { descriptor },
new DirectiveBlock(
new DirectiveChunkGenerator(descriptor),
Factory.CodeTransition(),
Factory.MetaCode("custom").Accepts(AcceptedCharacters.None),
Factory.Span(SpanKind.Markup, " ", markup: false).Accepts(AcceptedCharacters.WhiteSpace),
Factory.Span(SpanKind.Markup, "Hello", markup: false)
.With(new DirectiveTokenChunkGenerator(descriptor.Tokens[0]))
.Accepts(AcceptedCharacters.NonWhiteSpace)),
expectedErorr);
}
[Fact]
public void DirectiveDescriptor_ErrorsWhenMissingEndBrace()
{
// Arrange
var descriptor = DirectiveDescriptorBuilder.CreateCodeBlock("custom").AddString().Build();
var expectedErorr = new RazorError(
LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF("custom", "{", "}"),
new SourceLocation(14, 0, 14),
length: 1);
// Act & Assert
ParseCodeBlockTest(
"@custom Hello {",
new[] { descriptor },
new DirectiveBlock(
new DirectiveChunkGenerator(descriptor),
Factory.CodeTransition(),
Factory.MetaCode("custom").Accepts(AcceptedCharacters.None),
Factory.Span(SpanKind.Markup, " ", markup: false).Accepts(AcceptedCharacters.WhiteSpace),
Factory.Span(SpanKind.Markup, "Hello", markup: false)
.With(new DirectiveTokenChunkGenerator(descriptor.Tokens[0]))
.Accepts(AcceptedCharacters.NonWhiteSpace),
Factory.Span(SpanKind.Markup, " ", markup: false).Accepts(AcceptedCharacters.WhiteSpace),
Factory.MetaCode("{")
.AutoCompleteWith("}", atEndOfSpan: true)
.Accepts(AcceptedCharacters.None)),
expectedErorr);
}
[Fact]
public void TagHelperPrefixDirective_NoValueSucceeds()
{
@ -422,5 +788,16 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
Factory.MetaCode("}")
.Accepts(AcceptedCharacters.None)));
}
internal virtual void ParseCodeBlockTest(
string document,
IEnumerable<DirectiveDescriptor> descriptors,
Block expected,
params RazorError[] expectedErrors)
{
var result = ParseCodeBlock(document, descriptors, designTime: false);
EvaluateResults(result, expected, expectedErrors);
}
}
}

View File

@ -59,12 +59,20 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
}
internal virtual RazorSyntaxTree ParseCodeBlock(string document, bool designTime = false)
{
return ParseCodeBlock(document, Enumerable.Empty<DirectiveDescriptor>(), designTime);
}
internal virtual RazorSyntaxTree ParseCodeBlock(
string document,
IEnumerable<DirectiveDescriptor> descriptors,
bool designTime)
{
using (var reader = new SeekableTextReader(document))
{
var context = new ParserContext(reader, designTime);
var parser = new CSharpCodeParser(context);
var parser = new CSharpCodeParser(descriptors, context);
parser.HtmlParser = new HtmlMarkupParser(context)
{
CodeParser = parser,