From 518378f4990efff05ae4122a9206f6d8485fe669 Mon Sep 17 00:00:00 2001 From: "N. Taylor Mullen" Date: Tue, 22 Nov 2016 16:42:45 -0800 Subject: [PATCH] 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 --- .../DefaultRazorIRLoweringPhase.cs | 24 ++ .../DirectiveDescriptor.cs | 16 + .../DirectiveDescriptorBuilder.cs | 96 +++++ .../DirectiveDescriptorComparer.cs | 49 +++ .../DirectiveDescriptorKind.cs | 12 + .../DirectiveTokenDescriptor.cs | 12 + .../DirectiveTokenDescriptorComparer.cs | 44 ++ .../DirectiveTokenKind.cs | 13 + .../IDirectiveDescriptorBuilder.cs | 18 + .../Intermediate/DirectiveIRNode.cs | 34 ++ .../Intermediate/DirectiveTokenIRNode.cs | 31 ++ .../Intermediate/RazorIRNodeVisitor.cs | 10 + .../Intermediate/RazorIRNodeVisitorOfT.cs | 10 + .../Legacy/CSharpCodeParser.cs | 191 ++++++++- .../Legacy/DirectiveChunkGenerator.cs | 45 +++ .../Legacy/DirectiveTokenChunkGenerator.cs | 41 ++ .../Legacy/ParserVisitor.cs | 12 + .../LegacyResources.resx | 9 + .../Properties/LegacyResources.Designer.cs | 48 +++ .../DirectiveDescriptorBuilderTest.cs | 123 ++++++ .../Legacy/CSharpDirectivesTest.cs | 377 ++++++++++++++++++ .../Legacy/ParserTestBase.cs | 10 +- 22 files changed, 1222 insertions(+), 3 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/DirectiveDescriptor.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/DirectiveDescriptorBuilder.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/DirectiveDescriptorComparer.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/DirectiveDescriptorKind.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/DirectiveTokenDescriptor.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/DirectiveTokenDescriptorComparer.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/DirectiveTokenKind.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/IDirectiveDescriptorBuilder.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/DirectiveIRNode.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/DirectiveTokenIRNode.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/Legacy/DirectiveChunkGenerator.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/Legacy/DirectiveTokenChunkGenerator.cs create mode 100644 test/Microsoft.AspNetCore.Razor.Evolution.Test/DirectiveDescriptorBuilderTest.cs diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorIRLoweringPhase.cs b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorIRLoweringPhase.cs index a2f6c6cd7..b2b528dab 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorIRLoweringPhase.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorIRLoweringPhase.cs @@ -206,6 +206,30 @@ public override void VisitImportSpan(AddImportChunkGenerator chunkGenerator, Spa 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; diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/DirectiveDescriptor.cs b/src/Microsoft.AspNetCore.Razor.Evolution/DirectiveDescriptor.cs new file mode 100644 index 000000000..f6b869a97 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/DirectiveDescriptor.cs @@ -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 Tokens { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/DirectiveDescriptorBuilder.cs b/src/Microsoft.AspNetCore.Razor.Evolution/DirectiveDescriptorBuilder.cs new file mode 100644 index 000000000..d8d58e87d --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/DirectiveDescriptorBuilder.cs @@ -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 _tokenDescriptors; + private readonly string _name; + private readonly DirectiveDescriptorKind _type; + + public DefaultDirectiveDescriptorBuilder(string name, DirectiveDescriptorKind type) + { + _name = name; + _type = type; + _tokenDescriptors = new List(); + } + + 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; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/DirectiveDescriptorComparer.cs b/src/Microsoft.AspNetCore.Razor.Evolution/DirectiveDescriptorComparer.cs new file mode 100644 index 000000000..d46ba23dd --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/DirectiveDescriptorComparer.cs @@ -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 + { + 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; + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/DirectiveDescriptorKind.cs b/src/Microsoft.AspNetCore.Razor.Evolution/DirectiveDescriptorKind.cs new file mode 100644 index 000000000..363b6d643 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/DirectiveDescriptorKind.cs @@ -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 + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/DirectiveTokenDescriptor.cs b/src/Microsoft.AspNetCore.Razor.Evolution/DirectiveTokenDescriptor.cs new file mode 100644 index 000000000..fe06c94bb --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/DirectiveTokenDescriptor.cs @@ -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; } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/DirectiveTokenDescriptorComparer.cs b/src/Microsoft.AspNetCore.Razor.Evolution/DirectiveTokenDescriptorComparer.cs new file mode 100644 index 000000000..1a2fe3a27 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/DirectiveTokenDescriptorComparer.cs @@ -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 + { + 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; + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/DirectiveTokenKind.cs b/src/Microsoft.AspNetCore.Razor.Evolution/DirectiveTokenKind.cs new file mode 100644 index 000000000..0d5291480 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/DirectiveTokenKind.cs @@ -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 + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/IDirectiveDescriptorBuilder.cs b/src/Microsoft.AspNetCore.Razor.Evolution/IDirectiveDescriptorBuilder.cs new file mode 100644 index 000000000..8a537ca66 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/IDirectiveDescriptorBuilder.cs @@ -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(); + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/DirectiveIRNode.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/DirectiveIRNode.cs new file mode 100644 index 000000000..35999c85e --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/DirectiveIRNode.cs @@ -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 Children { get; } = new List(); + + public override RazorIRNode Parent { get; set; } + + internal override SourceLocation SourceLocation { get; set; } + + public string Name { get; set; } + + public IEnumerable Tokens => Children.OfType(); + + public DirectiveDescriptor Descriptor { get; set; } + + public override void Accept(RazorIRNodeVisitor visitor) + { + visitor.VisitDirective(this); + } + + public override TResult Accept(RazorIRNodeVisitor visitor) + { + return visitor.VisitDirective(this); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/DirectiveTokenIRNode.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/DirectiveTokenIRNode.cs new file mode 100644 index 000000000..cd5555430 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/DirectiveTokenIRNode.cs @@ -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 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(RazorIRNodeVisitor visitor) + { + return visitor.VisitDirectiveToken(this); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/RazorIRNodeVisitor.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/RazorIRNodeVisitor.cs index d96ca11d5..52297168e 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/RazorIRNodeVisitor.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/RazorIRNodeVisitor.cs @@ -14,6 +14,16 @@ public virtual void VisitDefault(RazorIRNode node) { } + public virtual void VisitDirectiveToken(DirectiveTokenIRNode node) + { + VisitDefault(node); + } + + public virtual void VisitDirective(DirectiveIRNode node) + { + VisitDefault(node); + } + public virtual void VisitTemplate(TemplateIRNode node) { VisitDefault(node); diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/RazorIRNodeVisitorOfT.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/RazorIRNodeVisitorOfT.cs index a02e0e11b..53fac5db7 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/RazorIRNodeVisitorOfT.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/RazorIRNodeVisitorOfT.cs @@ -15,6 +15,16 @@ public virtual TResult VisitDefault(RazorIRNode node) 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); diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpCodeParser.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpCodeParser.cs index 72e13c2bd..f6e7593bd 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpCodeParser.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpCodeParser.cs @@ -39,11 +39,16 @@ internal class CSharpCodeParser : TokenizerBackedParser> _keywordParsers = new Dictionary>(); public CSharpCodeParser(ParserContext context) + : this (directiveDescriptors: Enumerable.Empty(), context: context) + { + } + + public CSharpCodeParser(IEnumerable directiveDescriptors, ParserContext context) : base(CSharpLanguageCharacteristics.Instance, context) { Keywords = new HashSet(); SetUpKeywords(); - SetupDirectives(); + SetupDirectives(directiveDescriptors); SetUpExpressions(); } @@ -1404,8 +1409,13 @@ private void AwaitExpression(bool topLevel) } } - private void SetupDirectives() + private void SetupDirectives(IEnumerable 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 @@ private void SetupDirectives() 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
@hello.
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 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( diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/DirectiveChunkGenerator.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/DirectiveChunkGenerator.cs new file mode 100644 index 000000000..b3d2570bd --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/DirectiveChunkGenerator.cs @@ -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; + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/DirectiveTokenChunkGenerator.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/DirectiveTokenChunkGenerator.cs new file mode 100644 index 000000000..a47e4c65c --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/DirectiveTokenChunkGenerator.cs @@ -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; + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/ParserVisitor.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/ParserVisitor.cs index 987e96aa6..5f45e6811 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/ParserVisitor.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/ParserVisitor.cs @@ -89,10 +89,22 @@ public virtual void VisitLiteralAttributeSpan(LiteralAttributeChunkGenerator chu { } + 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) { } diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/LegacyResources.resx b/src/Microsoft.AspNetCore.Razor.Evolution/LegacyResources.resx index e995b2f85..1c7605f28 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/LegacyResources.resx +++ b/src/Microsoft.AspNetCore.Razor.Evolution/LegacyResources.resx @@ -179,6 +179,9 @@ <<white space>> + + The '{0}' directive expects an identifier. + "EndBlock" was called without a matching call to "StartBlock". @@ -381,4 +384,10 @@ Instead, wrap the contents of the block in "{{}}": 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} + + Unexpected literal following the '{0}' directive. Expected '{1}'. + + + Unexpected end of file following the '{0}' directive. Expected '{1}'. + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Properties/LegacyResources.Designer.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Properties/LegacyResources.Designer.cs index 76c0a0b5e..fe56041a0 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/Properties/LegacyResources.Designer.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Properties/LegacyResources.Designer.cs @@ -330,6 +330,22 @@ internal static string FormatCSharpSymbol_Whitespace() return GetString("CSharpSymbol_Whitespace"); } + /// + /// The '{0}' directive expects an identifier. + /// + internal static string DirectiveExpectsIdentifier + { + get { return GetString("DirectiveExpectsIdentifier"); } + } + + /// + /// The '{0}' directive expects an identifier. + /// + internal static string FormatDirectiveExpectsIdentifier(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("DirectiveExpectsIdentifier"), p0); + } + /// /// "EndBlock" was called without a matching call to "StartBlock". /// @@ -1318,6 +1334,38 @@ internal static string FormatTokenizerView_CannotPutBack(object p0, object p1) return string.Format(CultureInfo.CurrentCulture, GetString("TokenizerView_CannotPutBack"), p0, p1); } + /// + /// Unexpected literal following the '{0}' directive. Expected '{1}'. + /// + internal static string UnexpectedDirectiveLiteral + { + get { return GetString("UnexpectedDirectiveLiteral"); } + } + + /// + /// Unexpected literal following the '{0}' directive. Expected '{1}'. + /// + internal static string FormatUnexpectedDirectiveLiteral(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("UnexpectedDirectiveLiteral"), p0, p1); + } + + /// + /// Unexpected end of file following the '{0}' directive. Expected '{1}'. + /// + internal static string UnexpectedEOFAfterDirective + { + get { return GetString("UnexpectedEOFAfterDirective"); } + } + + /// + /// Unexpected end of file following the '{0}' directive. Expected '{1}'. + /// + 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); diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/DirectiveDescriptorBuilderTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DirectiveDescriptorBuilderTest.cs new file mode 100644 index 000000000..270d96164 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DirectiveDescriptorBuilderTest.cs @@ -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); + }); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpDirectivesTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpDirectivesTest.cs index 237c62cbe..0f4e0b443 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpDirectivesTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpDirectivesTest.cs @@ -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 {

F{o}o

}", + 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("

")), + Factory.Markup("F", "{", "o", "}", "o"), + new MarkupTagBlock( + Factory.Markup("

")), + 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 @@ public void SectionDirective() Factory.MetaCode("}") .Accepts(AcceptedCharacters.None))); } + + internal virtual void ParseCodeBlockTest( + string document, + IEnumerable descriptors, + Block expected, + params RazorError[] expectedErrors) + { + var result = ParseCodeBlock(document, descriptors, designTime: false); + + EvaluateResults(result, expected, expectedErrors); + } } } diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/ParserTestBase.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/ParserTestBase.cs index 4c674a48a..c1f6d55f8 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/ParserTestBase.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/ParserTestBase.cs @@ -59,12 +59,20 @@ internal virtual RazorSyntaxTree ParseHtmlBlock(string document, bool designTime } internal virtual RazorSyntaxTree ParseCodeBlock(string document, bool designTime = false) + { + return ParseCodeBlock(document, Enumerable.Empty(), designTime); + } + + internal virtual RazorSyntaxTree ParseCodeBlock( + string document, + IEnumerable 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,