diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorSyntaxTree.cs b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorSyntaxTree.cs new file mode 100644 index 0000000000..2890b981a3 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorSyntaxTree.cs @@ -0,0 +1,21 @@ +// 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 +{ + internal class DefaultRazorSyntaxTree : RazorSyntaxTree + { + public DefaultRazorSyntaxTree(Block root, IReadOnlyList diagnostics) + { + Root = root; + Diagnostics = diagnostics; + } + + internal override IReadOnlyList Diagnostics { get; } + + internal override Block Root { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/AcceptedCharacters.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/AcceptedCharacters.cs new file mode 100644 index 0000000000..1e11a476cf --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/AcceptedCharacters.cs @@ -0,0 +1,22 @@ +// 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; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + [Flags] + internal enum AcceptedCharacters + { + None = 0, + NewLine = 1, + WhiteSpace = 2, + + NonWhiteSpace = 4, + + AllWhiteSpace = NewLine | WhiteSpace, + Any = AllWhiteSpace | NonWhiteSpace, + + AnyExceptNewline = NonWhiteSpace | WhiteSpace + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/AddImportChunkGenerator.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/AddImportChunkGenerator.cs new file mode 100644 index 0000000000..ae0147117c --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/AddImportChunkGenerator.cs @@ -0,0 +1,47 @@ +// 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; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class AddImportChunkGenerator : SpanChunkGenerator + { + public AddImportChunkGenerator(string ns) + { + Namespace = ns; + } + + public string Namespace { get; } + + public override void GenerateChunk(Span target, ChunkGeneratorContext context) + { + var ns = Namespace; + + if (!string.IsNullOrEmpty(ns) && char.IsWhiteSpace(ns[0])) + { + ns = ns.Substring(1); + } + + //context.ChunkTreeBuilder.AddUsingChunk(ns, target); + } + + public override string ToString() + { + return "Import:" + Namespace + ";"; + } + + public override bool Equals(object obj) + { + var other = obj as AddImportChunkGenerator; + return other != null && + string.Equals(Namespace, other.Namespace, StringComparison.Ordinal); + } + + public override int GetHashCode() + { + // Hash code should include only immutable properties. + return Namespace == null ? 0 : StringComparer.Ordinal.GetHashCode(Namespace); + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/AddTagHelperChunkGenerator.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/AddTagHelperChunkGenerator.cs new file mode 100644 index 0000000000..e9577d7e4f --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/AddTagHelperChunkGenerator.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 AddTagHelperChunkGenerator : SpanChunkGenerator + { + public AddTagHelperChunkGenerator(string lookupText) + { + LookupText = lookupText; + } + + public string LookupText { get; } + + public override void GenerateChunk(Span target, ChunkGeneratorContext context) + { + //context.ChunkTreeBuilder.AddAddTagHelperChunk(LookupText, target); + } + + /// + public override bool Equals(object obj) + { + var other = obj as AddTagHelperChunkGenerator; + return base.Equals(other) && + string.Equals(LookupText, other.LookupText, StringComparison.Ordinal); + } + + /// + public override int GetHashCode() + { + var combiner = HashCodeCombiner.Start(); + combiner.Add(base.GetHashCode()); + combiner.Add(LookupText, StringComparer.Ordinal); + + return combiner.CombinedHash; + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/AttributeBlockChunkGenerator.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/AttributeBlockChunkGenerator.cs new file mode 100644 index 0000000000..662441405f --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/AttributeBlockChunkGenerator.cs @@ -0,0 +1,63 @@ +// 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.Globalization; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class AttributeBlockChunkGenerator : ParentChunkGenerator + { + public AttributeBlockChunkGenerator(string name, LocationTagged prefix, LocationTagged suffix) + { + Name = name; + Prefix = prefix; + Suffix = suffix; + } + + public string Name { get; } + + public LocationTagged Prefix { get; } + + public LocationTagged Suffix { get; } + + public override void GenerateStartParentChunk(Block target, ChunkGeneratorContext context) + { + //var chunk = context.ChunkTreeBuilder.StartParentChunk(target); + + //chunk.Attribute = Name; + //chunk.Prefix = Prefix; + //chunk.Suffix = Suffix; + } + + public override void GenerateEndParentChunk(Block target, ChunkGeneratorContext context) + { + //context.ChunkTreeBuilder.EndParentChunk(); + } + + public override string ToString() + { + return string.Format(CultureInfo.CurrentCulture, "Attr:{0},{1:F},{2:F}", Name, Prefix, Suffix); + } + + public override bool Equals(object obj) + { + var other = obj as AttributeBlockChunkGenerator; + return other != null && + string.Equals(other.Name, Name, StringComparison.Ordinal) && + Equals(other.Prefix, Prefix) && + Equals(other.Suffix, Suffix); + } + + public override int GetHashCode() + { + var hashCodeCombiner = HashCodeCombiner.Start(); + hashCodeCombiner.Add(Name, StringComparer.Ordinal); + hashCodeCombiner.Add(Prefix); + hashCodeCombiner.Add(Suffix); + + return hashCodeCombiner; + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/AutoCompleteEditHandler.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/AutoCompleteEditHandler.cs new file mode 100644 index 0000000000..f81777c9aa --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/AutoCompleteEditHandler.cs @@ -0,0 +1,69 @@ +// 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.Legacy +{ + internal class AutoCompleteEditHandler : SpanEditHandler + { + private static readonly int TypeHashCode = typeof(AutoCompleteEditHandler).GetHashCode(); + + public AutoCompleteEditHandler(Func> tokenizer) + : base(tokenizer) + { + } + + public AutoCompleteEditHandler(Func> tokenizer, bool autoCompleteAtEndOfSpan) + : this(tokenizer) + { + AutoCompleteAtEndOfSpan = autoCompleteAtEndOfSpan; + } + + public AutoCompleteEditHandler(Func> tokenizer, AcceptedCharacters accepted) + : base(tokenizer, accepted) + { + } + + public bool AutoCompleteAtEndOfSpan { get; } + + public string AutoCompleteString { get; set; } + + protected override PartialParseResult CanAcceptChange(Span target, TextChange normalizedChange) + { + if (((AutoCompleteAtEndOfSpan && IsAtEndOfSpan(target, normalizedChange)) || IsAtEndOfFirstLine(target, normalizedChange)) && + normalizedChange.IsInsert && + ParserHelpers.IsNewLine(normalizedChange.NewText) && + AutoCompleteString != null) + { + return PartialParseResult.Rejected | PartialParseResult.AutoCompleteBlock; + } + return PartialParseResult.Rejected; + } + + public override string ToString() + { + return base.ToString() + ",AutoComplete:[" + (AutoCompleteString ?? "") + "]" + (AutoCompleteAtEndOfSpan ? ";AtEnd" : ";AtEOL"); + } + + public override bool Equals(object obj) + { + var other = obj as AutoCompleteEditHandler; + return base.Equals(other) && + string.Equals(other.AutoCompleteString, AutoCompleteString, StringComparison.Ordinal) && + AutoCompleteAtEndOfSpan == other.AutoCompleteAtEndOfSpan; + } + + public override int GetHashCode() + { + // Hash code should include only immutable properties but Equals also checks the type. + var hashCodeCombiner = HashCodeCombiner.Start(); + hashCodeCombiner.Add(TypeHashCode); + hashCodeCombiner.Add(AutoCompleteAtEndOfSpan); + + return hashCodeCombiner.CombinedHash; + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/BalancingModes.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/BalancingModes.cs new file mode 100644 index 0000000000..d1a629ed7b --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/BalancingModes.cs @@ -0,0 +1,17 @@ +// 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; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + [Flags] + internal enum BalancingModes + { + None = 0, + BacktrackOnFailure = 1, + NoErrorOnFailure = 2, + AllowCommentsAndTemplates = 4, + AllowEmbeddedTransitions = 8 + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/Block.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/Block.cs new file mode 100644 index 0000000000..e5a27dc5fa --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/Block.cs @@ -0,0 +1,188 @@ +// 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.Globalization; +using System.Linq; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class Block : SyntaxTreeNode + { + public Block(BlockBuilder source) + : this(source.Type, source.Children, source.ChunkGenerator) + { + source.Reset(); + } + + protected Block(BlockType? type, IReadOnlyList children, IParentChunkGenerator generator) + { + if (type == null) + { + throw new InvalidOperationException(LegacyResources.Block_Type_Not_Specified); + } + + Type = type.Value; + Children = children; + ChunkGenerator = generator; + + // Perf: Avoid allocating an enumerator. + for (var i = 0; i < Children.Count; i++) + { + Children[i].Parent = this; + } + } + public IParentChunkGenerator ChunkGenerator { get; } + + public BlockType Type { get; } + + public IReadOnlyList Children { get; } + + public override bool IsBlock => true; + + public override SourceLocation Start + { + get + { + var child = Children.FirstOrDefault(); + if (child == null) + { + return SourceLocation.Zero; + } + else + { + return child.Start; + } + } + } + + public override int Length => Children.Sum(child => child.Length); + + public virtual IEnumerable Flatten() + { + // Perf: Avoid allocating an enumerator. + for (var i = 0; i < Children.Count; i++) + { + var element = Children[i]; + var span = element as Span; + if (span != null) + { + yield return span; + } + else + { + var block = element as Block; + foreach (Span childSpan in block.Flatten()) + { + yield return childSpan; + } + } + } + } + + public override string ToString() + { + return string.Format( + CultureInfo.CurrentCulture, + "{0} Block at {1}::{2} (Gen:{3})", + Type, + Start, + Length, + ChunkGenerator); + } + + public override bool Equals(object obj) + { + var other = obj as Block; + return other != null && + Type == other.Type && + Equals(ChunkGenerator, other.ChunkGenerator) && + ChildrenEqual(Children, other.Children); + } + + public override int GetHashCode() + { + var hashCodeCombiner = HashCodeCombiner.Start(); + hashCodeCombiner.Add(Type); + hashCodeCombiner.Add(ChunkGenerator); + hashCodeCombiner.Add(Children); + + return hashCodeCombiner; + } + + private static bool ChildrenEqual(IEnumerable left, IEnumerable right) + { + IEnumerator leftEnum = left.GetEnumerator(); + IEnumerator rightEnum = right.GetEnumerator(); + while (leftEnum.MoveNext()) + { + if (!rightEnum.MoveNext() || // More items in left than in right + !Equals(leftEnum.Current, rightEnum.Current)) + { + // Nodes are not equal + return false; + } + } + if (rightEnum.MoveNext()) + { + // More items in right than left + return false; + } + return true; + } + + public override bool EquivalentTo(SyntaxTreeNode node) + { + var other = node as Block; + if (other == null || other.Type != Type) + { + return false; + } + + return Enumerable.SequenceEqual(Children, other.Children, EquivalenceComparer.Default); + } + + public override int GetEquivalenceHash() + { + var hashCodeCombiner = HashCodeCombiner.Start(); + hashCodeCombiner.Add(Type); + foreach (var child in Children) + { + hashCodeCombiner.Add(child.GetEquivalenceHash()); + } + + return hashCodeCombiner.CombinedHash; + } + + private class EquivalenceComparer : IEqualityComparer + { + public static readonly EquivalenceComparer Default = new EquivalenceComparer(); + + private EquivalenceComparer() + { + } + + public bool Equals(SyntaxTreeNode nodeX, SyntaxTreeNode nodeY) + { + if (nodeX == nodeY) + { + return true; + } + + return nodeX != null && nodeX.EquivalentTo(nodeY); + } + + public int GetHashCode(SyntaxTreeNode node) + { + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + return node.GetEquivalenceHash(); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/BlockBuilder.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/BlockBuilder.cs new file mode 100644 index 0000000000..be23d9e2ff --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/BlockBuilder.cs @@ -0,0 +1,40 @@ +// 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.Legacy +{ + internal class BlockBuilder + { + public BlockBuilder() + { + Reset(); + } + + public BlockBuilder(Block original) + { + Type = original.Type; + Children = new List(original.Children); + ChunkGenerator = original.ChunkGenerator; + } + + public IParentChunkGenerator ChunkGenerator { get; set; } + + public BlockType? Type { get; set; } + + public List Children { get; private set; } + + public virtual Block Build() + { + return new Block(this); + } + + public virtual void Reset() + { + Type = null; + Children = new List(); + ChunkGenerator = ParentChunkGenerator.Null; + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/BlockType.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/BlockType.cs new file mode 100644 index 0000000000..fac152aea9 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/BlockType.cs @@ -0,0 +1,24 @@ +// 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.Legacy +{ + internal enum BlockType + { + // Code + Statement, + Directive, + Functions, + Expression, + Helper, + + // Markup + Markup, + Section, + Template, + + // Special + Comment, + Tag + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpCodeParser.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpCodeParser.cs new file mode 100644 index 0000000000..72e13c2bd3 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpCodeParser.cs @@ -0,0 +1,1786 @@ +// 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.Diagnostics; +using System.Linq; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class CSharpCodeParser : TokenizerBackedParser + { + internal static readonly int UsingKeywordLength = 5; // using + private static readonly Func IsValidStatementSpacingSymbol = + IsSpacingToken(includeNewLines: true, includeComments: true); + + internal static ISet DefaultKeywords = new HashSet() + { + SyntaxConstants.CSharp.TagHelperPrefixKeyword, + SyntaxConstants.CSharp.AddTagHelperKeyword, + SyntaxConstants.CSharp.RemoveTagHelperKeyword, + "if", + "do", + "try", + "for", + "foreach", + "while", + "switch", + "lock", + "using", + "section", + "inherits", + "functions", + "namespace", + "class", + }; + + private Dictionary _directiveParsers = new Dictionary(StringComparer.Ordinal); + private Dictionary> _keywordParsers = new Dictionary>(); + + public CSharpCodeParser(ParserContext context) + : base(CSharpLanguageCharacteristics.Instance, context) + { + Keywords = new HashSet(); + SetUpKeywords(); + SetupDirectives(); + SetUpExpressions(); + } + + public HtmlMarkupParser HtmlParser { get; set; } + + protected internal ISet Keywords { get; private set; } + + public bool IsNested { get; set; } + + protected override bool SymbolTypeEquals(CSharpSymbolType x, CSharpSymbolType y) => x == y; + + protected void MapDirectives(Action handler, params string[] directives) + { + foreach (string directive in directives) + { + _directiveParsers.Add(directive, handler); + Keywords.Add(directive); + } + } + + protected bool TryGetDirectiveHandler(string directive, out Action handler) + { + return _directiveParsers.TryGetValue(directive, out handler); + } + + private void MapExpressionKeyword(Action handler, CSharpKeyword keyword) + { + _keywordParsers.Add(keyword, handler); + + // Expression keywords don't belong in the regular keyword list + } + + private void MapKeywords(Action handler, params CSharpKeyword[] keywords) + { + MapKeywords(handler, topLevel: true, keywords: keywords); + } + + private void MapKeywords(Action handler, bool topLevel, params CSharpKeyword[] keywords) + { + foreach (CSharpKeyword keyword in keywords) + { + _keywordParsers.Add(keyword, handler); + if (topLevel) + { + Keywords.Add(CSharpLanguageCharacteristics.GetKeyword(keyword)); + } + } + } + + [Conditional("DEBUG")] + internal void Assert(CSharpKeyword expectedKeyword) + { + Debug.Assert(CurrentSymbol.Type == CSharpSymbolType.Keyword && + CurrentSymbol.Keyword.HasValue && + CurrentSymbol.Keyword.Value == expectedKeyword); + } + + protected internal bool At(CSharpKeyword keyword) + { + return At(CSharpSymbolType.Keyword) && + CurrentSymbol.Keyword.HasValue && + CurrentSymbol.Keyword.Value == keyword; + } + + protected internal bool AcceptIf(CSharpKeyword keyword) + { + if (At(keyword)) + { + AcceptAndMoveNext(); + return true; + } + return false; + } + + protected static Func IsSpacingToken(bool includeNewLines, bool includeComments) + { + return sym => sym.Type == CSharpSymbolType.WhiteSpace || + (includeNewLines && sym.Type == CSharpSymbolType.NewLine) || + (includeComments && sym.Type == CSharpSymbolType.Comment); + } + + public override void ParseBlock() + { + using (PushSpanConfig(DefaultSpanConfig)) + { + if (Context == null) + { + throw new InvalidOperationException(LegacyResources.Parser_Context_Not_Set); + } + + // Unless changed, the block is a statement block + using (Context.Builder.StartBlock(BlockType.Statement)) + { + NextToken(); + + AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + + var current = CurrentSymbol; + if (At(CSharpSymbolType.StringLiteral) && + CurrentSymbol.Content.Length > 0 && + CurrentSymbol.Content[0] == SyntaxConstants.TransitionCharacter) + { + var split = Language.SplitSymbol(CurrentSymbol, 1, CSharpSymbolType.Transition); + current = split.Item1; + Context.Source.Position = split.Item2.Start.AbsoluteIndex; + NextToken(); + } + else if (At(CSharpSymbolType.Transition)) + { + NextToken(); + } + + // Accept "@" if we see it, but if we don't, that's OK. We assume we were started for a good reason + if (current.Type == CSharpSymbolType.Transition) + { + if (Span.Symbols.Count > 0) + { + Output(SpanKind.Code); + } + AtTransition(current); + } + else + { + // No "@" => Jump straight to AfterTransition + AfterTransition(); + } + Output(SpanKind.Code); + } + } + } + + private void DefaultSpanConfig(SpanBuilder span) + { + span.EditHandler = SpanEditHandler.CreateDefault(Language.TokenizeString); + span.ChunkGenerator = new StatementChunkGenerator(); + } + + private void AtTransition(CSharpSymbol current) + { + Debug.Assert(current.Type == CSharpSymbolType.Transition); + Accept(current); + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + Span.ChunkGenerator = SpanChunkGenerator.Null; + + // Output the "@" span and continue here + Output(SpanKind.Transition); + AfterTransition(); + } + + private void AfterTransition() + { + using (PushSpanConfig(DefaultSpanConfig)) + { + EnsureCurrent(); + try + { + // What type of block is this? + if (!EndOfFile) + { + if (CurrentSymbol.Type == CSharpSymbolType.LeftParenthesis) + { + Context.Builder.CurrentBlock.Type = BlockType.Expression; + Context.Builder.CurrentBlock.ChunkGenerator = new ExpressionChunkGenerator(); + ExplicitExpression(); + return; + } + else if (CurrentSymbol.Type == CSharpSymbolType.Identifier) + { + Action handler; + if (TryGetDirectiveHandler(CurrentSymbol.Content, out handler)) + { + Span.ChunkGenerator = SpanChunkGenerator.Null; + handler(); + return; + } + else + { + if (string.Equals( + CurrentSymbol.Content, + SyntaxConstants.CSharp.HelperKeyword, + StringComparison.Ordinal)) + { + Context.ErrorSink.OnError( + CurrentLocation, + LegacyResources.FormatParseError_HelperDirectiveNotAvailable( + SyntaxConstants.CSharp.HelperKeyword), + CurrentSymbol.Content.Length); + } + + Context.Builder.CurrentBlock.Type = BlockType.Expression; + Context.Builder.CurrentBlock.ChunkGenerator = new ExpressionChunkGenerator(); + ImplicitExpression(); + return; + } + } + else if (CurrentSymbol.Type == CSharpSymbolType.Keyword) + { + KeywordBlock(topLevel: true); + return; + } + else if (CurrentSymbol.Type == CSharpSymbolType.LeftBrace) + { + VerbatimBlock(); + return; + } + } + + // Invalid character + Context.Builder.CurrentBlock.Type = BlockType.Expression; + Context.Builder.CurrentBlock.ChunkGenerator = new ExpressionChunkGenerator(); + AddMarkerSymbolIfNecessary(); + Span.ChunkGenerator = new ExpressionChunkGenerator(); + Span.EditHandler = new ImplicitExpressionEditHandler( + Language.TokenizeString, + DefaultKeywords, + acceptTrailingDot: IsNested) + { + AcceptedCharacters = AcceptedCharacters.NonWhiteSpace + }; + if (At(CSharpSymbolType.WhiteSpace) || At(CSharpSymbolType.NewLine)) + { + Context.ErrorSink.OnError( + CurrentLocation, + LegacyResources.ParseError_Unexpected_WhiteSpace_At_Start_Of_CodeBlock_CS, + CurrentSymbol.Content.Length); + } + else if (EndOfFile) + { + Context.ErrorSink.OnError( + CurrentLocation, + LegacyResources.ParseError_Unexpected_EndOfFile_At_Start_Of_CodeBlock, + length: 1 /* end of file */); + } + else + { + Context.ErrorSink.OnError( + CurrentLocation, + LegacyResources.FormatParseError_Unexpected_Character_At_Start_Of_CodeBlock_CS( + CurrentSymbol.Content), + CurrentSymbol.Content.Length); + } + } + finally + { + // Always put current character back in the buffer for the next parser. + PutCurrentBack(); + } + } + } + + private void VerbatimBlock() + { + Assert(CSharpSymbolType.LeftBrace); + var block = new Block(LegacyResources.BlockName_Code, CurrentLocation); + AcceptAndMoveNext(); + + // Set up the "{" span and output + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + Span.ChunkGenerator = SpanChunkGenerator.Null; + Output(SpanKind.MetaCode); + + // Set up auto-complete and parse the code block + var editHandler = new AutoCompleteEditHandler(Language.TokenizeString); + Span.EditHandler = editHandler; + CodeBlock(false, block); + + Span.ChunkGenerator = new StatementChunkGenerator(); + AddMarkerSymbolIfNecessary(); + if (!At(CSharpSymbolType.RightBrace)) + { + editHandler.AutoCompleteString = "}"; + } + Output(SpanKind.Code); + + if (Optional(CSharpSymbolType.RightBrace)) + { + // Set up the "}" span + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + Span.ChunkGenerator = SpanChunkGenerator.Null; + } + + if (!IsNested) + { + EnsureCurrent(); + if (At(CSharpSymbolType.NewLine) || + (At(CSharpSymbolType.WhiteSpace) && NextIs(CSharpSymbolType.NewLine))) + { + Context.NullGenerateWhitespaceAndNewLine = true; + } + } + + Output(SpanKind.MetaCode); + } + + private void ImplicitExpression() + { + ImplicitExpression(AcceptedCharacters.NonWhiteSpace); + } + + // Async implicit expressions include the "await" keyword and therefore need to allow spaces to + // separate the "await" and the following code. + private void AsyncImplicitExpression() + { + ImplicitExpression(AcceptedCharacters.AnyExceptNewline); + } + + private void ImplicitExpression(AcceptedCharacters acceptedCharacters) + { + Context.Builder.CurrentBlock.Type = BlockType.Expression; + Context.Builder.CurrentBlock.ChunkGenerator = new ExpressionChunkGenerator(); + + using (PushSpanConfig(span => + { + span.EditHandler = new ImplicitExpressionEditHandler( + Language.TokenizeString, + Keywords, + acceptTrailingDot: IsNested); + span.EditHandler.AcceptedCharacters = acceptedCharacters; + span.ChunkGenerator = new ExpressionChunkGenerator(); + })) + { + do + { + if (AtIdentifier(allowKeywords: true)) + { + AcceptAndMoveNext(); + } + } + while (MethodCallOrArrayIndex(acceptedCharacters)); + + PutCurrentBack(); + Output(SpanKind.Code); + } + } + + private bool MethodCallOrArrayIndex(AcceptedCharacters acceptedCharacters) + { + if (!EndOfFile) + { + if (CurrentSymbol.Type == CSharpSymbolType.LeftParenthesis || + CurrentSymbol.Type == CSharpSymbolType.LeftBracket) + { + // If we end within "(", whitespace is fine + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any; + + CSharpSymbolType right; + bool success; + + using (PushSpanConfig((span, prev) => + { + prev(span); + span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any; + })) + { + right = Language.FlipBracket(CurrentSymbol.Type); + success = Balance(BalancingModes.BacktrackOnFailure | BalancingModes.AllowCommentsAndTemplates); + } + + if (!success) + { + AcceptUntil(CSharpSymbolType.LessThan); + } + if (At(right)) + { + AcceptAndMoveNext(); + + // At the ending brace, restore the initial accepted characters. + Span.EditHandler.AcceptedCharacters = acceptedCharacters; + } + return MethodCallOrArrayIndex(acceptedCharacters); + } + if (At(CSharpSymbolType.QuestionMark)) + { + var next = Lookahead(count: 1); + + if (next != null) + { + if (next.Type == CSharpSymbolType.Dot) + { + // Accept null conditional dot operator (?.). + AcceptAndMoveNext(); + AcceptAndMoveNext(); + + // If the next piece after the ?. is a keyword or identifier then we want to continue. + return At(CSharpSymbolType.Identifier) || At(CSharpSymbolType.Keyword); + } + else if (next.Type == CSharpSymbolType.LeftBracket) + { + // We're at the ? for a null conditional bracket operator (?[). + AcceptAndMoveNext(); + + // Accept the [ and any content inside (it will attempt to balance). + return MethodCallOrArrayIndex(acceptedCharacters); + } + } + } + else if (At(CSharpSymbolType.Dot)) + { + var dot = CurrentSymbol; + if (NextToken()) + { + if (At(CSharpSymbolType.Identifier) || At(CSharpSymbolType.Keyword)) + { + // Accept the dot and return to the start + Accept(dot); + return true; // continue + } + else + { + // Put the symbol back + PutCurrentBack(); + } + } + if (!IsNested) + { + // Put the "." back + PutBack(dot); + } + else + { + Accept(dot); + } + } + else if (!At(CSharpSymbolType.WhiteSpace) && !At(CSharpSymbolType.NewLine)) + { + PutCurrentBack(); + } + } + + // Implicit Expression is complete + return false; + } + + protected void CompleteBlock() + { + CompleteBlock(insertMarkerIfNecessary: true); + } + + protected void CompleteBlock(bool insertMarkerIfNecessary) + { + CompleteBlock(insertMarkerIfNecessary, captureWhitespaceToEndOfLine: insertMarkerIfNecessary); + } + + protected void CompleteBlock(bool insertMarkerIfNecessary, bool captureWhitespaceToEndOfLine) + { + if (insertMarkerIfNecessary && Context.Builder.LastAcceptedCharacters != AcceptedCharacters.Any) + { + AddMarkerSymbolIfNecessary(); + } + + EnsureCurrent(); + + // Read whitespace, but not newlines + // If we're not inserting a marker span, we don't need to capture whitespace + if (!Context.WhiteSpaceIsSignificantToAncestorBlock && + Context.Builder.CurrentBlock.Type != BlockType.Expression && + captureWhitespaceToEndOfLine && + !Context.DesignTimeMode && + !IsNested) + { + CaptureWhitespaceAtEndOfCodeOnlyLine(); + } + else + { + PutCurrentBack(); + } + } + + private void CaptureWhitespaceAtEndOfCodeOnlyLine() + { + IEnumerable ws = ReadWhile(sym => sym.Type == CSharpSymbolType.WhiteSpace); + if (At(CSharpSymbolType.NewLine)) + { + Accept(ws); + AcceptAndMoveNext(); + PutCurrentBack(); + } + else + { + PutCurrentBack(); + PutBack(ws); + } + } + + private void ConfigureExplicitExpressionSpan(SpanBuilder sb) + { + sb.EditHandler = SpanEditHandler.CreateDefault(Language.TokenizeString); + sb.ChunkGenerator = new ExpressionChunkGenerator(); + } + + private void ExplicitExpression() + { + var block = new Block(LegacyResources.BlockName_ExplicitExpression, CurrentLocation); + Assert(CSharpSymbolType.LeftParenthesis); + AcceptAndMoveNext(); + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + Span.ChunkGenerator = SpanChunkGenerator.Null; + Output(SpanKind.MetaCode); + using (PushSpanConfig(ConfigureExplicitExpressionSpan)) + { + var success = Balance( + BalancingModes.BacktrackOnFailure | + BalancingModes.NoErrorOnFailure | + BalancingModes.AllowCommentsAndTemplates, + CSharpSymbolType.LeftParenthesis, + CSharpSymbolType.RightParenthesis, + block.Start); + + if (!success) + { + AcceptUntil(CSharpSymbolType.LessThan); + Context.ErrorSink.OnError( + block.Start, + LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF(block.Name, ")", "("), + length: 1 /* ( */); + } + + // If necessary, put an empty-content marker symbol here + if (Span.Symbols.Count == 0) + { + Accept(new CSharpSymbol(CurrentLocation, string.Empty, CSharpSymbolType.Unknown)); + } + + // Output the content span and then capture the ")" + Output(SpanKind.Code); + } + Optional(CSharpSymbolType.RightParenthesis); + if (!EndOfFile) + { + PutCurrentBack(); + } + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + Span.ChunkGenerator = SpanChunkGenerator.Null; + CompleteBlock(insertMarkerIfNecessary: false); + Output(SpanKind.MetaCode); + } + + private void Template() + { + if (Context.Builder.ActiveBlocks.Any(block => block.Type == BlockType.Template)) + { + Context.ErrorSink.OnError( + CurrentLocation, + LegacyResources.ParseError_InlineMarkup_Blocks_Cannot_Be_Nested, + length: 1 /* @ */); + } + Output(SpanKind.Code); + using (Context.Builder.StartBlock(BlockType.Template)) + { + Context.Builder.CurrentBlock.ChunkGenerator = new TemplateBlockChunkGenerator(); + PutCurrentBack(); + OtherParserBlock(); + } + } + + private void OtherParserBlock() + { + ParseWithOtherParser(p => p.ParseBlock()); + } + + private void SectionBlock(string left, string right, bool caseSensitive) + { + ParseWithOtherParser(p => p.ParseSection(Tuple.Create(left, right), caseSensitive)); + } + + private void NestedBlock() + { + Output(SpanKind.Code); + var wasNested = IsNested; + IsNested = true; + using (PushSpanConfig()) + { + ParseBlock(); + } + Initialize(Span); + IsNested = wasNested; + NextToken(); + } + + protected override bool IsAtEmbeddedTransition(bool allowTemplatesAndComments, bool allowTransitions) + { + // No embedded transitions in C#, so ignore that param + return allowTemplatesAndComments + && ((Language.IsTransition(CurrentSymbol) + && NextIs(CSharpSymbolType.LessThan, CSharpSymbolType.Colon, CSharpSymbolType.DoubleColon)) + || Language.IsCommentStart(CurrentSymbol)); + } + + protected override void HandleEmbeddedTransition() + { + if (Language.IsTransition(CurrentSymbol)) + { + PutCurrentBack(); + Template(); + } + else if (Language.IsCommentStart(CurrentSymbol)) + { + RazorComment(); + } + } + + private void ParseWithOtherParser(Action parseAction) + { + // 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()) + { + parseAction(HtmlParser); + } + Initialize(Span); + IsNested = wasNested; + NextToken(); + } + + private void SetUpKeywords() + { + MapKeywords( + ConditionalBlock, + CSharpKeyword.For, + CSharpKeyword.Foreach, + CSharpKeyword.While, + CSharpKeyword.Switch, + CSharpKeyword.Lock); + MapKeywords(CaseStatement, false, CSharpKeyword.Case, CSharpKeyword.Default); + MapKeywords(IfStatement, CSharpKeyword.If); + MapKeywords(TryStatement, CSharpKeyword.Try); + MapKeywords(UsingKeyword, CSharpKeyword.Using); + MapKeywords(DoStatement, CSharpKeyword.Do); + MapKeywords(ReservedDirective, CSharpKeyword.Namespace, CSharpKeyword.Class); + } + + protected virtual void ReservedDirective(bool topLevel) + { + Context.ErrorSink.OnError( + CurrentLocation, + LegacyResources.FormatParseError_ReservedWord(CurrentSymbol.Content), + CurrentSymbol.Content.Length); + AcceptAndMoveNext(); + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + Span.ChunkGenerator = SpanChunkGenerator.Null; + Context.Builder.CurrentBlock.Type = BlockType.Directive; + CompleteBlock(); + Output(SpanKind.MetaCode); + } + + private void KeywordBlock(bool topLevel) + { + HandleKeyword(topLevel, () => + { + Context.Builder.CurrentBlock.Type = BlockType.Expression; + Context.Builder.CurrentBlock.ChunkGenerator = new ExpressionChunkGenerator(); + ImplicitExpression(); + }); + } + + private void CaseStatement(bool topLevel) + { + Assert(CSharpSymbolType.Keyword); + Debug.Assert(CurrentSymbol.Keyword != null && + (CurrentSymbol.Keyword.Value == CSharpKeyword.Case || + CurrentSymbol.Keyword.Value == CSharpKeyword.Default)); + AcceptUntil(CSharpSymbolType.Colon); + Optional(CSharpSymbolType.Colon); + } + + private void DoStatement(bool topLevel) + { + Assert(CSharpKeyword.Do); + UnconditionalBlock(); + WhileClause(); + if (topLevel) + { + CompleteBlock(); + } + } + + private void WhileClause() + { + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any; + IEnumerable ws = SkipToNextImportantToken(); + + if (At(CSharpKeyword.While)) + { + Accept(ws); + Assert(CSharpKeyword.While); + AcceptAndMoveNext(); + AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + if (AcceptCondition() && Optional(CSharpSymbolType.Semicolon)) + { + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + } + } + else + { + PutCurrentBack(); + PutBack(ws); + } + } + + private void UsingKeyword(bool topLevel) + { + Assert(CSharpKeyword.Using); + var block = new Block(CurrentSymbol); + AcceptAndMoveNext(); + AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); + + if (At(CSharpSymbolType.LeftParenthesis)) + { + // using ( ==> Using Statement + UsingStatement(block); + } + else if (At(CSharpSymbolType.Identifier) || At(CSharpKeyword.Static)) + { + // using Identifier ==> Using Declaration + if (!topLevel) + { + Context.ErrorSink.OnError( + block.Start, + LegacyResources.ParseError_NamespaceImportAndTypeAlias_Cannot_Exist_Within_CodeBlock, + block.Name.Length); + StandardStatement(); + } + else + { + UsingDeclaration(); + } + } + + if (topLevel) + { + CompleteBlock(); + } + } + + private void UsingDeclaration() + { + // Set block type to directive + Context.Builder.CurrentBlock.Type = BlockType.Directive; + + if (At(CSharpSymbolType.Identifier)) + { + // non-static using + NamespaceOrTypeName(); + var whitespace = ReadWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + if (At(CSharpSymbolType.Assign)) + { + // Alias + Accept(whitespace); + Assert(CSharpSymbolType.Assign); + AcceptAndMoveNext(); + + AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + + // One more namespace or type name + NamespaceOrTypeName(); + } + else + { + PutCurrentBack(); + PutBack(whitespace); + } + } + else if (At(CSharpKeyword.Static)) + { + // static using + AcceptAndMoveNext(); + AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); + NamespaceOrTypeName(); + } + + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.AnyExceptNewline; + Span.ChunkGenerator = new AddImportChunkGenerator( + Span.GetContent(symbols => symbols.Skip(1))); + + // Optional ";" + if (EnsureCurrent()) + { + Optional(CSharpSymbolType.Semicolon); + } + } + + protected bool NamespaceOrTypeName() + { + if (Optional(CSharpSymbolType.Identifier) || Optional(CSharpSymbolType.Keyword)) + { + Optional(CSharpSymbolType.QuestionMark); // Nullable + if (Optional(CSharpSymbolType.DoubleColon)) + { + if (!Optional(CSharpSymbolType.Identifier)) + { + Optional(CSharpSymbolType.Keyword); + } + } + if (At(CSharpSymbolType.LessThan)) + { + TypeArgumentList(); + } + if (Optional(CSharpSymbolType.Dot)) + { + NamespaceOrTypeName(); + } + while (At(CSharpSymbolType.LeftBracket)) + { + Balance(BalancingModes.None); + Optional(CSharpSymbolType.RightBracket); + } + return true; + } + else + { + return false; + } + } + + private void TypeArgumentList() + { + Assert(CSharpSymbolType.LessThan); + Balance(BalancingModes.None); + Optional(CSharpSymbolType.GreaterThan); + } + + private void UsingStatement(Block block) + { + Assert(CSharpSymbolType.LeftParenthesis); + + // Parse condition + if (AcceptCondition()) + { + AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + + // Parse code block + ExpectCodeBlock(block); + } + } + + private void TryStatement(bool topLevel) + { + Assert(CSharpKeyword.Try); + UnconditionalBlock(); + AfterTryClause(); + if (topLevel) + { + CompleteBlock(); + } + } + + private void IfStatement(bool topLevel) + { + Assert(CSharpKeyword.If); + ConditionalBlock(topLevel: false); + AfterIfClause(); + if (topLevel) + { + CompleteBlock(); + } + } + + private void AfterTryClause() + { + // Grab whitespace + var whitespace = SkipToNextImportantToken(); + + // Check for a catch or finally part + if (At(CSharpKeyword.Catch)) + { + Accept(whitespace); + Assert(CSharpKeyword.Catch); + FilterableCatchBlock(); + AfterTryClause(); + } + else if (At(CSharpKeyword.Finally)) + { + Accept(whitespace); + Assert(CSharpKeyword.Finally); + UnconditionalBlock(); + } + else + { + // Return whitespace and end the block + PutCurrentBack(); + PutBack(whitespace); + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any; + } + } + + private void AfterIfClause() + { + // Grab whitespace and razor comments + IEnumerable ws = SkipToNextImportantToken(); + + // Check for an else part + if (At(CSharpKeyword.Else)) + { + Accept(ws); + Assert(CSharpKeyword.Else); + ElseClause(); + } + else + { + // No else, return whitespace + PutCurrentBack(); + PutBack(ws); + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any; + } + } + + private void ElseClause() + { + if (!At(CSharpKeyword.Else)) + { + return; + } + var block = new Block(CurrentSymbol); + + AcceptAndMoveNext(); + AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + if (At(CSharpKeyword.If)) + { + // ElseIf + block.Name = SyntaxConstants.CSharp.ElseIfKeyword; + ConditionalBlock(block); + AfterIfClause(); + } + else if (!EndOfFile) + { + // Else + ExpectCodeBlock(block); + } + } + + private void ExpectCodeBlock(Block block) + { + if (!EndOfFile) + { + // Check for "{" to make sure we're at a block + if (!At(CSharpSymbolType.LeftBrace)) + { + Context.ErrorSink.OnError( + CurrentLocation, + LegacyResources.FormatParseError_SingleLine_ControlFlowStatements_Not_Allowed( + Language.GetSample(CSharpSymbolType.LeftBrace), + CurrentSymbol.Content), + CurrentSymbol.Content.Length); + } + + // Parse the statement and then we're done + Statement(block); + } + } + + private void UnconditionalBlock() + { + Assert(CSharpSymbolType.Keyword); + var block = new Block(CurrentSymbol); + AcceptAndMoveNext(); + AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + ExpectCodeBlock(block); + } + + private void FilterableCatchBlock() + { + Assert(CSharpKeyword.Catch); + + var block = new Block(CurrentSymbol); + + // Accept "catch" + AcceptAndMoveNext(); + AcceptWhile(IsValidStatementSpacingSymbol); + + // Parse the catch condition if present. If not present, let the C# compiler complain. + if (AcceptCondition()) + { + AcceptWhile(IsValidStatementSpacingSymbol); + + if (At(CSharpKeyword.When)) + { + // Accept "when". + AcceptAndMoveNext(); + AcceptWhile(IsValidStatementSpacingSymbol); + + // Parse the filter condition if present. If not present, let the C# compiler complain. + if (!AcceptCondition()) + { + // Incomplete condition. + return; + } + + AcceptWhile(IsValidStatementSpacingSymbol); + } + + ExpectCodeBlock(block); + } + } + + private void ConditionalBlock(bool topLevel) + { + Assert(CSharpSymbolType.Keyword); + var block = new Block(CurrentSymbol); + ConditionalBlock(block); + if (topLevel) + { + CompleteBlock(); + } + } + + private void ConditionalBlock(Block block) + { + AcceptAndMoveNext(); + AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + + // Parse the condition, if present (if not present, we'll let the C# compiler complain) + if (AcceptCondition()) + { + AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + ExpectCodeBlock(block); + } + } + + private bool AcceptCondition() + { + if (At(CSharpSymbolType.LeftParenthesis)) + { + var complete = Balance(BalancingModes.BacktrackOnFailure | BalancingModes.AllowCommentsAndTemplates); + if (!complete) + { + AcceptUntil(CSharpSymbolType.NewLine); + } + else + { + Optional(CSharpSymbolType.RightParenthesis); + } + return complete; + } + return true; + } + + private void Statement() + { + Statement(null); + } + + private void Statement(Block block) + { + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any; + + // Accept whitespace but always keep the last whitespace node so we can put it back if necessary + var lastWhitespace = AcceptWhiteSpaceInLines(); + Debug.Assert(lastWhitespace == null || + (lastWhitespace.Start.AbsoluteIndex + lastWhitespace.Content.Length == CurrentLocation.AbsoluteIndex)); + + if (EndOfFile) + { + if (lastWhitespace != null) + { + Accept(lastWhitespace); + } + return; + } + + var type = CurrentSymbol.Type; + var loc = CurrentLocation; + + // Both cases @: and @:: are triggered as markup, second colon in second case will be triggered as a plain text + var isSingleLineMarkup = type == CSharpSymbolType.Transition && + (NextIs(CSharpSymbolType.Colon, CSharpSymbolType.DoubleColon)); + + var isMarkup = isSingleLineMarkup || + type == CSharpSymbolType.LessThan || + (type == CSharpSymbolType.Transition && NextIs(CSharpSymbolType.LessThan)); + + if (Context.DesignTimeMode || !isMarkup) + { + // CODE owns whitespace, MARKUP owns it ONLY in DesignTimeMode. + if (lastWhitespace != null) + { + Accept(lastWhitespace); + } + } + else + { + var nextSymbol = Lookahead(1); + + // MARKUP owns whitespace EXCEPT in DesignTimeMode. + PutCurrentBack(); + + // Put back the whitespace unless it precedes a '' tag. + if (nextSymbol != null && + !string.Equals(nextSymbol.Content, SyntaxConstants.TextTagName, StringComparison.Ordinal)) + { + PutBack(lastWhitespace); + } + } + + if (isMarkup) + { + if (type == CSharpSymbolType.Transition && !isSingleLineMarkup) + { + Context.ErrorSink.OnError( + loc, + LegacyResources.ParseError_AtInCode_Must_Be_Followed_By_Colon_Paren_Or_Identifier_Start, + length: 1 /* @ */); + } + + // Markup block + Output(SpanKind.Code); + if (Context.DesignTimeMode && CurrentSymbol != null && + (CurrentSymbol.Type == CSharpSymbolType.LessThan || CurrentSymbol.Type == CSharpSymbolType.Transition)) + { + PutCurrentBack(); + } + OtherParserBlock(); + } + else + { + // What kind of statement is this? + HandleStatement(block, type); + } + } + + private void HandleStatement(Block block, CSharpSymbolType type) + { + switch (type) + { + case CSharpSymbolType.RazorCommentTransition: + Output(SpanKind.Code); + RazorComment(); + Statement(block); + break; + case CSharpSymbolType.LeftBrace: + // Verbatim Block + block = block ?? new Block(LegacyResources.BlockName_Code, CurrentLocation); + AcceptAndMoveNext(); + CodeBlock(block); + break; + case CSharpSymbolType.Keyword: + // Keyword block + HandleKeyword(false, StandardStatement); + break; + case CSharpSymbolType.Transition: + // Embedded Expression block + EmbeddedExpression(); + break; + case CSharpSymbolType.RightBrace: + // Possible end of Code Block, just run the continuation + break; + case CSharpSymbolType.Comment: + AcceptAndMoveNext(); + break; + default: + // Other statement + StandardStatement(); + break; + } + } + + private void EmbeddedExpression() + { + // First, verify the type of the block + Assert(CSharpSymbolType.Transition); + var transition = CurrentSymbol; + NextToken(); + + if (At(CSharpSymbolType.Transition)) + { + // Escaped "@" + Output(SpanKind.Code); + + // Output "@" as hidden span + Accept(transition); + Span.ChunkGenerator = SpanChunkGenerator.Null; + Output(SpanKind.Code); + + Assert(CSharpSymbolType.Transition); + AcceptAndMoveNext(); + StandardStatement(); + } + else + { + // Throw errors as necessary, but continue parsing + if (At(CSharpSymbolType.LeftBrace)) + { + Context.ErrorSink.OnError( + CurrentLocation, + LegacyResources.ParseError_Unexpected_Nested_CodeBlock, + length: 1 /* { */); + } + + // @( or @foo - Nested expression, parse a child block + PutCurrentBack(); + PutBack(transition); + + // Before exiting, add a marker span if necessary + AddMarkerSymbolIfNecessary(); + + NestedBlock(); + } + } + + private void StandardStatement() + { + while (!EndOfFile) + { + var bookmark = CurrentLocation.AbsoluteIndex; + IEnumerable read = ReadWhile(sym => sym.Type != CSharpSymbolType.Semicolon && + sym.Type != CSharpSymbolType.RazorCommentTransition && + sym.Type != CSharpSymbolType.Transition && + sym.Type != CSharpSymbolType.LeftBrace && + sym.Type != CSharpSymbolType.LeftParenthesis && + sym.Type != CSharpSymbolType.LeftBracket && + sym.Type != CSharpSymbolType.RightBrace); + if (At(CSharpSymbolType.LeftBrace) || + At(CSharpSymbolType.LeftParenthesis) || + At(CSharpSymbolType.LeftBracket)) + { + Accept(read); + if (Balance(BalancingModes.AllowCommentsAndTemplates | BalancingModes.BacktrackOnFailure)) + { + Optional(CSharpSymbolType.RightBrace); + } + else + { + // Recovery + AcceptUntil(CSharpSymbolType.LessThan, CSharpSymbolType.RightBrace); + return; + } + } + else if (At(CSharpSymbolType.Transition) && (NextIs(CSharpSymbolType.LessThan, CSharpSymbolType.Colon))) + { + Accept(read); + Output(SpanKind.Code); + Template(); + } + else if (At(CSharpSymbolType.RazorCommentTransition)) + { + Accept(read); + RazorComment(); + } + else if (At(CSharpSymbolType.Semicolon)) + { + Accept(read); + AcceptAndMoveNext(); + return; + } + else if (At(CSharpSymbolType.RightBrace)) + { + Accept(read); + return; + } + else + { + Context.Source.Position = bookmark; + NextToken(); + AcceptUntil(CSharpSymbolType.LessThan, CSharpSymbolType.LeftBrace, CSharpSymbolType.RightBrace); + return; + } + } + } + + private void CodeBlock(Block block) + { + CodeBlock(true, block); + } + + private void CodeBlock(bool acceptTerminatingBrace, Block block) + { + EnsureCurrent(); + while (!EndOfFile && !At(CSharpSymbolType.RightBrace)) + { + // Parse a statement, then return here + Statement(); + EnsureCurrent(); + } + + if (EndOfFile) + { + Context.ErrorSink.OnError( + block.Start, + LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF(block.Name, '}', '{'), + length: 1 /* { OR } */); + } + else if (acceptTerminatingBrace) + { + Assert(CSharpSymbolType.RightBrace); + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + AcceptAndMoveNext(); + } + } + + private void HandleKeyword(bool topLevel, Action fallback) + { + Debug.Assert(CurrentSymbol.Type == CSharpSymbolType.Keyword && CurrentSymbol.Keyword != null); + Action handler; + if (_keywordParsers.TryGetValue(CurrentSymbol.Keyword.Value, out handler)) + { + handler(topLevel); + } + else + { + fallback(); + } + } + + private IEnumerable SkipToNextImportantToken() + { + while (!EndOfFile) + { + IEnumerable ws = ReadWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + if (At(CSharpSymbolType.RazorCommentTransition)) + { + Accept(ws); + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any; + RazorComment(); + } + else + { + return ws; + } + } + return Enumerable.Empty(); + } + + // Common code for Parsers, but FxCop REALLY doesn't like it in the base class.. moving it here for now. + protected override void OutputSpanBeforeRazorComment() + { + AddMarkerSymbolIfNecessary(); + Output(SpanKind.Code); + } + + private void SetUpExpressions() + { + MapExpressionKeyword(AwaitExpression, CSharpKeyword.Await); + } + + private void AwaitExpression(bool topLevel) + { + // Ensure that we're on the await statement (only runs in debug) + Assert(CSharpKeyword.Await); + + // Accept the "await" and move on + AcceptAndMoveNext(); + + // Accept 1 or more spaces between the await and the following code. + AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); + + // Top level basically indicates if we're within an expression or statement. + // Ex: topLevel true = @await Foo() | topLevel false = @{ await Foo(); } + // Note that in this case @{ @await Foo() } top level is true for await. + // Therefore, if we're top level then we want to act like an implicit expression, + // otherwise just act as whatever we're contained in. + if (topLevel) + { + // Setup the Span to be an async implicit expression (an implicit expresison that allows spaces). + // Spaces are allowed because of "@await Foo()". + AsyncImplicitExpression(); + } + } + + private void SetupDirectives() + { + MapDirectives(TagHelperPrefixDirective, SyntaxConstants.CSharp.TagHelperPrefixKeyword); + MapDirectives(AddTagHelperDirective, SyntaxConstants.CSharp.AddTagHelperKeyword); + MapDirectives(RemoveTagHelperDirective, SyntaxConstants.CSharp.RemoveTagHelperKeyword); + MapDirectives(InheritsDirective, SyntaxConstants.CSharp.InheritsKeyword); + MapDirectives(FunctionsDirective, SyntaxConstants.CSharp.FunctionsKeyword); + MapDirectives(SectionDirective, SyntaxConstants.CSharp.SectionKeyword); + } + + protected virtual void TagHelperPrefixDirective() + { + TagHelperDirective( + SyntaxConstants.CSharp.TagHelperPrefixKeyword, + prefix => new TagHelperPrefixDirectiveChunkGenerator(prefix)); + } + + protected virtual void AddTagHelperDirective() + { + TagHelperDirective( + SyntaxConstants.CSharp.AddTagHelperKeyword, + lookupText => new AddTagHelperChunkGenerator(lookupText)); + } + + protected virtual void RemoveTagHelperDirective() + { + TagHelperDirective( + SyntaxConstants.CSharp.RemoveTagHelperKeyword, + lookupText => new RemoveTagHelperChunkGenerator(lookupText)); + } + + protected virtual void SectionDirective() + { + var nested = Context.Builder.ActiveBlocks.Any(block => block.Type == BlockType.Section); + var errorReported = false; + + // Set the block and span type + Context.Builder.CurrentBlock.Type = BlockType.Section; + + // Verify we're on "section" and accept + AssertDirective(SyntaxConstants.CSharp.SectionKeyword); + var startLocation = CurrentLocation; + AcceptAndMoveNext(); + + if (nested) + { + Context.ErrorSink.OnError( + startLocation, + LegacyResources.FormatParseError_Sections_Cannot_Be_Nested(LegacyResources.SectionExample_CS), + Span.GetContent().Value.Length); + errorReported = true; + } + + var whitespace = ReadWhile(IsSpacingToken(includeNewLines: true, includeComments: false)); + + // Get the section name + var sectionName = string.Empty; + if (!Required(CSharpSymbolType.Identifier, + errorIfNotFound: true, + errorBase: LegacyResources.FormatParseError_Unexpected_Character_At_Section_Name_Start)) + { + if (!errorReported) + { + errorReported = true; + } + + PutCurrentBack(); + PutBack(whitespace); + AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: false)); + } + else + { + Accept(whitespace); + sectionName = CurrentSymbol.Content; + AcceptAndMoveNext(); + } + Context.Builder.CurrentBlock.ChunkGenerator = new SectionChunkGenerator(sectionName); + + var errorLocation = CurrentLocation; + whitespace = ReadWhile(IsSpacingToken(includeNewLines: true, includeComments: false)); + + // Get the starting brace + var sawStartingBrace = At(CSharpSymbolType.LeftBrace); + if (!sawStartingBrace) + { + if (!errorReported) + { + errorReported = true; + Context.ErrorSink.OnError( + errorLocation, + LegacyResources.ParseError_MissingOpenBraceAfterSection, + length: 1 /* { */); + } + + PutCurrentBack(); + PutBack(whitespace); + AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: false)); + Optional(CSharpSymbolType.NewLine); + Output(SpanKind.MetaCode); + CompleteBlock(); + return; + } + else + { + Accept(whitespace); + } + + var startingBraceLocation = CurrentLocation; + + // Set up edit handler + var editHandler = new AutoCompleteEditHandler(Language.TokenizeString, autoCompleteAtEndOfSpan: true); + + Span.EditHandler = editHandler; + Span.Accept(CurrentSymbol); + + // Output Metacode then switch to section parser + Output(SpanKind.MetaCode); + SectionBlock("{", "}", caseSensitive: true); + + Span.ChunkGenerator = SpanChunkGenerator.Null; + // Check for the terminating "}" + if (!Optional(CSharpSymbolType.RightBrace)) + { + editHandler.AutoCompleteString = "}"; + Context.ErrorSink.OnError( + startingBraceLocation, + LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF( + SyntaxConstants.CSharp.SectionKeyword, + Language.GetSample(CSharpSymbolType.RightBrace), + Language.GetSample(CSharpSymbolType.LeftBrace)), + length: 1 /* } */); + } + else + { + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + } + CompleteBlock(insertMarkerIfNecessary: false, captureWhitespaceToEndOfLine: true); + Output(SpanKind.MetaCode); + return; + } + + protected virtual void FunctionsDirective() + { + // Set the block type + Context.Builder.CurrentBlock.Type = BlockType.Functions; + + // Verify we're on "functions" and accept + AssertDirective(SyntaxConstants.CSharp.FunctionsKeyword); + var block = new Block(CurrentSymbol); + AcceptAndMoveNext(); + + AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: false)); + + if (!At(CSharpSymbolType.LeftBrace)) + { + Context.ErrorSink.OnError( + CurrentLocation, + LegacyResources.FormatParseError_Expected_X(Language.GetSample(CSharpSymbolType.LeftBrace)), + length: 1 /* { */); + CompleteBlock(); + Output(SpanKind.MetaCode); + return; + } + else + { + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + } + + // Capture start point and continue + var blockStart = CurrentLocation; + AcceptAndMoveNext(); + + // Output what we've seen and continue + Output(SpanKind.MetaCode); + + var editHandler = new AutoCompleteEditHandler(Language.TokenizeString); + Span.EditHandler = editHandler; + + Balance(BalancingModes.NoErrorOnFailure, CSharpSymbolType.LeftBrace, CSharpSymbolType.RightBrace, blockStart); + Span.ChunkGenerator = new TypeMemberChunkGenerator(); + if (!At(CSharpSymbolType.RightBrace)) + { + editHandler.AutoCompleteString = "}"; + Context.ErrorSink.OnError( + blockStart, + LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF(block.Name, "}", "{"), + length: 1 /* } */); + CompleteBlock(); + Output(SpanKind.Code); + } + else + { + Output(SpanKind.Code); + Assert(CSharpSymbolType.RightBrace); + Span.ChunkGenerator = SpanChunkGenerator.Null; + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + AcceptAndMoveNext(); + CompleteBlock(); + Output(SpanKind.MetaCode); + } + } + + protected virtual void InheritsDirective() + { + // Verify we're on the right keyword and accept + AssertDirective(SyntaxConstants.CSharp.InheritsKeyword); + AcceptAndMoveNext(); + + InheritsDirectiveCore(); + } + + [Conditional("DEBUG")] + protected void AssertDirective(string directive) + { + Assert(CSharpSymbolType.Identifier); + Debug.Assert(string.Equals(CurrentSymbol.Content, directive, StringComparison.Ordinal)); + } + + protected void InheritsDirectiveCore() + { + BaseTypeDirective( + LegacyResources.ParseError_InheritsKeyword_Must_Be_Followed_By_TypeName, + baseType => new SetBaseTypeChunkGenerator(baseType)); + } + + protected void BaseTypeDirective(string noTypeNameError, Func createChunkGenerator) + { + var keywordStartLocation = Span.Start; + + // Set the block type + Context.Builder.CurrentBlock.Type = BlockType.Directive; + + var keywordLength = Span.GetContent().Value.Length; + + // Accept whitespace + var remainingWhitespace = AcceptSingleWhiteSpaceCharacter(); + + if (Span.Symbols.Count > 1) + { + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + } + + Output(SpanKind.MetaCode); + + if (remainingWhitespace != null) + { + Accept(remainingWhitespace); + } + + AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); + + if (EndOfFile || At(CSharpSymbolType.WhiteSpace) || At(CSharpSymbolType.NewLine)) + { + Context.ErrorSink.OnError( + keywordStartLocation, + noTypeNameError, + keywordLength); + } + + // Parse to the end of the line + AcceptUntil(CSharpSymbolType.NewLine); + if (!Context.DesignTimeMode) + { + // We want the newline to be treated as code, but it causes issues at design-time. + Optional(CSharpSymbolType.NewLine); + } + + // Pull out the type name + string baseType = Span.GetContent(); + + // Set up chunk generation + Span.ChunkGenerator = createChunkGenerator(baseType.Trim()); + + // Output the span and finish the block + CompleteBlock(); + Output(SpanKind.Code, AcceptedCharacters.AnyExceptNewline); + } + + private void TagHelperDirective(string keyword, Func chunkGeneratorFactory) + { + AssertDirective(keyword); + var keywordStartLocation = CurrentLocation; + + // Accept the directive name + AcceptAndMoveNext(); + + // Set the block type + Context.Builder.CurrentBlock.Type = BlockType.Directive; + + var keywordLength = Span.GetContent().Value.Length; + + var foundWhitespace = At(CSharpSymbolType.WhiteSpace); + AcceptWhile(CSharpSymbolType.WhiteSpace); + + // If we found whitespace then any content placed within the whitespace MAY cause a destructive change + // to the document. We can't accept it. + Output(SpanKind.MetaCode, foundWhitespace ? AcceptedCharacters.None : AcceptedCharacters.AnyExceptNewline); + + ISpanChunkGenerator chunkGenerator; + if (EndOfFile || At(CSharpSymbolType.NewLine)) + { + Context.ErrorSink.OnError( + keywordStartLocation, + LegacyResources.FormatParseError_DirectiveMustHaveValue(keyword), + keywordLength); + + chunkGenerator = chunkGeneratorFactory(string.Empty); + } + else + { + // Need to grab the current location before we accept until the end of the line. + var startLocation = CurrentLocation; + + // Parse to the end of the line. Essentially accepts anything until end of line, comments, invalid code + // etc. + AcceptUntil(CSharpSymbolType.NewLine); + + // Pull out the value and remove whitespaces and optional quotes + var rawValue = Span.GetContent().Value.Trim(); + + var startsWithQuote = rawValue.StartsWith("\"", StringComparison.Ordinal); + var endsWithQuote = rawValue.EndsWith("\"", StringComparison.Ordinal); + if (startsWithQuote != endsWithQuote) + { + Context.ErrorSink.OnError( + startLocation, + LegacyResources.FormatParseError_IncompleteQuotesAroundDirective(keyword), + rawValue.Length); + } + else if (startsWithQuote) + { + if (rawValue.Length > 2) + { + // Remove extra quotes + rawValue = rawValue.Substring(1, rawValue.Length - 2); + } + else + { + // raw value is only quotes + rawValue = string.Empty; + } + } + + chunkGenerator = chunkGeneratorFactory(rawValue); + } + + Span.ChunkGenerator = chunkGenerator; + + // Output the span and finish the block + CompleteBlock(); + Output(SpanKind.Code, AcceptedCharacters.AnyExceptNewline); + } + + protected class Block + { + public Block(string name, SourceLocation start) + { + Name = name; + Start = start; + } + + public Block(CSharpSymbol symbol) + : this(GetName(symbol), symbol.Start) + { + } + + public string Name { get; set; } + public SourceLocation Start { get; set; } + + private static string GetName(CSharpSymbol sym) + { + if (sym.Type == CSharpSymbolType.Keyword) + { + return CSharpLanguageCharacteristics.GetKeyword(sym.Keyword.Value); + } + return sym.Content; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpKeyword.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpKeyword.cs new file mode 100644 index 0000000000..b1129253ef --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpKeyword.cs @@ -0,0 +1,88 @@ +// 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.Legacy +{ + internal enum CSharpKeyword + { + Await, + Abstract, + Byte, + Class, + Delegate, + Event, + Fixed, + If, + Internal, + New, + Override, + Readonly, + Short, + Struct, + Try, + Unsafe, + Volatile, + As, + Do, + Is, + Params, + Ref, + Switch, + Ushort, + While, + Case, + Const, + Explicit, + Float, + Null, + Sizeof, + Typeof, + Implicit, + Private, + This, + Using, + Extern, + Return, + Stackalloc, + Uint, + Base, + Catch, + Continue, + Double, + For, + In, + Lock, + Object, + Protected, + Static, + False, + Public, + Sbyte, + Throw, + Virtual, + Decimal, + Else, + Operator, + String, + Ulong, + Bool, + Char, + Default, + Foreach, + Long, + Void, + Enum, + Finally, + Int, + Out, + Sealed, + True, + Goto, + Unchecked, + Interface, + Break, + Checked, + Namespace, + When + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpLanguageCharacteristics.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpLanguageCharacteristics.cs new file mode 100644 index 0000000000..0aa0fedd7e --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpLanguageCharacteristics.cs @@ -0,0 +1,175 @@ +// 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.Diagnostics; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class CSharpLanguageCharacteristics : LanguageCharacteristics + { + private static readonly CSharpLanguageCharacteristics _instance = new CSharpLanguageCharacteristics(); + + private static Dictionary _symbolSamples = new Dictionary() + { + { CSharpSymbolType.Arrow, "->" }, + { CSharpSymbolType.Minus, "-" }, + { CSharpSymbolType.Decrement, "--" }, + { CSharpSymbolType.MinusAssign, "-=" }, + { CSharpSymbolType.NotEqual, "!=" }, + { CSharpSymbolType.Not, "!" }, + { CSharpSymbolType.Modulo, "%" }, + { CSharpSymbolType.ModuloAssign, "%=" }, + { CSharpSymbolType.AndAssign, "&=" }, + { CSharpSymbolType.And, "&" }, + { CSharpSymbolType.DoubleAnd, "&&" }, + { CSharpSymbolType.LeftParenthesis, "(" }, + { CSharpSymbolType.RightParenthesis, ")" }, + { CSharpSymbolType.Star, "*" }, + { CSharpSymbolType.MultiplyAssign, "*=" }, + { CSharpSymbolType.Comma, "," }, + { CSharpSymbolType.Dot, "." }, + { CSharpSymbolType.Slash, "/" }, + { CSharpSymbolType.DivideAssign, "/=" }, + { CSharpSymbolType.DoubleColon, "::" }, + { CSharpSymbolType.Colon, ":" }, + { CSharpSymbolType.Semicolon, ";" }, + { CSharpSymbolType.QuestionMark, "?" }, + { CSharpSymbolType.NullCoalesce, "??" }, + { CSharpSymbolType.RightBracket, "]" }, + { CSharpSymbolType.LeftBracket, "[" }, + { CSharpSymbolType.XorAssign, "^=" }, + { CSharpSymbolType.Xor, "^" }, + { CSharpSymbolType.LeftBrace, "{" }, + { CSharpSymbolType.OrAssign, "|=" }, + { CSharpSymbolType.DoubleOr, "||" }, + { CSharpSymbolType.Or, "|" }, + { CSharpSymbolType.RightBrace, "}" }, + { CSharpSymbolType.Tilde, "~" }, + { CSharpSymbolType.Plus, "+" }, + { CSharpSymbolType.PlusAssign, "+=" }, + { CSharpSymbolType.Increment, "++" }, + { CSharpSymbolType.LessThan, "<" }, + { CSharpSymbolType.LessThanEqual, "<=" }, + { CSharpSymbolType.LeftShift, "<<" }, + { CSharpSymbolType.LeftShiftAssign, "<<=" }, + { CSharpSymbolType.Assign, "=" }, + { CSharpSymbolType.Equals, "==" }, + { CSharpSymbolType.GreaterThan, ">" }, + { CSharpSymbolType.GreaterThanEqual, ">=" }, + { CSharpSymbolType.RightShift, ">>" }, + { CSharpSymbolType.RightShiftAssign, ">>>" }, + { CSharpSymbolType.Hash, "#" }, + { CSharpSymbolType.Transition, "@" }, + }; + + private CSharpLanguageCharacteristics() + { + } + + public static CSharpLanguageCharacteristics Instance => _instance; + + public override CSharpTokenizer CreateTokenizer(ITextDocument source) + { + return new CSharpTokenizer(source); + } + + protected override CSharpSymbol CreateSymbol(SourceLocation location, string content, CSharpSymbolType type, IReadOnlyList errors) + { + return new CSharpSymbol(location, content, type, errors); + } + + public override string GetSample(CSharpSymbolType type) + { + string sample; + if (!_symbolSamples.TryGetValue(type, out sample)) + { + switch (type) + { + case CSharpSymbolType.Identifier: + return LegacyResources.CSharpSymbol_Identifier; + case CSharpSymbolType.Keyword: + return LegacyResources.CSharpSymbol_Keyword; + case CSharpSymbolType.IntegerLiteral: + return LegacyResources.CSharpSymbol_IntegerLiteral; + case CSharpSymbolType.NewLine: + return LegacyResources.CSharpSymbol_Newline; + case CSharpSymbolType.WhiteSpace: + return LegacyResources.CSharpSymbol_Whitespace; + case CSharpSymbolType.Comment: + return LegacyResources.CSharpSymbol_Comment; + case CSharpSymbolType.RealLiteral: + return LegacyResources.CSharpSymbol_RealLiteral; + case CSharpSymbolType.CharacterLiteral: + return LegacyResources.CSharpSymbol_CharacterLiteral; + case CSharpSymbolType.StringLiteral: + return LegacyResources.CSharpSymbol_StringLiteral; + default: + return LegacyResources.Symbol_Unknown; + } + } + return sample; + } + + public override CSharpSymbol CreateMarkerSymbol(SourceLocation location) + { + return new CSharpSymbol(location, string.Empty, CSharpSymbolType.Unknown); + } + + public override CSharpSymbolType GetKnownSymbolType(KnownSymbolType type) + { + switch (type) + { + case KnownSymbolType.Identifier: + return CSharpSymbolType.Identifier; + case KnownSymbolType.Keyword: + return CSharpSymbolType.Keyword; + case KnownSymbolType.NewLine: + return CSharpSymbolType.NewLine; + case KnownSymbolType.WhiteSpace: + return CSharpSymbolType.WhiteSpace; + case KnownSymbolType.Transition: + return CSharpSymbolType.Transition; + case KnownSymbolType.CommentStart: + return CSharpSymbolType.RazorCommentTransition; + case KnownSymbolType.CommentStar: + return CSharpSymbolType.RazorCommentStar; + case KnownSymbolType.CommentBody: + return CSharpSymbolType.RazorComment; + default: + return CSharpSymbolType.Unknown; + } + } + + public override CSharpSymbolType FlipBracket(CSharpSymbolType bracket) + { + switch (bracket) + { + case CSharpSymbolType.LeftBrace: + return CSharpSymbolType.RightBrace; + case CSharpSymbolType.LeftBracket: + return CSharpSymbolType.RightBracket; + case CSharpSymbolType.LeftParenthesis: + return CSharpSymbolType.RightParenthesis; + case CSharpSymbolType.LessThan: + return CSharpSymbolType.GreaterThan; + case CSharpSymbolType.RightBrace: + return CSharpSymbolType.LeftBrace; + case CSharpSymbolType.RightBracket: + return CSharpSymbolType.LeftBracket; + case CSharpSymbolType.RightParenthesis: + return CSharpSymbolType.LeftParenthesis; + case CSharpSymbolType.GreaterThan: + return CSharpSymbolType.LessThan; + default: + Debug.Fail("FlipBracket must be called with a bracket character"); + return CSharpSymbolType.Unknown; + } + } + + public static string GetKeyword(CSharpKeyword keyword) + { + return keyword.ToString().ToLowerInvariant(); + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpSymbol.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpSymbol.cs new file mode 100644 index 0000000000..2a4f3f1d59 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpSymbol.cs @@ -0,0 +1,72 @@ +// 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; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class CSharpSymbol : SymbolBase + { + public CSharpSymbol(int absoluteIndex, int lineIndex, int characterIndex, string content, CSharpSymbolType type) + : this(new SourceLocation(absoluteIndex, lineIndex, characterIndex), content, type, RazorError.EmptyArray) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + } + + public CSharpSymbol(SourceLocation start, string content, CSharpSymbolType type) + : this(start, content, type, RazorError.EmptyArray) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + } + + public CSharpSymbol( + int offset, + int line, + int column, + string content, + CSharpSymbolType type, + IReadOnlyList errors) + : base(new SourceLocation(offset, line, column), content, type, errors) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + } + + public CSharpSymbol( + SourceLocation start, + string content, + CSharpSymbolType type, + IReadOnlyList errors) + : base(start, content, type, errors) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + } + + public CSharpKeyword? Keyword { get; set; } + + public override bool Equals(object obj) + { + var other = obj as CSharpSymbol; + return base.Equals(other) && + other.Keyword == Keyword; + } + + public override int GetHashCode() + { + // Hash code should include only immutable properties. + return base.GetHashCode(); + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpSymbolType.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpSymbolType.cs new file mode 100644 index 0000000000..4ae06b192a --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpSymbolType.cs @@ -0,0 +1,75 @@ +// 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.Legacy +{ + internal enum CSharpSymbolType + { + Unknown, + Identifier, + Keyword, + IntegerLiteral, + NewLine, + WhiteSpace, + Comment, + RealLiteral, + CharacterLiteral, + StringLiteral, + + // Operators + Arrow, + Minus, + Decrement, + MinusAssign, + NotEqual, + Not, + Modulo, + ModuloAssign, + AndAssign, + And, + DoubleAnd, + LeftParenthesis, + RightParenthesis, + Star, + MultiplyAssign, + Comma, + Dot, + Slash, + DivideAssign, + DoubleColon, + Colon, + Semicolon, + QuestionMark, + NullCoalesce, + RightBracket, + LeftBracket, + XorAssign, + Xor, + LeftBrace, + OrAssign, + DoubleOr, + Or, + RightBrace, + Tilde, + Plus, + PlusAssign, + Increment, + LessThan, + LessThanEqual, + LeftShift, + LeftShiftAssign, + Assign, + Equals, + GreaterThan, + GreaterThanEqual, + RightShift, + RightShiftAssign, + Hash, + Transition, + + // Razor specific + RazorCommentTransition, + RazorCommentStar, + RazorComment + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpTokenizer.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpTokenizer.cs new file mode 100644 index 0000000000..f7c1e08fbb --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/CSharpTokenizer.cs @@ -0,0 +1,625 @@ +// 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.Diagnostics; +using System.Globalization; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class CSharpTokenizer : Tokenizer + { + private Dictionary> _operatorHandlers; + + private static readonly Dictionary _keywords = new Dictionary(StringComparer.Ordinal) + { + { "await", CSharpKeyword.Await }, + { "abstract", CSharpKeyword.Abstract }, + { "byte", CSharpKeyword.Byte }, + { "class", CSharpKeyword.Class }, + { "delegate", CSharpKeyword.Delegate }, + { "event", CSharpKeyword.Event }, + { "fixed", CSharpKeyword.Fixed }, + { "if", CSharpKeyword.If }, + { "internal", CSharpKeyword.Internal }, + { "new", CSharpKeyword.New }, + { "override", CSharpKeyword.Override }, + { "readonly", CSharpKeyword.Readonly }, + { "short", CSharpKeyword.Short }, + { "struct", CSharpKeyword.Struct }, + { "try", CSharpKeyword.Try }, + { "unsafe", CSharpKeyword.Unsafe }, + { "volatile", CSharpKeyword.Volatile }, + { "as", CSharpKeyword.As }, + { "do", CSharpKeyword.Do }, + { "is", CSharpKeyword.Is }, + { "params", CSharpKeyword.Params }, + { "ref", CSharpKeyword.Ref }, + { "switch", CSharpKeyword.Switch }, + { "ushort", CSharpKeyword.Ushort }, + { "while", CSharpKeyword.While }, + { "case", CSharpKeyword.Case }, + { "const", CSharpKeyword.Const }, + { "explicit", CSharpKeyword.Explicit }, + { "float", CSharpKeyword.Float }, + { "null", CSharpKeyword.Null }, + { "sizeof", CSharpKeyword.Sizeof }, + { "typeof", CSharpKeyword.Typeof }, + { "implicit", CSharpKeyword.Implicit }, + { "private", CSharpKeyword.Private }, + { "this", CSharpKeyword.This }, + { "using", CSharpKeyword.Using }, + { "extern", CSharpKeyword.Extern }, + { "return", CSharpKeyword.Return }, + { "stackalloc", CSharpKeyword.Stackalloc }, + { "uint", CSharpKeyword.Uint }, + { "base", CSharpKeyword.Base }, + { "catch", CSharpKeyword.Catch }, + { "continue", CSharpKeyword.Continue }, + { "double", CSharpKeyword.Double }, + { "for", CSharpKeyword.For }, + { "in", CSharpKeyword.In }, + { "lock", CSharpKeyword.Lock }, + { "object", CSharpKeyword.Object }, + { "protected", CSharpKeyword.Protected }, + { "static", CSharpKeyword.Static }, + { "false", CSharpKeyword.False }, + { "public", CSharpKeyword.Public }, + { "sbyte", CSharpKeyword.Sbyte }, + { "throw", CSharpKeyword.Throw }, + { "virtual", CSharpKeyword.Virtual }, + { "decimal", CSharpKeyword.Decimal }, + { "else", CSharpKeyword.Else }, + { "operator", CSharpKeyword.Operator }, + { "string", CSharpKeyword.String }, + { "ulong", CSharpKeyword.Ulong }, + { "bool", CSharpKeyword.Bool }, + { "char", CSharpKeyword.Char }, + { "default", CSharpKeyword.Default }, + { "foreach", CSharpKeyword.Foreach }, + { "long", CSharpKeyword.Long }, + { "void", CSharpKeyword.Void }, + { "enum", CSharpKeyword.Enum }, + { "finally", CSharpKeyword.Finally }, + { "int", CSharpKeyword.Int }, + { "out", CSharpKeyword.Out }, + { "sealed", CSharpKeyword.Sealed }, + { "true", CSharpKeyword.True }, + { "goto", CSharpKeyword.Goto }, + { "unchecked", CSharpKeyword.Unchecked }, + { "interface", CSharpKeyword.Interface }, + { "break", CSharpKeyword.Break }, + { "checked", CSharpKeyword.Checked }, + { "namespace", CSharpKeyword.Namespace }, + { "when", CSharpKeyword.When } + }; + + public CSharpTokenizer(ITextDocument source) + : base(source) + { + base.CurrentState = StartState; + + _operatorHandlers = new Dictionary>() + { + { '-', MinusOperator }, + { '<', LessThanOperator }, + { '>', GreaterThanOperator }, + { '&', CreateTwoCharOperatorHandler(CSharpSymbolType.And, '=', CSharpSymbolType.AndAssign, '&', CSharpSymbolType.DoubleAnd) }, + { '|', CreateTwoCharOperatorHandler(CSharpSymbolType.Or, '=', CSharpSymbolType.OrAssign, '|', CSharpSymbolType.DoubleOr) }, + { '+', CreateTwoCharOperatorHandler(CSharpSymbolType.Plus, '=', CSharpSymbolType.PlusAssign, '+', CSharpSymbolType.Increment) }, + { '=', CreateTwoCharOperatorHandler(CSharpSymbolType.Assign, '=', CSharpSymbolType.Equals, '>', CSharpSymbolType.GreaterThanEqual) }, + { '!', CreateTwoCharOperatorHandler(CSharpSymbolType.Not, '=', CSharpSymbolType.NotEqual) }, + { '%', CreateTwoCharOperatorHandler(CSharpSymbolType.Modulo, '=', CSharpSymbolType.ModuloAssign) }, + { '*', CreateTwoCharOperatorHandler(CSharpSymbolType.Star, '=', CSharpSymbolType.MultiplyAssign) }, + { ':', CreateTwoCharOperatorHandler(CSharpSymbolType.Colon, ':', CSharpSymbolType.DoubleColon) }, + { '?', CreateTwoCharOperatorHandler(CSharpSymbolType.QuestionMark, '?', CSharpSymbolType.NullCoalesce) }, + { '^', CreateTwoCharOperatorHandler(CSharpSymbolType.Xor, '=', CSharpSymbolType.XorAssign) }, + { '(', () => CSharpSymbolType.LeftParenthesis }, + { ')', () => CSharpSymbolType.RightParenthesis }, + { '{', () => CSharpSymbolType.LeftBrace }, + { '}', () => CSharpSymbolType.RightBrace }, + { '[', () => CSharpSymbolType.LeftBracket }, + { ']', () => CSharpSymbolType.RightBracket }, + { ',', () => CSharpSymbolType.Comma }, + { ';', () => CSharpSymbolType.Semicolon }, + { '~', () => CSharpSymbolType.Tilde }, + { '#', () => CSharpSymbolType.Hash } + }; + } + + protected override int StartState => (int)CSharpTokenizerState.Data; + + private new CSharpTokenizerState? CurrentState => (CSharpTokenizerState?)base.CurrentState; + + public override CSharpSymbolType RazorCommentType => CSharpSymbolType.RazorComment; + + public override CSharpSymbolType RazorCommentTransitionType => CSharpSymbolType.RazorCommentTransition; + + public override CSharpSymbolType RazorCommentStarType => CSharpSymbolType.RazorCommentStar; + + protected override StateResult Dispatch() + { + switch (CurrentState) + { + case CSharpTokenizerState.Data: + return Data(); + case CSharpTokenizerState.BlockComment: + return BlockComment(); + case CSharpTokenizerState.QuotedCharacterLiteral: + return QuotedCharacterLiteral(); + case CSharpTokenizerState.QuotedStringLiteral: + return QuotedStringLiteral(); + case CSharpTokenizerState.VerbatimStringLiteral: + return VerbatimStringLiteral(); + case CSharpTokenizerState.AfterRazorCommentTransition: + return AfterRazorCommentTransition(); + case CSharpTokenizerState.EscapedRazorCommentTransition: + return EscapedRazorCommentTransition(); + case CSharpTokenizerState.RazorCommentBody: + return RazorCommentBody(); + case CSharpTokenizerState.StarAfterRazorCommentBody: + return StarAfterRazorCommentBody(); + case CSharpTokenizerState.AtSymbolAfterRazorCommentBody: + return AtSymbolAfterRazorCommentBody(); + default: + Debug.Fail("Invalid TokenizerState"); + return default(StateResult); + } + } + + protected override CSharpSymbol CreateSymbol(SourceLocation start, string content, CSharpSymbolType type, IReadOnlyList errors) + { + return new CSharpSymbol(start, content, type, errors); + } + + private StateResult Data() + { + if (ParserHelpers.IsNewLine(CurrentCharacter)) + { + // CSharp Spec §2.3.1 + var checkTwoCharNewline = CurrentCharacter == '\r'; + TakeCurrent(); + if (checkTwoCharNewline && CurrentCharacter == '\n') + { + TakeCurrent(); + } + return Stay(EndSymbol(CSharpSymbolType.NewLine)); + } + else if (ParserHelpers.IsWhitespace(CurrentCharacter)) + { + // CSharp Spec §2.3.3 + TakeUntil(c => !ParserHelpers.IsWhitespace(c)); + return Stay(EndSymbol(CSharpSymbolType.WhiteSpace)); + } + else if (IsIdentifierStart(CurrentCharacter)) + { + return Identifier(); + } + else if (char.IsDigit(CurrentCharacter)) + { + return NumericLiteral(); + } + switch (CurrentCharacter) + { + case '@': + return AtSymbol(); + case '\'': + TakeCurrent(); + return Transition(CSharpTokenizerState.QuotedCharacterLiteral); + case '"': + TakeCurrent(); + return Transition(CSharpTokenizerState.QuotedStringLiteral); + case '.': + if (char.IsDigit(Peek())) + { + return RealLiteral(); + } + return Stay(Single(CSharpSymbolType.Dot)); + case '/': + TakeCurrent(); + if (CurrentCharacter == '/') + { + TakeCurrent(); + return SingleLineComment(); + } + else if (CurrentCharacter == '*') + { + TakeCurrent(); + return Transition(CSharpTokenizerState.BlockComment); + } + else if (CurrentCharacter == '=') + { + TakeCurrent(); + return Stay(EndSymbol(CSharpSymbolType.DivideAssign)); + } + else + { + return Stay(EndSymbol(CSharpSymbolType.Slash)); + } + default: + return Stay(EndSymbol(Operator())); + } + } + + private StateResult AtSymbol() + { + TakeCurrent(); + if (CurrentCharacter == '"') + { + TakeCurrent(); + return Transition(CSharpTokenizerState.VerbatimStringLiteral); + } + else if (CurrentCharacter == '*') + { + return Transition( + CSharpTokenizerState.AfterRazorCommentTransition, + EndSymbol(CSharpSymbolType.RazorCommentTransition)); + } + else if (CurrentCharacter == '@') + { + // Could be escaped comment transition + return Transition( + CSharpTokenizerState.EscapedRazorCommentTransition, + EndSymbol(CSharpSymbolType.Transition)); + } + + return Stay(EndSymbol(CSharpSymbolType.Transition)); + } + + private StateResult EscapedRazorCommentTransition() + { + TakeCurrent(); + return Transition(CSharpTokenizerState.Data, EndSymbol(CSharpSymbolType.Transition)); + } + + private CSharpSymbolType Operator() + { + var first = CurrentCharacter; + TakeCurrent(); + Func handler; + if (_operatorHandlers.TryGetValue(first, out handler)) + { + return handler(); + } + return CSharpSymbolType.Unknown; + } + + private CSharpSymbolType LessThanOperator() + { + if (CurrentCharacter == '=') + { + TakeCurrent(); + return CSharpSymbolType.LessThanEqual; + } + return CSharpSymbolType.LessThan; + } + + private CSharpSymbolType GreaterThanOperator() + { + if (CurrentCharacter == '=') + { + TakeCurrent(); + return CSharpSymbolType.GreaterThanEqual; + } + return CSharpSymbolType.GreaterThan; + } + + private CSharpSymbolType MinusOperator() + { + if (CurrentCharacter == '>') + { + TakeCurrent(); + return CSharpSymbolType.Arrow; + } + else if (CurrentCharacter == '-') + { + TakeCurrent(); + return CSharpSymbolType.Decrement; + } + else if (CurrentCharacter == '=') + { + TakeCurrent(); + return CSharpSymbolType.MinusAssign; + } + return CSharpSymbolType.Minus; + } + + private Func CreateTwoCharOperatorHandler(CSharpSymbolType typeIfOnlyFirst, char second, CSharpSymbolType typeIfBoth) + { + return () => + { + if (CurrentCharacter == second) + { + TakeCurrent(); + return typeIfBoth; + } + return typeIfOnlyFirst; + }; + } + + private Func CreateTwoCharOperatorHandler(CSharpSymbolType typeIfOnlyFirst, char option1, CSharpSymbolType typeIfOption1, char option2, CSharpSymbolType typeIfOption2) + { + return () => + { + if (CurrentCharacter == option1) + { + TakeCurrent(); + return typeIfOption1; + } + else if (CurrentCharacter == option2) + { + TakeCurrent(); + return typeIfOption2; + } + return typeIfOnlyFirst; + }; + } + + private StateResult VerbatimStringLiteral() + { + TakeUntil(c => c == '"'); + if (CurrentCharacter == '"') + { + TakeCurrent(); + if (CurrentCharacter == '"') + { + TakeCurrent(); + // Stay in the literal, this is an escaped " + return Stay(); + } + } + else if (EndOfFile) + { + CurrentErrors.Add( + new RazorError( + LegacyResources.ParseError_Unterminated_String_Literal, + CurrentStart, + length: 1 /* end of file */)); + } + return Transition(CSharpTokenizerState.Data, EndSymbol(CSharpSymbolType.StringLiteral)); + } + + private StateResult QuotedCharacterLiteral() => QuotedLiteral('\'', CSharpSymbolType.CharacterLiteral); + + private StateResult QuotedStringLiteral() => QuotedLiteral('\"', CSharpSymbolType.StringLiteral); + + private StateResult QuotedLiteral(char quote, CSharpSymbolType literalType) + { + TakeUntil(c => c == '\\' || c == quote || ParserHelpers.IsNewLine(c)); + if (CurrentCharacter == '\\') + { + TakeCurrent(); // Take the '\' + + // If the next char is the same quote that started this + if (CurrentCharacter == quote || CurrentCharacter == '\\') + { + TakeCurrent(); // Take it so that we don't prematurely end the literal. + } + return Stay(); + } + else if (EndOfFile || ParserHelpers.IsNewLine(CurrentCharacter)) + { + CurrentErrors.Add( + new RazorError( + LegacyResources.ParseError_Unterminated_String_Literal, + CurrentStart, + length: 1 /* " */)); + } + else + { + TakeCurrent(); // No-op if at EOF + } + return Transition(CSharpTokenizerState.Data, EndSymbol(literalType)); + } + + // CSharp Spec §2.3.2 + private StateResult BlockComment() + { + TakeUntil(c => c == '*'); + if (EndOfFile) + { + CurrentErrors.Add( + new RazorError( + LegacyResources.ParseError_BlockComment_Not_Terminated, + CurrentStart, + length: 1 /* end of file */)); + return Transition(CSharpTokenizerState.Data, EndSymbol(CSharpSymbolType.Comment)); + } + if (CurrentCharacter == '*') + { + TakeCurrent(); + if (CurrentCharacter == '/') + { + TakeCurrent(); + return Transition(CSharpTokenizerState.Data, EndSymbol(CSharpSymbolType.Comment)); + } + } + return Stay(); + } + + // CSharp Spec §2.3.2 + private StateResult SingleLineComment() + { + TakeUntil(c => ParserHelpers.IsNewLine(c)); + return Stay(EndSymbol(CSharpSymbolType.Comment)); + } + + // CSharp Spec §2.4.4 + private StateResult NumericLiteral() + { + if (TakeAll("0x", caseSensitive: true)) + { + return HexLiteral(); + } + else + { + return DecimalLiteral(); + } + } + + private StateResult HexLiteral() + { + TakeUntil(c => !IsHexDigit(c)); + TakeIntegerSuffix(); + return Stay(EndSymbol(CSharpSymbolType.IntegerLiteral)); + } + + private StateResult DecimalLiteral() + { + TakeUntil(c => !Char.IsDigit(c)); + if (CurrentCharacter == '.' && Char.IsDigit(Peek())) + { + return RealLiteral(); + } + else if (IsRealLiteralSuffix(CurrentCharacter) || + CurrentCharacter == 'E' || CurrentCharacter == 'e') + { + return RealLiteralExponentPart(); + } + else + { + TakeIntegerSuffix(); + return Stay(EndSymbol(CSharpSymbolType.IntegerLiteral)); + } + } + + private StateResult RealLiteralExponentPart() + { + if (CurrentCharacter == 'E' || CurrentCharacter == 'e') + { + TakeCurrent(); + if (CurrentCharacter == '+' || CurrentCharacter == '-') + { + TakeCurrent(); + } + TakeUntil(c => !Char.IsDigit(c)); + } + if (IsRealLiteralSuffix(CurrentCharacter)) + { + TakeCurrent(); + } + return Stay(EndSymbol(CSharpSymbolType.RealLiteral)); + } + + // CSharp Spec §2.4.4.3 + private StateResult RealLiteral() + { + AssertCurrent('.'); + TakeCurrent(); + Debug.Assert(Char.IsDigit(CurrentCharacter)); + TakeUntil(c => !Char.IsDigit(c)); + return RealLiteralExponentPart(); + } + + private void TakeIntegerSuffix() + { + if (Char.ToLowerInvariant(CurrentCharacter) == 'u') + { + TakeCurrent(); + if (Char.ToLowerInvariant(CurrentCharacter) == 'l') + { + TakeCurrent(); + } + } + else if (Char.ToLowerInvariant(CurrentCharacter) == 'l') + { + TakeCurrent(); + if (Char.ToLowerInvariant(CurrentCharacter) == 'u') + { + TakeCurrent(); + } + } + } + + // CSharp Spec §2.4.2 + private StateResult Identifier() + { + Debug.Assert(IsIdentifierStart(CurrentCharacter)); + TakeCurrent(); + TakeUntil(c => !IsIdentifierPart(c)); + CSharpSymbol symbol = null; + if (HaveContent) + { + CSharpKeyword keyword; + var type = CSharpSymbolType.Identifier; + if (_keywords.TryGetValue(Buffer.ToString(), out keyword)) + { + type = CSharpSymbolType.Keyword; + } + + symbol = new CSharpSymbol(CurrentStart, Buffer.ToString(), type) + { + Keyword = type == CSharpSymbolType.Keyword ? (CSharpKeyword?)keyword : null, + }; + } + StartSymbol(); + return Stay(symbol); + } + + private StateResult Transition(CSharpTokenizerState state) + { + return Transition((int)state, result: null); + } + + private StateResult Transition(CSharpTokenizerState state, CSharpSymbol result) + { + return Transition((int)state, result); + } + + private static bool IsIdentifierStart(char character) + { + return char.IsLetter(character) || + character == '_' || + CharUnicodeInfo.GetUnicodeCategory(character) == UnicodeCategory.LetterNumber; + } + + private static bool IsIdentifierPart(char character) + { + return char.IsDigit(character) || + IsIdentifierStart(character) || + IsIdentifierPartByUnicodeCategory(character); + } + + private static bool IsRealLiteralSuffix(char character) + { + return character == 'F' || + character == 'f' || + character == 'D' || + character == 'd' || + character == 'M' || + character == 'm'; + } + + private static bool IsIdentifierPartByUnicodeCategory(char character) + { + var category = CharUnicodeInfo.GetUnicodeCategory(character); + + return category == UnicodeCategory.NonSpacingMark || // Mn + category == UnicodeCategory.SpacingCombiningMark || // Mc + category == UnicodeCategory.ConnectorPunctuation || // Pc + category == UnicodeCategory.Format; // Cf + } + + private static bool IsHexDigit(char value) + { + return (value >= '0' && value <= '9') || (value >= 'A' && value <= 'F') || (value >= 'a' && value <= 'f'); + } + + private enum CSharpTokenizerState + { + Data, + BlockComment, + QuotedCharacterLiteral, + QuotedStringLiteral, + VerbatimStringLiteral, + + // Razor Comments - need to be the same for HTML and CSharp + AfterRazorCommentTransition = RazorCommentTokenizerState.AfterRazorCommentTransition, + EscapedRazorCommentTransition = RazorCommentTokenizerState.EscapedRazorCommentTransition, + RazorCommentBody = RazorCommentTokenizerState.RazorCommentBody, + StarAfterRazorCommentBody = RazorCommentTokenizerState.StarAfterRazorCommentBody, + AtSymbolAfterRazorCommentBody = RazorCommentTokenizerState.AtSymbolAfterRazorCommentBody, + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/ChunkGeneratorContext.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/ChunkGeneratorContext.cs new file mode 100644 index 0000000000..1085c72724 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/ChunkGeneratorContext.cs @@ -0,0 +1,25 @@ +// 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.Legacy +{ + internal class ChunkGeneratorContext + { + public ChunkGeneratorContext( + string className, + string rootNamespace, + string sourceFile, + bool shouldGenerateLinePragmas) + { + SourceFile = shouldGenerateLinePragmas ? sourceFile : null; + RootNamespace = rootNamespace; + ClassName = className; + } + + public string SourceFile { get; internal set; } + + public string RootNamespace { get; } + + public string ClassName { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/DisposableAction.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/DisposableAction.cs new file mode 100644 index 0000000000..c0bacf7826 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/DisposableAction.cs @@ -0,0 +1,32 @@ +// 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; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class DisposableAction : IDisposable + { + private readonly Action _action; + private bool _invoked; + + public DisposableAction(Action action) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + _action = action; + } + + public void Dispose() + { + if (!_invoked) + { + _action(); + _invoked = true; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/DynamicAttributeBlockChunkGenerator.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/DynamicAttributeBlockChunkGenerator.cs new file mode 100644 index 0000000000..c1d790ee4e --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/DynamicAttributeBlockChunkGenerator.cs @@ -0,0 +1,54 @@ +// 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.Globalization; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class DynamicAttributeBlockChunkGenerator : ParentChunkGenerator + { + public DynamicAttributeBlockChunkGenerator(LocationTagged prefix, int offset, int line, int col) + : this(prefix, new SourceLocation(offset, line, col)) + { + } + + public DynamicAttributeBlockChunkGenerator(LocationTagged prefix, SourceLocation valueStart) + { + Prefix = prefix; + ValueStart = valueStart; + } + + public LocationTagged Prefix { get; } + + public SourceLocation ValueStart { get; } + + public override void GenerateStartParentChunk(Block target, ChunkGeneratorContext context) + { + //var chunk = context.ChunkTreeBuilder.StartParentChunk(target); + //chunk.Start = ValueStart; + //chunk.Prefix = Prefix; + } + + public override void GenerateEndParentChunk(Block target, ChunkGeneratorContext context) + { + //context.ChunkTreeBuilder.EndParentChunk(); + } + + public override string ToString() + { + return string.Format(CultureInfo.CurrentCulture, "DynAttr:{0:F}", Prefix); + } + + public override bool Equals(object obj) + { + var other = obj as DynamicAttributeBlockChunkGenerator; + return other != null && + Equals(other.Prefix, Prefix); + } + + public override int GetHashCode() + { + return Prefix == null ? 0 : Prefix.GetHashCode(); + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/EditResult.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/EditResult.cs new file mode 100644 index 0000000000..b54264592a --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/EditResult.cs @@ -0,0 +1,17 @@ +// 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.Legacy +{ + internal class EditResult + { + public EditResult(PartialParseResult result, SpanBuilder editedSpan) + { + Result = result; + EditedSpan = editedSpan; + } + + public PartialParseResult Result { get; set; } + public SpanBuilder EditedSpan { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/ErrorSink.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/ErrorSink.cs new file mode 100644 index 0000000000..a048078561 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/ErrorSink.cs @@ -0,0 +1,46 @@ +// 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.Legacy +{ + /// + /// Used to manage s encountered during the Razor parsing phase. + /// + internal class ErrorSink + { + private readonly List _errors; + + /// + /// Instantiates a new instance of . + /// + public ErrorSink() + { + _errors = new List(); + } + + /// + /// s collected. + /// + public IEnumerable Errors => _errors; + + /// + /// Tracks the given . + /// + /// The to track. + public void OnError(RazorError error) =>_errors.Add(error); + + /// + /// Creates and tracks a new . + /// + /// of the error. + /// A message describing the error. + /// The length of the error. + public void OnError(SourceLocation location, string message, int length) + { + var error = new RazorError(message, location, length); + _errors.Add(error); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/ExpressionChunkGenerator.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/ExpressionChunkGenerator.cs new file mode 100644 index 0000000000..4bc1e7789b --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/ExpressionChunkGenerator.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. + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class ExpressionChunkGenerator : ISpanChunkGenerator, IParentChunkGenerator + { + private static readonly int TypeHashCode = typeof(ExpressionChunkGenerator).GetHashCode(); + + public void GenerateStartParentChunk(Block target, ChunkGeneratorContext context) + { + //context.ChunkTreeBuilder.StartParentChunk(target); + } + + public void GenerateChunk(Span target, ChunkGeneratorContext context) + { + //context.ChunkTreeBuilder.AddExpressionChunk(target.Content, target); + } + + public void GenerateEndParentChunk(Block target, ChunkGeneratorContext context) + { + //context.ChunkTreeBuilder.EndParentChunk(); + } + + public override string ToString() + { + return "Expr"; + } + + public override bool Equals(object obj) + { + return obj != null && + GetType() == obj.GetType(); + } + + public override int GetHashCode() + { + return TypeHashCode; + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/HtmlLanguageCharacteristics.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/HtmlLanguageCharacteristics.cs new file mode 100644 index 0000000000..db0126990b --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/HtmlLanguageCharacteristics.cs @@ -0,0 +1,133 @@ +// 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.Diagnostics; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class HtmlLanguageCharacteristics : LanguageCharacteristics + { + private static readonly HtmlLanguageCharacteristics _instance = new HtmlLanguageCharacteristics(); + + private HtmlLanguageCharacteristics() + { + } + + public static HtmlLanguageCharacteristics Instance + { + get { return _instance; } + } + + public override string GetSample(HtmlSymbolType type) + { + switch (type) + { + case HtmlSymbolType.Text: + return LegacyResources.HtmlSymbol_Text; + case HtmlSymbolType.WhiteSpace: + return LegacyResources.HtmlSymbol_WhiteSpace; + case HtmlSymbolType.NewLine: + return LegacyResources.HtmlSymbol_NewLine; + case HtmlSymbolType.OpenAngle: + return "<"; + case HtmlSymbolType.Bang: + return "!"; + case HtmlSymbolType.ForwardSlash: + return "/"; + case HtmlSymbolType.QuestionMark: + return "?"; + case HtmlSymbolType.DoubleHyphen: + return "--"; + case HtmlSymbolType.LeftBracket: + return "["; + case HtmlSymbolType.CloseAngle: + return ">"; + case HtmlSymbolType.RightBracket: + return "]"; + case HtmlSymbolType.Equals: + return "="; + case HtmlSymbolType.DoubleQuote: + return "\""; + case HtmlSymbolType.SingleQuote: + return "'"; + case HtmlSymbolType.Transition: + return "@"; + case HtmlSymbolType.Colon: + return ":"; + case HtmlSymbolType.RazorComment: + return LegacyResources.HtmlSymbol_RazorComment; + case HtmlSymbolType.RazorCommentStar: + return "*"; + case HtmlSymbolType.RazorCommentTransition: + return "@"; + default: + return LegacyResources.Symbol_Unknown; + } + } + + public override HtmlTokenizer CreateTokenizer(ITextDocument source) + { + return new HtmlTokenizer(source); + } + + public override HtmlSymbolType FlipBracket(HtmlSymbolType bracket) + { + switch (bracket) + { + case HtmlSymbolType.LeftBracket: + return HtmlSymbolType.RightBracket; + case HtmlSymbolType.OpenAngle: + return HtmlSymbolType.CloseAngle; + case HtmlSymbolType.RightBracket: + return HtmlSymbolType.LeftBracket; + case HtmlSymbolType.CloseAngle: + return HtmlSymbolType.OpenAngle; + default: +#if NET451 + // No Debug.Fail in CoreCLR + + Debug.Fail("FlipBracket must be called with a bracket character"); +#else + Debug.Assert(false, "FlipBracket must be called with a bracket character"); +#endif + return HtmlSymbolType.Unknown; + } + } + + public override HtmlSymbol CreateMarkerSymbol(SourceLocation location) + { + return new HtmlSymbol(location, string.Empty, HtmlSymbolType.Unknown); + } + + public override HtmlSymbolType GetKnownSymbolType(KnownSymbolType type) + { + switch (type) + { + case KnownSymbolType.CommentStart: + return HtmlSymbolType.RazorCommentTransition; + case KnownSymbolType.CommentStar: + return HtmlSymbolType.RazorCommentStar; + case KnownSymbolType.CommentBody: + return HtmlSymbolType.RazorComment; + case KnownSymbolType.Identifier: + return HtmlSymbolType.Text; + case KnownSymbolType.Keyword: + return HtmlSymbolType.Text; + case KnownSymbolType.NewLine: + return HtmlSymbolType.NewLine; + case KnownSymbolType.Transition: + return HtmlSymbolType.Transition; + case KnownSymbolType.WhiteSpace: + return HtmlSymbolType.WhiteSpace; + default: + return HtmlSymbolType.Unknown; + } + } + + protected override HtmlSymbol CreateSymbol(SourceLocation location, string content, HtmlSymbolType type, IReadOnlyList errors) + { + return new HtmlSymbol(location, content, type, errors); + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/HtmlMarkupParser.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/HtmlMarkupParser.cs new file mode 100644 index 0000000000..82753dfb6e --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/HtmlMarkupParser.cs @@ -0,0 +1,1757 @@ +// 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.Diagnostics; +using System.Linq; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class HtmlMarkupParser : TokenizerBackedParser + { + private const string ScriptTagName = "script"; + + private static readonly char[] ValidAfterTypeAttributeNameCharacters = { ' ', '\t', '\r', '\n', '\f', '=' }; + private SourceLocation _lastTagStart = SourceLocation.Zero; + private HtmlSymbol _bufferedOpenAngle; + + //From http://dev.w3.org/html5/spec/Overview.html#elements-0 + private ISet _voidElements = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "area", + "base", + "br", + "col", + "command", + "embed", + "hr", + "img", + "input", + "keygen", + "link", + "meta", + "param", + "source", + "track", + "wbr" + }; + + public HtmlMarkupParser(ParserContext context) + : base(HtmlLanguageCharacteristics.Instance, context) + { + } + + public ParserBase CodeParser { get; set; } + + public ISet VoidElements + { + get { return _voidElements; } + } + + private bool CaseSensitive { get; set; } + + private StringComparison Comparison + { + get { return CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; } + } + + protected override bool SymbolTypeEquals(HtmlSymbolType x, HtmlSymbolType y) => x == y; + + public override void BuildSpan(SpanBuilder span, SourceLocation start, string content) + { + span.Kind = SpanKind.Markup; + span.ChunkGenerator = new MarkupChunkGenerator(); + base.BuildSpan(span, start, content); + } + + protected override void OutputSpanBeforeRazorComment() + { + Output(SpanKind.Markup); + } + + protected void SkipToAndParseCode(HtmlSymbolType type) + { + SkipToAndParseCode(sym => sym.Type == type); + } + + protected void SkipToAndParseCode(Func condition) + { + HtmlSymbol last = null; + var startOfLine = false; + while (!EndOfFile && !condition(CurrentSymbol)) + { + if (Context.NullGenerateWhitespaceAndNewLine) + { + Context.NullGenerateWhitespaceAndNewLine = false; + Span.ChunkGenerator = SpanChunkGenerator.Null; + AcceptWhile(symbol => symbol.Type == HtmlSymbolType.WhiteSpace); + if (At(HtmlSymbolType.NewLine)) + { + AcceptAndMoveNext(); + } + + Output(SpanKind.Markup); + } + else if (At(HtmlSymbolType.NewLine)) + { + if (last != null) + { + Accept(last); + } + + // Mark the start of a new line + startOfLine = true; + last = null; + AcceptAndMoveNext(); + } + else if (At(HtmlSymbolType.Transition)) + { + var transition = CurrentSymbol; + NextToken(); + if (At(HtmlSymbolType.Transition)) + { + if (last != null) + { + Accept(last); + last = null; + } + Output(SpanKind.Markup); + Accept(transition); + Span.ChunkGenerator = SpanChunkGenerator.Null; + Output(SpanKind.Markup); + AcceptAndMoveNext(); + continue; // while + } + else + { + if (!EndOfFile) + { + PutCurrentBack(); + } + PutBack(transition); + } + + // Handle whitespace rewriting + if (last != null) + { + if (!Context.DesignTimeMode && last.Type == HtmlSymbolType.WhiteSpace && startOfLine) + { + // Put the whitespace back too + startOfLine = false; + PutBack(last); + last = null; + } + else + { + // Accept last + Accept(last); + last = null; + } + } + + OtherParserBlock(); + } + else if (At(HtmlSymbolType.RazorCommentTransition)) + { + if (last != null) + { + // Don't render the whitespace between the start of the line and the razor comment. + if (startOfLine && last.Type == HtmlSymbolType.WhiteSpace) + { + AddMarkerSymbolIfNecessary(); + // Output the symbols that may have been accepted prior to the whitespace. + Output(SpanKind.Markup); + + Span.ChunkGenerator = SpanChunkGenerator.Null; + } + + Accept(last); + last = null; + } + + AddMarkerSymbolIfNecessary(); + Output(SpanKind.Markup); + + RazorComment(); + + // Handle the whitespace and newline at the end of a razor comment. + if (startOfLine && + (At(HtmlSymbolType.NewLine) || + (At(HtmlSymbolType.WhiteSpace) && NextIs(HtmlSymbolType.NewLine)))) + { + AcceptWhile(IsSpacingToken(includeNewLines: false)); + AcceptAndMoveNext(); + Span.ChunkGenerator = SpanChunkGenerator.Null; + Output(SpanKind.Markup); + } + } + else + { + // As long as we see whitespace, we're still at the "start" of the line + startOfLine &= At(HtmlSymbolType.WhiteSpace); + + // If there's a last token, accept it + if (last != null) + { + Accept(last); + last = null; + } + + // Advance + last = CurrentSymbol; + NextToken(); + } + } + + if (last != null) + { + Accept(last); + } + } + + protected static Func IsSpacingToken(bool includeNewLines) + { + return sym => sym.Type == HtmlSymbolType.WhiteSpace || (includeNewLines && sym.Type == HtmlSymbolType.NewLine); + } + + private void OtherParserBlock() + { + AddMarkerSymbolIfNecessary(); + Output(SpanKind.Markup); + using (PushSpanConfig()) + { + CodeParser.ParseBlock(); + } + Initialize(Span); + NextToken(); + } + + private bool IsBangEscape(int lookahead) + { + var potentialBang = Lookahead(lookahead); + + if (potentialBang != null && + potentialBang.Type == HtmlSymbolType.Bang) + { + var afterBang = Lookahead(lookahead + 1); + + return afterBang != null && + afterBang.Type == HtmlSymbolType.Text && + !string.Equals(afterBang.Content, "DOCTYPE", StringComparison.OrdinalIgnoreCase); + } + + return false; + } + + private void OptionalBangEscape() + { + if (IsBangEscape(lookahead: 0)) + { + Output(SpanKind.Markup); + + // Accept the parser escape character '!'. + Assert(HtmlSymbolType.Bang); + AcceptAndMoveNext(); + + // Setup the metacode span that we will be outputing. + Span.ChunkGenerator = SpanChunkGenerator.Null; + Output(SpanKind.MetaCode, AcceptedCharacters.None); + } + } + + public override void ParseBlock() + { + if (Context == null) + { + throw new InvalidOperationException(LegacyResources.Parser_Context_Not_Set); + } + + using (PushSpanConfig(DefaultMarkupSpan)) + { + using (Context.Builder.StartBlock(BlockType.Markup)) + { + if (!NextToken()) + { + return; + } + + AcceptWhile(IsSpacingToken(includeNewLines: true)); + + if (CurrentSymbol.Type == HtmlSymbolType.OpenAngle) + { + // "<" => Implicit Tag Block + TagBlock(new Stack>()); + } + else if (CurrentSymbol.Type == HtmlSymbolType.Transition) + { + // "@" => Explicit Tag/Single Line Block OR Template + Output(SpanKind.Markup); + + // Definitely have a transition span + Assert(HtmlSymbolType.Transition); + AcceptAndMoveNext(); + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + Span.ChunkGenerator = SpanChunkGenerator.Null; + Output(SpanKind.Transition); + if (At(HtmlSymbolType.Transition)) + { + Span.ChunkGenerator = SpanChunkGenerator.Null; + AcceptAndMoveNext(); + Output(SpanKind.MetaCode); + } + AfterTransition(); + } + else + { + Context.ErrorSink.OnError( + CurrentSymbol.Start, + LegacyResources.ParseError_MarkupBlock_Must_Start_With_Tag, + CurrentSymbol.Content.Length); + } + Output(SpanKind.Markup); + } + } + } + + private void DefaultMarkupSpan(SpanBuilder span) + { + span.ChunkGenerator = new MarkupChunkGenerator(); + span.EditHandler = new SpanEditHandler(Language.TokenizeString, AcceptedCharacters.Any); + } + + private void AfterTransition() + { + // "@:" => Explicit Single Line Block + if (CurrentSymbol.Type == HtmlSymbolType.Text && CurrentSymbol.Content.Length > 0 && CurrentSymbol.Content[0] == ':') + { + // Split the token + Tuple split = Language.SplitSymbol(CurrentSymbol, 1, HtmlSymbolType.Colon); + + // The first part (left) is added to this span and we return a MetaCode span + Accept(split.Item1); + Span.ChunkGenerator = SpanChunkGenerator.Null; + Output(SpanKind.MetaCode); + if (split.Item2 != null) + { + Accept(split.Item2); + } + NextToken(); + SingleLineMarkup(); + } + else if (CurrentSymbol.Type == HtmlSymbolType.OpenAngle) + { + TagBlock(new Stack>()); + } + } + + private void SingleLineMarkup() + { + // Parse until a newline, it's that simple! + // First, signal to code parser that whitespace is significant to us. + var old = Context.WhiteSpaceIsSignificantToAncestorBlock; + Context.WhiteSpaceIsSignificantToAncestorBlock = true; + Span.EditHandler = new SpanEditHandler(Language.TokenizeString); + SkipToAndParseCode(HtmlSymbolType.NewLine); + if (!EndOfFile && CurrentSymbol.Type == HtmlSymbolType.NewLine) + { + AcceptAndMoveNext(); + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + } + PutCurrentBack(); + Context.WhiteSpaceIsSignificantToAncestorBlock = old; + Output(SpanKind.Markup); + } + + private void TagBlock(Stack> tags) + { + // Skip Whitespace and Text + var complete = false; + do + { + SkipToAndParseCode(HtmlSymbolType.OpenAngle); + + // Output everything prior to the OpenAngle into a markup span + Output(SpanKind.Markup); + + // Do not want to start a new tag block if we're at the end of the file. + IDisposable tagBlockWrapper = null; + try + { + var atSpecialTag = AtSpecialTag; + + if (!EndOfFile && !atSpecialTag) + { + // Start a Block tag. This is used to wrap things like

or etc. + tagBlockWrapper = Context.Builder.StartBlock(BlockType.Tag); + } + + if (EndOfFile) + { + EndTagBlock(tags, complete: true); + } + else + { + _bufferedOpenAngle = null; + _lastTagStart = CurrentLocation; + Assert(HtmlSymbolType.OpenAngle); + _bufferedOpenAngle = CurrentSymbol; + var tagStart = CurrentLocation; + if (!NextToken()) + { + Accept(_bufferedOpenAngle); + EndTagBlock(tags, complete: false); + } + else + { + complete = AfterTagStart(tagStart, tags, atSpecialTag, tagBlockWrapper); + } + } + + if (complete) + { + // Completed tags have no accepted characters inside of blocks. + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + } + + // Output the contents of the tag into its own markup span. + Output(SpanKind.Markup); + } + finally + { + // Will be null if we were at end of file or special tag when initially created. + if (tagBlockWrapper != null) + { + // End tag block + tagBlockWrapper.Dispose(); + } + } + } + while (tags.Count > 0); + + EndTagBlock(tags, complete); + } + + private bool AfterTagStart(SourceLocation tagStart, + Stack> tags, + bool atSpecialTag, + IDisposable tagBlockWrapper) + { + if (!EndOfFile) + { + switch (CurrentSymbol.Type) + { + case HtmlSymbolType.ForwardSlash: + // End Tag + return EndTag(tagStart, tags, tagBlockWrapper); + case HtmlSymbolType.Bang: + // Comment, CDATA, DOCTYPE, or a parser-escaped HTML tag. + if (atSpecialTag) + { + Accept(_bufferedOpenAngle); + return BangTag(); + } + else + { + goto default; + } + case HtmlSymbolType.QuestionMark: + // XML PI + Accept(_bufferedOpenAngle); + return XmlPI(); + default: + // Start Tag + return StartTag(tags, tagBlockWrapper); + } + } + if (tags.Count == 0) + { + Context.ErrorSink.OnError( + CurrentLocation, + LegacyResources.ParseError_OuterTagMissingName, + length: 1 /* end of file */); + } + return false; + } + + private bool XmlPI() + { + // Accept "?" + Assert(HtmlSymbolType.QuestionMark); + AcceptAndMoveNext(); + return AcceptUntilAll(HtmlSymbolType.QuestionMark, HtmlSymbolType.CloseAngle); + } + + private bool BangTag() + { + // Accept "!" + Assert(HtmlSymbolType.Bang); + + if (AcceptAndMoveNext()) + { + if (CurrentSymbol.Type == HtmlSymbolType.DoubleHyphen) + { + AcceptAndMoveNext(); + + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any; + while (!EndOfFile) + { + SkipToAndParseCode(HtmlSymbolType.DoubleHyphen); + if (At(HtmlSymbolType.DoubleHyphen)) + { + AcceptWhile(HtmlSymbolType.DoubleHyphen); + + if (At(HtmlSymbolType.Text) && + string.Equals(CurrentSymbol.Content, "-", StringComparison.Ordinal)) + { + AcceptAndMoveNext(); + } + + if (At(HtmlSymbolType.CloseAngle)) + { + AcceptAndMoveNext(); + return true; + } + } + } + + return false; + } + else if (CurrentSymbol.Type == HtmlSymbolType.LeftBracket) + { + if (AcceptAndMoveNext()) + { + return CData(); + } + } + else + { + AcceptAndMoveNext(); + return AcceptUntilAll(HtmlSymbolType.CloseAngle); + } + } + + return false; + } + + private bool CData() + { + if (CurrentSymbol.Type == HtmlSymbolType.Text && string.Equals(CurrentSymbol.Content, "cdata", StringComparison.OrdinalIgnoreCase)) + { + if (AcceptAndMoveNext()) + { + if (CurrentSymbol.Type == HtmlSymbolType.LeftBracket) + { + return AcceptUntilAll(HtmlSymbolType.RightBracket, HtmlSymbolType.RightBracket, HtmlSymbolType.CloseAngle); + } + } + } + + return false; + } + + private bool EndTag(SourceLocation tagStart, + Stack> tags, + IDisposable tagBlockWrapper) + { + // Accept "/" and move next + Assert(HtmlSymbolType.ForwardSlash); + var forwardSlash = CurrentSymbol; + if (!NextToken()) + { + Accept(_bufferedOpenAngle); + Accept(forwardSlash); + return false; + } + else + { + var tagName = string.Empty; + HtmlSymbol bangSymbol = null; + + if (At(HtmlSymbolType.Bang)) + { + bangSymbol = CurrentSymbol; + + var nextSymbol = Lookahead(count: 1); + + if (nextSymbol != null && nextSymbol.Type == HtmlSymbolType.Text) + { + tagName = "!" + nextSymbol.Content; + } + } + else if (At(HtmlSymbolType.Text)) + { + tagName = CurrentSymbol.Content; + } + + var matched = RemoveTag(tags, tagName, tagStart); + + if (tags.Count == 0 && + // Note tagName may contain a '!' escape character. This ensures doesn't match here. + // tags are treated like any other escaped HTML end tag. + string.Equals(tagName, SyntaxConstants.TextTagName, StringComparison.OrdinalIgnoreCase) && + matched) + { + return EndTextTag(forwardSlash, tagBlockWrapper); + } + Accept(_bufferedOpenAngle); + Accept(forwardSlash); + + OptionalBangEscape(); + + AcceptUntil(HtmlSymbolType.CloseAngle); + + // Accept the ">" + return Optional(HtmlSymbolType.CloseAngle); + } + } + + private void RecoverTextTag() + { + // We don't want to skip-to and parse because there shouldn't be anything in the body of text tags. + AcceptUntil(HtmlSymbolType.CloseAngle, HtmlSymbolType.NewLine); + + // Include the close angle in the text tag block if it's there, otherwise just move on + Optional(HtmlSymbolType.CloseAngle); + } + + private bool EndTextTag(HtmlSymbol solidus, IDisposable tagBlockWrapper) + { + Accept(_bufferedOpenAngle); + Accept(solidus); + + var textLocation = CurrentLocation; + Assert(HtmlSymbolType.Text); + AcceptAndMoveNext(); + + var seenCloseAngle = Optional(HtmlSymbolType.CloseAngle); + + if (!seenCloseAngle) + { + Context.ErrorSink.OnError( + textLocation, + LegacyResources.ParseError_TextTagCannotContainAttributes, + length: 4 /* text */); + + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any; + RecoverTextTag(); + } + else + { + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + } + + Span.ChunkGenerator = SpanChunkGenerator.Null; + + CompleteTagBlockWithSpan(tagBlockWrapper, Span.EditHandler.AcceptedCharacters, SpanKind.Transition); + + return seenCloseAngle; + } + + // Special tags include + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Value cannot be null or an empty string. + + + Value must be between {0} and {1}. + + + Value must be a value from the "{0}" enumeration. + + + Value must be greater than {0}. + + + Value must be greater than or equal to {0}. + + + Value must be less than {0}. + + + Value must be less than or equal to {0}. + + + Value cannot be an empty string. It must either be null or a non-empty string. + + + code + This is a literal used when composing ParserError_* messages. Most blocks are named by the keyword that starts them, for example "if". However, for those without keywords, a (localizable) name must be used. This literal is ALWAYS used mid-sentence, thus should not be capitalized. + + + explicit expression + This is a literal used when composing ParserError_* messages. Most blocks are named by the keyword that starts them, for example "if". However, for those without keywords, a (localizable) name must be used. This literal is ALWAYS used mid-sentence, thus should not be capitalized. + + + Block cannot be built because a Type has not been specified in the BlockBuilder + + + <<character literal>> + + + <<comment>> + + + <<identifier>> + + + <<integer literal>> + + + <<keyword>> + + + <<newline sequence>> + + + <<real literal>> + + + <<string literal>> + + + <<white space>> + + + "EndBlock" was called without a matching call to "StartBlock". + + + "{0}" character + + + end of file + + + line break + + + space or line break + + + <<newline sequence>> + + + <<razor comment>> + + + <<text>> + + + <<white space>> + + + Cannot use built-in RazorComment handler, language characteristics does not define the CommentStart, CommentStar and CommentBody known symbol types or parser does not override TokenizerBackedParser.OutputSpanBeforeRazorComment + + + The "@" character must be followed by a ":", "(", or a C# identifier. If you intended to switch to markup, use an HTML start tag, for example: + +@if(isLoggedIn) { + <p>Hello, @user!</p> +} + + + End of file was reached before the end of the block comment. All comments started with "/*" sequence must be terminated with a matching "*/" sequence. + + + Directive '{0}' must have a value. + + + An opening "{0}" is missing the corresponding closing "{1}". + + + The {0} block is missing a closing "{1}" character. Make sure you have a matching "{1}" character for all the "{2}" characters within this block, and that none of the "{1}" characters are being interpreted as markup. + + + Expected "{0}". + + + The {0} directive is not supported. + + + Optional quote around the directive '{0}' is missing the corresponding opening or closing quote. + + + The 'inherits' keyword must be followed by a type name on the same line. + + + Inline markup blocks (@<p>Content</p>) cannot be nested. Only one level of inline markup is allowed. + + + Markup in a code block must start with a tag and all start tags must be matched with end tags. Do not use unclosed tags like "<br>". Instead use self-closing tags like "<br/>". + + + The "{0}" element was not closed. All elements must be either self-closing or have a matching end tag. + + + Sections cannot be empty. The "@section" keyword must be followed by a block of markup surrounded by "{}". For example: + +@section Sidebar { + <!-- Markup and text goes here --> +} + + + Namespace imports and type aliases cannot be placed within code blocks. They must immediately follow an "@" character in markup. It is recommended that you put them at the top of the page, as in the following example: + +@using System.Drawing; +@{ + // OK here to use types from System.Drawing in the page. +} + + + Outer tag is missing a name. The first character of a markup block must be an HTML tag with a valid name. + + + End of file was reached before the end of the block comment. All comments that start with the "@*" sequence must be terminated with a matching "*@" sequence. + + + "{0}" is a reserved word and cannot be used in implicit expressions. An explicit expression ("@()") must be used. + + + Section blocks ("{0}") cannot be nested. Only one level of section blocks are allowed. + + + Expected a "{0}" but found a "{1}". Block statements must be enclosed in "{{" and "}}". You cannot use single-statement control-flow statements in CSHTML pages. For example, the following is not allowed: + +@if(isLoggedIn) + <p>Hello, @user</p> + +Instead, wrap the contents of the block in "{{}}": + +@if(isLoggedIn) {{ + <p>Hello, @user</p> +}} + {0} is only ever a single character + + + "<text>" and "</text>" tags cannot contain attributes. + + + Encountered end tag "{0}" with no matching start tag. Are your start/end tags properly balanced? + + + Unexpected {0} after section keyword. Section names must start with an "_" or alphabetic character, and the remaining characters must be either "_" or alphanumeric. + + + "{0}" is not valid at the start of a code block. Only identifiers, keywords, comments, "(" and "{{" are valid. + "{{" is an escape sequence for string.Format, when outputted to the user it will be displayed as "{" + + + End-of-file was found after the "@" character. "@" must be followed by a valid code block. If you want to output an "@", escape it using the sequence: "@@" + + + End-of-file was found after the "@" character. "@" must be followed by a valid code block. If you want to output an "@", escape it using the sequence: "@@" + + + Unexpected "{" after "@" character. Once inside the body of a code block (@if {}, @{}, etc.) you do not need to use "@{" to switch to code. + + + A space or line break was encountered after the "@" character. Only valid identifiers, keywords, comments, "(" and "{" are valid at the start of a code block and they must occur immediately following "@" with no space in between. + + + End of file or an unexpected character was reached before the "{0}" tag could be parsed. Elements inside markup blocks must be complete. They must either be self-closing ("<br />") or have matching end tags ("<p>Hello</p>"). If you intended to display a "<" character, use the "&lt;" HTML entity. + + + Unterminated string literal. Strings that start with a quotation mark (") must be terminated before the end of the line. However, strings that start with @ and a quotation mark (@") can span multiple lines. + + + Cannot complete the tree, StartBlock must be called at least once. + + + Cannot complete the tree, there are still open blocks. + + + Cannot finish span, there is no current block. Call StartBlock at least once before finishing a span + + + Cannot complete action, the parser has finished. Only CompleteParse can be called to extract the final parser results after the parser has finished + + + Parser was started with a null Context property. The Context property must be set BEFORE calling any methods on the parser. + + + @section Header { ... } + In CSHTML, the @section keyword is case-sensitive and lowercase (as with all C# keywords) + + + Cannot perform '{1}' operations on '{0}' instances with different file paths. + + + <<unknown>> + + + 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} + + \ 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 new file mode 100644 index 0000000000..b4aa9a11b4 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Properties/LegacyResources.Designer.cs @@ -0,0 +1,1162 @@ +// +namespace Microsoft.AspNetCore.Razor.Evolution +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class LegacyResources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNetCore.Razor.Evolution.LegacyResources", typeof(LegacyResources).GetTypeInfo().Assembly); + + ///

+ /// Value cannot be null or an empty string. + /// + internal static string Argument_Cannot_Be_Null_Or_Empty + { + get { return GetString("Argument_Cannot_Be_Null_Or_Empty"); } + } + + /// + /// Value cannot be null or an empty string. + /// + internal static string FormatArgument_Cannot_Be_Null_Or_Empty() + { + return GetString("Argument_Cannot_Be_Null_Or_Empty"); + } + + /// + /// Value must be between {0} and {1}. + /// + internal static string Argument_Must_Be_Between + { + get { return GetString("Argument_Must_Be_Between"); } + } + + /// + /// Value must be between {0} and {1}. + /// + internal static string FormatArgument_Must_Be_Between(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("Argument_Must_Be_Between"), p0, p1); + } + + /// + /// Value must be a value from the "{0}" enumeration. + /// + internal static string Argument_Must_Be_Enum_Member + { + get { return GetString("Argument_Must_Be_Enum_Member"); } + } + + /// + /// Value must be a value from the "{0}" enumeration. + /// + internal static string FormatArgument_Must_Be_Enum_Member(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("Argument_Must_Be_Enum_Member"), p0); + } + + /// + /// Value must be greater than {0}. + /// + internal static string Argument_Must_Be_GreaterThan + { + get { return GetString("Argument_Must_Be_GreaterThan"); } + } + + /// + /// Value must be greater than {0}. + /// + internal static string FormatArgument_Must_Be_GreaterThan(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("Argument_Must_Be_GreaterThan"), p0); + } + + /// + /// Value must be greater than or equal to {0}. + /// + internal static string Argument_Must_Be_GreaterThanOrEqualTo + { + get { return GetString("Argument_Must_Be_GreaterThanOrEqualTo"); } + } + + /// + /// Value must be greater than or equal to {0}. + /// + internal static string FormatArgument_Must_Be_GreaterThanOrEqualTo(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("Argument_Must_Be_GreaterThanOrEqualTo"), p0); + } + + /// + /// Value must be less than {0}. + /// + internal static string Argument_Must_Be_LessThan + { + get { return GetString("Argument_Must_Be_LessThan"); } + } + + /// + /// Value must be less than {0}. + /// + internal static string FormatArgument_Must_Be_LessThan(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("Argument_Must_Be_LessThan"), p0); + } + + /// + /// Value must be less than or equal to {0}. + /// + internal static string Argument_Must_Be_LessThanOrEqualTo + { + get { return GetString("Argument_Must_Be_LessThanOrEqualTo"); } + } + + /// + /// Value must be less than or equal to {0}. + /// + internal static string FormatArgument_Must_Be_LessThanOrEqualTo(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("Argument_Must_Be_LessThanOrEqualTo"), p0); + } + + /// + /// Value cannot be an empty string. It must either be null or a non-empty string. + /// + internal static string Argument_Must_Be_Null_Or_Non_Empty + { + get { return GetString("Argument_Must_Be_Null_Or_Non_Empty"); } + } + + /// + /// Value cannot be an empty string. It must either be null or a non-empty string. + /// + internal static string FormatArgument_Must_Be_Null_Or_Non_Empty() + { + return GetString("Argument_Must_Be_Null_Or_Non_Empty"); + } + + /// + /// code + /// + internal static string BlockName_Code + { + get { return GetString("BlockName_Code"); } + } + + /// + /// code + /// + internal static string FormatBlockName_Code() + { + return GetString("BlockName_Code"); + } + + /// + /// explicit expression + /// + internal static string BlockName_ExplicitExpression + { + get { return GetString("BlockName_ExplicitExpression"); } + } + + /// + /// explicit expression + /// + internal static string FormatBlockName_ExplicitExpression() + { + return GetString("BlockName_ExplicitExpression"); + } + + /// + /// Block cannot be built because a Type has not been specified in the BlockBuilder + /// + internal static string Block_Type_Not_Specified + { + get { return GetString("Block_Type_Not_Specified"); } + } + + /// + /// Block cannot be built because a Type has not been specified in the BlockBuilder + /// + internal static string FormatBlock_Type_Not_Specified() + { + return GetString("Block_Type_Not_Specified"); + } + + /// + /// <<character literal>> + /// + internal static string CSharpSymbol_CharacterLiteral + { + get { return GetString("CSharpSymbol_CharacterLiteral"); } + } + + /// + /// <<character literal>> + /// + internal static string FormatCSharpSymbol_CharacterLiteral() + { + return GetString("CSharpSymbol_CharacterLiteral"); + } + + /// + /// <<comment>> + /// + internal static string CSharpSymbol_Comment + { + get { return GetString("CSharpSymbol_Comment"); } + } + + /// + /// <<comment>> + /// + internal static string FormatCSharpSymbol_Comment() + { + return GetString("CSharpSymbol_Comment"); + } + + /// + /// <<identifier>> + /// + internal static string CSharpSymbol_Identifier + { + get { return GetString("CSharpSymbol_Identifier"); } + } + + /// + /// <<identifier>> + /// + internal static string FormatCSharpSymbol_Identifier() + { + return GetString("CSharpSymbol_Identifier"); + } + + /// + /// <<integer literal>> + /// + internal static string CSharpSymbol_IntegerLiteral + { + get { return GetString("CSharpSymbol_IntegerLiteral"); } + } + + /// + /// <<integer literal>> + /// + internal static string FormatCSharpSymbol_IntegerLiteral() + { + return GetString("CSharpSymbol_IntegerLiteral"); + } + + /// + /// <<keyword>> + /// + internal static string CSharpSymbol_Keyword + { + get { return GetString("CSharpSymbol_Keyword"); } + } + + /// + /// <<keyword>> + /// + internal static string FormatCSharpSymbol_Keyword() + { + return GetString("CSharpSymbol_Keyword"); + } + + /// + /// <<newline sequence>> + /// + internal static string CSharpSymbol_Newline + { + get { return GetString("CSharpSymbol_Newline"); } + } + + /// + /// <<newline sequence>> + /// + internal static string FormatCSharpSymbol_Newline() + { + return GetString("CSharpSymbol_Newline"); + } + + /// + /// <<real literal>> + /// + internal static string CSharpSymbol_RealLiteral + { + get { return GetString("CSharpSymbol_RealLiteral"); } + } + + /// + /// <<real literal>> + /// + internal static string FormatCSharpSymbol_RealLiteral() + { + return GetString("CSharpSymbol_RealLiteral"); + } + + /// + /// <<string literal>> + /// + internal static string CSharpSymbol_StringLiteral + { + get { return GetString("CSharpSymbol_StringLiteral"); } + } + + /// + /// <<string literal>> + /// + internal static string FormatCSharpSymbol_StringLiteral() + { + return GetString("CSharpSymbol_StringLiteral"); + } + + /// + /// <<white space>> + /// + internal static string CSharpSymbol_Whitespace + { + get { return GetString("CSharpSymbol_Whitespace"); } + } + + /// + /// <<white space>> + /// + internal static string FormatCSharpSymbol_Whitespace() + { + return GetString("CSharpSymbol_Whitespace"); + } + + /// + /// "EndBlock" was called without a matching call to "StartBlock". + /// + internal static string EndBlock_Called_Without_Matching_StartBlock + { + get { return GetString("EndBlock_Called_Without_Matching_StartBlock"); } + } + + /// + /// "EndBlock" was called without a matching call to "StartBlock". + /// + internal static string FormatEndBlock_Called_Without_Matching_StartBlock() + { + return GetString("EndBlock_Called_Without_Matching_StartBlock"); + } + + /// + /// "{0}" character + /// + internal static string ErrorComponent_Character + { + get { return GetString("ErrorComponent_Character"); } + } + + /// + /// "{0}" character + /// + internal static string FormatErrorComponent_Character(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ErrorComponent_Character"), p0); + } + + /// + /// end of file + /// + internal static string ErrorComponent_EndOfFile + { + get { return GetString("ErrorComponent_EndOfFile"); } + } + + /// + /// end of file + /// + internal static string FormatErrorComponent_EndOfFile() + { + return GetString("ErrorComponent_EndOfFile"); + } + + /// + /// line break + /// + internal static string ErrorComponent_Newline + { + get { return GetString("ErrorComponent_Newline"); } + } + + /// + /// line break + /// + internal static string FormatErrorComponent_Newline() + { + return GetString("ErrorComponent_Newline"); + } + + /// + /// space or line break + /// + internal static string ErrorComponent_Whitespace + { + get { return GetString("ErrorComponent_Whitespace"); } + } + + /// + /// space or line break + /// + internal static string FormatErrorComponent_Whitespace() + { + return GetString("ErrorComponent_Whitespace"); + } + + /// + /// <<newline sequence>> + /// + internal static string HtmlSymbol_NewLine + { + get { return GetString("HtmlSymbol_NewLine"); } + } + + /// + /// <<newline sequence>> + /// + internal static string FormatHtmlSymbol_NewLine() + { + return GetString("HtmlSymbol_NewLine"); + } + + /// + /// <<razor comment>> + /// + internal static string HtmlSymbol_RazorComment + { + get { return GetString("HtmlSymbol_RazorComment"); } + } + + /// + /// <<razor comment>> + /// + internal static string FormatHtmlSymbol_RazorComment() + { + return GetString("HtmlSymbol_RazorComment"); + } + + /// + /// <<text>> + /// + internal static string HtmlSymbol_Text + { + get { return GetString("HtmlSymbol_Text"); } + } + + /// + /// <<text>> + /// + internal static string FormatHtmlSymbol_Text() + { + return GetString("HtmlSymbol_Text"); + } + + /// + /// <<white space>> + /// + internal static string HtmlSymbol_WhiteSpace + { + get { return GetString("HtmlSymbol_WhiteSpace"); } + } + + /// + /// <<white space>> + /// + internal static string FormatHtmlSymbol_WhiteSpace() + { + return GetString("HtmlSymbol_WhiteSpace"); + } + + /// + /// Cannot use built-in RazorComment handler, language characteristics does not define the CommentStart, CommentStar and CommentBody known symbol types or parser does not override TokenizerBackedParser.OutputSpanBeforeRazorComment + /// + internal static string Language_Does_Not_Support_RazorComment + { + get { return GetString("Language_Does_Not_Support_RazorComment"); } + } + + /// + /// Cannot use built-in RazorComment handler, language characteristics does not define the CommentStart, CommentStar and CommentBody known symbol types or parser does not override TokenizerBackedParser.OutputSpanBeforeRazorComment + /// + internal static string FormatLanguage_Does_Not_Support_RazorComment() + { + return GetString("Language_Does_Not_Support_RazorComment"); + } + + /// + /// The "@" character must be followed by a ":", "(", or a C# identifier. If you intended to switch to markup, use an HTML start tag, for example: + /// + /// @if(isLoggedIn) { + /// <p>Hello, @user!</p> + /// } + /// + internal static string ParseError_AtInCode_Must_Be_Followed_By_Colon_Paren_Or_Identifier_Start + { + get { return GetString("ParseError_AtInCode_Must_Be_Followed_By_Colon_Paren_Or_Identifier_Start"); } + } + + /// + /// The "@" character must be followed by a ":", "(", or a C# identifier. If you intended to switch to markup, use an HTML start tag, for example: + /// + /// @if(isLoggedIn) { + /// <p>Hello, @user!</p> + /// } + /// + internal static string FormatParseError_AtInCode_Must_Be_Followed_By_Colon_Paren_Or_Identifier_Start() + { + return GetString("ParseError_AtInCode_Must_Be_Followed_By_Colon_Paren_Or_Identifier_Start"); + } + + /// + /// End of file was reached before the end of the block comment. All comments started with "/*" sequence must be terminated with a matching "*/" sequence. + /// + internal static string ParseError_BlockComment_Not_Terminated + { + get { return GetString("ParseError_BlockComment_Not_Terminated"); } + } + + /// + /// End of file was reached before the end of the block comment. All comments started with "/*" sequence must be terminated with a matching "*/" sequence. + /// + internal static string FormatParseError_BlockComment_Not_Terminated() + { + return GetString("ParseError_BlockComment_Not_Terminated"); + } + + /// + /// Directive '{0}' must have a value. + /// + internal static string ParseError_DirectiveMustHaveValue + { + get { return GetString("ParseError_DirectiveMustHaveValue"); } + } + + /// + /// Directive '{0}' must have a value. + /// + internal static string FormatParseError_DirectiveMustHaveValue(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ParseError_DirectiveMustHaveValue"), p0); + } + + /// + /// An opening "{0}" is missing the corresponding closing "{1}". + /// + internal static string ParseError_Expected_CloseBracket_Before_EOF + { + get { return GetString("ParseError_Expected_CloseBracket_Before_EOF"); } + } + + /// + /// An opening "{0}" is missing the corresponding closing "{1}". + /// + internal static string FormatParseError_Expected_CloseBracket_Before_EOF(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ParseError_Expected_CloseBracket_Before_EOF"), p0, p1); + } + + /// + /// The {0} block is missing a closing "{1}" character. Make sure you have a matching "{1}" character for all the "{2}" characters within this block, and that none of the "{1}" characters are being interpreted as markup. + /// + internal static string ParseError_Expected_EndOfBlock_Before_EOF + { + get { return GetString("ParseError_Expected_EndOfBlock_Before_EOF"); } + } + + /// + /// The {0} block is missing a closing "{1}" character. Make sure you have a matching "{1}" character for all the "{2}" characters within this block, and that none of the "{1}" characters are being interpreted as markup. + /// + internal static string FormatParseError_Expected_EndOfBlock_Before_EOF(object p0, object p1, object p2) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ParseError_Expected_EndOfBlock_Before_EOF"), p0, p1, p2); + } + + /// + /// Expected "{0}". + /// + internal static string ParseError_Expected_X + { + get { return GetString("ParseError_Expected_X"); } + } + + /// + /// Expected "{0}". + /// + internal static string FormatParseError_Expected_X(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ParseError_Expected_X"), p0); + } + + /// + /// The {0} directive is not supported. + /// + internal static string ParseError_HelperDirectiveNotAvailable + { + get { return GetString("ParseError_HelperDirectiveNotAvailable"); } + } + + /// + /// The {0} directive is not supported. + /// + internal static string FormatParseError_HelperDirectiveNotAvailable(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ParseError_HelperDirectiveNotAvailable"), p0); + } + + /// + /// Optional quote around the directive '{0}' is missing the corresponding opening or closing quote. + /// + internal static string ParseError_IncompleteQuotesAroundDirective + { + get { return GetString("ParseError_IncompleteQuotesAroundDirective"); } + } + + /// + /// Optional quote around the directive '{0}' is missing the corresponding opening or closing quote. + /// + internal static string FormatParseError_IncompleteQuotesAroundDirective(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ParseError_IncompleteQuotesAroundDirective"), p0); + } + + /// + /// The 'inherits' keyword must be followed by a type name on the same line. + /// + internal static string ParseError_InheritsKeyword_Must_Be_Followed_By_TypeName + { + get { return GetString("ParseError_InheritsKeyword_Must_Be_Followed_By_TypeName"); } + } + + /// + /// The 'inherits' keyword must be followed by a type name on the same line. + /// + internal static string FormatParseError_InheritsKeyword_Must_Be_Followed_By_TypeName() + { + return GetString("ParseError_InheritsKeyword_Must_Be_Followed_By_TypeName"); + } + + /// + /// Inline markup blocks (@<p>Content</p>) cannot be nested. Only one level of inline markup is allowed. + /// + internal static string ParseError_InlineMarkup_Blocks_Cannot_Be_Nested + { + get { return GetString("ParseError_InlineMarkup_Blocks_Cannot_Be_Nested"); } + } + + /// + /// Inline markup blocks (@<p>Content</p>) cannot be nested. Only one level of inline markup is allowed. + /// + internal static string FormatParseError_InlineMarkup_Blocks_Cannot_Be_Nested() + { + return GetString("ParseError_InlineMarkup_Blocks_Cannot_Be_Nested"); + } + + /// + /// Markup in a code block must start with a tag and all start tags must be matched with end tags. Do not use unclosed tags like "<br>". Instead use self-closing tags like "<br/>". + /// + internal static string ParseError_MarkupBlock_Must_Start_With_Tag + { + get { return GetString("ParseError_MarkupBlock_Must_Start_With_Tag"); } + } + + /// + /// Markup in a code block must start with a tag and all start tags must be matched with end tags. Do not use unclosed tags like "<br>". Instead use self-closing tags like "<br/>". + /// + internal static string FormatParseError_MarkupBlock_Must_Start_With_Tag() + { + return GetString("ParseError_MarkupBlock_Must_Start_With_Tag"); + } + + /// + /// The "{0}" element was not closed. All elements must be either self-closing or have a matching end tag. + /// + internal static string ParseError_MissingEndTag + { + get { return GetString("ParseError_MissingEndTag"); } + } + + /// + /// The "{0}" element was not closed. All elements must be either self-closing or have a matching end tag. + /// + internal static string FormatParseError_MissingEndTag(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ParseError_MissingEndTag"), p0); + } + + /// + /// Sections cannot be empty. The "@section" keyword must be followed by a block of markup surrounded by "{}". For example: + /// + /// @section Sidebar { + /// <!-- Markup and text goes here --> + /// } + /// + internal static string ParseError_MissingOpenBraceAfterSection + { + get { return GetString("ParseError_MissingOpenBraceAfterSection"); } + } + + /// + /// Sections cannot be empty. The "@section" keyword must be followed by a block of markup surrounded by "{}". For example: + /// + /// @section Sidebar { + /// <!-- Markup and text goes here --> + /// } + /// + internal static string FormatParseError_MissingOpenBraceAfterSection() + { + return GetString("ParseError_MissingOpenBraceAfterSection"); + } + + /// + /// Namespace imports and type aliases cannot be placed within code blocks. They must immediately follow an "@" character in markup. It is recommended that you put them at the top of the page, as in the following example: + /// + /// @using System.Drawing; + /// @{ + /// // OK here to use types from System.Drawing in the page. + /// } + /// + internal static string ParseError_NamespaceImportAndTypeAlias_Cannot_Exist_Within_CodeBlock + { + get { return GetString("ParseError_NamespaceImportAndTypeAlias_Cannot_Exist_Within_CodeBlock"); } + } + + /// + /// Namespace imports and type aliases cannot be placed within code blocks. They must immediately follow an "@" character in markup. It is recommended that you put them at the top of the page, as in the following example: + /// + /// @using System.Drawing; + /// @{ + /// // OK here to use types from System.Drawing in the page. + /// } + /// + internal static string FormatParseError_NamespaceImportAndTypeAlias_Cannot_Exist_Within_CodeBlock() + { + return GetString("ParseError_NamespaceImportAndTypeAlias_Cannot_Exist_Within_CodeBlock"); + } + + /// + /// Outer tag is missing a name. The first character of a markup block must be an HTML tag with a valid name. + /// + internal static string ParseError_OuterTagMissingName + { + get { return GetString("ParseError_OuterTagMissingName"); } + } + + /// + /// Outer tag is missing a name. The first character of a markup block must be an HTML tag with a valid name. + /// + internal static string FormatParseError_OuterTagMissingName() + { + return GetString("ParseError_OuterTagMissingName"); + } + + /// + /// End of file was reached before the end of the block comment. All comments that start with the "@*" sequence must be terminated with a matching "*@" sequence. + /// + internal static string ParseError_RazorComment_Not_Terminated + { + get { return GetString("ParseError_RazorComment_Not_Terminated"); } + } + + /// + /// End of file was reached before the end of the block comment. All comments that start with the "@*" sequence must be terminated with a matching "*@" sequence. + /// + internal static string FormatParseError_RazorComment_Not_Terminated() + { + return GetString("ParseError_RazorComment_Not_Terminated"); + } + + /// + /// "{0}" is a reserved word and cannot be used in implicit expressions. An explicit expression ("@()") must be used. + /// + internal static string ParseError_ReservedWord + { + get { return GetString("ParseError_ReservedWord"); } + } + + /// + /// "{0}" is a reserved word and cannot be used in implicit expressions. An explicit expression ("@()") must be used. + /// + internal static string FormatParseError_ReservedWord(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ParseError_ReservedWord"), p0); + } + + /// + /// Section blocks ("{0}") cannot be nested. Only one level of section blocks are allowed. + /// + internal static string ParseError_Sections_Cannot_Be_Nested + { + get { return GetString("ParseError_Sections_Cannot_Be_Nested"); } + } + + /// + /// Section blocks ("{0}") cannot be nested. Only one level of section blocks are allowed. + /// + internal static string FormatParseError_Sections_Cannot_Be_Nested(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ParseError_Sections_Cannot_Be_Nested"), p0); + } + + /// + /// Expected a "{0}" but found a "{1}". Block statements must be enclosed in "{{" and "}}". You cannot use single-statement control-flow statements in CSHTML pages. For example, the following is not allowed: + /// + /// @if(isLoggedIn) + /// <p>Hello, @user</p> + /// + /// Instead, wrap the contents of the block in "{{}}": + /// + /// @if(isLoggedIn) {{ + /// <p>Hello, @user</p> + /// }} + /// + internal static string ParseError_SingleLine_ControlFlowStatements_Not_Allowed + { + get { return GetString("ParseError_SingleLine_ControlFlowStatements_Not_Allowed"); } + } + + /// + /// Expected a "{0}" but found a "{1}". Block statements must be enclosed in "{{" and "}}". You cannot use single-statement control-flow statements in CSHTML pages. For example, the following is not allowed: + /// + /// @if(isLoggedIn) + /// <p>Hello, @user</p> + /// + /// Instead, wrap the contents of the block in "{{}}": + /// + /// @if(isLoggedIn) {{ + /// <p>Hello, @user</p> + /// }} + /// + internal static string FormatParseError_SingleLine_ControlFlowStatements_Not_Allowed(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ParseError_SingleLine_ControlFlowStatements_Not_Allowed"), p0, p1); + } + + /// + /// "<text>" and "</text>" tags cannot contain attributes. + /// + internal static string ParseError_TextTagCannotContainAttributes + { + get { return GetString("ParseError_TextTagCannotContainAttributes"); } + } + + /// + /// "<text>" and "</text>" tags cannot contain attributes. + /// + internal static string FormatParseError_TextTagCannotContainAttributes() + { + return GetString("ParseError_TextTagCannotContainAttributes"); + } + + /// + /// Encountered end tag "{0}" with no matching start tag. Are your start/end tags properly balanced? + /// + internal static string ParseError_UnexpectedEndTag + { + get { return GetString("ParseError_UnexpectedEndTag"); } + } + + /// + /// Encountered end tag "{0}" with no matching start tag. Are your start/end tags properly balanced? + /// + internal static string FormatParseError_UnexpectedEndTag(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ParseError_UnexpectedEndTag"), p0); + } + + /// + /// Unexpected {0} after section keyword. Section names must start with an "_" or alphabetic character, and the remaining characters must be either "_" or alphanumeric. + /// + internal static string ParseError_Unexpected_Character_At_Section_Name_Start + { + get { return GetString("ParseError_Unexpected_Character_At_Section_Name_Start"); } + } + + /// + /// Unexpected {0} after section keyword. Section names must start with an "_" or alphabetic character, and the remaining characters must be either "_" or alphanumeric. + /// + internal static string FormatParseError_Unexpected_Character_At_Section_Name_Start(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ParseError_Unexpected_Character_At_Section_Name_Start"), p0); + } + + /// + /// "{0}" is not valid at the start of a code block. Only identifiers, keywords, comments, "(" and "{{" are valid. + /// + internal static string ParseError_Unexpected_Character_At_Start_Of_CodeBlock_CS + { + get { return GetString("ParseError_Unexpected_Character_At_Start_Of_CodeBlock_CS"); } + } + + /// + /// "{0}" is not valid at the start of a code block. Only identifiers, keywords, comments, "(" and "{{" are valid. + /// + internal static string FormatParseError_Unexpected_Character_At_Start_Of_CodeBlock_CS(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ParseError_Unexpected_Character_At_Start_Of_CodeBlock_CS"), p0); + } + + /// + /// End-of-file was found after the "@" character. "@" must be followed by a valid code block. If you want to output an "@", escape it using the sequence: "@@" + /// + internal static string ParseError_Unexpected_EndOfFile_At_Start_Of_CodeBlock + { + get { return GetString("ParseError_Unexpected_EndOfFile_At_Start_Of_CodeBlock"); } + } + + /// + /// End-of-file was found after the "@" character. "@" must be followed by a valid code block. If you want to output an "@", escape it using the sequence: "@@" + /// + internal static string FormatParseError_Unexpected_EndOfFile_At_Start_Of_CodeBlock() + { + return GetString("ParseError_Unexpected_EndOfFile_At_Start_Of_CodeBlock"); + } + + /// + /// End-of-file was found after the "@" character. "@" must be followed by a valid code block. If you want to output an "@", escape it using the sequence: "@@" + /// + internal static string ParseError_Unexpected_EndOfFile_At_Start_Of_CodeBlock1 + { + get { return GetString("ParseError_Unexpected_EndOfFile_At_Start_Of_CodeBlock1"); } + } + + /// + /// End-of-file was found after the "@" character. "@" must be followed by a valid code block. If you want to output an "@", escape it using the sequence: "@@" + /// + internal static string FormatParseError_Unexpected_EndOfFile_At_Start_Of_CodeBlock1() + { + return GetString("ParseError_Unexpected_EndOfFile_At_Start_Of_CodeBlock1"); + } + + /// + /// Unexpected "{" after "@" character. Once inside the body of a code block (@if {}, @{}, etc.) you do not need to use "@{" to switch to code. + /// + internal static string ParseError_Unexpected_Nested_CodeBlock + { + get { return GetString("ParseError_Unexpected_Nested_CodeBlock"); } + } + + /// + /// Unexpected "{" after "@" character. Once inside the body of a code block (@if {}, @{}, etc.) you do not need to use "@{" to switch to code. + /// + internal static string FormatParseError_Unexpected_Nested_CodeBlock() + { + return GetString("ParseError_Unexpected_Nested_CodeBlock"); + } + + /// + /// A space or line break was encountered after the "@" character. Only valid identifiers, keywords, comments, "(" and "{" are valid at the start of a code block and they must occur immediately following "@" with no space in between. + /// + internal static string ParseError_Unexpected_WhiteSpace_At_Start_Of_CodeBlock_CS + { + get { return GetString("ParseError_Unexpected_WhiteSpace_At_Start_Of_CodeBlock_CS"); } + } + + /// + /// A space or line break was encountered after the "@" character. Only valid identifiers, keywords, comments, "(" and "{" are valid at the start of a code block and they must occur immediately following "@" with no space in between. + /// + internal static string FormatParseError_Unexpected_WhiteSpace_At_Start_Of_CodeBlock_CS() + { + return GetString("ParseError_Unexpected_WhiteSpace_At_Start_Of_CodeBlock_CS"); + } + + /// + /// End of file or an unexpected character was reached before the "{0}" tag could be parsed. Elements inside markup blocks must be complete. They must either be self-closing ("<br />") or have matching end tags ("<p>Hello</p>"). If you intended to display a "<" character, use the "&lt;" HTML entity. + /// + internal static string ParseError_UnfinishedTag + { + get { return GetString("ParseError_UnfinishedTag"); } + } + + /// + /// End of file or an unexpected character was reached before the "{0}" tag could be parsed. Elements inside markup blocks must be complete. They must either be self-closing ("<br />") or have matching end tags ("<p>Hello</p>"). If you intended to display a "<" character, use the "&lt;" HTML entity. + /// + internal static string FormatParseError_UnfinishedTag(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ParseError_UnfinishedTag"), p0); + } + + /// + /// Unterminated string literal. Strings that start with a quotation mark (") must be terminated before the end of the line. However, strings that start with @ and a quotation mark (@") can span multiple lines. + /// + internal static string ParseError_Unterminated_String_Literal + { + get { return GetString("ParseError_Unterminated_String_Literal"); } + } + + /// + /// Unterminated string literal. Strings that start with a quotation mark (") must be terminated before the end of the line. However, strings that start with @ and a quotation mark (@") can span multiple lines. + /// + internal static string FormatParseError_Unterminated_String_Literal() + { + return GetString("ParseError_Unterminated_String_Literal"); + } + + /// + /// Cannot complete the tree, StartBlock must be called at least once. + /// + internal static string ParserContext_CannotCompleteTree_NoRootBlock + { + get { return GetString("ParserContext_CannotCompleteTree_NoRootBlock"); } + } + + /// + /// Cannot complete the tree, StartBlock must be called at least once. + /// + internal static string FormatParserContext_CannotCompleteTree_NoRootBlock() + { + return GetString("ParserContext_CannotCompleteTree_NoRootBlock"); + } + + /// + /// Cannot complete the tree, there are still open blocks. + /// + internal static string ParserContext_CannotCompleteTree_OutstandingBlocks + { + get { return GetString("ParserContext_CannotCompleteTree_OutstandingBlocks"); } + } + + /// + /// Cannot complete the tree, there are still open blocks. + /// + internal static string FormatParserContext_CannotCompleteTree_OutstandingBlocks() + { + return GetString("ParserContext_CannotCompleteTree_OutstandingBlocks"); + } + + /// + /// Cannot finish span, there is no current block. Call StartBlock at least once before finishing a span + /// + internal static string ParserContext_NoCurrentBlock + { + get { return GetString("ParserContext_NoCurrentBlock"); } + } + + /// + /// Cannot finish span, there is no current block. Call StartBlock at least once before finishing a span + /// + internal static string FormatParserContext_NoCurrentBlock() + { + return GetString("ParserContext_NoCurrentBlock"); + } + + /// + /// Cannot complete action, the parser has finished. Only CompleteParse can be called to extract the final parser results after the parser has finished + /// + internal static string ParserContext_ParseComplete + { + get { return GetString("ParserContext_ParseComplete"); } + } + + /// + /// Cannot complete action, the parser has finished. Only CompleteParse can be called to extract the final parser results after the parser has finished + /// + internal static string FormatParserContext_ParseComplete() + { + return GetString("ParserContext_ParseComplete"); + } + + /// + /// Parser was started with a null Context property. The Context property must be set BEFORE calling any methods on the parser. + /// + internal static string Parser_Context_Not_Set + { + get { return GetString("Parser_Context_Not_Set"); } + } + + /// + /// Parser was started with a null Context property. The Context property must be set BEFORE calling any methods on the parser. + /// + internal static string FormatParser_Context_Not_Set() + { + return GetString("Parser_Context_Not_Set"); + } + + /// + /// @section Header { ... } + /// + internal static string SectionExample_CS + { + get { return GetString("SectionExample_CS"); } + } + + /// + /// @section Header { ... } + /// + internal static string FormatSectionExample_CS() + { + return GetString("SectionExample_CS"); + } + + /// + /// Cannot perform '{1}' operations on '{0}' instances with different file paths. + /// + internal static string SourceLocationFilePathDoesNotMatch + { + get { return GetString("SourceLocationFilePathDoesNotMatch"); } + } + + /// + /// Cannot perform '{1}' operations on '{0}' instances with different file paths. + /// + internal static string FormatSourceLocationFilePathDoesNotMatch(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("SourceLocationFilePathDoesNotMatch"), p0, p1); + } + + /// + /// <<unknown>> + /// + internal static string Symbol_Unknown + { + get { return GetString("Symbol_Unknown"); } + } + + /// + /// <<unknown>> + /// + internal static string FormatSymbol_Unknown() + { + return GetString("Symbol_Unknown"); + } + + /// + /// 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} + /// + internal static string TokenizerView_CannotPutBack + { + get { return GetString("TokenizerView_CannotPutBack"); } + } + + /// + /// 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} + /// + internal static string FormatTokenizerView_CannotPutBack(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TokenizerView_CannotPutBack"), p0, p1); + } + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/RazorSyntaxTree.cs b/src/Microsoft.AspNetCore.Razor.Evolution/RazorSyntaxTree.cs new file mode 100644 index 0000000000..401cec77ec --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/RazorSyntaxTree.cs @@ -0,0 +1,46 @@ +// 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.AspNetCore.Razor.Evolution.Legacy; + +namespace Microsoft.AspNetCore.Razor.Evolution +{ + public abstract class RazorSyntaxTree + { + internal static RazorSyntaxTree Create(Block root, IEnumerable diagnostics) + { + if (root == null) + { + throw new ArgumentNullException(nameof(root)); + } + + if (diagnostics == null) + { + throw new ArgumentNullException(nameof(diagnostics)); + } + + return new DefaultRazorSyntaxTree(root, new List(diagnostics)); + } + + public static RazorSyntaxTree Parse(RazorSourceDocument source) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + var parser = new RazorParser(); + + using (var reader = source.CreateReader()) + { + return parser.Parse(reader); + } + } + + internal abstract IReadOnlyList Diagnostics { get; } + + internal abstract Block Root { get; } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/BaselineWriter.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/BaselineWriter.cs new file mode 100644 index 0000000000..b06f8b188f --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/BaselineWriter.cs @@ -0,0 +1,46 @@ +// 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.Diagnostics; +using System.IO; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + public static class BaselineWriter + { + private static object baselineLock = new object(); + + [Conditional("GENERATE_BASELINES")] + public static void WriteBaseline(string baselineFile, string output) + { + var root = RecursiveFind("Razor.sln", Path.GetFullPath(".")); + var baselinePath = Path.Combine(root, baselineFile); + + // Serialize writes to minimize contention for file handles and directory access. + lock (baselineLock) + { + // Update baseline + using (var stream = File.Open(baselinePath, FileMode.Create, FileAccess.Write)) + { + using (var writer = new StreamWriter(stream)) + { + writer.Write(output); + } + } + } + } + + private static string RecursiveFind(string path, string start) + { + var test = Path.Combine(start, path); + if (File.Exists(test)) + { + return start; + } + else + { + return RecursiveFind(path, new DirectoryInfo(start).Parent.FullName); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/BlockExtensions.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/BlockExtensions.cs new file mode 100644 index 0000000000..8edde939fe --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/BlockExtensions.cs @@ -0,0 +1,28 @@ +// 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.Legacy +{ + internal static class BlockExtensions + { + public static void LinkNodes(this Block self) + { + Span first = null; + Span previous = null; + foreach (Span span in self.Flatten()) + { + if (first == null) + { + first = span; + } + span.Previous = previous; + + if (previous != null) + { + previous.Next = span; + } + previous = span; + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/BlockFactory.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/BlockFactory.cs new file mode 100644 index 0000000000..d289a60977 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/BlockFactory.cs @@ -0,0 +1,58 @@ +// 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.Legacy +{ + internal class BlockFactory + { + private SpanFactory _factory; + + public BlockFactory(SpanFactory factory) + { + _factory = factory; + } + + public Block EscapedMarkupTagBlock(string prefix, string suffix) + { + return EscapedMarkupTagBlock(prefix, suffix, AcceptedCharacters.Any); + } + + public Block EscapedMarkupTagBlock(string prefix, string suffix, params SyntaxTreeNode[] children) + { + return EscapedMarkupTagBlock(prefix, suffix, AcceptedCharacters.Any, children); + } + + public Block EscapedMarkupTagBlock( + string prefix, + string suffix, + AcceptedCharacters acceptedCharacters, + params SyntaxTreeNode[] children) + { + var newChildren = new List( + new SyntaxTreeNode[] + { + _factory.Markup(prefix), + _factory.BangEscape(), + _factory.Markup(suffix).Accepts(acceptedCharacters) + }); + + newChildren.AddRange(children); + + return new MarkupTagBlock(newChildren.ToArray()); + } + + public Block MarkupTagBlock(string content) + { + return MarkupTagBlock(content, AcceptedCharacters.Any); + } + + public Block MarkupTagBlock(string content, AcceptedCharacters acceptedCharacters) + { + return new MarkupTagBlock( + _factory.Markup(content).Accepts(acceptedCharacters) + ); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/BlockTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/BlockTest.cs new file mode 100644 index 0000000000..afbf9e8e7c --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/BlockTest.cs @@ -0,0 +1,62 @@ +// 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.Linq; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class BlockTest + { + [Fact] + public void ConstructorWithBlockBuilderSetsParent() + { + // Arrange + var builder = new BlockBuilder() { Type = BlockType.Comment }; + var span = new SpanBuilder() { Kind = SpanKind.Code }.Build(); + builder.Children.Add(span); + + // Act + var block = builder.Build(); + + // Assert + Assert.Same(block, span.Parent); + } + + [Fact] + public void ConstructorTransfersInstanceOfChunkGeneratorFromBlockBuilder() + { + // Arrange + var expected = new ExpressionChunkGenerator(); + var builder = new BlockBuilder() + { + Type = BlockType.Helper, + ChunkGenerator = expected + }; + + // Act + var actual = builder.Build(); + + // Assert + Assert.Same(expected, actual.ChunkGenerator); + } + + [Fact] + public void ConstructorTransfersChildrenFromBlockBuilder() + { + // Arrange + var expected = new SpanBuilder() { Kind = SpanKind.Code }.Build(); + var builder = new BlockBuilder() + { + Type = BlockType.Functions + }; + builder.Children.Add(expected); + + // Act + var block = builder.Build(); + + // Assert + Assert.Same(expected, block.Children.Single()); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/BlockTypes.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/BlockTypes.cs new file mode 100644 index 0000000000..d05d4d3f4c --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/BlockTypes.cs @@ -0,0 +1,202 @@ +// 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.Legacy +{ + // The product code doesn't need this, but having subclasses for the block types makes tests much cleaner :) + + internal class StatementBlock : Block + { + private const BlockType ThisBlockType = BlockType.Statement; + + public StatementBlock(IParentChunkGenerator chunkGenerator, IReadOnlyList children) + : base(ThisBlockType, children, chunkGenerator) + { + } + + public StatementBlock(IParentChunkGenerator chunkGenerator, params SyntaxTreeNode[] children) + : this(chunkGenerator, (IReadOnlyList)children) + { + } + + public StatementBlock(params SyntaxTreeNode[] children) + : this(ParentChunkGenerator.Null, children) + { + } + } + + internal class DirectiveBlock : Block + { + private const BlockType ThisBlockType = BlockType.Directive; + + public DirectiveBlock(IParentChunkGenerator chunkGenerator, IReadOnlyList children) + : base(ThisBlockType, children, chunkGenerator) + { + } + + public DirectiveBlock(IParentChunkGenerator chunkGenerator, params SyntaxTreeNode[] children) + : this(chunkGenerator, (IReadOnlyList)children) + { + } + + public DirectiveBlock(params SyntaxTreeNode[] children) + : this(ParentChunkGenerator.Null, children) + { + } + } + + internal class FunctionsBlock : Block + { + private const BlockType ThisBlockType = BlockType.Functions; + + public FunctionsBlock(IParentChunkGenerator chunkGenerator, IReadOnlyList children) + : base(ThisBlockType, children, chunkGenerator) + { + } + + public FunctionsBlock(IParentChunkGenerator chunkGenerator, params SyntaxTreeNode[] children) + : this(chunkGenerator, (IReadOnlyList)children) + { + } + + public FunctionsBlock(params SyntaxTreeNode[] children) + : this(ParentChunkGenerator.Null, children) + { + } + } + + internal class ExpressionBlock : Block + { + private const BlockType ThisBlockType = BlockType.Expression; + + public ExpressionBlock(IParentChunkGenerator chunkGenerator, IReadOnlyList children) + : base(ThisBlockType, children, chunkGenerator) + { + } + + public ExpressionBlock(IParentChunkGenerator chunkGenerator, params SyntaxTreeNode[] children) + : this(chunkGenerator, (IReadOnlyList)children) + { + } + + public ExpressionBlock(params SyntaxTreeNode[] children) + : this(new ExpressionChunkGenerator(), children) + { + } + } + + internal class MarkupTagBlock : Block + { + private const BlockType ThisBlockType = BlockType.Tag; + + public MarkupTagBlock(params SyntaxTreeNode[] children) + : base(ThisBlockType, children, ParentChunkGenerator.Null) + { + } + } + + internal class MarkupBlock : Block + { + private const BlockType ThisBlockType = BlockType.Markup; + + public MarkupBlock( + BlockType blockType, + IParentChunkGenerator chunkGenerator, + IReadOnlyList children) + : base(blockType, children, chunkGenerator) + { + } + + public MarkupBlock(IParentChunkGenerator chunkGenerator, IReadOnlyList children) + : this(ThisBlockType, chunkGenerator, children) + { + } + + public MarkupBlock(IParentChunkGenerator chunkGenerator, params SyntaxTreeNode[] children) + : this(chunkGenerator, (IReadOnlyList)children) + { + } + + public MarkupBlock(params SyntaxTreeNode[] children) + : this(ParentChunkGenerator.Null, children) + { + } + } + + internal class SectionBlock : Block + { + private const BlockType ThisBlockType = BlockType.Section; + + public SectionBlock(IParentChunkGenerator chunkGenerator, IReadOnlyList children) + : base(ThisBlockType, children, chunkGenerator) + { + } + + public SectionBlock(IParentChunkGenerator chunkGenerator, params SyntaxTreeNode[] children) + : this(chunkGenerator, (IReadOnlyList)children) + { + } + + public SectionBlock(params SyntaxTreeNode[] children) + : this(ParentChunkGenerator.Null, children) + { + } + + public SectionBlock(IReadOnlyList children) + : this(ParentChunkGenerator.Null, children) + { + } + } + + internal class TemplateBlock : Block + { + private const BlockType ThisBlockType = BlockType.Template; + + public TemplateBlock(IParentChunkGenerator chunkGenerator, IReadOnlyList children) + : base(ThisBlockType, children, chunkGenerator) + { + } + + public TemplateBlock(IParentChunkGenerator chunkGenerator, params SyntaxTreeNode[] children) + : this(chunkGenerator, (IReadOnlyList)children) + { + } + + public TemplateBlock(params SyntaxTreeNode[] children) + : this(new TemplateBlockChunkGenerator(), children) + { + } + + public TemplateBlock(IReadOnlyList children) + : this(new TemplateBlockChunkGenerator(), children) + { + } + } + + internal class CommentBlock : Block + { + private const BlockType ThisBlockType = BlockType.Comment; + + public CommentBlock(IParentChunkGenerator chunkGenerator, IReadOnlyList children) + : base(ThisBlockType, children, chunkGenerator) + { + } + + public CommentBlock(IParentChunkGenerator chunkGenerator, params SyntaxTreeNode[] children) + : this(chunkGenerator, (IReadOnlyList)children) + { + } + + public CommentBlock(params SyntaxTreeNode[] children) + : this(new RazorCommentChunkGenerator(), children) + { + } + + public CommentBlock(IReadOnlyList children) + : this(new RazorCommentChunkGenerator(), children) + { + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpAutoCompleteTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpAutoCompleteTest.cs new file mode 100644 index 0000000000..cb9ab28a40 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpAutoCompleteTest.cs @@ -0,0 +1,138 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class CSharpAutoCompleteTest : CsHtmlCodeParserTestBase + { + [Fact] + public void FunctionsDirectiveAutoCompleteAtEOF() + { + ParseBlockTest( + "@functions{", + new FunctionsBlock( + Factory.CodeTransition("@") + .Accepts(AcceptedCharacters.None), + Factory.MetaCode("functions{") + .Accepts(AcceptedCharacters.None), + Factory.EmptyCSharp() + .AsFunctionsBody() + .With(new AutoCompleteEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString) + { + AutoCompleteString = "}" + })), + new RazorError( + LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF("functions", "}", "{"), + new SourceLocation(10, 0, 10), + length: 1)); + } + + [Fact] + public void SectionDirectiveAutoCompleteAtEOF() + { + ParseBlockTest("@section Header {", + new SectionBlock(new SectionChunkGenerator("Header"), + Factory.CodeTransition(), + Factory.MetaCode("section Header {") + .AutoCompleteWith("}", atEndOfSpan: true) + .Accepts(AcceptedCharacters.Any), + new MarkupBlock()), + new RazorError( + LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF("section", "}", "{"), + new SourceLocation(16, 0, 16), + length: 1)); + } + + [Fact] + public void VerbatimBlockAutoCompleteAtEOF() + { + ParseBlockTest("@{", + new StatementBlock( + Factory.CodeTransition(), + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.EmptyCSharp() + .AsStatement() + .With(new AutoCompleteEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString) { AutoCompleteString = "}" }) + ), + new RazorError( + LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF( + LegacyResources.BlockName_Code, "}", "{"), + new SourceLocation(1, 0, 1), + length: 1)); + } + + [Fact] + public void FunctionsDirectiveAutoCompleteAtStartOfFile() + { + ParseBlockTest("@functions{" + Environment.NewLine + + "foo", + new FunctionsBlock( + Factory.CodeTransition("@") + .Accepts(AcceptedCharacters.None), + Factory.MetaCode("functions{") + .Accepts(AcceptedCharacters.None), + Factory.Code(Environment.NewLine + "foo") + .AsFunctionsBody() + .With(new AutoCompleteEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString) + { + AutoCompleteString = "}" + })), + new RazorError( + LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF("functions", "}", "{"), + new SourceLocation(10, 0, 10), + length: 1)); + } + + [Fact] + public void SectionDirectiveAutoCompleteAtStartOfFile() + { + ParseBlockTest("@section Header {" + Environment.NewLine + + "

Foo

", + new SectionBlock(new SectionChunkGenerator("Header"), + Factory.CodeTransition(), + Factory.MetaCode("section Header {") + .AutoCompleteWith("}", atEndOfSpan: true) + .Accepts(AcceptedCharacters.Any), + new MarkupBlock( + Factory.Markup(Environment.NewLine), + new MarkupTagBlock( + Factory.Markup("

")), + Factory.Markup("Foo"), + new MarkupTagBlock( + Factory.Markup("

")))), + new RazorError( + LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF("section", "}", "{"), + new SourceLocation(16, 0, 16), + length: 1)); + } + + [Fact] + public void VerbatimBlockAutoCompleteAtStartOfFile() + { + ParseBlockTest("@{" + Environment.NewLine + + "

", + new StatementBlock( + Factory.CodeTransition(), + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.Code(Environment.NewLine) + .AsStatement() + .With(new AutoCompleteEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString) { AutoCompleteString = "}" }), + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None))), + Factory.Span(SpanKind.Code, new CSharpSymbol(Factory.LocationTracker.CurrentLocation, string.Empty, CSharpSymbolType.Unknown)) + .With(new StatementChunkGenerator()) + ), + new RazorError( + LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF( + LegacyResources.BlockName_Code, "}", "{"), + new SourceLocation(1, 0, 1), + length: 1)); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpBlockTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpBlockTest.cs new file mode 100644 index 0000000000..ea9d24ad1a --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpBlockTest.cs @@ -0,0 +1,1258 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class CSharpBlockTest : CsHtmlCodeParserTestBase + { + [Fact] + public void ParseBlock_NestedCodeBlockWithCSharpAt() + { + ParseBlockTest("{ if (true) { var val = @x; if (val != 3) { } } }", + new StatementBlock( + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory + .Code(" if (true) { var val = @x; if (val != 3) { } } ") + .AsStatement() + .Accepts(AcceptedCharacters.Any) + .AutoCompleteWith(autoCompleteString: null, atEndOfSpan: false), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None))); + } + + [Fact] + public void ParseBlock_NestedCodeBlockWithMarkupSetsDotAsMarkup() + { + ParseBlockTest("if (true) { @if(false) {
@something.
} }", + new StatementBlock( + Factory.Code("if (true) { ").AsStatement(), + new StatementBlock( + Factory.CodeTransition(), + Factory.Code("if(false) {").AsStatement(), + new MarkupBlock( + Factory.Markup(" "), + BlockFactory.MarkupTagBlock("
", AcceptedCharacters.None), + Factory.EmptyHtml(), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.Code("something") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: false) + .Accepts(AcceptedCharacters.NonWhiteSpace)), + Factory.Markup("."), + BlockFactory.MarkupTagBlock("
", AcceptedCharacters.None), + Factory.Markup(" ").Accepts(AcceptedCharacters.None)), + Factory.Code("}").AsStatement()), + Factory.Code(" }").AsStatement())); + } + + [Fact] + public void BalancingBracketsIgnoresStringLiteralCharactersAndBracketsInsideSingleLineComments() + { + SingleSpanBlockTest(@"if(foo) { + // bar } "" baz ' + zoop(); +}", BlockType.Statement, SpanKind.Code); + } + + [Fact] + public void NestedCodeBlockWithAtDoesntCauseError() + { + ParseBlockTest("if (true) { @if(false) { } }", + new StatementBlock( + Factory.Code("if (true) { ").AsStatement(), + new StatementBlock( + Factory.CodeTransition(), + Factory.Code("if(false) { }").AsStatement() + ), + Factory.Code(" }").AsStatement())); + } + + [Fact] + public void BalancingBracketsIgnoresStringLiteralCharactersAndBracketsInsideBlockComments() + { + SingleSpanBlockTest( + @"if(foo) { + /* bar } "" */ ' baz } ' + zoop(); +}", BlockType.Statement, SpanKind.Code); + } + + [Fact] + public void ParseBlockSkipsParenthesisedExpressionAndThenBalancesBracesIfFirstIdentifierIsForKeyword() + { + SingleSpanBlockTest( + "for(int i = 0; i < 10; new Foo { Bar = \"baz\" }) { Debug.WriteLine(@\"foo } bar\"); }", + BlockType.Statement, + SpanKind.Code, + acceptedCharacters: AcceptedCharacters.None); + } + + [Fact] + public void ParseBlockSkipsParenthesisedExpressionAndThenBalancesBracesIfFirstIdentifierIsForeachKeyword() + { + SingleSpanBlockTest( + "foreach(int i = 0; i < 10; new Foo { Bar = \"baz\" }) { Debug.WriteLine(@\"foo } bar\"); }", + BlockType.Statement, + SpanKind.Code, + acceptedCharacters: AcceptedCharacters.None); + } + + [Fact] + public void ParseBlockSkipsParenthesisedExpressionAndThenBalancesBracesIfFirstIdentifierIsWhileKeyword() + { + SingleSpanBlockTest( + "while(int i = 0; i < 10; new Foo { Bar = \"baz\" }) { Debug.WriteLine(@\"foo } bar\"); }", + BlockType.Statement, + SpanKind.Code, + acceptedCharacters: AcceptedCharacters.None); + } + + [Fact] + public void ParseBlockSkipsParenthesisedExpressionAndThenBalancesBracesIfFirstIdentifierIsUsingKeywordFollowedByParen() + { + SingleSpanBlockTest( + "using(int i = 0; i < 10; new Foo { Bar = \"baz\" }) { Debug.WriteLine(@\"foo } bar\"); }", + BlockType.Statement, + SpanKind.Code, acceptedCharacters: AcceptedCharacters.None); + } + + [Fact] + public void ParseBlockSupportsUsingsNestedWithinOtherBlocks() + { + SingleSpanBlockTest( + "if(foo) { using(int i = 0; i < 10; new Foo { Bar = \"baz\" }) { Debug.WriteLine(@\"foo } bar\"); } }", + BlockType.Statement, + SpanKind.Code); + } + + [Fact] + public void ParseBlockSkipsParenthesisedExpressionAndThenBalancesBracesIfFirstIdentifierIsIfKeywordWithNoElseBranches() + { + SingleSpanBlockTest( + "if(int i = 0; i < 10; new Foo { Bar = \"baz\" }) { Debug.WriteLine(@\"foo } bar\"); }", + BlockType.Statement, + SpanKind.Code); + } + + [Fact] + public void ParseBlockAllowsEmptyBlockStatement() + { + SingleSpanBlockTest("if(false) { }", BlockType.Statement, SpanKind.Code); + } + + [Fact] + public void ParseBlockTerminatesParenBalancingAtEOF() + { + ImplicitExpressionTest( + "Html.En(code()", "Html.En(code()", + AcceptedCharacters.Any, + new RazorError( + LegacyResources.FormatParseError_Expected_CloseBracket_Before_EOF("(", ")"), + new SourceLocation(8, 0, 8), + length: 1)); + } + + [Fact] + public void ParseBlockSupportsBlockCommentBetweenIfAndElseClause() + { + SingleSpanBlockTest( + "if(foo) { bar(); } /* Foo */ /* Bar */ else { baz(); }", + BlockType.Statement, + SpanKind.Code, + acceptedCharacters: AcceptedCharacters.None); + } + + [Fact] + public void ParseBlockSupportsRazorCommentBetweenIfAndElseClause() + { + RunRazorCommentBetweenClausesTest( + "if(foo) { bar(); } ", " else { baz(); }", + acceptedCharacters: AcceptedCharacters.None); + } + + [Fact] + public void ParseBlockSupportsBlockCommentBetweenElseIfAndElseClause() + { + SingleSpanBlockTest( + "if(foo) { bar(); } else if(bar) { baz(); } /* Foo */ /* Bar */ else { biz(); }", + BlockType.Statement, + SpanKind.Code, + acceptedCharacters: AcceptedCharacters.None); + } + + [Fact] + public void ParseBlockSupportsRazorCommentBetweenElseIfAndElseClause() + { + RunRazorCommentBetweenClausesTest( + "if(foo) { bar(); } else if(bar) { baz(); } ", " else { baz(); }", + acceptedCharacters: AcceptedCharacters.None); + } + + [Fact] + public void ParseBlockSupportsBlockCommentBetweenIfAndElseIfClause() + { + SingleSpanBlockTest( + "if(foo) { bar(); } /* Foo */ /* Bar */ else if(bar) { baz(); }", + BlockType.Statement, + SpanKind.Code); + } + + [Fact] + public void ParseBlockSupportsRazorCommentBetweenIfAndElseIfClause() + { + RunRazorCommentBetweenClausesTest("if(foo) { bar(); } ", " else if(bar) { baz(); }"); + } + + [Fact] + public void ParseBlockSupportsLineCommentBetweenIfAndElseClause() + { + SingleSpanBlockTest(@"if(foo) { bar(); } +// Foo +// Bar +else { baz(); }", BlockType.Statement, SpanKind.Code, acceptedCharacters: AcceptedCharacters.None); + } + + [Fact] + public void ParseBlockSupportsLineCommentBetweenElseIfAndElseClause() + { + SingleSpanBlockTest(@"if(foo) { bar(); } else if(bar) { baz(); } +// Foo +// Bar +else { biz(); }", BlockType.Statement, SpanKind.Code, acceptedCharacters: AcceptedCharacters.None); + } + + [Fact] + public void ParseBlockSupportsLineCommentBetweenIfAndElseIfClause() + { + SingleSpanBlockTest(@"if(foo) { bar(); } +// Foo +// Bar +else if(bar) { baz(); }", BlockType.Statement, SpanKind.Code); + } + + [Fact] + public void ParseBlockParsesElseIfBranchesOfIfStatement() + { + const string ifStatement = @"if(int i = 0; i < 10; new Foo { Bar = ""baz"" }) { + Debug.WriteLine(@""foo } bar""); +}"; + const string elseIfBranch = @" else if(int i = 0; i < 10; new Foo { Bar = ""baz"" }) { + Debug.WriteLine(@""bar } baz""); +}"; + const string document = ifStatement + elseIfBranch; + + SingleSpanBlockTest(document, BlockType.Statement, SpanKind.Code); + } + + [Fact] + public void ParseBlockParsesMultipleElseIfBranchesOfIfStatement() + { + const string ifStatement = @"if(int i = 0; i < 10; new Foo { Bar = ""baz"" }) { + Debug.WriteLine(@""foo } bar""); +}"; + const string elseIfBranch = @" else if(int i = 0; i < 10; new Foo { Bar = ""baz"" }) { + Debug.WriteLine(@""bar } baz""); +}"; + const string document = ifStatement + elseIfBranch + elseIfBranch + elseIfBranch + elseIfBranch; + SingleSpanBlockTest(document, BlockType.Statement, SpanKind.Code); + } + + [Fact] + public void ParseBlockParsesMultipleElseIfBranchesOfIfStatementFollowedByOneElseBranch() + { + const string ifStatement = @"if(int i = 0; i < 10; new Foo { Bar = ""baz"" }) { + Debug.WriteLine(@""foo } bar""); +}"; + const string elseIfBranch = @" else if(int i = 0; i < 10; new Foo { Bar = ""baz"" }) { + Debug.WriteLine(@""bar } baz""); +}"; + const string elseBranch = @" else { Debug.WriteLine(@""bar } baz""); }"; + const string document = ifStatement + elseIfBranch + elseIfBranch + elseBranch; + + SingleSpanBlockTest(document, BlockType.Statement, SpanKind.Code, acceptedCharacters: AcceptedCharacters.None); + } + + [Fact] + public void ParseBlockStopsParsingCodeAfterElseBranch() + { + const string ifStatement = @"if(int i = 0; i < 10; new Foo { Bar = ""baz"" }) { + Debug.WriteLine(@""foo } bar""); +}"; + const string elseIfBranch = @" else if(int i = 0; i < 10; new Foo { Bar = ""baz"" }) { + Debug.WriteLine(@""bar } baz""); +}"; + const string elseBranch = @" else { Debug.WriteLine(@""bar } baz""); }"; + const string document = ifStatement + elseIfBranch + elseBranch + elseIfBranch; + const string expected = ifStatement + elseIfBranch + elseBranch; + + ParseBlockTest( + document, + new StatementBlock(Factory.Code(expected).AsStatement().Accepts(AcceptedCharacters.None))); + } + + [Fact] + public void ParseBlockStopsParsingIfIfStatementNotFollowedByElse() + { + const string document = @"if(int i = 0; i < 10; new Foo { Bar = ""baz"" }) { + Debug.WriteLine(@""foo } bar""); +}"; + + SingleSpanBlockTest(document, BlockType.Statement, SpanKind.Code); + } + + [Fact] + public void ParseBlockAcceptsElseIfWithNoCondition() + { + // We don't want to be a full C# parser - If the else if is missing it's condition, the C# compiler + // can handle that, we have all the info we need to keep parsing + const string ifBranch = @"if(int i = 0; i < 10; new Foo { Bar = ""baz"" }) { + Debug.WriteLine(@""foo } bar""); +}"; + const string elseIfBranch = @" else if { foo(); }"; + const string document = ifBranch + elseIfBranch; + + SingleSpanBlockTest(document, BlockType.Statement, SpanKind.Code); + } + + [Fact] + public void ParseBlockCorrectlyParsesDoWhileBlock() + { + SingleSpanBlockTest( + "do { var foo = bar; } while(foo != bar);", + BlockType.Statement, + SpanKind.Code, + acceptedCharacters: AcceptedCharacters.None); + } + + [Fact] + public void ParseBlockCorrectlyParsesDoWhileBlockMissingSemicolon() + { + SingleSpanBlockTest("do { var foo = bar; } while(foo != bar)", BlockType.Statement, SpanKind.Code); + } + + [Fact] + public void ParseBlockCorrectlyParsesDoWhileBlockMissingWhileCondition() + { + SingleSpanBlockTest("do { var foo = bar; } while", BlockType.Statement, SpanKind.Code); + } + + [Fact] + public void ParseBlockCorrectlyParsesDoWhileBlockMissingWhileConditionWithSemicolon() + { + SingleSpanBlockTest( + "do { var foo = bar; } while;", + BlockType.Statement, + SpanKind.Code, + acceptedCharacters: AcceptedCharacters.None); + } + + [Fact] + public void ParseBlockCorrectlyParsesDoWhileBlockMissingWhileClauseEntirely() + { + SingleSpanBlockTest("do { var foo = bar; } narf;", "do { var foo = bar; }", BlockType.Statement, SpanKind.Code); + } + + [Fact] + public void ParseBlockSupportsBlockCommentBetweenDoAndWhileClause() + { + SingleSpanBlockTest( + "do { var foo = bar; } /* Foo */ /* Bar */ while(true);", + BlockType.Statement, + SpanKind.Code, + acceptedCharacters: AcceptedCharacters.None); + } + + [Fact] + public void ParseBlockSupportsLineCommentBetweenDoAndWhileClause() + { + SingleSpanBlockTest(@"do { var foo = bar; } +// Foo +// Bar +while(true);", BlockType.Statement, SpanKind.Code, acceptedCharacters: AcceptedCharacters.None); + } + + [Fact] + public void ParseBlockSupportsRazorCommentBetweenDoAndWhileClause() + { + RunRazorCommentBetweenClausesTest( + "do { var foo = bar; } ", " while(true);", + acceptedCharacters: AcceptedCharacters.None); + } + + [Fact] + public void ParseBlockCorrectlyParsesMarkupInDoWhileBlock() + { + ParseBlockTest("@do { var foo = bar;

Foo

foo++; } while (foo);", + new StatementBlock( + Factory.CodeTransition(), + Factory.Code("do { var foo = bar;").AsStatement(), + new MarkupBlock( + Factory.Markup(" "), + BlockFactory.MarkupTagBlock("

", AcceptedCharacters.None), + Factory.Markup("Foo"), + BlockFactory.MarkupTagBlock("

", AcceptedCharacters.None), + Factory.Markup(" ").Accepts(AcceptedCharacters.None) + ), + Factory.Code("foo++; } while (foo);").AsStatement().Accepts(AcceptedCharacters.None) + )); + } + + [Fact] + public void ParseBlockSkipsParenthesisedExpressionAndThenBalancesBracesIfFirstIdentifierIsSwitchKeyword() + { + SingleSpanBlockTest(@"switch(foo) { + case 0: + break; + case 1: + { + break; + } + case 2: + return; + default: + return; +}", BlockType.Statement, SpanKind.Code, acceptedCharacters: AcceptedCharacters.None); + } + + [Fact] + public void ParseBlockSkipsParenthesisedExpressionAndThenBalancesBracesIfFirstIdentifierIsLockKeyword() + { + SingleSpanBlockTest( + "lock(foo) { Debug.WriteLine(@\"foo } bar\"); }", + BlockType.Statement, + SpanKind.Code, + acceptedCharacters: AcceptedCharacters.None); + } + + [Fact] + public void ParseBlockHasErrorsIfNamespaceImportMissingSemicolon() + { + NamespaceImportTest( + "using Foo.Bar.Baz", + " Foo.Bar.Baz", + acceptedCharacters: AcceptedCharacters.NonWhiteSpace | AcceptedCharacters.WhiteSpace, + location: new SourceLocation(17, 0, 17)); + } + + [Fact] + public void ParseBlockHasErrorsIfNamespaceAliasMissingSemicolon() + { + NamespaceImportTest( + "using Foo.Bar.Baz = FooBarBaz", + " Foo.Bar.Baz = FooBarBaz", + acceptedCharacters: AcceptedCharacters.NonWhiteSpace | AcceptedCharacters.WhiteSpace, + location: new SourceLocation(29, 0, 29)); + } + + [Fact] + public void ParseBlockParsesNamespaceImportWithSemicolonForUsingKeywordIfIsInValidFormat() + { + NamespaceImportTest( + "using Foo.Bar.Baz;", + " Foo.Bar.Baz", + AcceptedCharacters.NonWhiteSpace | AcceptedCharacters.WhiteSpace); + } + + [Fact] + public void ParseBlockDoesntCaptureWhitespaceAfterUsing() + { + ParseBlockTest("using Foo ", + new DirectiveBlock( + Factory.Code("using Foo") + .AsNamespaceImport(" Foo") + .Accepts(AcceptedCharacters.NonWhiteSpace | AcceptedCharacters.WhiteSpace))); + } + + [Fact] + public void ParseBlockParsesNamespaceAliasWithSemicolonForUsingKeywordIfIsInValidFormat() + { + NamespaceImportTest( + "using FooBarBaz = FooBarBaz;", + " FooBarBaz = FooBarBaz", + AcceptedCharacters.NonWhiteSpace | AcceptedCharacters.WhiteSpace); + } + + [Fact] + public void ParseBlockTerminatesUsingKeywordAtEOFAndOutputsFileCodeBlock() + { + SingleSpanBlockTest("using ", BlockType.Statement, SpanKind.Code); + } + + [Fact] + public void ParseBlockTerminatesSingleLineCommentAtEndOfFile() + { + const string document = "foreach(var f in Foo) { // foo bar baz"; + SingleSpanBlockTest( + document, + document, + BlockType.Statement, + SpanKind.Code, + new RazorError( + LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF("foreach", '}', '{'), + SourceLocation.Zero, + length: 1)); + } + + [Fact] + public void ParseBlockTerminatesBlockCommentAtEndOfFile() + { + const string document = "foreach(var f in Foo) { /* foo bar baz"; + SingleSpanBlockTest( + document, + document, + BlockType.Statement, + SpanKind.Code, + new RazorError( + LegacyResources.ParseError_BlockComment_Not_Terminated, + new SourceLocation(24, 0, 24), + length: 1), + new RazorError( + LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF("foreach", '}', '{'), + SourceLocation.Zero, + length: 1)); + } + + [Fact] + public void ParseBlockTerminatesSingleSlashAtEndOfFile() + { + const string document = "foreach(var f in Foo) { / foo bar baz"; + SingleSpanBlockTest( + document, + document, + BlockType.Statement, + SpanKind.Code, + new RazorError( + LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF("foreach", '}', '{'), + SourceLocation.Zero, + length: 1)); + } + + [Fact] + public void ParseBlockSupportsBlockCommentBetweenTryAndFinallyClause() + { + SingleSpanBlockTest("try { bar(); } /* Foo */ /* Bar */ finally { baz(); }", BlockType.Statement, SpanKind.Code, acceptedCharacters: AcceptedCharacters.None); + } + + [Fact] + public void ParseBlockSupportsRazorCommentBetweenTryAndFinallyClause() + { + RunRazorCommentBetweenClausesTest("try { bar(); } ", " finally { biz(); }", acceptedCharacters: AcceptedCharacters.None); + } + + [Fact] + public void ParseBlockSupportsBlockCommentBetweenCatchAndFinallyClause() + { + SingleSpanBlockTest( + "try { bar(); } catch(bar) { baz(); } /* Foo */ /* Bar */ finally { biz(); }", + BlockType.Statement, + SpanKind.Code, + acceptedCharacters: AcceptedCharacters.None); + } + + [Fact] + public void ParseBlockSupportsRazorCommentBetweenCatchAndFinallyClause() + { + RunRazorCommentBetweenClausesTest( + "try { bar(); } catch(bar) { baz(); } ", " finally { biz(); }", + acceptedCharacters: AcceptedCharacters.None); + } + + [Fact] + public void ParseBlockSupportsBlockCommentBetweenTryAndCatchClause() + { + SingleSpanBlockTest("try { bar(); } /* Foo */ /* Bar */ catch(bar) { baz(); }", BlockType.Statement, SpanKind.Code); + } + + [Fact] + public void ParseBlockSupportsRazorCommentBetweenTryAndCatchClause() + { + RunRazorCommentBetweenClausesTest("try { bar(); }", " catch(bar) { baz(); }"); + } + + [Fact] + public void ParseBlockSupportsLineCommentBetweenTryAndFinallyClause() + { + SingleSpanBlockTest(@"try { bar(); } +// Foo +// Bar +finally { baz(); }", BlockType.Statement, SpanKind.Code, acceptedCharacters: AcceptedCharacters.None); + } + + [Fact] + public void ParseBlockSupportsLineCommentBetweenCatchAndFinallyClause() + { + SingleSpanBlockTest(@"try { bar(); } catch(bar) { baz(); } +// Foo +// Bar +finally { biz(); }", BlockType.Statement, SpanKind.Code, acceptedCharacters: AcceptedCharacters.None); + } + + [Fact] + public void ParseBlockSupportsLineCommentBetweenTryAndCatchClause() + { + SingleSpanBlockTest(@"try { bar(); } +// Foo +// Bar +catch(bar) { baz(); }", BlockType.Statement, SpanKind.Code); + } + + [Fact] + public void ParseBlockSupportsTryStatementWithNoAdditionalClauses() + { + SingleSpanBlockTest("try { var foo = new { } }", BlockType.Statement, SpanKind.Code); + } + + [Fact] + public void ParseBlockSupportsMarkupWithinTryClause() + { + RunSimpleWrappedMarkupTest( + prefix: "try {", + markup: "

Foo

", + suffix: "}", + expectedMarkup: new MarkupBlock( + Factory.Markup(" "), + BlockFactory.MarkupTagBlock("

", AcceptedCharacters.None), + Factory.Markup("Foo"), + BlockFactory.MarkupTagBlock("

", AcceptedCharacters.None), + Factory.Markup(" ").Accepts(AcceptedCharacters.None))); + } + + [Fact] + public void ParseBlockSupportsTryStatementWithOneCatchClause() + { + SingleSpanBlockTest("try { var foo = new { } } catch(Foo Bar Baz) { var foo = new { } }", BlockType.Statement, SpanKind.Code); + } + + [Fact] + public void ParseBlockSupportsMarkupWithinCatchClause() + { + RunSimpleWrappedMarkupTest( + prefix: "try { var foo = new { } } catch(Foo Bar Baz) {", + markup: "

Foo

", + suffix: "}", + expectedMarkup: new MarkupBlock( + Factory.Markup(" "), + BlockFactory.MarkupTagBlock("

", AcceptedCharacters.None), + Factory.Markup("Foo"), + BlockFactory.MarkupTagBlock("

", AcceptedCharacters.None), + Factory.Markup(" ").Accepts(AcceptedCharacters.None))); + } + + [Fact] + public void ParseBlockSupportsTryStatementWithMultipleCatchClause() + { + SingleSpanBlockTest( + "try { var foo = new { } } catch(Foo Bar Baz) { var foo = new { } } catch(Foo Bar Baz) " + + "{ var foo = new { } } catch(Foo Bar Baz) { var foo = new { } }", + BlockType.Statement, + SpanKind.Code); + } + + [Fact] + public void ParseBlockSupportsExceptionLessCatchClauses() + { + SingleSpanBlockTest("try { var foo = new { } } catch { var foo = new { } }", BlockType.Statement, SpanKind.Code); + } + + [Fact] + public void ParseBlockSupportsMarkupWithinAdditionalCatchClauses() + { + RunSimpleWrappedMarkupTest( + prefix: "try { var foo = new { } } catch(Foo Bar Baz) { var foo = new { } } catch(Foo Bar Baz) " + + "{ var foo = new { } } catch(Foo Bar Baz) {", + markup: "

Foo

", + suffix: "}", + expectedMarkup: new MarkupBlock( + Factory.Markup(" "), + BlockFactory.MarkupTagBlock("

", AcceptedCharacters.None), + Factory.Markup("Foo"), + BlockFactory.MarkupTagBlock("

", AcceptedCharacters.None), + Factory.Markup(" ").Accepts(AcceptedCharacters.None))); + } + + [Fact] + public void ParseBlockSupportsTryStatementWithFinallyClause() + { + SingleSpanBlockTest("try { var foo = new { } } finally { var foo = new { } }", BlockType.Statement, SpanKind.Code, acceptedCharacters: AcceptedCharacters.None); + } + + [Fact] + public void ParseBlockSupportsMarkupWithinFinallyClause() + { + RunSimpleWrappedMarkupTest( + prefix: "try { var foo = new { } } finally {", + markup: "

Foo

", + suffix: "}", + expectedMarkup: new MarkupBlock( + Factory.Markup(" "), + BlockFactory.MarkupTagBlock("

", AcceptedCharacters.None), + Factory.Markup("Foo"), + BlockFactory.MarkupTagBlock("

", AcceptedCharacters.None), + Factory.Markup(" ").Accepts(AcceptedCharacters.None)), + acceptedCharacters: AcceptedCharacters.None); + } + + [Fact] + public void ParseBlockStopsParsingCatchClausesAfterFinallyBlock() + { + var expectedContent = "try { var foo = new { } } finally { var foo = new { } }"; + SingleSpanBlockTest(expectedContent + " catch(Foo Bar Baz) { }", expectedContent, BlockType.Statement, SpanKind.Code, acceptedCharacters: AcceptedCharacters.None); + } + + [Fact] + public void ParseBlockDoesNotAllowMultipleFinallyBlocks() + { + var expectedContent = "try { var foo = new { } } finally { var foo = new { } }"; + SingleSpanBlockTest(expectedContent + " finally { }", expectedContent, BlockType.Statement, SpanKind.Code, acceptedCharacters: AcceptedCharacters.None); + } + + [Fact] + public void ParseBlockAcceptsTrailingDotIntoImplicitExpressionWhenEmbeddedInCode() + { + // Arrange + ParseBlockTest(@"if(foo) { @foo. }", + new StatementBlock( + Factory.Code("if(foo) { ").AsStatement(), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.Code("foo.") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true) + .Accepts(AcceptedCharacters.NonWhiteSpace) + ), + Factory.Code(" }").AsStatement() + )); + } + + [Fact] + public void ParseBlockParsesExpressionOnSwitchCharacterFollowedByOpenParen() + { + // Arrange + ParseBlockTest(@"if(foo) { @(foo + bar) }", + new StatementBlock( + Factory.Code("if(foo) { ").AsStatement(), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.MetaCode("(").Accepts(AcceptedCharacters.None), + Factory.Code("foo + bar").AsExpression(), + Factory.MetaCode(")").Accepts(AcceptedCharacters.None) + ), + Factory.Code(" }").AsStatement() + )); + } + + [Fact] + public void ParseBlockParsesExpressionOnSwitchCharacterFollowedByIdentifierStart() + { + // Arrange + ParseBlockTest(@"if(foo) { @foo[4].bar() }", + new StatementBlock( + Factory.Code("if(foo) { ").AsStatement(), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.Code("foo[4].bar()") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true) + .Accepts(AcceptedCharacters.NonWhiteSpace) + ), + Factory.Code(" }").AsStatement() + )); + } + + [Fact] + public void ParseBlockTreatsDoubleAtSignAsEscapeSequenceIfAtStatementStart() + { + // Arrange + ParseBlockTest(@"if(foo) { @@class.Foo() }", + new StatementBlock( + Factory.Code("if(foo) { ").AsStatement(), + Factory.Code("@").Hidden(), + Factory.Code("@class.Foo() }").AsStatement() + )); + } + + [Fact] + public void ParseBlockTreatsAtSignsAfterFirstPairAsPartOfCSharpStatement() + { + // Arrange + ParseBlockTest(@"if(foo) { @@@@class.Foo() }", + new StatementBlock( + Factory.Code("if(foo) { ").AsStatement(), + Factory.Code("@").Hidden(), + Factory.Code("@@@class.Foo() }").AsStatement() + )); + } + + [Fact] + public void ParseBlockDoesNotParseMarkupStatementOrExpressionOnSwitchCharacterNotFollowedByOpenAngleOrColon() + { + // Arrange + ParseBlockTest("if(foo) { @\"Foo\".ToString(); }", + new StatementBlock( + Factory.Code("if(foo) { @\"Foo\".ToString(); }").AsStatement())); + } + + [Fact] + public void ParsersCanNestRecursively() + { + // Arrange + ParseBlockTest("foreach(var c in db.Categories) {" + Environment.NewLine + + "
" + Environment.NewLine + + " }", + new StatementBlock( + Factory.Code("foreach(var c in db.Categories) {" + Environment.NewLine).AsStatement(), + new MarkupBlock( + Factory.Markup(" "), + BlockFactory.MarkupTagBlock("
", AcceptedCharacters.None), + Factory.Markup(Environment.NewLine + " "), + BlockFactory.MarkupTagBlock("

", AcceptedCharacters.None), + Factory.EmptyHtml(), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.Code("c.Name") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace)), + BlockFactory.MarkupTagBlock("

", AcceptedCharacters.None), + Factory.Markup(Environment.NewLine + " "), + BlockFactory.MarkupTagBlock("
    ", AcceptedCharacters.None), + Factory.Markup(Environment.NewLine), + new StatementBlock( + Factory.Code(@" ").AsStatement(), + Factory.CodeTransition(), + Factory.Code("foreach(var p in c.Products) {" + Environment.NewLine).AsStatement(), + new MarkupBlock( + Factory.Markup(" "), + BlockFactory.MarkupTagBlock("
  • ", AcceptedCharacters.None), + new MarkupTagBlock( + Factory.Markup("(" href=\"", 183 + Environment.NewLine.Length * 5, 5, 30), + new LocationTagged("\"", 246 + Environment.NewLine.Length * 5, 5, 93)), + Factory.Markup(" href=\"").With(SpanChunkGenerator.Null), + new MarkupBlock( + new DynamicAttributeBlockChunkGenerator( + new LocationTagged(string.Empty, 190 + Environment.NewLine.Length * 5, 5, 37), 190 + Environment.NewLine.Length * 5, 5, 37), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.Code("Html.ActionUrl(\"Products\", \"Detail\", new { id = p.Id })") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace))), + Factory.Markup("\"").With(SpanChunkGenerator.Null)), + Factory.Markup(">").Accepts(AcceptedCharacters.None)), + Factory.EmptyHtml(), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.Code("p.Name") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace)), + BlockFactory.MarkupTagBlock("", AcceptedCharacters.None), + BlockFactory.MarkupTagBlock("
  • ", AcceptedCharacters.None), + Factory.Markup(Environment.NewLine).Accepts(AcceptedCharacters.None)), + Factory.Code(" }" + Environment.NewLine).AsStatement().Accepts(AcceptedCharacters.None)), + Factory.Markup(" "), + BlockFactory.MarkupTagBlock("
", AcceptedCharacters.None), + Factory.Markup(Environment.NewLine + " "), + BlockFactory.MarkupTagBlock("
", AcceptedCharacters.None), + Factory.Markup(Environment.NewLine).Accepts(AcceptedCharacters.None)), + Factory.Code(" }").AsStatement().Accepts(AcceptedCharacters.None))); + } + + public static TheoryData BlockWithEscapedTransitionData + { + get + { + var factory = new SpanFactory(); + var datetimeBlock = new ExpressionBlock( + factory.CodeTransition(), + factory.Code("DateTime.Now") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace)); + + return new TheoryData + { + { + // Double transition in attribute value + "{}", + CreateStatementBlock( + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo='", 6, 0, 6), new LocationTagged("'", 14, 0, 14)), + factory.Markup(" foo='").With(SpanChunkGenerator.Null), + new MarkupBlock( + factory.Markup("@").With(new LiteralAttributeChunkGenerator(new LocationTagged(string.Empty, 12, 0, 12), new LocationTagged("@", 12, 0, 12))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanChunkGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup("'").With(SpanChunkGenerator.Null)), + factory.Markup(" />").Accepts(AcceptedCharacters.None)))) + }, + { + // Double transition at the end of attribute value + "{}", + CreateStatementBlock( + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo='", 6, 0, 6), new LocationTagged("'", 17, 0, 17)), + factory.Markup(" foo='").With(SpanChunkGenerator.Null), + factory.Markup("abc").With(new LiteralAttributeChunkGenerator(new LocationTagged(string.Empty, 12, 0, 12), new LocationTagged("abc", 12, 0, 12))), + new MarkupBlock( + factory.Markup("@").With(new LiteralAttributeChunkGenerator(new LocationTagged(string.Empty, 15, 0, 15), new LocationTagged("@", 15, 0, 15))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanChunkGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup("'").With(SpanChunkGenerator.Null)), + factory.Markup(" />").Accepts(AcceptedCharacters.None)))) + }, + { + // Double transition at the beginning attribute value + "{}", + CreateStatementBlock( + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo='", 6, 0, 6), new LocationTagged("'", 17, 0, 17)), + factory.Markup(" foo='").With(SpanChunkGenerator.Null), + new MarkupBlock( + factory.Markup("@").With(new LiteralAttributeChunkGenerator(new LocationTagged(string.Empty, 12, 0, 12), new LocationTagged("@", 12, 0, 12))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanChunkGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup("def").With(new LiteralAttributeChunkGenerator(new LocationTagged(string.Empty, 14, 0, 14), new LocationTagged("def", 14, 0, 14))), + factory.Markup("'").With(SpanChunkGenerator.Null)), + factory.Markup(" />").Accepts(AcceptedCharacters.None)))) + }, + { + // Double transition in between attribute value + "{}", + CreateStatementBlock( + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo='", 6, 0, 6), new LocationTagged("'", 22, 0, 22)), + factory.Markup(" foo='").With(SpanChunkGenerator.Null), + factory.Markup("abc").With(new LiteralAttributeChunkGenerator(new LocationTagged(string.Empty, 12, 0, 12), new LocationTagged("abc", 12, 0, 12))), + new MarkupBlock( + factory.Markup(" @").With(new LiteralAttributeChunkGenerator(new LocationTagged(" ", 15, 0, 15), new LocationTagged("@", 16, 0, 16))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanChunkGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup(" def").With(new LiteralAttributeChunkGenerator(new LocationTagged(" ", 18, 0, 18), new LocationTagged("def", 19, 0, 19))), + factory.Markup("'").With(SpanChunkGenerator.Null)), + factory.Markup(" />").Accepts(AcceptedCharacters.None)))) + }, + { + // Double transition with expression block + "{}", + CreateStatementBlock( + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo='", 6, 0, 6), new LocationTagged("'", 27, 0, 27)), + factory.Markup(" foo='").With(SpanChunkGenerator.Null), + new MarkupBlock( + factory.Markup("@").With(new LiteralAttributeChunkGenerator(new LocationTagged(string.Empty, 12, 0, 12), new LocationTagged("@", 12, 0, 12))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanChunkGenerator.Null).Accepts(AcceptedCharacters.None)), + new MarkupBlock( + new DynamicAttributeBlockChunkGenerator(new LocationTagged(string.Empty, 14, 0, 14), 14, 0, 14), + factory.EmptyHtml().With(SpanChunkGenerator.Null), + datetimeBlock), + factory.Markup("'").With(SpanChunkGenerator.Null)), + factory.Markup(" />").Accepts(AcceptedCharacters.None)))) + }, + { + "{}", + CreateStatementBlock( + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo='", 6, 0, 6), new LocationTagged("'", 28, 0, 28)), + factory.Markup(" foo='").With(SpanChunkGenerator.Null), + new MarkupBlock( + new DynamicAttributeBlockChunkGenerator(new LocationTagged(string.Empty, 12, 0, 12), 12, 0, 12), + datetimeBlock), + new MarkupBlock( + factory.Markup(" @").With(new LiteralAttributeChunkGenerator(new LocationTagged(" ", 25, 0, 25), new LocationTagged("@", 26, 0, 26))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanChunkGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup("'").With(SpanChunkGenerator.Null)), + factory.Markup(" />").Accepts(AcceptedCharacters.None)))) + }, + { + "{}", + CreateStatementBlock( + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo='", 6, 0, 6), new LocationTagged("'", 27, 0, 27)), + factory.Markup(" foo='").With(SpanChunkGenerator.Null), + new MarkupBlock( + new DynamicAttributeBlockChunkGenerator(new LocationTagged(string.Empty, 12, 0, 12), 12, 0, 12), + datetimeBlock), + new MarkupBlock( + factory.Markup("@").With(new LiteralAttributeChunkGenerator(new LocationTagged(string.Empty, 25, 0, 25), new LocationTagged("@", 25, 0, 25))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanChunkGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup("'").With(SpanChunkGenerator.Null)), + factory.Markup(" />").Accepts(AcceptedCharacters.None)))) + }, + { + "{}", + CreateStatementBlock( + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo='", 6, 0, 6), new LocationTagged("'", 33, 0, 33)), + factory.Markup(" foo='").With(SpanChunkGenerator.Null), + new MarkupBlock( + new DynamicAttributeBlockChunkGenerator(new LocationTagged(string.Empty, 12, 0, 12), 12, 0, 12), + new ExpressionBlock( + factory.CodeTransition(), + factory.MetaCode("(").With(SpanChunkGenerator.Null).Accepts(AcceptedCharacters.None), + factory.Code("2+3").AsExpression(), + factory.MetaCode(")").With(SpanChunkGenerator.Null).Accepts(AcceptedCharacters.None))), + new MarkupBlock( + factory.Markup("@").With(new LiteralAttributeChunkGenerator(new LocationTagged(string.Empty, 18, 0, 18), new LocationTagged("@", 18, 0, 18))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanChunkGenerator.Null).Accepts(AcceptedCharacters.None)), + new MarkupBlock( + new DynamicAttributeBlockChunkGenerator(new LocationTagged(string.Empty, 20, 0, 20), 20, 0, 20), + factory.EmptyHtml().With(SpanChunkGenerator.Null), + datetimeBlock), + factory.Markup("'").With(SpanChunkGenerator.Null)), + factory.Markup(" />").Accepts(AcceptedCharacters.None)))) + }, + { + "{}", + CreateStatementBlock( + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo='", 6, 0, 6), new LocationTagged("'", 20, 0, 20)), + factory.Markup(" foo='").With(SpanChunkGenerator.Null), + new MarkupBlock( + factory.Markup("@").With(new LiteralAttributeChunkGenerator(new LocationTagged(string.Empty, 12, 0, 12), new LocationTagged("@", 12, 0, 12))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanChunkGenerator.Null).Accepts(AcceptedCharacters.None)), + new MarkupBlock( + new DynamicAttributeBlockChunkGenerator(new LocationTagged(string.Empty, 14, 0, 14), 14, 0, 14), + factory.EmptyHtml().With(SpanChunkGenerator.Null), + new ExpressionBlock( + factory.CodeTransition(), + factory.MetaCode("(").With(SpanChunkGenerator.Null).Accepts(AcceptedCharacters.None), + factory.Code("2+3").AsExpression(), + factory.MetaCode(")").With(SpanChunkGenerator.Null).Accepts(AcceptedCharacters.None))), + factory.Markup("'").With(SpanChunkGenerator.Null)), + factory.Markup(" />").Accepts(AcceptedCharacters.None)))) + }, + { + // Double transition with email in attribute value + "{}", + CreateStatementBlock( + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo='", 6, 0, 6), new LocationTagged("'", 26, 0, 26)), + factory.Markup(" foo='").With(SpanChunkGenerator.Null), + factory.Markup("abc@def.com").With(new LiteralAttributeChunkGenerator(new LocationTagged(string.Empty, 12, 0, 12), new LocationTagged("abc@def.com", 12, 0, 12))), + new MarkupBlock( + factory.Markup(" @").With(new LiteralAttributeChunkGenerator(new LocationTagged(" ", 23, 0, 23), new LocationTagged("@", 24, 0, 24))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanChunkGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup("'").With(SpanChunkGenerator.Null)), + factory.Markup(" />").Accepts(AcceptedCharacters.None)))) + }, + { + "{}", + CreateStatementBlock( + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo='", 6, 0, 6), new LocationTagged("'", 27, 0, 27)), + factory.Markup(" foo='").With(SpanChunkGenerator.Null), + factory.Markup("abc").With(new LiteralAttributeChunkGenerator(new LocationTagged(string.Empty, 12, 0, 12), new LocationTagged("abc", 12, 0, 12))), + new MarkupBlock( + factory.Markup("@").With(new LiteralAttributeChunkGenerator(new LocationTagged(string.Empty, 15, 0, 15), new LocationTagged("@", 15, 0, 15))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanChunkGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup("def.com").With(new LiteralAttributeChunkGenerator(new LocationTagged(string.Empty, 17, 0, 17), new LocationTagged("def.com", 17, 0, 17))), + new MarkupBlock( + factory.Markup(" @").With(new LiteralAttributeChunkGenerator(new LocationTagged(" ", 24, 0, 24), new LocationTagged("@", 25, 0, 25))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanChunkGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup("'").With(SpanChunkGenerator.Null)), + factory.Markup(" />").Accepts(AcceptedCharacters.None)))) + }, + { + // Double transition in complex regex in attribute value + @"{}", + CreateStatementBlock( + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo=\"", 6, 0, 6), new LocationTagged("\"", 112, 0, 112)), + factory.Markup(" foo=\"").With(SpanChunkGenerator.Null), + factory.Markup(@"/^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+").With(new LiteralAttributeChunkGenerator(new LocationTagged(string.Empty, 12, 0, 12), new LocationTagged(@"/^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+", 12, 0, 12))), + new MarkupBlock( + factory.Markup("@").With(new LiteralAttributeChunkGenerator(new LocationTagged(string.Empty, 44, 0, 44), new LocationTagged("@", 44, 0, 44))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanChunkGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup(@"[a-z0-9]([a-z0-9-]*[a-z0-9])?\.([a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i").With(new LiteralAttributeChunkGenerator(new LocationTagged(string.Empty, 46, 0, 46), new LocationTagged(@"[a-z0-9]([a-z0-9-]*[a-z0-9])?\.([a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i", 46, 0, 46))), + factory.Markup("\"").With(SpanChunkGenerator.Null)), + factory.Markup(" />").Accepts(AcceptedCharacters.None)))) + }, + }; + } + } + + [Theory] + [MemberData(nameof(BlockWithEscapedTransitionData))] + public void ParseBlock_WithDoubleTransition_DoesNotThrow(string input, Block expected) + { + // Act & Assert + ParseBlockTest(input, expected); + } + + [Fact] + public void ParseBlock_WithDoubleTransition_EndOfFile_Throws() + { + // Arrange + var expected = new StatementBlock( + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("(" foo='", 6, 0, 6), new LocationTagged(string.Empty, 14, 0, 14)), + Factory.Markup(" foo='").With(SpanChunkGenerator.Null), + new MarkupBlock( + Factory.Markup("@").With(new LiteralAttributeChunkGenerator(new LocationTagged(string.Empty, 12, 0, 12), new LocationTagged("@", 12, 0, 12))).Accepts(AcceptedCharacters.None), + Factory.Markup("@").With(SpanChunkGenerator.Null).Accepts(AcceptedCharacters.None)))), + Factory.EmptyHtml())); + var expectedErrors = new RazorError[] + { + new RazorError( + @"End of file or an unexpected character was reached before the ""span"" tag could be parsed. Elements inside markup blocks must be complete. They must either be self-closing (""
"") or have matching end tags (""

Hello

""). If you intended to display a ""<"" character, use the ""<"" HTML entity.", + new SourceLocation(2, 0, 2), + length: 4), + new RazorError( + @"The code block is missing a closing ""}"" character. Make sure you have a matching ""}"" character for all the ""{"" characters within this block, and that none of the ""}"" characters are being interpreted as markup.", + SourceLocation.Zero, + length: 1), + }; + + // Act & Assert + ParseBlockTest("{("'", 15, 0, 15)), + Factory.Markup(" foo='").With(SpanChunkGenerator.Null), + new MarkupBlock( + new DynamicAttributeBlockChunkGenerator(new LocationTagged(string.Empty, 12, 0, 12), 12, 0, 12), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.EmptyCSharp().AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace))), + new MarkupBlock( + new DynamicAttributeBlockChunkGenerator(new LocationTagged(" ", 13, 0, 13), 13, 0, 13), + Factory.Markup(" ").With(SpanChunkGenerator.Null), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.EmptyCSharp().AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace))), + Factory.Markup("'").With(SpanChunkGenerator.Null)), + Factory.Markup(" />").Accepts(AcceptedCharacters.None))), + Factory.EmptyCSharp().AsStatement(), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None)); + var expectedErrors = new RazorError[] + { + new RazorError( + @"A space or line break was encountered after the ""@"" character. Only valid identifiers, keywords, comments, ""("" and ""{"" are valid at the start of a code block and they must occur immediately following ""@"" with no space in between.", + new SourceLocation(13, 0, 13), + length: 1), + new RazorError( + @"""' />}"" is not valid at the start of a code block. Only identifiers, keywords, comments, ""("" and ""{"" are valid.", + new SourceLocation(15, 0, 15), + length: 5), + }; + + // Act & Assert + ParseBlockTest("{}", expected, expectedErrors); + } + + private void RunRazorCommentBetweenClausesTest(string preComment, string postComment, AcceptedCharacters acceptedCharacters = AcceptedCharacters.Any) + { + ParseBlockTest(preComment + "@* Foo *@ @* Bar *@" + postComment, + new StatementBlock( + Factory.Code(preComment).AsStatement(), + new CommentBlock( + Factory.CodeTransition(CSharpSymbolType.RazorCommentTransition), + Factory.MetaCode("*", CSharpSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None), + Factory.Comment(" Foo ", CSharpSymbolType.RazorComment), + Factory.MetaCode("*", CSharpSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None), + Factory.CodeTransition(CSharpSymbolType.RazorCommentTransition) + ), + Factory.Code(" ").AsStatement(), + new CommentBlock( + Factory.CodeTransition(CSharpSymbolType.RazorCommentTransition), + Factory.MetaCode("*", CSharpSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None), + Factory.Comment(" Bar ", CSharpSymbolType.RazorComment), + Factory.MetaCode("*", CSharpSymbolType.RazorCommentStar).Accepts(AcceptedCharacters.None), + Factory.CodeTransition(CSharpSymbolType.RazorCommentTransition) + ), + Factory.Code(postComment).AsStatement().Accepts(acceptedCharacters))); + } + + private void RunSimpleWrappedMarkupTest(string prefix, string markup, string suffix, MarkupBlock expectedMarkup, AcceptedCharacters acceptedCharacters = AcceptedCharacters.Any) + { + ParseBlockTest(prefix + markup + suffix, + new StatementBlock( + Factory.Code(prefix).AsStatement(), + expectedMarkup, + Factory.Code(suffix).AsStatement().Accepts(acceptedCharacters) + )); + } + + private void NamespaceImportTest(string content, string expectedNS, AcceptedCharacters acceptedCharacters = AcceptedCharacters.None, string errorMessage = null, SourceLocation? location = null) + { + var errors = new RazorError[0]; + if (!string.IsNullOrEmpty(errorMessage) && location.HasValue) + { + errors = new RazorError[] + { + new RazorError(errorMessage, location.Value, length: 1) + }; + } + ParseBlockTest(content, + new DirectiveBlock( + Factory.Code(content) + .AsNamespaceImport(expectedNS) + .Accepts(acceptedCharacters)), + errors); + } + + private static StatementBlock CreateStatementBlock(MarkupBlock block) + { + var factory = new SpanFactory(); + return new StatementBlock( + factory.MetaCode("{").Accepts(AcceptedCharacters.None), + block, + factory.EmptyCSharp().AsStatement(), + factory.MetaCode("}").Accepts(AcceptedCharacters.None)); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpDirectivesTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpDirectivesTest.cs new file mode 100644 index 0000000000..ba1e002c5a --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpDirectivesTest.cs @@ -0,0 +1,426 @@ +// 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.Legacy +{ + internal class CSharpDirectivesTest : CsHtmlCodeParserTestBase + { + [Fact] + public void TagHelperPrefixDirective_NoValueSucceeds() + { + ParseBlockTest("@tagHelperPrefix \"\"", + new DirectiveBlock( + Factory.CodeTransition(), + Factory + .MetaCode(SyntaxConstants.CSharp.TagHelperPrefixKeyword + " ") + .Accepts(AcceptedCharacters.None), + Factory.Code("\"\"") + .AsTagHelperPrefixDirective(string.Empty))); + } + + [Fact] + public void TagHelperPrefixDirective_Succeeds() + { + ParseBlockTest("@tagHelperPrefix Foo", + new DirectiveBlock( + Factory.CodeTransition(), + Factory + .MetaCode(SyntaxConstants.CSharp.TagHelperPrefixKeyword + " ") + .Accepts(AcceptedCharacters.None), + Factory.Code("Foo") + .AsTagHelperPrefixDirective("Foo"))); + } + + [Fact] + public void TagHelperPrefixDirective_WithQuotes_Succeeds() + { + ParseBlockTest("@tagHelperPrefix \"Foo\"", + new DirectiveBlock( + Factory.CodeTransition(), + Factory + .MetaCode(SyntaxConstants.CSharp.TagHelperPrefixKeyword + " ") + .Accepts(AcceptedCharacters.None), + Factory.Code("\"Foo\"") + .AsTagHelperPrefixDirective("Foo"))); + } + + [Fact] + public void TagHelperPrefixDirective_RequiresValue() + { + ParseBlockTest("@tagHelperPrefix ", + new DirectiveBlock( + Factory.CodeTransition(), + Factory + .MetaCode(SyntaxConstants.CSharp.TagHelperPrefixKeyword + " ") + .Accepts(AcceptedCharacters.None), + Factory.EmptyCSharp() + .AsTagHelperPrefixDirective(string.Empty) + .Accepts(AcceptedCharacters.AnyExceptNewline)), + new RazorError( + LegacyResources.FormatParseError_DirectiveMustHaveValue( + SyntaxConstants.CSharp.TagHelperPrefixKeyword), + absoluteIndex: 1, lineIndex: 0, columnIndex: 1, length: 15)); + } + + [Fact] + public void TagHelperPrefixDirective_StartQuoteRequiresDoubleQuotesAroundValue() + { + ParseBlockTest("@tagHelperPrefix \"Foo", + new DirectiveBlock( + Factory.CodeTransition(), + Factory + .MetaCode(SyntaxConstants.CSharp.TagHelperPrefixKeyword + " ") + .Accepts(AcceptedCharacters.None), + Factory.Code("\"Foo") + .AsTagHelperPrefixDirective("\"Foo")), + new RazorError( + LegacyResources.ParseError_Unterminated_String_Literal, + absoluteIndex: 17, lineIndex: 0, columnIndex: 17, length: 1), + new RazorError( + LegacyResources.FormatParseError_IncompleteQuotesAroundDirective( + SyntaxConstants.CSharp.TagHelperPrefixKeyword), + absoluteIndex: 17, lineIndex: 0, columnIndex: 17, length: 4)); + } + + [Fact] + public void TagHelperPrefixDirective_EndQuoteRequiresDoubleQuotesAroundValue() + { + ParseBlockTest("@tagHelperPrefix Foo \"", + new DirectiveBlock( + Factory.CodeTransition(), + Factory + .MetaCode(SyntaxConstants.CSharp.TagHelperPrefixKeyword + " ") + .Accepts(AcceptedCharacters.None), + Factory.Code("Foo \"") + .AsTagHelperPrefixDirective("Foo \"")), + new RazorError( + LegacyResources.ParseError_Unterminated_String_Literal, + absoluteIndex: 23, lineIndex: 0, columnIndex: 23, length: 1), + new RazorError( + LegacyResources.FormatParseError_IncompleteQuotesAroundDirective( + SyntaxConstants.CSharp.TagHelperPrefixKeyword), + absoluteIndex: 17, lineIndex: 0, columnIndex: 17, length: 7)); + } + + [Fact] + public void RemoveTagHelperDirective_NoValue_Succeeds() + { + ParseBlockTest("@removeTagHelper \"\"", + new DirectiveBlock( + Factory.CodeTransition(), + Factory.MetaCode(SyntaxConstants.CSharp.RemoveTagHelperKeyword + " ") + .Accepts(AcceptedCharacters.None), + Factory.Code("\"\"") + .AsRemoveTagHelper(string.Empty))); + } + + [Fact] + public void RemoveTagHelperDirective_Succeeds() + { + ParseBlockTest("@removeTagHelper Foo", + new DirectiveBlock( + Factory.CodeTransition(), + Factory.MetaCode(SyntaxConstants.CSharp.RemoveTagHelperKeyword + " ") + .Accepts(AcceptedCharacters.None), + Factory.Code("Foo") + .AsRemoveTagHelper("Foo"))); + } + + [Fact] + public void RemoveTagHelperDirective_WithQuotes_Succeeds() + { + ParseBlockTest("@removeTagHelper \"Foo\"", + new DirectiveBlock( + Factory.CodeTransition(), + Factory.MetaCode(SyntaxConstants.CSharp.RemoveTagHelperKeyword + " ") + .Accepts(AcceptedCharacters.None), + Factory.Code("\"Foo\"") + .AsRemoveTagHelper("Foo"))); + } + + [Fact] + public void RemoveTagHelperDirective_SupportsSpaces() + { + ParseBlockTest("@removeTagHelper Foo, Bar ", + new DirectiveBlock( + Factory.CodeTransition(), + Factory.MetaCode(SyntaxConstants.CSharp.RemoveTagHelperKeyword + " ") + .Accepts(AcceptedCharacters.None), + Factory.Code("Foo, Bar ") + .AsRemoveTagHelper("Foo, Bar") + .Accepts(AcceptedCharacters.AnyExceptNewline))); + } + + [Fact] + public void RemoveTagHelperDirective_RequiresValue() + { + ParseBlockTest("@removeTagHelper ", + new DirectiveBlock( + Factory.CodeTransition(), + Factory.MetaCode(SyntaxConstants.CSharp.RemoveTagHelperKeyword + " ") + .Accepts(AcceptedCharacters.None), + Factory.EmptyCSharp() + .AsRemoveTagHelper(string.Empty) + .Accepts(AcceptedCharacters.AnyExceptNewline)), + new RazorError( + LegacyResources.FormatParseError_DirectiveMustHaveValue( + SyntaxConstants.CSharp.RemoveTagHelperKeyword), + absoluteIndex: 1, lineIndex: 0, columnIndex: 1, length: 15)); + } + + [Fact] + public void RemoveTagHelperDirective_StartQuoteRequiresDoubleQuotesAroundValue() + { + ParseBlockTest("@removeTagHelper \"Foo", + new DirectiveBlock( + Factory.CodeTransition(), + Factory.MetaCode(SyntaxConstants.CSharp.RemoveTagHelperKeyword + " ") + .Accepts(AcceptedCharacters.None), + Factory.Code("\"Foo") + .AsRemoveTagHelper("\"Foo")), + new RazorError( + LegacyResources.ParseError_Unterminated_String_Literal, + absoluteIndex: 17, lineIndex: 0, columnIndex: 17, length: 1), + new RazorError( + LegacyResources.FormatParseError_IncompleteQuotesAroundDirective( + SyntaxConstants.CSharp.RemoveTagHelperKeyword), + absoluteIndex: 17, lineIndex: 0, columnIndex: 17, length: 4)); + } + + [Fact] + public void RemoveTagHelperDirective_EndQuoteRequiresDoubleQuotesAroundValue() + { + ParseBlockTest("@removeTagHelper Foo\"", + new DirectiveBlock( + Factory.CodeTransition(), + Factory.MetaCode(SyntaxConstants.CSharp.RemoveTagHelperKeyword + " ") + .Accepts(AcceptedCharacters.None), + Factory.Code("Foo\"") + .AsRemoveTagHelper("Foo\"") + .Accepts(AcceptedCharacters.AnyExceptNewline)), + new RazorError( + LegacyResources.ParseError_Unterminated_String_Literal, + absoluteIndex: 20, lineIndex: 0, columnIndex: 20, length: 1), + new RazorError( + LegacyResources.FormatParseError_IncompleteQuotesAroundDirective( + SyntaxConstants.CSharp.RemoveTagHelperKeyword), + absoluteIndex: 17, lineIndex: 0, columnIndex: 17, length: 4)); + } + + [Fact] + public void AddTagHelperDirective_NoValue_Succeeds() + { + ParseBlockTest("@addTagHelper \"\"", + new DirectiveBlock( + Factory.CodeTransition(), + Factory.MetaCode(SyntaxConstants.CSharp.AddTagHelperKeyword + " ") + .Accepts(AcceptedCharacters.None), + Factory.Code("\"\"") + .AsAddTagHelper(string.Empty))); + } + + [Fact] + public void AddTagHelperDirective_Succeeds() + { + ParseBlockTest("@addTagHelper Foo", + new DirectiveBlock( + Factory.CodeTransition(), + Factory.MetaCode(SyntaxConstants.CSharp.AddTagHelperKeyword + " ") + .Accepts(AcceptedCharacters.None), + Factory.Code("Foo") + .AsAddTagHelper("Foo"))); + } + + [Fact] + public void AddTagHelperDirective_WithQuotes_Succeeds() + { + ParseBlockTest("@addTagHelper \"Foo\"", + new DirectiveBlock( + Factory.CodeTransition(), + Factory.MetaCode(SyntaxConstants.CSharp.AddTagHelperKeyword + " ") + .Accepts(AcceptedCharacters.None), + Factory.Code("\"Foo\"") + .AsAddTagHelper("Foo"))); + } + + [Fact] + public void AddTagHelperDirectiveSupportsSpaces() + { + ParseBlockTest("@addTagHelper Foo, Bar ", + new DirectiveBlock( + Factory.CodeTransition(), + Factory.MetaCode(SyntaxConstants.CSharp.AddTagHelperKeyword + " ") + .Accepts(AcceptedCharacters.None), + Factory.Code("Foo, Bar ") + .AsAddTagHelper("Foo, Bar"))); + } + + [Fact] + public void AddTagHelperDirectiveRequiresValue() + { + ParseBlockTest("@addTagHelper ", + new DirectiveBlock( + Factory.CodeTransition(), + Factory.MetaCode(SyntaxConstants.CSharp.AddTagHelperKeyword + " ") + .Accepts(AcceptedCharacters.None), + Factory.EmptyCSharp() + .AsAddTagHelper(string.Empty) + .Accepts(AcceptedCharacters.AnyExceptNewline)), + new RazorError( + LegacyResources.FormatParseError_DirectiveMustHaveValue(SyntaxConstants.CSharp.AddTagHelperKeyword), + absoluteIndex: 1, lineIndex: 0, columnIndex: 1, length: 12)); + } + + [Fact] + public void AddTagHelperDirective_StartQuoteRequiresDoubleQuotesAroundValue() + { + ParseBlockTest("@addTagHelper \"Foo", + new DirectiveBlock( + Factory.CodeTransition(), + Factory.MetaCode(SyntaxConstants.CSharp.AddTagHelperKeyword + " ") + .Accepts(AcceptedCharacters.None), + Factory.Code("\"Foo") + .AsAddTagHelper("\"Foo")), + new RazorError( + LegacyResources.ParseError_Unterminated_String_Literal, + absoluteIndex: 14, lineIndex: 0, columnIndex: 14, length: 1), + new RazorError( + LegacyResources.FormatParseError_IncompleteQuotesAroundDirective( + SyntaxConstants.CSharp.AddTagHelperKeyword), + absoluteIndex: 14, lineIndex: 0, columnIndex: 14, length: 4)); + } + + [Fact] + public void AddTagHelperDirective_EndQuoteRequiresDoubleQuotesAroundValue() + { + ParseBlockTest("@addTagHelper Foo\"", + new DirectiveBlock( + Factory.CodeTransition(), + Factory.MetaCode(SyntaxConstants.CSharp.AddTagHelperKeyword + " ") + .Accepts(AcceptedCharacters.None), + Factory.Code("Foo\"") + .AsAddTagHelper("Foo\"") + .Accepts(AcceptedCharacters.AnyExceptNewline)), + new RazorError( + LegacyResources.ParseError_Unterminated_String_Literal, + absoluteIndex: 17, lineIndex: 0, columnIndex: 17, length: 1), + new RazorError( + LegacyResources.FormatParseError_IncompleteQuotesAroundDirective( + SyntaxConstants.CSharp.AddTagHelperKeyword), + absoluteIndex: 14, lineIndex: 0, columnIndex: 14, length: 4)); + } + + [Fact] + public void InheritsDirective() + { + ParseBlockTest("@inherits System.Web.WebPages.WebPage", + new DirectiveBlock( + Factory.CodeTransition(), + Factory.MetaCode(SyntaxConstants.CSharp.InheritsKeyword + " ") + .Accepts(AcceptedCharacters.None), + Factory.Code("System.Web.WebPages.WebPage") + .AsBaseType("System.Web.WebPages.WebPage"))); + } + + [Fact] + public void InheritsDirectiveSupportsArrays() + { + ParseBlockTest("@inherits string[[]][]", + new DirectiveBlock( + Factory.CodeTransition(), + Factory.MetaCode(SyntaxConstants.CSharp.InheritsKeyword + " ") + .Accepts(AcceptedCharacters.None), + Factory.Code("string[[]][]") + .AsBaseType("string[[]][]"))); + } + + [Fact] + public void InheritsDirectiveSupportsNestedGenerics() + { + ParseBlockTest("@inherits System.Web.Mvc.WebViewPage>", + new DirectiveBlock( + Factory.CodeTransition(), + Factory.MetaCode(SyntaxConstants.CSharp.InheritsKeyword + " ") + .Accepts(AcceptedCharacters.None), + Factory.Code("System.Web.Mvc.WebViewPage>") + .AsBaseType("System.Web.Mvc.WebViewPage>"))); + } + + [Fact] + public void InheritsDirectiveSupportsTypeKeywords() + { + ParseBlockTest("@inherits string", + new DirectiveBlock( + Factory.CodeTransition(), + Factory.MetaCode(SyntaxConstants.CSharp.InheritsKeyword + " ") + .Accepts(AcceptedCharacters.None), + Factory.Code("string") + .AsBaseType("string"))); + } + + [Fact] + public void InheritsDirectiveSupportsVSTemplateTokens() + { + ParseBlockTest("@inherits $rootnamespace$.MyBase", + new DirectiveBlock( + Factory.CodeTransition(), + Factory.MetaCode(SyntaxConstants.CSharp.InheritsKeyword + " ") + .Accepts(AcceptedCharacters.None), + Factory.Code("$rootnamespace$.MyBase") + .AsBaseType("$rootnamespace$.MyBase"))); + } + + [Fact] + public void FunctionsDirective() + { + ParseBlockTest("@functions { foo(); bar(); }", + new FunctionsBlock( + Factory.CodeTransition(), + Factory.MetaCode(SyntaxConstants.CSharp.FunctionsKeyword + " {") + .Accepts(AcceptedCharacters.None), + Factory.Code(" foo(); bar(); ") + .AsFunctionsBody() + .AutoCompleteWith(autoCompleteString: null), + Factory.MetaCode("}") + .Accepts(AcceptedCharacters.None))); + } + + [Fact] + public void EmptyFunctionsDirective() + { + ParseBlockTest("@functions { }", + new FunctionsBlock( + Factory.CodeTransition(), + Factory.MetaCode(SyntaxConstants.CSharp.FunctionsKeyword + " {") + .Accepts(AcceptedCharacters.None), + Factory.Code(" ") + .AsFunctionsBody() + .AutoCompleteWith(autoCompleteString: null), + Factory.MetaCode("}") + .Accepts(AcceptedCharacters.None))); + } + + [Fact] + public void SectionDirective() + { + ParseBlockTest("@section Header {

F{o}o

}", + new SectionBlock(new SectionChunkGenerator("Header"), + Factory.CodeTransition(), + Factory.MetaCode("section Header {") + .AutoCompleteWith(null, atEndOfSpan: true) + .Accepts(AcceptedCharacters.Any), + new MarkupBlock( + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("

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

")), + Factory.Markup(" ")), + Factory.MetaCode("}") + .Accepts(AcceptedCharacters.None))); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpErrorTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpErrorTest.cs new file mode 100644 index 0000000000..a79556c0fd --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpErrorTest.cs @@ -0,0 +1,695 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class CSharpErrorTest : CsHtmlCodeParserTestBase + { + [Fact] + public void ParseBlockHandlesQuotesAfterTransition() + { + ParseBlockTest("@\"", + new ExpressionBlock( + Factory.CodeTransition(), + Factory.EmptyCSharp() + .AsImplicitExpression(KeywordSet) + .Accepts(AcceptedCharacters.NonWhiteSpace) + ), + new RazorError( + LegacyResources.FormatParseError_Unexpected_Character_At_Start_Of_CodeBlock_CS('"'), + new SourceLocation(1, 0, 1), + length: 1)); + } + + [Fact] + public void ParseBlockWithHelperDirectiveProducesError() + { + ParseBlockTest("@helper fooHelper { }", + new ExpressionBlock( + Factory.CodeTransition(), + Factory.Code("helper") + .AsImplicitExpression(KeywordSet) + .Accepts(AcceptedCharacters.NonWhiteSpace)), + new RazorError( + LegacyResources.FormatParseError_HelperDirectiveNotAvailable(SyntaxConstants.CSharp.HelperKeyword), + new SourceLocation(1, 0, 1), + length: 6)); + } + + [Fact] + public void ParseBlockCapturesWhitespaceToEndOfLineInInvalidUsingStatementAndTreatsAsFileCode() + { + ParseBlockTest("using " + Environment.NewLine + + Environment.NewLine, + new StatementBlock( + Factory.Code("using " + Environment.NewLine).AsStatement() + )); + } + + [Fact] + public void ParseBlockMethodOutputsOpenCurlyAsCodeSpanIfEofFoundAfterOpenCurlyBrace() + { + ParseBlockTest("{", + new StatementBlock( + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.EmptyCSharp() + .AsStatement() + .With(new AutoCompleteEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString) { AutoCompleteString = "}" }) + ), + new RazorError( + LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF(LegacyResources.BlockName_Code, "}", "{"), + SourceLocation.Zero, + length: 1)); + } + + [Fact] + public void ParseBlockMethodOutputsZeroLengthCodeSpanIfStatementBlockEmpty() + { + ParseBlockTest("{}", + new StatementBlock( + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.EmptyCSharp() + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None) + )); + } + + [Fact] + public void ParseBlockMethodProducesErrorIfNewlineFollowsTransition() + { + ParseBlockTest("@" + Environment.NewLine, + new ExpressionBlock( + Factory.CodeTransition(), + Factory.EmptyCSharp() + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace)), + new RazorError( + LegacyResources.ParseError_Unexpected_WhiteSpace_At_Start_Of_CodeBlock_CS, + new SourceLocation(1, 0, 1), + Environment.NewLine.Length)); + } + + [Fact] + public void ParseBlockMethodProducesErrorIfWhitespaceBetweenTransitionAndBlockStartInEmbeddedExpression() + { + ParseBlockTest("{" + Environment.NewLine + + " @ {}" + Environment.NewLine + + "}", + new StatementBlock( + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.Code(Environment.NewLine + " ") + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.EmptyCSharp() + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true) + .Accepts(AcceptedCharacters.NonWhiteSpace)), + Factory.Code(" {}" + Environment.NewLine).AsStatement(), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None) + ), + new RazorError( + LegacyResources.ParseError_Unexpected_WhiteSpace_At_Start_Of_CodeBlock_CS, + new SourceLocation(6 + Environment.NewLine.Length, 1, 5), + length: 3)); + } + + [Fact] + public void ParseBlockMethodProducesErrorIfEOFAfterTransitionInEmbeddedExpression() + { + ParseBlockTest("{" + Environment.NewLine + + " @", + new StatementBlock( + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.Code(Environment.NewLine + " ") + .AsStatement() + .AutoCompleteWith("}"), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.EmptyCSharp() + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true) + .Accepts(AcceptedCharacters.NonWhiteSpace)), + Factory.EmptyCSharp().AsStatement() + ), + new RazorError( + LegacyResources.ParseError_Unexpected_EndOfFile_At_Start_Of_CodeBlock, + 6 + Environment.NewLine.Length, 1, 5, length: 1), + new RazorError( + LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF(LegacyResources.BlockName_Code, "}", "{"), + SourceLocation.Zero, + length: 1)); + } + + [Fact] + public void ParseBlockMethodParsesNothingIfFirstCharacterIsNotIdentifierStartOrParenOrBrace() + { + ParseBlockTest("@!!!", + new ExpressionBlock( + Factory.CodeTransition(), + Factory.EmptyCSharp() + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace)), + new RazorError( + LegacyResources.FormatParseError_Unexpected_Character_At_Start_Of_CodeBlock_CS("!"), + new SourceLocation(1, 0, 1), + length: 1)); + } + + [Fact] + public void ParseBlockShouldReportErrorAndTerminateAtEOFIfIfParenInExplicitExpressionUnclosed() + { + ParseBlockTest("(foo bar" + Environment.NewLine + + "baz", + new ExpressionBlock( + Factory.MetaCode("(").Accepts(AcceptedCharacters.None), + Factory.Code($"foo bar{Environment.NewLine}baz").AsExpression() + ), + new RazorError( + LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF(LegacyResources.BlockName_ExplicitExpression, ')', '('), + SourceLocation.Zero, + length: 1)); + } + + [Fact] + public void ParseBlockShouldReportErrorAndTerminateAtMarkupIfIfParenInExplicitExpressionUnclosed() + { + ParseBlockTest("(foo bar" + Environment.NewLine + + "" + Environment.NewLine + + "baz" + Environment.NewLine + + "@Html.Foo(Bar);" + Environment.NewLine, + new ExpressionBlock( + Factory.Code("Href(" + Environment.NewLine) + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + ), + new RazorError( + LegacyResources.FormatParseError_Expected_CloseBracket_Before_EOF("(", ")"), + new SourceLocation(4, 0, 4), + length: 1)); + } + + [Fact] + // Test for fix to Dev10 884975 - Incorrect Error Messaging + public void ParseBlockShouldReportErrorAndTerminateAtEOFIfParenInImplicitExpressionUnclosed() + { + ParseBlockTest("Foo(Bar(Baz)" + Environment.NewLine + + "Biz" + Environment.NewLine + + "Boz", + new ExpressionBlock( + Factory.Code($"Foo(Bar(Baz){Environment.NewLine}Biz{Environment.NewLine}Boz") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + ), + new RazorError( + LegacyResources.FormatParseError_Expected_CloseBracket_Before_EOF("(", ")"), + new SourceLocation(3, 0, 3), + length: 1)); + } + + [Fact] + // Test for fix to Dev10 884975 - Incorrect Error Messaging + public void ParseBlockShouldReportErrorAndTerminateAtMarkupIfParenInImplicitExpressionUnclosed() + { + ParseBlockTest("Foo(Bar(Baz)" + Environment.NewLine + + "Biz" + Environment.NewLine + + "" + Environment.NewLine + + "Boz" + Environment.NewLine + + "", + new ExpressionBlock( + Factory.Code($"Foo(Bar(Baz){Environment.NewLine}Biz{Environment.NewLine}") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + ), + new RazorError( + LegacyResources.FormatParseError_Expected_CloseBracket_Before_EOF("(", ")"), + new SourceLocation(3, 0, 3), + length: 1)); + } + + [Fact] + // Test for fix to Dev10 884975 - Incorrect Error Messaging + public void ParseBlockShouldReportErrorAndTerminateAtEOFIfBracketInImplicitExpressionUnclosed() + { + ParseBlockTest("Foo[Bar[Baz]" + Environment.NewLine + + "Biz" + Environment.NewLine + + "Boz", + new ExpressionBlock( + Factory.Code($"Foo[Bar[Baz]{Environment.NewLine}Biz{Environment.NewLine}Boz") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + ), + new RazorError( + LegacyResources.FormatParseError_Expected_CloseBracket_Before_EOF("[", "]"), + new SourceLocation(3, 0, 3), + length: 1)); + } + + [Fact] + // Test for fix to Dev10 884975 - Incorrect Error Messaging + public void ParseBlockShouldReportErrorAndTerminateAtMarkupIfBracketInImplicitExpressionUnclosed() + { + ParseBlockTest("Foo[Bar[Baz]" + Environment.NewLine + + "Biz" + Environment.NewLine + + "" + Environment.NewLine + + "Boz" + Environment.NewLine + + "", + new ExpressionBlock( + Factory.Code($"Foo[Bar[Baz]{Environment.NewLine}Biz{Environment.NewLine}") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + ), + new RazorError( + LegacyResources.FormatParseError_Expected_CloseBracket_Before_EOF("[", "]"), + new SourceLocation(3, 0, 3), + length: 1)); + } + + // Simple EOF handling errors: + [Fact] + public void ParseBlockReportsErrorIfExplicitCodeBlockUnterminatedAtEOF() + { + ParseBlockTest("{ var foo = bar; if(foo != null) { bar(); } ", + new StatementBlock( + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.Code(" var foo = bar; if(foo != null) { bar(); } ") + .AsStatement() + .AutoCompleteWith("}")), + new RazorError( + LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF( + LegacyResources.BlockName_Code, '}', '{'), + SourceLocation.Zero, + length: 1)); + } + + [Fact] + public void ParseBlockReportsErrorIfClassBlockUnterminatedAtEOF() + { + ParseBlockTest("functions { var foo = bar; if(foo != null) { bar(); } ", + new FunctionsBlock( + Factory.MetaCode("functions {").Accepts(AcceptedCharacters.None), + Factory.Code(" var foo = bar; if(foo != null) { bar(); } ") + .AsFunctionsBody() + .AutoCompleteWith("}")), + new RazorError( + LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF("functions", '}', '{'), + new SourceLocation(10, 0, 10), + length: 1)); + } + + [Fact] + public void ParseBlockReportsErrorIfIfBlockUnterminatedAtEOF() + { + RunUnterminatedSimpleKeywordBlock("if"); + } + + [Fact] + public void ParseBlockReportsErrorIfElseBlockUnterminatedAtEOF() + { + ParseBlockTest("if(foo) { baz(); } else { var foo = bar; if(foo != null) { bar(); } ", + new StatementBlock( + Factory.Code("if(foo) { baz(); } else { var foo = bar; if(foo != null) { bar(); } ").AsStatement() + ), + new RazorError( + LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF("else", '}', '{'), + new SourceLocation(19, 0, 19), + length: 1)); + } + + [Fact] + public void ParseBlockReportsErrorIfElseIfBlockUnterminatedAtEOF() + { + ParseBlockTest("if(foo) { baz(); } else if { var foo = bar; if(foo != null) { bar(); } ", + new StatementBlock( + Factory.Code("if(foo) { baz(); } else if { var foo = bar; if(foo != null) { bar(); } ").AsStatement() + ), + new RazorError( + LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF("else if", '}', '{'), + new SourceLocation(19, 0, 19), + length: 1)); + } + + [Fact] + public void ParseBlockReportsErrorIfDoBlockUnterminatedAtEOF() + { + ParseBlockTest("do { var foo = bar; if(foo != null) { bar(); } ", + new StatementBlock( + Factory.Code("do { var foo = bar; if(foo != null) { bar(); } ").AsStatement() + ), + new RazorError( + LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF("do", '}', '{'), + SourceLocation.Zero, + length: 1)); + } + + [Fact] + public void ParseBlockReportsErrorIfTryBlockUnterminatedAtEOF() + { + ParseBlockTest("try { var foo = bar; if(foo != null) { bar(); } ", + new StatementBlock( + Factory.Code("try { var foo = bar; if(foo != null) { bar(); } ").AsStatement() + ), + new RazorError( + LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF("try", '}', '{'), + SourceLocation.Zero, + length: 1)); + } + + [Fact] + public void ParseBlockReportsErrorIfCatchBlockUnterminatedAtEOF() + { + ParseBlockTest("try { baz(); } catch(Foo) { var foo = bar; if(foo != null) { bar(); } ", + new StatementBlock( + Factory.Code("try { baz(); } catch(Foo) { var foo = bar; if(foo != null) { bar(); } ").AsStatement() + ), + new RazorError( + LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF("catch", '}', '{'), + new SourceLocation(15, 0, 15), + length: 1)); + } + + [Fact] + public void ParseBlockReportsErrorIfFinallyBlockUnterminatedAtEOF() + { + ParseBlockTest("try { baz(); } finally { var foo = bar; if(foo != null) { bar(); } ", + new StatementBlock( + Factory.Code("try { baz(); } finally { var foo = bar; if(foo != null) { bar(); } ").AsStatement() + ), + new RazorError( + LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF("finally", '}', '{'), + new SourceLocation(15, 0, 15), + length: 1)); + } + + [Fact] + public void ParseBlockReportsErrorIfForBlockUnterminatedAtEOF() + { + RunUnterminatedSimpleKeywordBlock("for"); + } + + [Fact] + public void ParseBlockReportsErrorIfForeachBlockUnterminatedAtEOF() + { + RunUnterminatedSimpleKeywordBlock("foreach"); + } + + [Fact] + public void ParseBlockReportsErrorIfWhileBlockUnterminatedAtEOF() + { + RunUnterminatedSimpleKeywordBlock("while"); + } + + [Fact] + public void ParseBlockReportsErrorIfSwitchBlockUnterminatedAtEOF() + { + RunUnterminatedSimpleKeywordBlock("switch"); + } + + [Fact] + public void ParseBlockReportsErrorIfLockBlockUnterminatedAtEOF() + { + RunUnterminatedSimpleKeywordBlock("lock"); + } + + [Fact] + public void ParseBlockReportsErrorIfUsingBlockUnterminatedAtEOF() + { + RunUnterminatedSimpleKeywordBlock("using"); + } + + [Fact] + public void ParseBlockRequiresControlFlowStatementsToHaveBraces() + { + var expectedMessage = LegacyResources.FormatParseError_SingleLine_ControlFlowStatements_Not_Allowed("{", "<"); + ParseBlockTest("if(foo)

Bar

else if(bar)

Baz

else

Boz

", + new StatementBlock( + Factory.Code("if(foo) ").AsStatement(), + new MarkupBlock( + BlockFactory.MarkupTagBlock("

", AcceptedCharacters.None), + Factory.Markup("Bar"), + BlockFactory.MarkupTagBlock("

", AcceptedCharacters.None), + Factory.Markup(" ").Accepts(AcceptedCharacters.None)), + Factory.Code("else if(bar) ").AsStatement(), + new MarkupBlock( + BlockFactory.MarkupTagBlock("

", AcceptedCharacters.None), + Factory.Markup("Baz"), + BlockFactory.MarkupTagBlock("

", AcceptedCharacters.None), + Factory.Markup(" ").Accepts(AcceptedCharacters.None)), + Factory.Code("else ").AsStatement(), + new MarkupBlock( + BlockFactory.MarkupTagBlock("

", AcceptedCharacters.None), + Factory.Markup("Boz"), + BlockFactory.MarkupTagBlock("

", AcceptedCharacters.None)), + Factory.EmptyCSharp().AsStatement() + ), + new RazorError(expectedMessage, 8, 0, 8, 1), + new RazorError(expectedMessage, 32, 0, 32, 1), + new RazorError(expectedMessage, 48, 0, 48, 1)); + } + + [Fact] + public void ParseBlockIncludesUnexpectedCharacterInSingleStatementControlFlowStatementError() + { + ParseBlockTest("if(foo)) { var bar = foo; }", + new StatementBlock( + Factory.Code("if(foo)) { var bar = foo; }").AsStatement() + ), + new RazorError( + LegacyResources.FormatParseError_SingleLine_ControlFlowStatements_Not_Allowed("{", ")"), + new SourceLocation(7, 0, 7), + length: 1)); + } + + [Fact] + public void ParseBlockOutputsErrorIfAtSignFollowedByLessThanSignAtStatementStart() + { + ParseBlockTest("if(foo) { @

Bar

}", + new StatementBlock( + Factory.Code("if(foo) {").AsStatement(), + new MarkupBlock( + Factory.Markup(" "), + Factory.MarkupTransition(), + BlockFactory.MarkupTagBlock("

", AcceptedCharacters.None), + Factory.Markup("Bar"), + BlockFactory.MarkupTagBlock("

", AcceptedCharacters.None), + Factory.Markup(" ").Accepts(AcceptedCharacters.None)), + Factory.Code("}").AsStatement() + ), + new RazorError( + LegacyResources.ParseError_AtInCode_Must_Be_Followed_By_Colon_Paren_Or_Identifier_Start, + new SourceLocation(10, 0, 10), + length: 1)); + } + + [Fact] + public void ParseBlockTerminatesIfBlockAtEOLWhenRecoveringFromMissingCloseParen() + { + ParseBlockTest("if(foo bar" + Environment.NewLine + + "baz", + new StatementBlock( + Factory.Code("if(foo bar" + Environment.NewLine).AsStatement() + ), + new RazorError( + LegacyResources.FormatParseError_Expected_CloseBracket_Before_EOF("(", ")"), + new SourceLocation(2, 0, 2), + length: 1)); + } + + [Fact] + public void ParseBlockTerminatesForeachBlockAtEOLWhenRecoveringFromMissingCloseParen() + { + ParseBlockTest("foreach(foo bar" + Environment.NewLine + + "baz", + new StatementBlock( + Factory.Code("foreach(foo bar" + Environment.NewLine).AsStatement() + ), + new RazorError( + LegacyResources.FormatParseError_Expected_CloseBracket_Before_EOF("(", ")"), + new SourceLocation(7, 0, 7), + length: 1)); + } + + [Fact] + public void ParseBlockTerminatesWhileClauseInDoStatementAtEOLWhenRecoveringFromMissingCloseParen() + { + ParseBlockTest("do { } while(foo bar" + Environment.NewLine + + "baz", + new StatementBlock( + Factory.Code("do { } while(foo bar" + Environment.NewLine).AsStatement() + ), + new RazorError( + LegacyResources.FormatParseError_Expected_CloseBracket_Before_EOF("(", ")"), + new SourceLocation(12, 0, 12), + length: 1)); + } + + [Fact] + public void ParseBlockTerminatesUsingBlockAtEOLWhenRecoveringFromMissingCloseParen() + { + ParseBlockTest("using(foo bar" + Environment.NewLine + + "baz", + new StatementBlock( + Factory.Code("using(foo bar" + Environment.NewLine).AsStatement() + ), + new RazorError( + LegacyResources.FormatParseError_Expected_CloseBracket_Before_EOF("(", ")"), + new SourceLocation(5, 0, 5), + length: 1)); + } + + [Fact] + public void ParseBlockResumesIfStatementAfterOpenParen() + { + ParseBlockTest("if(" + Environment.NewLine + + "else {

Foo

}", + new StatementBlock( + Factory.Code($"if({Environment.NewLine}else {{").AsStatement(), + new MarkupBlock( + Factory.Markup(" "), + BlockFactory.MarkupTagBlock("

", AcceptedCharacters.None), + Factory.Markup("Foo"), + BlockFactory.MarkupTagBlock("

", AcceptedCharacters.None), + Factory.Markup(" ").Accepts(AcceptedCharacters.None)), + Factory.Code("}").AsStatement().Accepts(AcceptedCharacters.None) + ), + new RazorError( + LegacyResources.FormatParseError_Expected_CloseBracket_Before_EOF("(", ")"), + new SourceLocation(2, 0, 2), + length: 1)); + } + + [Fact] + public void ParseBlockTerminatesNormalCSharpStringsAtEOLIfEndQuoteMissing() + { + SingleSpanBlockTest("if(foo) {" + Environment.NewLine + + " var p = \"foo bar baz" + Environment.NewLine + + ";" + Environment.NewLine + + "}", + BlockType.Statement, SpanKind.Code, + new RazorError( + LegacyResources.ParseError_Unterminated_String_Literal, + new SourceLocation(21 + Environment.NewLine.Length, 1, 12), + length: 1)); + } + + [Fact] + public void ParseBlockTerminatesNormalStringAtEndOfFile() + { + SingleSpanBlockTest("if(foo) { var foo = \"blah blah blah blah blah", BlockType.Statement, SpanKind.Code, + new RazorError( + LegacyResources.ParseError_Unterminated_String_Literal, + new SourceLocation(20, 0, 20), + length: 1), + new RazorError( + LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF("if", '}', '{'), + SourceLocation.Zero, + length: 1)); + } + + [Fact] + public void ParseBlockTerminatesVerbatimStringAtEndOfFile() + { + SingleSpanBlockTest("if(foo) { var foo = @\"blah " + Environment.NewLine + + "blah; " + Environment.NewLine + + "

Foo

" + Environment.NewLine + + "blah " + Environment.NewLine + + "blah", + BlockType.Statement, SpanKind.Code, + new RazorError( + LegacyResources.ParseError_Unterminated_String_Literal, + new SourceLocation(20, 0, 20), + length: 1), + new RazorError( + LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF("if", '}', '{'), + SourceLocation.Zero, + length: 1)); + } + + [Fact] + public void ParseBlockCorrectlyParsesMarkupIncorrectyAssumedToBeWithinAStatement() + { + ParseBlockTest("if(foo) {" + Environment.NewLine + + " var foo = \"foo bar baz" + Environment.NewLine + + "

Foo is @foo

" + Environment.NewLine + + "}", + new StatementBlock( + Factory.Code($"if(foo) {{{Environment.NewLine} var foo = \"foo bar baz{Environment.NewLine} ").AsStatement(), + new MarkupBlock( + BlockFactory.MarkupTagBlock("

", AcceptedCharacters.None), + Factory.Markup("Foo is "), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.Code("foo") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace)), + BlockFactory.MarkupTagBlock("

", AcceptedCharacters.None), + Factory.Markup(Environment.NewLine).Accepts(AcceptedCharacters.None)), + Factory.Code("}").AsStatement() + ), + new RazorError( + LegacyResources.ParseError_Unterminated_String_Literal, + new SourceLocation(23 + Environment.NewLine.Length, 1, 14), + length: 1)); + } + + [Fact] + public void ParseBlockCorrectlyParsesAtSignInDelimitedBlock() + { + ParseBlockTest("(Request[\"description\"] ?? @photo.Description)", + new ExpressionBlock( + Factory.MetaCode("(").Accepts(AcceptedCharacters.None), + Factory.Code("Request[\"description\"] ?? @photo.Description").AsExpression(), + Factory.MetaCode(")").Accepts(AcceptedCharacters.None) + )); + } + + [Fact] + public void ParseBlockCorrectlyRecoversFromMissingCloseParenInExpressionWithinCode() + { + ParseBlockTest(@"{string.Format(}", + new StatementBlock( + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.Code("string.Format(") + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + new MarkupBlock( + BlockFactory.MarkupTagBlock("", AcceptedCharacters.None), + BlockFactory.MarkupTagBlock("", AcceptedCharacters.None)), + Factory.EmptyCSharp().AsStatement(), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + expectedErrors: new[] + { + new RazorError( + LegacyResources.FormatParseError_Expected_CloseBracket_Before_EOF("(", ")"), + new SourceLocation(14, 0, 14), + length: 1) + }); + + } + + private void RunUnterminatedSimpleKeywordBlock(string keyword) + { + SingleSpanBlockTest( + keyword + " (foo) { var foo = bar; if(foo != null) { bar(); } ", + BlockType.Statement, + SpanKind.Code, + new RazorError( + LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF(keyword, '}', '{'), + SourceLocation.Zero, + length: 1)); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpExplicitExpressionTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpExplicitExpressionTest.cs new file mode 100644 index 0000000000..1d05b53729 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpExplicitExpressionTest.cs @@ -0,0 +1,139 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class CSharpExplicitExpressionTest : CsHtmlCodeParserTestBase + { + [Fact] + public void ParseBlockShouldOutputZeroLengthCodeSpanIfExplicitExpressionIsEmpty() + { + ParseBlockTest("@()", + new ExpressionBlock( + Factory.CodeTransition(), + Factory.MetaCode("(").Accepts(AcceptedCharacters.None), + Factory.EmptyCSharp().AsExpression(), + Factory.MetaCode(")").Accepts(AcceptedCharacters.None) + )); + } + + [Fact] + public void ParseBlockShouldOutputZeroLengthCodeSpanIfEOFOccursAfterStartOfExplicitExpression() + { + ParseBlockTest("@(", + new ExpressionBlock( + Factory.CodeTransition(), + Factory.MetaCode("(").Accepts(AcceptedCharacters.None), + Factory.EmptyCSharp().AsExpression() + ), + new RazorError( + LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF( + LegacyResources.BlockName_ExplicitExpression, ")", "("), + new SourceLocation(1, 0, 1), + length: 1)); + } + + [Fact] + public void ParseBlockShouldAcceptEscapedQuoteInNonVerbatimStrings() + { + ParseBlockTest("@(\"\\\"\")", + new ExpressionBlock( + Factory.CodeTransition(), + Factory.MetaCode("(").Accepts(AcceptedCharacters.None), + Factory.Code("\"\\\"\"").AsExpression(), + Factory.MetaCode(")").Accepts(AcceptedCharacters.None) + )); + } + + [Fact] + public void ParseBlockShouldAcceptEscapedQuoteInVerbatimStrings() + { + ParseBlockTest("@(@\"\"\"\")", + new ExpressionBlock( + Factory.CodeTransition(), + Factory.MetaCode("(").Accepts(AcceptedCharacters.None), + Factory.Code("@\"\"\"\"").AsExpression(), + Factory.MetaCode(")").Accepts(AcceptedCharacters.None) + )); + } + + [Fact] + public void ParseBlockShouldAcceptMultipleRepeatedEscapedQuoteInVerbatimStrings() + { + ParseBlockTest("@(@\"\"\"\"\"\")", + new ExpressionBlock( + Factory.CodeTransition(), + Factory.MetaCode("(").Accepts(AcceptedCharacters.None), + Factory.Code("@\"\"\"\"\"\"").AsExpression(), + Factory.MetaCode(")").Accepts(AcceptedCharacters.None) + )); + } + + [Fact] + public void ParseBlockShouldAcceptMultiLineVerbatimStrings() + { + ParseBlockTest(@"@(@""" + Environment.NewLine + + @"Foo" + Environment.NewLine + + @"Bar" + Environment.NewLine + + @"Baz" + Environment.NewLine + + @""")", + new ExpressionBlock( + Factory.CodeTransition(), + Factory.MetaCode("(").Accepts(AcceptedCharacters.None), + Factory.Code($"@\"{Environment.NewLine}Foo{Environment.NewLine}Bar{Environment.NewLine}Baz{Environment.NewLine}\"").AsExpression(), + Factory.MetaCode(")").Accepts(AcceptedCharacters.None) + )); + } + + [Fact] + public void ParseBlockShouldAcceptMultipleEscapedQuotesInNonVerbatimStrings() + { + ParseBlockTest("@(\"\\\"hello, world\\\"\")", + new ExpressionBlock( + Factory.CodeTransition(), + Factory.MetaCode("(").Accepts(AcceptedCharacters.None), + Factory.Code("\"\\\"hello, world\\\"\"").AsExpression(), + Factory.MetaCode(")").Accepts(AcceptedCharacters.None) + )); + } + + [Fact] + public void ParseBlockShouldAcceptMultipleEscapedQuotesInVerbatimStrings() + { + ParseBlockTest("@(@\"\"\"hello, world\"\"\")", + new ExpressionBlock( + Factory.CodeTransition(), + Factory.MetaCode("(").Accepts(AcceptedCharacters.None), + Factory.Code("@\"\"\"hello, world\"\"\"").AsExpression(), + Factory.MetaCode(")").Accepts(AcceptedCharacters.None) + )); + } + + [Fact] + public void ParseBlockShouldAcceptConsecutiveEscapedQuotesInNonVerbatimStrings() + { + ParseBlockTest("@(\"\\\"\\\"\")", + new ExpressionBlock( + Factory.CodeTransition(), + Factory.MetaCode("(").Accepts(AcceptedCharacters.None), + Factory.Code("\"\\\"\\\"\"").AsExpression(), + Factory.MetaCode(")").Accepts(AcceptedCharacters.None) + )); + } + + [Fact] + public void ParseBlockShouldAcceptConsecutiveEscapedQuotesInVerbatimStrings() + { + ParseBlockTest("@(@\"\"\"\"\"\")", + new ExpressionBlock( + Factory.CodeTransition(), + Factory.MetaCode("(").Accepts(AcceptedCharacters.None), + Factory.Code("@\"\"\"\"\"\"").AsExpression(), + Factory.MetaCode(")").Accepts(AcceptedCharacters.None) + )); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpImplicitExpressionTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpImplicitExpressionTest.cs new file mode 100644 index 0000000000..4608d2b1e5 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpImplicitExpressionTest.cs @@ -0,0 +1,297 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class CSharpImplicitExpressionTest : CsHtmlCodeParserTestBase + { + private const string TestExtraKeyword = "model"; + + public static TheoryData NullConditionalOperatorData_Bracket + { + get + { + var noErrors = new RazorError[0]; + Func missingEndParenError = (index) => + new RazorError[1] + { + new RazorError( + "An opening \"(\" is missing the corresponding closing \")\".", + new SourceLocation(index, 0, index), + length: 1) + }; + Func missingEndBracketError = (index) => + new RazorError[1] + { + new RazorError( + "An opening \"[\" is missing the corresponding closing \"]\".", + new SourceLocation(index, 0, index), + length: 1) + }; + + // implicitExpression, expectedImplicitExpression, acceptedCharacters, expectedErrors + return new TheoryData + { + { "val??[", "val", AcceptedCharacters.NonWhiteSpace, noErrors }, + { "val??[0", "val", AcceptedCharacters.NonWhiteSpace, noErrors }, + { "val?[", "val?[", AcceptedCharacters.Any, missingEndBracketError(5) }, + { "val?(", "val", AcceptedCharacters.NonWhiteSpace, noErrors }, + { "val?[more", "val?[more", AcceptedCharacters.Any, missingEndBracketError(5) }, + { "val?[0]", "val?[0]", AcceptedCharacters.NonWhiteSpace, noErrors }, + { "val?[

", "val?[", AcceptedCharacters.Any, missingEndBracketError(5) }, + { "val?[more.

", "val?[more.", AcceptedCharacters.Any, missingEndBracketError(5) }, + { "val??[more

", "val", AcceptedCharacters.NonWhiteSpace, noErrors }, + { "val?[-1]?", "val?[-1]", AcceptedCharacters.NonWhiteSpace, noErrors }, + { "val?[abc]?[def", "val?[abc]?[def", AcceptedCharacters.Any, missingEndBracketError(11) }, + { "val?[abc]?[2]", "val?[abc]?[2]", AcceptedCharacters.NonWhiteSpace, noErrors }, + { "val?[abc]?.more?[def]", "val?[abc]?.more?[def]", AcceptedCharacters.NonWhiteSpace, noErrors }, + { "val?[abc]?.more?.abc", "val?[abc]?.more?.abc", AcceptedCharacters.NonWhiteSpace, noErrors }, + { "val?[null ?? true]", "val?[null ?? true]", AcceptedCharacters.NonWhiteSpace, noErrors }, + { "val?[abc?.gef?[-1]]", "val?[abc?.gef?[-1]]", AcceptedCharacters.NonWhiteSpace, noErrors }, + }; + } + } + + [Theory] + [MemberData(nameof(NullConditionalOperatorData_Bracket))] + public void ParseBlockMethodParsesNullConditionalOperatorImplicitExpression_Bracket( + string implicitExpresison, + string expectedImplicitExpression, + AcceptedCharacters acceptedCharacters, + RazorError[] expectedErrors) + { + // Act & Assert + ImplicitExpressionTest( + implicitExpresison, + expectedImplicitExpression, + acceptedCharacters, + expectedErrors); + } + + public static TheoryData NullConditionalOperatorData_Dot + { + get + { + // implicitExpression, expectedImplicitExpression + return new TheoryData + { + { "val?", "val" }, + { "val??", "val" }, + { "val??more", "val" }, + { "val?!", "val" }, + { "val?.", "val?." }, + { "val??.", "val" }, + { "val?.(abc)", "val?." }, + { "val?.

", "val?." }, + { "val?.more", "val?.more" }, + { "val?.more

", "val?.more" }, + { "val??.more

", "val" }, + { "val?.more(false)?.

", "val?.more(false)?." }, + { "val?.more(false)?.abc", "val?.more(false)?.abc" }, + { "val?.more(null ?? true)?.abc", "val?.more(null ?? true)?.abc" }, + }; + } + } + + [Theory] + [MemberData(nameof(NullConditionalOperatorData_Dot))] + public void ParseBlockMethodParsesNullConditionalOperatorImplicitExpression_Dot( + string implicitExpresison, + string expectedImplicitExpression) + { + // Act & Assert + ImplicitExpressionTest(implicitExpresison, expectedImplicitExpression); + } + + [Fact] + public void NestedImplicitExpression() + { + ParseBlockTest("if (true) { @foo }", + new StatementBlock( + Factory.Code("if (true) { ").AsStatement(), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.Code("foo") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true) + .Accepts(AcceptedCharacters.NonWhiteSpace)), + Factory.Code(" }").AsStatement())); + } + + [Fact] + public void ParseBlockAcceptsNonEnglishCharactersThatAreValidIdentifiers() + { + ImplicitExpressionTest("हळूँजद॔.", "हळूँजद॔"); + } + + [Fact] + public void ParseBlockOutputsZeroLengthCodeSpanIfInvalidCharacterFollowsTransition() + { + ParseBlockTest("@/", + new ExpressionBlock( + Factory.CodeTransition(), + Factory.EmptyCSharp() + .AsImplicitExpression(KeywordSet) + .Accepts(AcceptedCharacters.NonWhiteSpace)), + new RazorError( + LegacyResources.FormatParseError_Unexpected_Character_At_Start_Of_CodeBlock_CS("/"), + new SourceLocation(1, 0, 1), + length: 1)); + } + + [Fact] + public void ParseBlockOutputsZeroLengthCodeSpanIfEOFOccursAfterTransition() + { + ParseBlockTest("@", + new ExpressionBlock( + Factory.CodeTransition(), + Factory.EmptyCSharp() + .AsImplicitExpression(KeywordSet) + .Accepts(AcceptedCharacters.NonWhiteSpace)), + new RazorError( + LegacyResources.ParseError_Unexpected_EndOfFile_At_Start_Of_CodeBlock, + new SourceLocation(1, 0, 1), + length: 1)); + } + + [Fact] + public void ParseBlockSupportsSlashesWithinComplexImplicitExpressions() + { + ImplicitExpressionTest("DataGridColumn.Template(\"Years of Service\", e => (int)Math.Round((DateTime.Now - dt).TotalDays / 365))"); + } + + [Fact] + public void ParseBlockMethodParsesSingleIdentifierAsImplicitExpression() + { + ImplicitExpressionTest("foo"); + } + + [Fact] + public void ParseBlockMethodDoesNotAcceptSemicolonIfExpressionTerminatedByWhitespace() + { + ImplicitExpressionTest("foo ;", "foo"); + } + + [Fact] + public void ParseBlockMethodIgnoresSemicolonAtEndOfSimpleImplicitExpression() + { + RunTrailingSemicolonTest("foo"); + } + + [Fact] + public void ParseBlockMethodParsesDottedIdentifiersAsImplicitExpression() + { + ImplicitExpressionTest("foo.bar.baz"); + } + + [Fact] + public void ParseBlockMethodIgnoresSemicolonAtEndOfDottedIdentifiers() + { + RunTrailingSemicolonTest("foo.bar.baz"); + } + + [Fact] + public void ParseBlockMethodDoesNotIncludeDotAtEOFInImplicitExpression() + { + ImplicitExpressionTest("foo.bar.", "foo.bar"); + } + + [Fact] + public void ParseBlockMethodDoesNotIncludeDotFollowedByInvalidIdentifierCharacterInImplicitExpression() + { + ImplicitExpressionTest("foo.bar.0", "foo.bar"); + ImplicitExpressionTest("foo.bar.

", "foo.bar"); + } + + [Fact] + public void ParseBlockMethodDoesNotIncludeSemicolonAfterDot() + { + ImplicitExpressionTest("foo.bar.;", "foo.bar"); + } + + [Fact] + public void ParseBlockMethodTerminatesAfterIdentifierUnlessFollowedByDotOrParenInImplicitExpression() + { + ImplicitExpressionTest("foo.bar

", "foo.bar"); + } + + [Fact] + public void ParseBlockProperlyParsesParenthesesAndBalancesThemInImplicitExpression() + { + ImplicitExpressionTest(@"foo().bar(""bi\""z"", 4)(""chained method; call"").baz(@""bo""""z"", '\'', () => { return 4; }, (4+5+new { foo = bar[4] }))"); + } + + [Fact] + public void ParseBlockProperlyParsesBracketsAndBalancesThemInImplicitExpression() + { + ImplicitExpressionTest(@"foo.bar[4 * (8 + 7)][""fo\""o""].baz"); + } + + [Fact] + public void ParseBlockTerminatesImplicitExpressionAtHtmlEndTag() + { + ImplicitExpressionTest("foo().bar.baz

zoop", "foo().bar.baz"); + } + + [Fact] + public void ParseBlockTerminatesImplicitExpressionAtHtmlStartTag() + { + ImplicitExpressionTest("foo().bar.baz

zoop", "foo().bar.baz"); + } + + [Fact] + public void ParseBlockTerminatesImplicitExpressionBeforeDotIfDotNotFollowedByIdentifierStartCharacter() + { + ImplicitExpressionTest("foo().bar.baz.42", "foo().bar.baz"); + } + + [Fact] + public void ParseBlockStopsBalancingParenthesesAtEOF() + { + ImplicitExpressionTest( + "foo(()", "foo(()", + acceptedCharacters: AcceptedCharacters.Any, + errors: new RazorError( + LegacyResources.FormatParseError_Expected_CloseBracket_Before_EOF("(", ")"), + new SourceLocation(4, 0, 4), + length: 1)); + } + + [Fact] + public void ParseBlockTerminatesImplicitExpressionIfCloseParenFollowedByAnyWhiteSpace() + { + ImplicitExpressionTest("foo.bar() (baz)", "foo.bar()"); + } + + [Fact] + public void ParseBlockTerminatesImplicitExpressionIfIdentifierFollowedByAnyWhiteSpace() + { + ImplicitExpressionTest("foo .bar() (baz)", "foo"); + } + + [Fact] + public void ParseBlockTerminatesImplicitExpressionAtLastValidPointIfDotFollowedByWhitespace() + { + ImplicitExpressionTest("foo. bar() (baz)", "foo"); + } + + [Fact] + public void ParseBlockOutputExpressionIfModuleTokenNotFollowedByBrace() + { + ImplicitExpressionTest("module.foo()"); + } + + private void RunTrailingSemicolonTest(string expr) + { + ParseBlockTest(SyntaxConstants.TransitionString + expr + ";", + new ExpressionBlock( + Factory.CodeTransition(), + Factory.Code(expr) + .AsImplicitExpression(KeywordSet) + .Accepts(AcceptedCharacters.NonWhiteSpace) + )); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpNestedStatementsTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpNestedStatementsTest.cs new file mode 100644 index 0000000000..84530f04ab --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpNestedStatementsTest.cs @@ -0,0 +1,104 @@ +// 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.Legacy +{ + internal class CSharpNestedStatementsTest : CsHtmlCodeParserTestBase + { + [Fact] + public void NestedSimpleStatement() + { + ParseBlockTest("@while(true) { foo(); }", + new StatementBlock( + Factory.CodeTransition(), + Factory.Code("while(true) { foo(); }") + .AsStatement() + .Accepts(AcceptedCharacters.None))); + } + + [Fact] + public void NestedKeywordStatement() + { + ParseBlockTest("@while(true) { for(int i = 0; i < 10; i++) { foo(); } }", + new StatementBlock( + Factory.CodeTransition(), + Factory.Code("while(true) { for(int i = 0; i < 10; i++) { foo(); } }") + .AsStatement() + .Accepts(AcceptedCharacters.None))); + } + + [Fact] + public void NestedCodeBlock() + { + ParseBlockTest("@while(true) { { { { foo(); } } } }", + new StatementBlock( + Factory.CodeTransition(), + Factory.Code("while(true) { { { { foo(); } } } }") + .AsStatement() + .Accepts(AcceptedCharacters.None))); + } + + [Fact] + public void NestedImplicitExpression() + { + ParseBlockTest("@while(true) { @foo }", + new StatementBlock( + Factory.CodeTransition(), + Factory.Code("while(true) { ") + .AsStatement(), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.Code("foo") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true) + .Accepts(AcceptedCharacters.NonWhiteSpace)), + Factory.Code(" }") + .AsStatement() + .Accepts(AcceptedCharacters.None))); + } + + [Fact] + public void NestedExplicitExpression() + { + ParseBlockTest("@while(true) { @(foo) }", + new StatementBlock( + Factory.CodeTransition(), + Factory.Code("while(true) { ") + .AsStatement(), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.MetaCode("(") + .Accepts(AcceptedCharacters.None), + Factory.Code("foo") + .AsExpression(), + Factory.MetaCode(")") + .Accepts(AcceptedCharacters.None)), + Factory.Code(" }") + .AsStatement() + .Accepts(AcceptedCharacters.None))); + } + + [Fact] + public void NestedMarkupBlock() + { + ParseBlockTest("@while(true) {

Hello

}", + new StatementBlock( + Factory.CodeTransition(), + Factory.Code("while(true) {") + .AsStatement(), + new MarkupBlock( + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup("Hello"), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup(" ").Accepts(AcceptedCharacters.None) + ), + Factory.Code("}") + .AsStatement() + .Accepts(AcceptedCharacters.None))); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpRazorCommentsTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpRazorCommentsTest.cs new file mode 100644 index 0000000000..36a546b1b7 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpRazorCommentsTest.cs @@ -0,0 +1,423 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class CSharpRazorCommentsTest : CsHtmlMarkupParserTestBase + { + [Fact] + public void UnterminatedRazorComment() + { + ParseDocumentTest("@*", + new MarkupBlock( + Factory.EmptyHtml(), + new CommentBlock( + Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition) + .Accepts(AcceptedCharacters.None), + Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar) + .Accepts(AcceptedCharacters.None), + Factory.Span(SpanKind.Comment, new HtmlSymbol( + Factory.LocationTracker.CurrentLocation, + string.Empty, + HtmlSymbolType.Unknown)) + .Accepts(AcceptedCharacters.Any))), + new RazorError( + LegacyResources.ParseError_RazorComment_Not_Terminated, + SourceLocation.Zero, + length: 2)); + } + + [Fact] + public void EmptyRazorComment() + { + ParseDocumentTest("@**@", + new MarkupBlock( + Factory.EmptyHtml(), + new CommentBlock( + Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition) + .Accepts(AcceptedCharacters.None), + Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar) + .Accepts(AcceptedCharacters.None), + Factory.Span(SpanKind.Comment, new HtmlSymbol( + Factory.LocationTracker.CurrentLocation, + string.Empty, + HtmlSymbolType.Unknown)) + .Accepts(AcceptedCharacters.Any), + Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar) + .Accepts(AcceptedCharacters.None), + Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition) + .Accepts(AcceptedCharacters.None)), + Factory.EmptyHtml())); + } + + [Fact] + public void RazorCommentInImplicitExpressionMethodCall() + { + ParseDocumentTest("@foo(" + Environment.NewLine + + "@**@" + Environment.NewLine, + new MarkupBlock( + Factory.EmptyHtml(), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.Code("foo(" + Environment.NewLine) + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords), + new CommentBlock( + Factory.CodeTransition(CSharpSymbolType.RazorCommentTransition) + .Accepts(AcceptedCharacters.None), + Factory.MetaCode("*", CSharpSymbolType.RazorCommentStar) + .Accepts(AcceptedCharacters.None), + Factory.Span(SpanKind.Comment, new CSharpSymbol( + Factory.LocationTracker.CurrentLocation, + string.Empty, + CSharpSymbolType.Unknown)) + .Accepts(AcceptedCharacters.Any), + Factory.MetaCode("*", CSharpSymbolType.RazorCommentStar) + .Accepts(AcceptedCharacters.None), + Factory.CodeTransition(CSharpSymbolType.RazorCommentTransition) + .Accepts(AcceptedCharacters.None)), + Factory.Code(Environment.NewLine) + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords))), + new RazorError( + LegacyResources.FormatParseError_Expected_CloseBracket_Before_EOF("(", ")"), + new SourceLocation(4, 0, 4), + length: 1)); + } + + [Fact] + public void UnterminatedRazorCommentInImplicitExpressionMethodCall() + { + ParseDocumentTest("@foo(@*", + new MarkupBlock( + Factory.EmptyHtml(), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.Code("foo(") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords), + new CommentBlock( + Factory.CodeTransition(CSharpSymbolType.RazorCommentTransition) + .Accepts(AcceptedCharacters.None), + Factory.MetaCode("*", CSharpSymbolType.RazorCommentStar) + .Accepts(AcceptedCharacters.None), + Factory.Span(SpanKind.Comment, new CSharpSymbol( + Factory.LocationTracker.CurrentLocation, + string.Empty, + CSharpSymbolType.Unknown)) + .Accepts(AcceptedCharacters.Any)))), + new RazorError( + LegacyResources.ParseError_RazorComment_Not_Terminated, + new SourceLocation(5, 0, 5), + length: 2), + new RazorError( + LegacyResources.FormatParseError_Expected_CloseBracket_Before_EOF("(", ")"), + new SourceLocation(4, 0, 4), + length: 1)); + } + + [Fact] + public void RazorCommentInVerbatimBlock() + { + ParseDocumentTest("@{" + Environment.NewLine + + " " + Environment.NewLine + + "@**@" + Environment.NewLine + + "

", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("

")), + Factory.Markup(Environment.NewLine), + new CommentBlock( + Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition) + .Accepts(AcceptedCharacters.None), + Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar) + .Accepts(AcceptedCharacters.None), + Factory.Span(SpanKind.Comment, new HtmlSymbol( + Factory.LocationTracker.CurrentLocation, + string.Empty, + HtmlSymbolType.Unknown)) + .Accepts(AcceptedCharacters.Any), + Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar) + .Accepts(AcceptedCharacters.None), + Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition) + .Accepts(AcceptedCharacters.None)), + Factory.Markup(Environment.NewLine).With(SpanChunkGenerator.Null), + new MarkupTagBlock( + Factory.Markup("

")) + )); + } + + [Fact] + public void MultipleRazorCommentInMarkup() + { + ParseDocumentTest( + "

" + Environment.NewLine + + " @**@ " + Environment.NewLine + + "@**@" + Environment.NewLine + + "

", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("

")), + Factory.Markup(Environment.NewLine), + Factory.Markup(" ").With(SpanChunkGenerator.Null), + new CommentBlock( + Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition) + .Accepts(AcceptedCharacters.None), + Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar) + .Accepts(AcceptedCharacters.None), + Factory.Span(SpanKind.Comment, new HtmlSymbol( + Factory.LocationTracker.CurrentLocation, + string.Empty, + HtmlSymbolType.Unknown)) + .Accepts(AcceptedCharacters.Any), + Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar) + .Accepts(AcceptedCharacters.None), + Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition) + .Accepts(AcceptedCharacters.None)), + Factory.Markup(" " + Environment.NewLine).With(SpanChunkGenerator.Null), + new CommentBlock( + Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition) + .Accepts(AcceptedCharacters.None), + Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar) + .Accepts(AcceptedCharacters.None), + Factory.Span(SpanKind.Comment, new HtmlSymbol( + Factory.LocationTracker.CurrentLocation, + string.Empty, + HtmlSymbolType.Unknown)) + .Accepts(AcceptedCharacters.Any), + Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar) + .Accepts(AcceptedCharacters.None), + Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition) + .Accepts(AcceptedCharacters.None)), + Factory.Markup(Environment.NewLine).With(SpanChunkGenerator.Null), + new MarkupTagBlock( + Factory.Markup("

")) + )); + } + + [Fact] + public void MultipleRazorCommentsInSameLineInMarkup() + { + ParseDocumentTest( + "

" + Environment.NewLine + + "@**@ @**@" + Environment.NewLine + + "

", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("

")), + Factory.Markup(Environment.NewLine), + new CommentBlock( + Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition) + .Accepts(AcceptedCharacters.None), + Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar) + .Accepts(AcceptedCharacters.None), + Factory.Span(SpanKind.Comment, new HtmlSymbol( + Factory.LocationTracker.CurrentLocation, + string.Empty, + HtmlSymbolType.Unknown)) + .Accepts(AcceptedCharacters.Any), + Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar) + .Accepts(AcceptedCharacters.None), + Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition) + .Accepts(AcceptedCharacters.None)), + Factory.EmptyHtml(), + Factory.Markup(" ").With(SpanChunkGenerator.Null), + new CommentBlock( + Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition) + .Accepts(AcceptedCharacters.None), + Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar) + .Accepts(AcceptedCharacters.None), + Factory.Span(SpanKind.Comment, new HtmlSymbol( + Factory.LocationTracker.CurrentLocation, + string.Empty, + HtmlSymbolType.Unknown)) + .Accepts(AcceptedCharacters.Any), + Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar) + .Accepts(AcceptedCharacters.None), + Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition) + .Accepts(AcceptedCharacters.None)), + Factory.Markup(Environment.NewLine).With(SpanChunkGenerator.Null), + new MarkupTagBlock( + Factory.Markup("

")) + )); + } + + [Fact] + public void RazorCommentsSurroundingMarkup() + { + ParseDocumentTest( + "

" + Environment.NewLine + + "@* hello *@ content @* world *@" + Environment.NewLine + + "

", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("

")), + Factory.Markup(Environment.NewLine), + new CommentBlock( + Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition) + .Accepts(AcceptedCharacters.None), + Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar) + .Accepts(AcceptedCharacters.None), + Factory.Span(SpanKind.Comment, new HtmlSymbol( + Factory.LocationTracker.CurrentLocation, + " hello ", + HtmlSymbolType.RazorComment)) + .Accepts(AcceptedCharacters.Any), + Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar) + .Accepts(AcceptedCharacters.None), + Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition) + .Accepts(AcceptedCharacters.None)), + Factory.Markup(" content "), + new CommentBlock( + Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition) + .Accepts(AcceptedCharacters.None), + Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar) + .Accepts(AcceptedCharacters.None), + Factory.Span(SpanKind.Comment, new HtmlSymbol( + Factory.LocationTracker.CurrentLocation, + " world ", + HtmlSymbolType.RazorComment)) + .Accepts(AcceptedCharacters.Any), + Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar) + .Accepts(AcceptedCharacters.None), + Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition) + .Accepts(AcceptedCharacters.None)), + Factory.Markup(Environment.NewLine), + new MarkupTagBlock( + Factory.Markup("

")) + )); + } + + [Fact] + public void RazorCommentWithExtraNewLineInMarkup() + { + ParseDocumentTest( + "

" + Environment.NewLine + Environment.NewLine + + "@* content *@" + Environment.NewLine + + "@*" + Environment.NewLine + + "content" + Environment.NewLine + + "*@" + Environment.NewLine + Environment.NewLine + + "

", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("

")), + Factory.Markup(Environment.NewLine + Environment.NewLine), + new CommentBlock( + Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition) + .Accepts(AcceptedCharacters.None), + Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar) + .Accepts(AcceptedCharacters.None), + Factory.Span(SpanKind.Comment, new HtmlSymbol( + Factory.LocationTracker.CurrentLocation, + " content ", + HtmlSymbolType.RazorComment)) + .Accepts(AcceptedCharacters.Any), + Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar) + .Accepts(AcceptedCharacters.None), + Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition) + .Accepts(AcceptedCharacters.None)), + Factory.Markup(Environment.NewLine).With(SpanChunkGenerator.Null), + new CommentBlock( + Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition) + .Accepts(AcceptedCharacters.None), + Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar) + .Accepts(AcceptedCharacters.None), + Factory.Span(SpanKind.Comment, new HtmlSymbol( + Factory.LocationTracker.CurrentLocation, + Environment.NewLine + "content" + Environment.NewLine, + HtmlSymbolType.RazorComment)) + .Accepts(AcceptedCharacters.Any), + Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar) + .Accepts(AcceptedCharacters.None), + Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition) + .Accepts(AcceptedCharacters.None)), + Factory.Markup(Environment.NewLine).With(SpanChunkGenerator.Null), + Factory.Markup(Environment.NewLine), + new MarkupTagBlock( + Factory.Markup("

")) + )); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpReservedWordsTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpReservedWordsTest.cs new file mode 100644 index 0000000000..8815d18ebb --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpReservedWordsTest.cs @@ -0,0 +1,42 @@ +// 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.Legacy +{ + internal class CSharpReservedWordsTest : CsHtmlCodeParserTestBase + { + [Theory] + [InlineData("namespace")] + [InlineData("class")] + public void ReservedWords(string word) + { + ParseBlockTest(word, + new DirectiveBlock( + Factory.MetaCode(word).Accepts(AcceptedCharacters.None) + ), + new RazorError( + LegacyResources.FormatParseError_ReservedWord(word), + SourceLocation.Zero, + word.Length)); + } + + [Theory] + [InlineData("Namespace")] + [InlineData("Class")] + [InlineData("NAMESPACE")] + [InlineData("CLASS")] + [InlineData("nameSpace")] + [InlineData("NameSpace")] + private void ReservedWordsAreCaseSensitive(string word) + { + ParseBlockTest(word, + new ExpressionBlock( + Factory.Code(word) + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace) + )); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpSectionTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpSectionTest.cs new file mode 100644 index 0000000000..85ff3f178b --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpSectionTest.cs @@ -0,0 +1,588 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class CSharpSectionTest : CsHtmlMarkupParserTestBase + { + [Fact] + public void ParseSectionBlockCapturesNewlineImmediatelyFollowing() + { + ParseDocumentTest("@section" + Environment.NewLine, + new MarkupBlock( + Factory.EmptyHtml(), + new SectionBlock(new SectionChunkGenerator(string.Empty), + Factory.CodeTransition(), + Factory.MetaCode("section" + Environment.NewLine))), + new RazorError( + LegacyResources.FormatParseError_Unexpected_Character_At_Section_Name_Start( + LegacyResources.ErrorComponent_EndOfFile), + new SourceLocation(8 + Environment.NewLine.Length, 1, 0), + length: 1)); + } + + [Fact] + public void ParseSectionBlockCapturesWhitespaceToEndOfLineInSectionStatementMissingOpenBrace() + { + ParseDocumentTest("@section Foo " + Environment.NewLine + + " ", + new MarkupBlock( + Factory.EmptyHtml(), + new SectionBlock(new SectionChunkGenerator("Foo"), + Factory.CodeTransition(), + Factory.MetaCode("section Foo " + Environment.NewLine)), + Factory.Markup(" ")), + new RazorError( + LegacyResources.ParseError_MissingOpenBraceAfterSection, + new SourceLocation(12, 0, 12), + length: 1)); + } + + [Fact] + public void ParseSectionBlockCapturesWhitespaceToEndOfLineInSectionStatementMissingName() + { + ParseDocumentTest("@section " + Environment.NewLine + + " ", + new MarkupBlock( + Factory.EmptyHtml(), + new SectionBlock(new SectionChunkGenerator(string.Empty), + Factory.CodeTransition(), + Factory.MetaCode("section " + Environment.NewLine)), + Factory.Markup(" ")), + new RazorError( + LegacyResources.FormatParseError_Unexpected_Character_At_Section_Name_Start( + LegacyResources.ErrorComponent_EndOfFile), + new SourceLocation(21 + Environment.NewLine.Length, 1, 4), + length: 1)); + } + + [Fact] + public void ParseSectionBlockIgnoresSectionUnlessAllLowerCase() + { + ParseDocumentTest("@Section foo", + new MarkupBlock( + Factory.EmptyHtml(), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.Code("Section") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace)), + Factory.Markup(" foo"))); + } + + [Fact] + public void ParseSectionBlockReportsErrorAndTerminatesSectionBlockIfKeywordNotFollowedByIdentifierStartCharacter() + { + ParseDocumentTest("@section 9 {

Foo

}", + new MarkupBlock( + Factory.EmptyHtml(), + new SectionBlock(new SectionChunkGenerator(string.Empty), + Factory.CodeTransition(), + Factory.MetaCode("section ")), + Factory.Markup("9 { "), + new MarkupTagBlock( + Factory.Markup("

")), + Factory.Markup("Foo"), + new MarkupTagBlock( + Factory.Markup("

")), + Factory.Markup(" }")), + new RazorError( + LegacyResources.FormatParseError_Unexpected_Character_At_Section_Name_Start( + LegacyResources.FormatErrorComponent_Character("9")), + new SourceLocation(9, 0, 9), + length: 1)); + } + + [Fact] + public void ParseSectionBlockReportsErrorAndTerminatesSectionBlockIfNameNotFollowedByOpenBrace() + { + ParseDocumentTest("@section foo-bar {

Foo

}", + new MarkupBlock( + Factory.EmptyHtml(), + new SectionBlock(new SectionChunkGenerator("foo"), + Factory.CodeTransition(), + Factory.MetaCode("section foo")), + Factory.Markup("-bar { "), + new MarkupTagBlock( + Factory.Markup("

")), + Factory.Markup("Foo"), + new MarkupTagBlock( + Factory.Markup("

")), + Factory.Markup(" }")), + new RazorError( + LegacyResources.ParseError_MissingOpenBraceAfterSection, + new SourceLocation(12, 0, 12), + length: 1)); + } + + [Fact] + public void ParserOutputsErrorOnNestedSections() + { + ParseDocumentTest("@section foo { @section bar {

Foo

} }", + new MarkupBlock( + Factory.EmptyHtml(), + new SectionBlock(new SectionChunkGenerator("foo"), + Factory.CodeTransition(), + Factory.MetaCode("section foo {") + .AutoCompleteWith(null, atEndOfSpan: true), + new MarkupBlock( + Factory.Markup(" "), + new SectionBlock(new SectionChunkGenerator("bar"), + Factory.CodeTransition(), + Factory.MetaCode("section bar {") + .AutoCompleteWith(null, atEndOfSpan: true), + new MarkupBlock( + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("

")), + Factory.Markup("Foo"), + new MarkupTagBlock( + Factory.Markup("

")), + Factory.Markup(" ")), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + Factory.Markup(" ")), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + Factory.EmptyHtml()), + new RazorError( + LegacyResources.FormatParseError_Sections_Cannot_Be_Nested(LegacyResources.SectionExample_CS), + new SourceLocation(16, 0, 16), + 7)); + } + + [Fact] + public void ParseSectionBlockHandlesEOFAfterOpenBrace() + { + ParseDocumentTest("@section foo {", + new MarkupBlock( + Factory.EmptyHtml(), + new SectionBlock(new SectionChunkGenerator("foo"), + Factory.CodeTransition(), + Factory.MetaCode("section foo {") + .AutoCompleteWith("}", atEndOfSpan: true), + new MarkupBlock())), + new RazorError( + LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF("section", "}", "{"), + new SourceLocation(13, 0, 13), + length: 1)); + } + + [Theory] + [InlineData(" ")] + [InlineData("\n")] + [InlineData(" abc")] + [InlineData(" \n abc")] + public void ParseSectionBlockHandlesEOFAfterOpenContent(string postStartBrace) + { + ParseDocumentTest("@section foo {" + postStartBrace, + new MarkupBlock( + Factory.EmptyHtml(), + new SectionBlock(new SectionChunkGenerator("foo"), + Factory.CodeTransition(), + Factory.MetaCode("section foo {") + .AutoCompleteWith("}", atEndOfSpan: true), + new MarkupBlock( + Factory.Markup(postStartBrace)))), + new RazorError( + LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF("section", "}", "{"), + new SourceLocation(13, 0, 13), + length: 1)); + } + + [Fact] + public void ParseSectionBlockHandlesUnterminatedSection() + { + ParseDocumentTest("@section foo {

Foo{}

", + new MarkupBlock( + Factory.EmptyHtml(), + new SectionBlock(new SectionChunkGenerator("foo"), + Factory.CodeTransition(), + Factory.MetaCode("section foo {") + .AutoCompleteWith("}", atEndOfSpan: true), + new MarkupBlock( + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("

")), + // Need to provide the markup span as fragments, since the parser will split the {} into separate symbols. + Factory.Markup("Foo", "{", "}"), + new MarkupTagBlock( + Factory.Markup("

"))))), + new RazorError( + LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF("section", "}", "{"), + new SourceLocation(13, 0, 13), + length: 1)); + } + + [Fact] + public void ParseSectionBlockHandlesUnterminatedSectionWithNestedIf() + { + var newLine = Environment.NewLine; + var spaces = " "; + ParseDocumentTest( + string.Format( + "@section Test{0}{{{0}{1}@if(true){0}{1}{{{0}{1}{1}

Hello World

{0}{1}}}", + newLine, + spaces), + new MarkupBlock( + Factory.EmptyHtml(), + new SectionBlock(new SectionChunkGenerator("Test"), + Factory.CodeTransition(), + Factory.MetaCode($"section Test{newLine}{{") + .AutoCompleteWith("}", atEndOfSpan: true), + new MarkupBlock( + Factory.Markup(newLine), + new StatementBlock( + Factory.Code(spaces).AsStatement(), + Factory.CodeTransition(), + Factory.Code($"if(true){newLine}{spaces}{{{newLine}").AsStatement(), + new MarkupBlock( + Factory.Markup($"{spaces}{spaces}"), + BlockFactory.MarkupTagBlock("

", AcceptedCharacters.None), + Factory.Markup("Hello World"), + BlockFactory.MarkupTagBlock("

", AcceptedCharacters.None), + Factory.Markup(newLine).Accepts(AcceptedCharacters.None)), + Factory.Code($"{spaces}}}").AsStatement())))), + new RazorError( + LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF("section", "}", "{"), + new SourceLocation(13 + newLine.Length, 1, 0), + length: 1)); + } + + [Fact] + public void ParseSectionBlockReportsErrorAndAcceptsWhitespaceToEndOfLineIfSectionNotFollowedByOpenBrace() + { + ParseDocumentTest("@section foo " + Environment.NewLine, + new MarkupBlock( + Factory.EmptyHtml(), + new SectionBlock(new SectionChunkGenerator("foo"), + Factory.CodeTransition(), + Factory.MetaCode("section foo " + Environment.NewLine))), + new RazorError( + LegacyResources.ParseError_MissingOpenBraceAfterSection, + new SourceLocation(12, 0, 12), + length: 1)); + } + + [Fact] + public void ParseSectionBlockAcceptsOpenBraceMultipleLinesBelowSectionName() + { + ParseDocumentTest("@section foo " + Environment.NewLine + + Environment.NewLine + + Environment.NewLine + + Environment.NewLine + + Environment.NewLine + + Environment.NewLine + + "{" + Environment.NewLine + + "

Foo

" + Environment.NewLine + + "}", + new MarkupBlock( + Factory.EmptyHtml(), + new SectionBlock(new SectionChunkGenerator("foo"), + Factory.CodeTransition(), + Factory.MetaCode(string.Format("section foo {0}{0}{0}{0}{0}{0}{{", Environment.NewLine)) + .AutoCompleteWith(null, atEndOfSpan: true), + new MarkupBlock( + Factory.Markup(Environment.NewLine), + new MarkupTagBlock( + Factory.Markup("

")), + Factory.Markup("Foo"), + new MarkupTagBlock( + Factory.Markup("

")), + Factory.Markup(Environment.NewLine)), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + Factory.EmptyHtml())); + } + + [Fact] + public void ParseSectionBlockParsesNamedSectionCorrectly() + { + ParseDocumentTest("@section foo {

Foo

}", + new MarkupBlock( + Factory.EmptyHtml(), + new SectionBlock(new SectionChunkGenerator("foo"), + Factory.CodeTransition(), + Factory.MetaCode("section foo {") + .AutoCompleteWith(null, atEndOfSpan: true), + new MarkupBlock( + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("

")), + Factory.Markup("Foo"), + new MarkupTagBlock( + Factory.Markup("

")), + Factory.Markup(" ")), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + Factory.EmptyHtml())); + } + + [Fact] + public void ParseSectionBlockDoesNotRequireSpaceBetweenSectionNameAndOpenBrace() + { + ParseDocumentTest("@section foo{

Foo

}", + new MarkupBlock( + Factory.EmptyHtml(), + new SectionBlock(new SectionChunkGenerator("foo"), + Factory.CodeTransition(), + Factory.MetaCode("section foo{") + .AutoCompleteWith(null, atEndOfSpan: true), + new MarkupBlock( + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("

")), + Factory.Markup("Foo"), + new MarkupTagBlock( + Factory.Markup("

")), + Factory.Markup(" ")), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + Factory.EmptyHtml())); + } + + [Fact] + public void ParseSectionBlockBalancesBraces() + { + ParseDocumentTest("@section foo { }", + new MarkupBlock( + Factory.EmptyHtml(), + new SectionBlock(new SectionChunkGenerator("foo"), + Factory.CodeTransition(), + Factory.MetaCode("section foo {") + .AutoCompleteWith(null, atEndOfSpan: true), + new MarkupBlock( + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("")), + Factory.Markup(" ")), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + Factory.EmptyHtml())); + } + + [Fact] + public void ParseSectionBlockAllowsBracesInCSharpExpression() + { + ParseDocumentTest("@section foo { I really want to render a close brace, so here I go: @(\"}\") }", + new MarkupBlock( + Factory.EmptyHtml(), + new SectionBlock(new SectionChunkGenerator("foo"), + Factory.CodeTransition(), + Factory.MetaCode("section foo {") + .AutoCompleteWith(null, atEndOfSpan: true), + new MarkupBlock( + Factory.Markup(" I really want to render a close brace, so here I go: "), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.MetaCode("(").Accepts(AcceptedCharacters.None), + Factory.Code("\"}\"").AsExpression(), + Factory.MetaCode(")").Accepts(AcceptedCharacters.None)), + Factory.Markup(" ")), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + Factory.EmptyHtml())); + } + + [Fact] + public void SectionIsCorrectlyTerminatedWhenCloseBraceImmediatelyFollowsCodeBlock() + { + ParseDocumentTest("@section Foo {" + Environment.NewLine + + "@if(true) {" + Environment.NewLine + + "}" + Environment.NewLine + + "}", + new MarkupBlock( + Factory.EmptyHtml(), + new SectionBlock(new SectionChunkGenerator("Foo"), + Factory.CodeTransition(), + Factory.MetaCode("section Foo {") + .AutoCompleteWith(null, atEndOfSpan: true), + new MarkupBlock( + Factory.Markup(Environment.NewLine), + new StatementBlock( + Factory.CodeTransition(), + Factory.Code($"if(true) {{{Environment.NewLine}}}{Environment.NewLine}").AsStatement() + )), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + Factory.EmptyHtml())); + } + + [Fact] + public void SectionIsCorrectlyTerminatedWhenCloseBraceImmediatelyFollowsCodeBlockNoWhitespace() + { + ParseDocumentTest("@section Foo {" + Environment.NewLine + + "@if(true) {" + Environment.NewLine + + "}}", + new MarkupBlock( + Factory.EmptyHtml(), + new SectionBlock(new SectionChunkGenerator("Foo"), + Factory.CodeTransition(), + Factory.MetaCode("section Foo {") + .AutoCompleteWith(null, atEndOfSpan: true), + new MarkupBlock( + Factory.Markup(Environment.NewLine), + new StatementBlock( + Factory.CodeTransition(), + Factory.Code($"if(true) {{{Environment.NewLine}}}").AsStatement() + )), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + Factory.EmptyHtml())); + } + + [Fact] + public void ParseSectionBlockCorrectlyTerminatesWhenCloseBraceImmediatelyFollowsMarkup() + { + ParseDocumentTest("@section foo {something}", + new MarkupBlock( + Factory.EmptyHtml(), + new SectionBlock(new SectionChunkGenerator("foo"), + Factory.CodeTransition(), + Factory.MetaCode("section foo {") + .AutoCompleteWith(null, atEndOfSpan: true), + new MarkupBlock( + Factory.Markup("something")), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + Factory.EmptyHtml())); + } + + [Fact] + public void ParseSectionBlockParsesComment() + { + ParseDocumentTest("@section s {}", + new MarkupBlock( + Factory.EmptyHtml(), + new SectionBlock(new SectionChunkGenerator("s"), + Factory.CodeTransition(), + Factory.MetaCode("section s {") + .AutoCompleteWith(null, atEndOfSpan: true), + new MarkupBlock( + Factory.Markup("")), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + Factory.EmptyHtml())); + } + + // This was a user reported bug (codeplex #710), the section parser wasn't handling + // comments. + [Fact] + public void ParseSectionBlockParsesCommentWithDelimiters() + { + ParseDocumentTest("@section s {}", + new MarkupBlock( + Factory.EmptyHtml(), + new SectionBlock(new SectionChunkGenerator("s"), + Factory.CodeTransition(), + Factory.MetaCode("section s {") + .AutoCompleteWith(null, atEndOfSpan: true), + new MarkupBlock( + Factory.Markup("")), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + Factory.EmptyHtml())); + } + + [Fact] + public void ParseSectionBlockCommentRecoversFromUnclosedTag() + { + ParseDocumentTest( + "@section s {" + Environment.NewLine + " \" '-->}", + new MarkupBlock( + Factory.EmptyHtml(), + new SectionBlock(new SectionChunkGenerator("s"), + Factory.CodeTransition(), + Factory.MetaCode("section s {") + .AutoCompleteWith(null, atEndOfSpan: true), + new MarkupBlock( + Factory.Markup(Environment.NewLine), + new MarkupTagBlock( + Factory.Markup(" \" '-->")), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + Factory.EmptyHtml())); + } + + [Fact] + public void ParseSectionBlockParsesXmlProcessingInstruction() + { + ParseDocumentTest( + "@section s { }", + new MarkupBlock( + Factory.EmptyHtml(), + new SectionBlock(new SectionChunkGenerator("s"), + Factory.CodeTransition(), + Factory.MetaCode("section s {") + .AutoCompleteWith(null, atEndOfSpan: true), + new MarkupBlock( + Factory.Markup(" ")), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + Factory.EmptyHtml())); + } + + public static TheoryData SectionWithEscapedTransitionData + { + get + { + var factory = new SpanFactory(); + + return new TheoryData + { + { + "@section s {}", + new MarkupBlock( + factory.EmptyHtml(), + new SectionBlock(new SectionChunkGenerator("s"), + factory.CodeTransition(), + factory.MetaCode("section s {") + .AutoCompleteWith(null, atEndOfSpan: true), + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo='", 17, 0, 17), new LocationTagged("'", 25, 0, 25)), + factory.Markup(" foo='").With(SpanChunkGenerator.Null), + new MarkupBlock( + factory.Markup("@").With(new LiteralAttributeChunkGenerator(new LocationTagged(string.Empty, 23, 0, 23), new LocationTagged("@", 23, 0, 23))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanChunkGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup("'").With(SpanChunkGenerator.Null)), + factory.Markup(" />"))), + factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + factory.EmptyHtml()) + }, + { + "@section s {}", + new MarkupBlock( + factory.EmptyHtml(), + new SectionBlock(new SectionChunkGenerator("s"), + factory.CodeTransition(), + factory.MetaCode("section s {") + .AutoCompleteWith(null, atEndOfSpan: true), + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo='", 17, 0, 17), new LocationTagged("'", 39, 0, 39)), + factory.Markup(" foo='").With(SpanChunkGenerator.Null), + new MarkupBlock( + new DynamicAttributeBlockChunkGenerator(new LocationTagged(string.Empty, 23, 0, 23), 23, 0, 23), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("DateTime.Now") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace))), + new MarkupBlock( + factory.Markup(" @").With(new LiteralAttributeChunkGenerator(new LocationTagged(" ", 36, 0, 36), new LocationTagged("@", 37, 0, 37))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanChunkGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup("'").With(SpanChunkGenerator.Null)), + factory.Markup(" />"))), + factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + factory.EmptyHtml()) + }, + }; + } + } + + [Theory] + [MemberData(nameof(SectionWithEscapedTransitionData))] + public void ParseSectionBlock_WithDoubleTransition_DoesNotThrow(string input, Block expected) + { + ParseDocumentTest(input, expected); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpSpecialBlockTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpSpecialBlockTest.cs new file mode 100644 index 0000000000..859f7de659 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpSpecialBlockTest.cs @@ -0,0 +1,217 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class CSharpSpecialBlockTest : CsHtmlCodeParserTestBase + { + [Fact] + public void ParseInheritsStatementMarksInheritsSpanAsCanGrowIfMissingTrailingSpace() + { + ParseBlockTest("inherits", + new DirectiveBlock( + Factory.MetaCode("inherits").Accepts(AcceptedCharacters.Any) + ), + new RazorError( + LegacyResources.ParseError_InheritsKeyword_Must_Be_Followed_By_TypeName, + new SourceLocation(0, 0, 0), 8)); + } + + [Fact] + public void InheritsBlockAcceptsMultipleGenericArguments() + { + ParseBlockTest("inherits Foo.Bar, string, int>.Baz", + new DirectiveBlock( + Factory.MetaCode("inherits ").Accepts(AcceptedCharacters.None), + Factory.Code("Foo.Bar, string, int>.Baz") + .AsBaseType("Foo.Bar, string, int>.Baz") + )); + } + + [Fact] + public void InheritsBlockOutputsErrorIfInheritsNotFollowedByTypeButAcceptsEntireLineAsCode() + { + ParseBlockTest("inherits " + Environment.NewLine + + "foo", + new DirectiveBlock( + Factory.MetaCode("inherits ").Accepts(AcceptedCharacters.None), + Factory.Code(" " + Environment.NewLine) + .AsBaseType(string.Empty) + ), + new RazorError(LegacyResources.ParseError_InheritsKeyword_Must_Be_Followed_By_TypeName, 0, 0, 0, 8)); + } + + [Fact] + public void NamespaceImportInsideCodeBlockCausesError() + { + ParseBlockTest("{ using Foo.Bar.Baz; var foo = bar; }", + new StatementBlock( + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.Code(" using Foo.Bar.Baz; var foo = bar; ") + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None) + ), + new RazorError( + LegacyResources.ParseError_NamespaceImportAndTypeAlias_Cannot_Exist_Within_CodeBlock, + new SourceLocation(2, 0, 2), + length: 5)); + } + + [Fact] + public void TypeAliasInsideCodeBlockIsNotHandledSpecially() + { + ParseBlockTest("{ using Foo = Bar.Baz; var foo = bar; }", + new StatementBlock( + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.Code(" using Foo = Bar.Baz; var foo = bar; ") + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None) + ), + new RazorError( + LegacyResources.ParseError_NamespaceImportAndTypeAlias_Cannot_Exist_Within_CodeBlock, + new SourceLocation(2, 0, 2), + length: 5)); + } + + [Fact] + public void Plan9FunctionsKeywordInsideCodeBlockIsNotHandledSpecially() + { + ParseBlockTest("{ functions Foo; }", + new StatementBlock( + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.Code(" functions Foo; ") + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None) + )); + } + + [Fact] + public void NonKeywordStatementInCodeBlockIsHandledCorrectly() + { + ParseBlockTest("{" + Environment.NewLine + + " List photos = gallery.Photo.ToList();" + Environment.NewLine + + "}", + new StatementBlock( + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.Code($"{Environment.NewLine} List photos = gallery.Photo.ToList();{Environment.NewLine}") + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None) + )); + } + + [Fact] + public void ParseBlockBalancesBracesOutsideStringsIfFirstCharacterIsBraceAndReturnsSpanOfTypeCode() + { + // Arrange + const string code = "foo\"b}ar\" if(condition) { string.Format(\"{0}\"); } "; + + // Act/Assert + ParseBlockTest("{" + code + "}", + new StatementBlock( + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.Code(code) + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None) + )); + } + + [Fact] + public void ParseBlockBalancesParensOutsideStringsIfFirstCharacterIsParenAndReturnsSpanOfTypeExpression() + { + // Arrange + const string code = "foo\"b)ar\" if(condition) { string.Format(\"{0}\"); } "; + + // Act/Assert + ParseBlockTest("(" + code + ")", + new ExpressionBlock( + Factory.MetaCode("(").Accepts(AcceptedCharacters.None), + Factory.Code(code).AsExpression(), + Factory.MetaCode(")").Accepts(AcceptedCharacters.None) + )); + } + + [Fact] + public void ParseBlockBalancesBracesAndOutputsContentAsClassLevelCodeSpanIfFirstIdentifierIsFunctionsKeyword() + { + const string code = " foo(); \"bar}baz\" "; + ParseBlockTest("functions {" + code + "} zoop", + new FunctionsBlock( + Factory.MetaCode("functions {").Accepts(AcceptedCharacters.None), + Factory.Code(code) + .AsFunctionsBody() + .AutoCompleteWith(autoCompleteString: null), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None) + )); + } + + [Fact] + public void ParseBlockDoesNoErrorRecoveryForFunctionsBlock() + { + ParseBlockTest("functions { { { { { } zoop", + new FunctionsBlock( + Factory.MetaCode("functions {").Accepts(AcceptedCharacters.None), + Factory.Code(" { { { { } zoop") + .AsFunctionsBody() + .AutoCompleteWith("}") + ), + new RazorError( + LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF("functions", "}", "{"), + new SourceLocation(10, 0, 10), + length: 1)); + } + + [Fact] + public void ParseBlockIgnoresFunctionsUnlessAllLowerCase() + { + ParseBlockTest("Functions { foo() }", + new ExpressionBlock( + Factory.Code("Functions") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace))); + } + + [Fact] + public void ParseBlockIgnoresSingleSlashAtStart() + { + ParseBlockTest("@/ foo", + new ExpressionBlock( + Factory.CodeTransition(), + Factory.EmptyCSharp() + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace)), + new RazorError( + LegacyResources.FormatParseError_Unexpected_Character_At_Start_Of_CodeBlock_CS("/"), + new SourceLocation(1, 0, 1), + length: 1)); + } + + [Fact] + public void ParseBlockTerminatesSingleLineCommentAtEndOfLine() + { + ParseBlockTest("if(!false) {" + Environment.NewLine + + " // Foo" + Environment.NewLine + + "\t

A real tag!

" + Environment.NewLine + + "}", + new StatementBlock( + Factory.Code($"if(!false) {{{Environment.NewLine} // Foo{Environment.NewLine}").AsStatement(), + new MarkupBlock( + Factory.Markup("\t"), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup("A real tag!"), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup(Environment.NewLine).Accepts(AcceptedCharacters.None)), + Factory.Code("}").AsStatement() + )); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpStatementTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpStatementTest.cs new file mode 100644 index 0000000000..a6726ce429 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpStatementTest.cs @@ -0,0 +1,418 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + // Basic Tests for C# Statements: + // * Basic case for each statement + // * Basic case for ALL clauses + + // This class DOES NOT contain + // * Error cases + // * Tests for various types of nested statements + // * Comment tests + + internal class CSharpStatementTest : CsHtmlCodeParserTestBase + { + [Fact] + public void ForStatement() + { + ParseBlockTest("@for(int i = 0; i++; i < length) { foo(); }", + new StatementBlock( + Factory.CodeTransition(), + Factory.Code("for(int i = 0; i++; i < length) { foo(); }") + .AsStatement() + .Accepts(AcceptedCharacters.None) + )); + } + + [Fact] + public void ForEachStatement() + { + ParseBlockTest("@foreach(var foo in bar) { foo(); }", + new StatementBlock( + Factory.CodeTransition(), + Factory.Code("foreach(var foo in bar) { foo(); }") + .AsStatement() + .Accepts(AcceptedCharacters.None) + )); + } + + [Fact] + public void WhileStatement() + { + ParseBlockTest("@while(true) { foo(); }", + new StatementBlock( + Factory.CodeTransition(), + Factory.Code("while(true) { foo(); }") + .AsStatement() + .Accepts(AcceptedCharacters.None) + )); + } + + [Fact] + public void SwitchStatement() + { + ParseBlockTest("@switch(foo) { foo(); }", + new StatementBlock( + Factory.CodeTransition(), + Factory.Code("switch(foo) { foo(); }") + .AsStatement() + .Accepts(AcceptedCharacters.None) + )); + } + + [Fact] + public void LockStatement() + { + ParseBlockTest("@lock(baz) { foo(); }", + new StatementBlock( + Factory.CodeTransition(), + Factory.Code("lock(baz) { foo(); }") + .AsStatement() + .Accepts(AcceptedCharacters.None) + )); + } + + [Fact] + public void IfStatement() + { + ParseBlockTest("@if(true) { foo(); }", + new StatementBlock( + Factory.CodeTransition(), + Factory.Code("if(true) { foo(); }") + .AsStatement() + )); + } + + [Fact] + public void ElseIfClause() + { + ParseBlockTest("@if(true) { foo(); } else if(false) { foo(); } else if(!false) { foo(); }", + new StatementBlock( + Factory.CodeTransition(), + Factory.Code("if(true) { foo(); } else if(false) { foo(); } else if(!false) { foo(); }") + .AsStatement() + )); + } + + [Fact] + public void ElseClause() + { + ParseBlockTest("@if(true) { foo(); } else { foo(); }", + new StatementBlock( + Factory.CodeTransition(), + Factory.Code("if(true) { foo(); } else { foo(); }") + .AsStatement() + .Accepts(AcceptedCharacters.None) + )); + } + + [Fact] + public void TryStatement() + { + ParseBlockTest("@try { foo(); }", + new StatementBlock( + Factory.CodeTransition(), + Factory.Code("try { foo(); }") + .AsStatement() + )); + } + + [Fact] + public void CatchClause() + { + ParseBlockTest("@try { foo(); } catch(IOException ioex) { handleIO(); } catch(Exception ex) { handleOther(); }", + new StatementBlock( + Factory.CodeTransition(), + Factory.Code("try { foo(); } catch(IOException ioex) { handleIO(); } catch(Exception ex) { handleOther(); }") + .AsStatement() + )); + } + + public static TheoryData ExceptionFilterData + { + get + { + var factory = new SpanFactory(); + + // document, expectedStatement + return new TheoryData + { + { + "@try { someMethod(); } catch(Exception) when (true) { handleIO(); }", + new StatementBlock( + factory.CodeTransition(), + factory + .Code("try { someMethod(); } catch(Exception) when (true) { handleIO(); }") + .AsStatement()) + }, + { + "@try { A(); } catch(Exception) when (true) { B(); } finally { C(); }", + new StatementBlock( + factory.CodeTransition(), + factory + .Code("try { A(); } catch(Exception) when (true) { B(); } finally { C(); }") + .AsStatement() + .Accepts(AcceptedCharacters.None)) + }, + { + "@try { A(); } catch(Exception) when (true) { B(); } catch(IOException) when (false) { C(); }", + new StatementBlock( + factory.CodeTransition(), + factory + .Code("try { A(); } catch(Exception) when (true) { B(); } catch(IOException) " + + "when (false) { C(); }") + .AsStatement()) + }, + { + string.Format("@try{0}{{{0} A();{0}}}{0}catch(Exception) when (true)", Environment.NewLine) + + string.Format("{0}{{{0} B();{0}}}{0}catch(IOException) when (false)", Environment.NewLine) + + string.Format("{0}{{{0} C();{0}}}", Environment.NewLine), + new StatementBlock( + factory.CodeTransition(), + factory + .Code( + string.Format("try{0}{{{0} A();{0}}}{0}catch(Exception) ", Environment.NewLine) + + string.Format("when (true){0}{{{0} B();{0}}}{0}", Environment.NewLine) + + string.Format("catch(IOException) when (false){0}{{{0} ", Environment.NewLine) + + string.Format("C();{0}}}", Environment.NewLine)) + .AsStatement()) + }, + + // Wrapped in @{ block. + { + "@{try { someMethod(); } catch(Exception) when (true) { handleIO(); }}", + new StatementBlock( + factory.CodeTransition(), + factory.MetaCode("{").Accepts(AcceptedCharacters.None), + factory + .Code("try { someMethod(); } catch(Exception) when (true) { handleIO(); }") + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + factory.MetaCode("}").Accepts(AcceptedCharacters.None)) + }, + + // Partial exception filter data + { + "@try { someMethod(); } catch(Exception) when", + new StatementBlock( + factory.CodeTransition(), + factory + .Code("try { someMethod(); } catch(Exception) when") + .AsStatement()) + }, + { + "@try { someMethod(); } when", + new StatementBlock( + factory.CodeTransition(), + factory + .Code("try { someMethod(); }") + .AsStatement()) + }, + { + "@try { someMethod(); } catch(Exception) when { anotherMethod(); }", + new StatementBlock( + factory.CodeTransition(), + factory + .Code("try { someMethod(); } catch(Exception) when { anotherMethod(); }") + .AsStatement()) + }, + { + "@try { someMethod(); } catch(Exception) when (true)", + new StatementBlock( + factory.CodeTransition(), + factory + .Code("try { someMethod(); } catch(Exception) when (true)") + .AsStatement()) + }, + }; + } + } + + [Theory] + [MemberData(nameof(ExceptionFilterData))] + public void ExceptionFilters(string document, StatementBlock expectedStatement) + { + // Act & Assert + ParseBlockTest(document, expectedStatement); + } + + public static TheoryData ExceptionFilterErrorData + { + get + { + var factory = new SpanFactory(); + var unbalancedParenErrorString = "An opening \"(\" is missing the corresponding closing \")\"."; + var unbalancedBracketCatchErrorString = "The catch block is missing a closing \"}\" character. " + + "Make sure you have a matching \"}\" character for all the \"{\" characters within this block, " + + "and that none of the \"}\" characters are being interpreted as markup."; + + // document, expectedStatement, expectedErrors + return new TheoryData + { + { + "@try { someMethod(); } catch(Exception) when (", + new StatementBlock( + factory.CodeTransition(), + factory + .Code("try { someMethod(); } catch(Exception) when (") + .AsStatement()), + new[] { new RazorError(unbalancedParenErrorString, 45, 0, 45, 1) } + }, + { + "@try { someMethod(); } catch(Exception) when (someMethod(", + new StatementBlock( + factory.CodeTransition(), + factory + .Code("try { someMethod(); } catch(Exception) when (someMethod(") + .AsStatement()), + new[] { new RazorError(unbalancedParenErrorString, 45, 0, 45, 1) } + }, + { + "@try { someMethod(); } catch(Exception) when (true) {", + new StatementBlock( + factory.CodeTransition(), + factory + .Code("try { someMethod(); } catch(Exception) when (true) {") + .AsStatement()), + new[] { new RazorError(unbalancedBracketCatchErrorString, 23, 0, 23, 1) } + }, + }; + } + } + + [Theory] + [MemberData(nameof(ExceptionFilterErrorData))] + public void ExceptionFilterErrors( + string document, + StatementBlock expectedStatement, + RazorError[] expectedErrors) + { + // Act & Assert + ParseBlockTest(document, expectedStatement, expectedErrors); + } + + [Fact] + public void FinallyClause() + { + ParseBlockTest("@try { foo(); } finally { Dispose(); }", + new StatementBlock( + Factory.CodeTransition(), + Factory.Code("try { foo(); } finally { Dispose(); }") + .AsStatement() + .Accepts(AcceptedCharacters.None) + )); + } + + public static TheoryData StaticUsingData + { + get + { + var factory = new SpanFactory(); + Func createUsing = (code, import) => + new DirectiveBlock( + factory.CodeTransition(), + factory.Code(code) + .AsNamespaceImport(import) + .Accepts(AcceptedCharacters.AnyExceptNewline)); + + // document, expectedResult + return new TheoryData + { + { "@using static", createUsing("using static", " static") }, + { "@using static ", createUsing("using static ", " static ") }, + { "@using static ", createUsing("using static ", " static ") }, + { "@using static System", createUsing("using static System", " static System") }, + { + "@using static System", + createUsing("using static System", " static System") + }, + { + "@using static System.Console", + createUsing("using static System.Console", " static System.Console") + }, + { + "@using static global::System.Console", + createUsing("using static global::System.Console", " static global::System.Console") + }, + { + "@using static global::System.Console ", + createUsing("using static global::System.Console", " static global::System.Console") + }, + }; + } + } + + [Theory] + [MemberData(nameof(StaticUsingData))] + public void StaticUsingImport(string document, DirectiveBlock expectedResult) + { + // Act & Assert + ParseBlockTest(document, expectedResult); + } + + [Fact] + public void UsingStatement() + { + ParseBlockTest("@using(var foo = new Foo()) { foo.Bar(); }", + new StatementBlock( + Factory.CodeTransition(), + Factory.Code("using(var foo = new Foo()) { foo.Bar(); }") + .AsStatement() + .Accepts(AcceptedCharacters.None) + )); + } + + [Fact] + public void UsingTypeAlias() + { + ParseBlockTest("@using StringDictionary = System.Collections.Generic.Dictionary", + new DirectiveBlock( + Factory.CodeTransition(), + Factory.Code("using StringDictionary = System.Collections.Generic.Dictionary") + .AsNamespaceImport(" StringDictionary = System.Collections.Generic.Dictionary") + .Accepts(AcceptedCharacters.AnyExceptNewline) + )); + } + + [Fact] + public void UsingNamespaceImport() + { + ParseBlockTest("@using System.Text.Encoding.ASCIIEncoding", + new DirectiveBlock( + Factory.CodeTransition(), + Factory.Code("using System.Text.Encoding.ASCIIEncoding") + .AsNamespaceImport(" System.Text.Encoding.ASCIIEncoding") + .Accepts(AcceptedCharacters.AnyExceptNewline) + )); + } + + [Fact] + public void DoStatement() + { + ParseBlockTest("@do { foo(); } while(true);", + new StatementBlock( + Factory.CodeTransition(), + Factory.Code("do { foo(); } while(true);") + .AsStatement() + .Accepts(AcceptedCharacters.None) + )); + } + + [Fact] + public void NonBlockKeywordTreatedAsImplicitExpression() + { + ParseBlockTest("@is foo", + new ExpressionBlock(new ExpressionChunkGenerator(), + Factory.CodeTransition(), + Factory.Code("is") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace) + )); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpTemplateTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpTemplateTest.cs new file mode 100644 index 0000000000..3cce547916 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpTemplateTest.cs @@ -0,0 +1,321 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class CSharpTemplateTest : CsHtmlCodeParserTestBase + { + private const string TestTemplateCode = " @

Foo #@item

"; + + private TemplateBlock TestTemplate() + { + return new TemplateBlock( + new MarkupBlock( + Factory.MarkupTransition(), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup("Foo #"), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.Code("item") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace) + ), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)) + ) + ); + } + + private const string TestNestedTemplateCode = " @

Foo #@Html.Repeat(10, @

@item

)

"; + + private TemplateBlock TestNestedTemplate() + { + return new TemplateBlock( + new MarkupBlock( + Factory.MarkupTransition(), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup("Foo #"), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.Code("Html.Repeat(10, ") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords), + new TemplateBlock( + new MarkupBlock( + Factory.MarkupTransition(), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.EmptyHtml(), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.Code("item") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace) + ), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)) + ) + ), + Factory.Code(")") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace) + ), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)) + ) + ); + } + + [Fact] + public void ParseBlockHandlesSingleLineTemplate() + { + ParseBlockTest("{ var foo = @: bar" + Environment.NewLine + + "; }", + new StatementBlock( + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.Code(" var foo = ") + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + new TemplateBlock( + new MarkupBlock( + Factory.MarkupTransition(), + Factory.MetaMarkup(":", HtmlSymbolType.Colon), + Factory.Markup(" bar" + Environment.NewLine) + .With(new SpanEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString)) + .Accepts(AcceptedCharacters.None) + ) + ), + Factory.Code("; ").AsStatement(), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None) + )); + } + + [Fact] + public void ParseBlockHandlesSingleLineImmediatelyFollowingStatementChar() + { + ParseBlockTest("{i@: bar" + Environment.NewLine + + "}", + new StatementBlock( + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.Code("i") + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + new TemplateBlock( + new MarkupBlock( + Factory.MarkupTransition(), + Factory.MetaMarkup(":", HtmlSymbolType.Colon), + Factory.Markup(" bar" + Environment.NewLine) + .With(new SpanEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString)) + .Accepts(AcceptedCharacters.None) + ) + ), + Factory.EmptyCSharp().AsStatement(), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None) + )); + } + + [Fact] + public void ParseBlockHandlesSimpleTemplateInExplicitExpressionParens() + { + ParseBlockTest("(Html.Repeat(10," + TestTemplateCode + "))", + new ExpressionBlock( + Factory.MetaCode("(").Accepts(AcceptedCharacters.None), + Factory.Code("Html.Repeat(10, ").AsExpression(), + TestTemplate(), + Factory.Code(")").AsExpression(), + Factory.MetaCode(")").Accepts(AcceptedCharacters.None) + )); + } + + [Fact] + public void ParseBlockHandlesSimpleTemplateInImplicitExpressionParens() + { + ParseBlockTest("Html.Repeat(10," + TestTemplateCode + ")", + new ExpressionBlock( + Factory.Code("Html.Repeat(10, ") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords), + TestTemplate(), + Factory.Code(")") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace) + )); + } + + [Fact] + public void ParseBlockHandlesTwoTemplatesInImplicitExpressionParens() + { + ParseBlockTest("Html.Repeat(10," + TestTemplateCode + "," + TestTemplateCode + ")", + new ExpressionBlock( + Factory.Code("Html.Repeat(10, ") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords), + TestTemplate(), + Factory.Code(", ") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords), + TestTemplate(), + Factory.Code(")") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace) + )); + } + + [Fact] + public void ParseBlockProducesErrorButCorrectlyParsesNestedTemplateInImplicitExpressionParens() + { + ParseBlockTest("Html.Repeat(10," + TestNestedTemplateCode + ")", + new ExpressionBlock( + Factory.Code("Html.Repeat(10, ") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords), + TestNestedTemplate(), + Factory.Code(")") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace) + ), + GetNestedTemplateError(42)); + } + + [Fact] + public void ParseBlockHandlesSimpleTemplateInStatementWithinCodeBlock() + { + ParseBlockTest("foreach(foo in Bar) { Html.ExecuteTemplate(foo," + TestTemplateCode + "); }", + new StatementBlock( + Factory.Code("foreach(foo in Bar) { Html.ExecuteTemplate(foo, ").AsStatement(), + TestTemplate(), + Factory.Code("); }") + .AsStatement() + .Accepts(AcceptedCharacters.None) + )); + } + + [Fact] + public void ParseBlockHandlesTwoTemplatesInStatementWithinCodeBlock() + { + ParseBlockTest("foreach(foo in Bar) { Html.ExecuteTemplate(foo," + TestTemplateCode + "," + TestTemplateCode + "); }", + new StatementBlock( + Factory.Code("foreach(foo in Bar) { Html.ExecuteTemplate(foo, ").AsStatement(), + TestTemplate(), + Factory.Code(", ").AsStatement(), + TestTemplate(), + Factory.Code("); }") + .AsStatement() + .Accepts(AcceptedCharacters.None) + )); + } + + [Fact] + public void ParseBlockProducesErrorButCorrectlyParsesNestedTemplateInStatementWithinCodeBlock() + { + ParseBlockTest("foreach(foo in Bar) { Html.ExecuteTemplate(foo," + TestNestedTemplateCode + "); }", + new StatementBlock( + Factory.Code("foreach(foo in Bar) { Html.ExecuteTemplate(foo, ") + .AsStatement(), + TestNestedTemplate(), + Factory.Code("); }") + .AsStatement() + .Accepts(AcceptedCharacters.None) + ), + GetNestedTemplateError(74)); + } + + [Fact] + public void ParseBlockHandlesSimpleTemplateInStatementWithinStatementBlock() + { + ParseBlockTest("{ var foo = bar; Html.ExecuteTemplate(foo," + TestTemplateCode + "); }", + new StatementBlock( + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.Code(" var foo = bar; Html.ExecuteTemplate(foo, ") + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + TestTemplate(), + Factory.Code("); ").AsStatement(), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None) + )); + } + + [Fact] + public void ParseBlockHandlessTwoTemplatesInStatementWithinStatementBlock() + { + ParseBlockTest("{ var foo = bar; Html.ExecuteTemplate(foo," + TestTemplateCode + "," + TestTemplateCode + "); }", + new StatementBlock( + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.Code(" var foo = bar; Html.ExecuteTemplate(foo, ") + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + TestTemplate(), + Factory.Code(", ").AsStatement(), + TestTemplate(), + Factory.Code("); ").AsStatement(), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None) + )); + } + + [Fact] + public void ParseBlockProducesErrorButCorrectlyParsesNestedTemplateInStatementWithinStatementBlock() + { + ParseBlockTest("{ var foo = bar; Html.ExecuteTemplate(foo," + TestNestedTemplateCode + "); }", + new StatementBlock( + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.Code(" var foo = bar; Html.ExecuteTemplate(foo, ") + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + TestNestedTemplate(), + Factory.Code("); ").AsStatement(), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None) + ), + GetNestedTemplateError(69)); + } + + [Fact] + public void ParseBlock_WithDoubleTransition_DoesNotThrow() + { + // Arrange + var testTemplateWithDoubleTransitionCode = " @

Foo #@item

"; + var testTemplateWithDoubleTransition = new TemplateBlock( + new MarkupBlock( + Factory.MarkupTransition(), + new MarkupTagBlock( + Factory.Markup("(" foo='", 46, 0, 46), new LocationTagged("'", 54, 0, 54)), + Factory.Markup(" foo='").With(SpanChunkGenerator.Null), + new MarkupBlock( + Factory.Markup("@").With(new LiteralAttributeChunkGenerator(new LocationTagged(string.Empty, 52, 0, 52), new LocationTagged("@", 52, 0, 52))).Accepts(AcceptedCharacters.None), + Factory.Markup("@").With(SpanChunkGenerator.Null).Accepts(AcceptedCharacters.None)), + Factory.Markup("'").With(SpanChunkGenerator.Null)), + Factory.Markup(">").Accepts(AcceptedCharacters.None)), + Factory.Markup("Foo #"), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.Code("item") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace) + ), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)) + ) + ); + + var expected = new StatementBlock( + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.Code(" var foo = bar; Html.ExecuteTemplate(foo, ") + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + testTemplateWithDoubleTransition, + Factory.Code("); ").AsStatement(), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None)); + + // Act & Assert + ParseBlockTest("{ var foo = bar; Html.ExecuteTemplate(foo," + testTemplateWithDoubleTransitionCode + "); }", expected); + } + + private static RazorError GetNestedTemplateError(int characterIndex) + { + return new RazorError( + LegacyResources.ParseError_InlineMarkup_Blocks_Cannot_Be_Nested, + new SourceLocation(characterIndex, 0, characterIndex), + length: 1); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpToMarkupSwitchTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpToMarkupSwitchTest.cs new file mode 100644 index 0000000000..afdd73662e --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpToMarkupSwitchTest.cs @@ -0,0 +1,693 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class CSharpToMarkupSwitchTest : CsHtmlCodeParserTestBase + { + [Fact] + public void SingleAngleBracketDoesNotCauseSwitchIfOuterBlockIsTerminated() + { + ParseBlockTest("{ List< }", + new StatementBlock( + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.Code(" List< ") + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None))); + } + + [Fact] + public void ParseBlockGivesSpacesToCodeOnAtTagTemplateTransitionInDesignTimeMode() + { + ParseBlockTest("Foo( @

Foo

)", + new ExpressionBlock( + Factory.Code("Foo( ") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.Any), + new TemplateBlock( + new MarkupBlock( + Factory.MarkupTransition(), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup("Foo"), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)) + ) + ), + Factory.Code(" )") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace) + ), designTime: true); + } + + [Fact] + public void ParseBlockGivesSpacesToCodeOnAtColonTemplateTransitionInDesignTimeMode() + { + ParseBlockTest("Foo( " + Environment.NewLine + + "@:

Foo

" + Environment.NewLine + + ")", + new ExpressionBlock( + Factory.Code("Foo( " + Environment.NewLine).AsImplicitExpression(CSharpCodeParser.DefaultKeywords), + new TemplateBlock( + new MarkupBlock( + Factory.MarkupTransition(), + Factory.MetaMarkup(":", HtmlSymbolType.Colon), + Factory.Markup("

Foo

" + Environment.NewLine) + .With(new SpanEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString, AcceptedCharacters.None)) + ) + ), + Factory.Code(")") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace) + ), designTime: true); + } + + [Fact] + public void ParseBlockGivesSpacesToCodeOnTagTransitionInDesignTimeMode() + { + ParseBlockTest("{" + Environment.NewLine + + "

Foo

" + Environment.NewLine + + "}", + new StatementBlock( + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.Code(Environment.NewLine + " ") + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup("Foo"), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)) + ), + Factory.Code(" " + Environment.NewLine).AsStatement(), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None) + ), designTime: true); + } + + [Fact] + public void ParseBlockGivesSpacesToCodeOnInvalidAtTagTransitionInDesignTimeMode() + { + ParseBlockTest("{" + Environment.NewLine + + " @

Foo

" + Environment.NewLine + + "}", + new StatementBlock( + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.Code(Environment.NewLine + " ") + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + new MarkupBlock( + Factory.MarkupTransition(), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup("Foo"), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)) + ), + Factory.Code(" " + Environment.NewLine).AsStatement(), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None) + ), true, + new RazorError( + LegacyResources.ParseError_AtInCode_Must_Be_Followed_By_Colon_Paren_Or_Identifier_Start, + new SourceLocation(5 + Environment.NewLine.Length, 1, 4), + length: 1)); + } + + [Fact] + public void ParseBlockGivesSpacesToCodeOnAtColonTransitionInDesignTimeMode() + { + ParseBlockTest("{" + Environment.NewLine + + " @:

Foo

" + Environment.NewLine + + "}", + new StatementBlock( + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.Code(Environment.NewLine + " ") + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + new MarkupBlock( + Factory.MarkupTransition(), + Factory.MetaMarkup(":", HtmlSymbolType.Colon), + Factory.Markup("

Foo

" + Environment.NewLine) + .With(new SpanEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString, AcceptedCharacters.None)) + ), + Factory.EmptyCSharp().AsStatement(), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None) + ), designTime: true); + } + + [Fact] + public void ParseBlockShouldSupportSingleLineMarkupContainingStatementBlock() + { + ParseBlockTest("Repeat(10," + Environment.NewLine + + " @: @{}" + Environment.NewLine + + ")", + new ExpressionBlock( + Factory.Code($"Repeat(10,{Environment.NewLine} ") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords), + new TemplateBlock( + new MarkupBlock( + Factory.MarkupTransition(), + Factory.MetaMarkup(":", HtmlSymbolType.Colon), + Factory.Markup(" ") + .With(new SpanEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString)), + new StatementBlock( + Factory.CodeTransition(), + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.EmptyCSharp() + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None) + ), + Factory.Markup(Environment.NewLine) + .Accepts(AcceptedCharacters.None) + ) + ), + Factory.Code(")") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace) + )); + } + + [Fact] + public void ParseBlockShouldSupportMarkupWithoutPreceedingWhitespace() + { + ParseBlockTest("foreach(var file in files){" + Environment.NewLine + + Environment.NewLine + + Environment.NewLine + + "@:Baz" + Environment.NewLine + + "
" + Environment.NewLine + + "Foo" + Environment.NewLine + + "@:Bar" + Environment.NewLine + + "}", + new StatementBlock( + Factory.Code(string.Format("foreach(var file in files){{{0}{0}{0}", Environment.NewLine)).AsStatement(), + new MarkupBlock( + Factory.MarkupTransition(), + Factory.MetaMarkup(":", HtmlSymbolType.Colon), + Factory.Markup("Baz" + Environment.NewLine) + .With(new SpanEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString, AcceptedCharacters.None)) + ), + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("
").Accepts(AcceptedCharacters.None)), + Factory.Markup(Environment.NewLine).Accepts(AcceptedCharacters.None) + ), + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + Factory.Markup("Foo"), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + Factory.Markup(Environment.NewLine).Accepts(AcceptedCharacters.None)), + new MarkupBlock( + Factory.MarkupTransition(), + Factory.MetaMarkup(":", HtmlSymbolType.Colon), + Factory.Markup("Bar" + Environment.NewLine) + .With(new SpanEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString, AcceptedCharacters.None)) + ), + Factory.Code("}").AsStatement().Accepts(AcceptedCharacters.None) + )); + } + + [Fact] + public void ParseBlockGivesAllWhitespaceOnSameLineExcludingPreceedingNewlineButIncludingTrailingNewLineToMarkup() + { + ParseBlockTest("if(foo) {" + Environment.NewLine + + " var foo = \"After this statement there are 10 spaces\"; " + Environment.NewLine + + "

" + Environment.NewLine + + " Foo" + Environment.NewLine + + " @bar" + Environment.NewLine + + "

" + Environment.NewLine + + " @:Hello!" + Environment.NewLine + + " var biz = boz;" + Environment.NewLine + + "}", + new StatementBlock( + Factory.Code( + $"if(foo) {{{Environment.NewLine} var foo = \"After this statement there are " + + "10 spaces\"; " + Environment.NewLine).AsStatement(), + new MarkupBlock( + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup($"{Environment.NewLine} Foo{Environment.NewLine}"), + new ExpressionBlock( + Factory.Code(" ").AsStatement(), + Factory.CodeTransition(), + Factory.Code("bar").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace) + ), + Factory.Markup(Environment.NewLine + " "), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup(Environment.NewLine).Accepts(AcceptedCharacters.None) + ), + new MarkupBlock( + Factory.Markup(" "), + Factory.MarkupTransition(), + Factory.MetaMarkup(":", HtmlSymbolType.Colon), + Factory.Markup("Hello!" + Environment.NewLine).With(new SpanEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString, AcceptedCharacters.None)) + ), + Factory.Code($" var biz = boz;{Environment.NewLine}}}").AsStatement())); + } + + [Fact] + public void ParseBlockAllowsMarkupInIfBodyWithBraces() + { + ParseBlockTest("if(foo) {

Bar

} else if(bar) {

Baz

} else {

Boz

}", + new StatementBlock( + Factory.Code("if(foo) {").AsStatement(), + new MarkupBlock( + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup("Bar"), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup(" ").Accepts(AcceptedCharacters.None) + ), + Factory.Code("} else if(bar) {").AsStatement(), + new MarkupBlock( + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup("Baz"), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup(" ").Accepts(AcceptedCharacters.None) + ), + Factory.Code("} else {").AsStatement(), + new MarkupBlock( + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup("Boz"), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup(" ").Accepts(AcceptedCharacters.None) + ), + Factory.Code("}").AsStatement().Accepts(AcceptedCharacters.None) + )); + } + + [Fact] + public void ParseBlockAllowsMarkupInIfBodyWithBracesWithinCodeBlock() + { + ParseBlockTest("{ if(foo) {

Bar

} else if(bar) {

Baz

} else {

Boz

} }", + new StatementBlock( + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.Code(" if(foo) {") + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + new MarkupBlock( + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup("Bar"), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup(" ").Accepts(AcceptedCharacters.None) + ), + Factory.Code("} else if(bar) {").AsStatement(), + new MarkupBlock( + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup("Baz"), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup(" ").Accepts(AcceptedCharacters.None) + ), + Factory.Code("} else {").AsStatement(), + new MarkupBlock( + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup("Boz"), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup(" ").Accepts(AcceptedCharacters.None) + ), + Factory.Code("} ").AsStatement(), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None) + )); + } + + [Fact] + public void ParseBlockSupportsMarkupInCaseAndDefaultBranchesOfSwitch() + { + // Arrange + ParseBlockTest("switch(foo) {" + Environment.NewLine + + " case 0:" + Environment.NewLine + + "

Foo

" + Environment.NewLine + + " break;" + Environment.NewLine + + " case 1:" + Environment.NewLine + + "

Bar

" + Environment.NewLine + + " return;" + Environment.NewLine + + " case 2:" + Environment.NewLine + + " {" + Environment.NewLine + + "

Baz

" + Environment.NewLine + + "

Boz

" + Environment.NewLine + + " }" + Environment.NewLine + + " default:" + Environment.NewLine + + "

Biz

" + Environment.NewLine + + "}", + new StatementBlock( + Factory.Code($"switch(foo) {{{Environment.NewLine} case 0:{Environment.NewLine}").AsStatement(), + new MarkupBlock( + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup("Foo"), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup(Environment.NewLine).Accepts(AcceptedCharacters.None) + ), + Factory.Code($" break;{Environment.NewLine} case 1:{Environment.NewLine}").AsStatement(), + new MarkupBlock( + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup("Bar"), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup(Environment.NewLine).Accepts(AcceptedCharacters.None) + ), + Factory.Code( + $" return;{Environment.NewLine} case 2:{Environment.NewLine}" + + " {" + Environment.NewLine).AsStatement(), + new MarkupBlock( + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup("Baz"), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup(Environment.NewLine).Accepts(AcceptedCharacters.None) + ), + new MarkupBlock( + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup("Boz"), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup(Environment.NewLine).Accepts(AcceptedCharacters.None) + ), + Factory.Code($" }}{Environment.NewLine} default:{Environment.NewLine}").AsStatement(), + new MarkupBlock( + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup("Biz"), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup(Environment.NewLine).Accepts(AcceptedCharacters.None) + ), + Factory.Code("}").AsStatement().Accepts(AcceptedCharacters.None))); + } + + [Fact] + public void ParseBlockSupportsMarkupInCaseAndDefaultBranchesOfSwitchInCodeBlock() + { + // Arrange + ParseBlockTest("{ switch(foo) {" + Environment.NewLine + + " case 0:" + Environment.NewLine + + "

Foo

" + Environment.NewLine + + " break;" + Environment.NewLine + + " case 1:" + Environment.NewLine + + "

Bar

" + Environment.NewLine + + " return;" + Environment.NewLine + + " case 2:" + Environment.NewLine + + " {" + Environment.NewLine + + "

Baz

" + Environment.NewLine + + "

Boz

" + Environment.NewLine + + " }" + Environment.NewLine + + " default:" + Environment.NewLine + + "

Biz

" + Environment.NewLine + + "} }", + new StatementBlock( + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.Code($" switch(foo) {{{Environment.NewLine} case 0:{Environment.NewLine}") + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + new MarkupBlock( + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup("Foo"), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup(Environment.NewLine).Accepts(AcceptedCharacters.None) + ), + Factory.Code($" break;{Environment.NewLine} case 1:{Environment.NewLine}").AsStatement(), + new MarkupBlock( + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup("Bar"), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup(Environment.NewLine).Accepts(AcceptedCharacters.None) + ), + Factory.Code( + $" return;{Environment.NewLine} case 2:{Environment.NewLine}" + + " {" + Environment.NewLine).AsStatement(), + new MarkupBlock( + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup("Baz"), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup(Environment.NewLine).Accepts(AcceptedCharacters.None) + ), + new MarkupBlock( + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup("Boz"), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup(Environment.NewLine).Accepts(AcceptedCharacters.None) + ), + Factory.Code($" }}{Environment.NewLine} default:{Environment.NewLine}").AsStatement(), + new MarkupBlock( + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup("Biz"), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup(Environment.NewLine).Accepts(AcceptedCharacters.None) + ), + Factory.Code("} ").AsStatement(), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None))); + } + + [Fact] + public void ParseBlockParsesMarkupStatementOnOpenAngleBracket() + { + ParseBlockTest("for(int i = 0; i < 10; i++) {

Foo

}", + new StatementBlock( + Factory.Code("for(int i = 0; i < 10; i++) {").AsStatement(), + new MarkupBlock( + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup("Foo"), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup(" ").Accepts(AcceptedCharacters.None) + ), + Factory.Code("}").AsStatement().Accepts(AcceptedCharacters.None) + )); + } + + [Fact] + public void ParseBlockParsesMarkupStatementOnOpenAngleBracketInCodeBlock() + { + ParseBlockTest("{ for(int i = 0; i < 10; i++) {

Foo

} }", + new StatementBlock( + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.Code(" for(int i = 0; i < 10; i++) {") + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + new MarkupBlock( + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup("Foo"), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup(" ").Accepts(AcceptedCharacters.None) + ), + Factory.Code("} ").AsStatement(), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None))); + } + + [Fact] + public void ParseBlockParsesMarkupStatementOnSwitchCharacterFollowedByColon() + { + // Arrange + ParseBlockTest("if(foo) { @:Bar" + Environment.NewLine + + "} zoop", + new StatementBlock( + Factory.Code("if(foo) {").AsStatement(), + new MarkupBlock( + Factory.Markup(" "), + Factory.MarkupTransition(), + Factory.MetaMarkup(":", HtmlSymbolType.Colon), + Factory.Markup("Bar" + Environment.NewLine) + .With(new SpanEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString, AcceptedCharacters.None)) + ), + Factory.Code("}").AsStatement())); + } + + [Fact] + public void ParseBlockParsesMarkupStatementOnSwitchCharacterFollowedByDoubleColon() + { + // Arrange + ParseBlockTest("if(foo) { @::Sometext" + Environment.NewLine + + "}", + new StatementBlock( + Factory.Code("if(foo) {").AsStatement(), + new MarkupBlock( + Factory.Markup(" "), + Factory.MarkupTransition(), + Factory.MetaMarkup(":", HtmlSymbolType.Colon), + Factory.Markup(":Sometext" + Environment.NewLine) + .With(new SpanEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString, AcceptedCharacters.None)) + ), + Factory.Code("}").AsStatement())); + } + + + [Fact] + public void ParseBlockParsesMarkupStatementOnSwitchCharacterFollowedByTripleColon() + { + // Arrange + ParseBlockTest("if(foo) { @:::Sometext" + Environment.NewLine + + "}", + new StatementBlock( + Factory.Code("if(foo) {").AsStatement(), + new MarkupBlock( + Factory.Markup(" "), + Factory.MarkupTransition(), + Factory.MetaMarkup(":", HtmlSymbolType.Colon), + Factory.Markup("::Sometext" + Environment.NewLine) + .With(new SpanEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString, AcceptedCharacters.None)) + ), + Factory.Code("}").AsStatement())); + } + + [Fact] + public void ParseBlockParsesMarkupStatementOnSwitchCharacterFollowedByColonInCodeBlock() + { + // Arrange + ParseBlockTest("{ if(foo) { @:Bar" + Environment.NewLine + + "} } zoop", + new StatementBlock( + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.Code(" if(foo) {") + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + new MarkupBlock( + Factory.Markup(" "), + Factory.MarkupTransition(), + Factory.MetaMarkup(":", HtmlSymbolType.Colon), + Factory.Markup("Bar" + Environment.NewLine).Accepts(AcceptedCharacters.None) + .With(new SpanEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString, AcceptedCharacters.None)) + ), + Factory.Code("} ").AsStatement(), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None))); + } + + [Fact] + public void ParseBlockCorrectlyReturnsFromMarkupBlockWithPseudoTag() + { + ParseBlockTest("if (i > 0) { ; }", + new StatementBlock( + Factory.Code("if (i > 0) {").AsStatement(), + new MarkupBlock( + new MarkupTagBlock( + Factory.MarkupTransition("").Accepts(AcceptedCharacters.None)), + Factory.Markup(";").Accepts(AcceptedCharacters.None), + new MarkupTagBlock( + Factory.MarkupTransition("").Accepts(AcceptedCharacters.None))), + Factory.Code(" }").AsStatement())); + } + + [Fact] + public void ParseBlockCorrectlyReturnsFromMarkupBlockWithPseudoTagInCodeBlock() + { + ParseBlockTest("{ if (i > 0) { ; } }", + new StatementBlock( + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.Code(" if (i > 0) {") + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + new MarkupBlock( + new MarkupTagBlock( + Factory.MarkupTransition("").Accepts(AcceptedCharacters.None)), + Factory.Markup(";").Accepts(AcceptedCharacters.None), + new MarkupTagBlock( + Factory.MarkupTransition("").Accepts(AcceptedCharacters.None))), + Factory.Code(" } ").AsStatement(), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None))); + } + + [Fact] + public void ParseBlockSupportsAllKindsOfImplicitMarkupInCodeBlock() + { + ParseBlockTest("{" + Environment.NewLine + + " if(true) {" + Environment.NewLine + + " @:Single Line Markup" + Environment.NewLine + + " }" + Environment.NewLine + + " foreach (var p in Enumerable.Range(1, 10)) {" + Environment.NewLine + + " The number is @p" + Environment.NewLine + + " }" + Environment.NewLine + + " if(!false) {" + Environment.NewLine + + "

A real tag!

" + Environment.NewLine + + " }" + Environment.NewLine + + "}", + new StatementBlock( + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.Code($"{Environment.NewLine} if(true) {{{Environment.NewLine}") + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + new MarkupBlock( + Factory.Markup(" "), + Factory.MarkupTransition(), + Factory.MetaMarkup(":", HtmlSymbolType.Colon), + Factory.Markup("Single Line Markup" + Environment.NewLine) + .With(new SpanEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString, AcceptedCharacters.None)) + ), + Factory.Code($" }}{Environment.NewLine} foreach (var p in Enumerable.Range(1, 10)) {{{Environment.NewLine}").AsStatement(), + new MarkupBlock( + new MarkupTagBlock( + Factory.MarkupTransition("").Accepts(AcceptedCharacters.None)), + Factory.Markup("The number is ").Accepts(AcceptedCharacters.None), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.Code("p").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace) + ), + new MarkupTagBlock( + Factory.MarkupTransition("").Accepts(AcceptedCharacters.None))), + Factory.Code($"{Environment.NewLine} }}{Environment.NewLine} if(!false) {{{Environment.NewLine}").AsStatement(), + new MarkupBlock( + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup("A real tag!"), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)), + Factory.Markup(Environment.NewLine).Accepts(AcceptedCharacters.None) + ), + Factory.Code(" }" + Environment.NewLine).AsStatement(), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None))); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpTokenizerCommentTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpTokenizerCommentTest.cs new file mode 100644 index 0000000000..1b33fc7b69 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpTokenizerCommentTest.cs @@ -0,0 +1,89 @@ +// 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.Legacy +{ + internal class CSharpTokenizerCommentTest : CSharpTokenizerTestBase + { + [Fact] + public void Next_Ignores_Star_At_EOF_In_RazorComment() + { + TestTokenizer("@* Foo * Bar * Baz *", + new CSharpSymbol(0, 0, 0, "@", CSharpSymbolType.RazorCommentTransition), + new CSharpSymbol(1, 0, 1, "*", CSharpSymbolType.RazorCommentStar), + new CSharpSymbol(2, 0, 2, " Foo * Bar * Baz *", CSharpSymbolType.RazorComment)); + } + + [Fact] + public void Next_Ignores_Star_Without_Trailing_At() + { + TestTokenizer("@* Foo * Bar * Baz *@", + new CSharpSymbol(0, 0, 0, "@", CSharpSymbolType.RazorCommentTransition), + new CSharpSymbol(1, 0, 1, "*", CSharpSymbolType.RazorCommentStar), + new CSharpSymbol(2, 0, 2, " Foo * Bar * Baz ", CSharpSymbolType.RazorComment), + new CSharpSymbol(19, 0, 19, "*", CSharpSymbolType.RazorCommentStar), + new CSharpSymbol(20, 0, 20, "@", CSharpSymbolType.RazorCommentTransition)); + } + + [Fact] + public void Next_Returns_RazorComment_Token_For_Entire_Razor_Comment() + { + TestTokenizer("@* Foo Bar Baz *@", + new CSharpSymbol(0, 0, 0, "@", CSharpSymbolType.RazorCommentTransition), + new CSharpSymbol(1, 0, 1, "*", CSharpSymbolType.RazorCommentStar), + new CSharpSymbol(2, 0, 2, " Foo Bar Baz ", CSharpSymbolType.RazorComment), + new CSharpSymbol(15, 0, 15, "*", CSharpSymbolType.RazorCommentStar), + new CSharpSymbol(16, 0, 16, "@", CSharpSymbolType.RazorCommentTransition)); + } + + [Fact] + public void Next_Returns_Comment_Token_For_Entire_Single_Line_Comment() + { + TestTokenizer("// Foo Bar Baz", new CSharpSymbol(0, 0, 0, "// Foo Bar Baz", CSharpSymbolType.Comment)); + } + + [Fact] + public void Single_Line_Comment_Is_Terminated_By_Newline() + { + TestTokenizer("// Foo Bar Baz\na", new CSharpSymbol(0, 0, 0, "// Foo Bar Baz", CSharpSymbolType.Comment), IgnoreRemaining); + } + + [Fact] + public void Multi_Line_Comment_In_Single_Line_Comment_Has_No_Effect() + { + TestTokenizer("// Foo/*Bar*/ Baz\na", new CSharpSymbol(0, 0, 0, "// Foo/*Bar*/ Baz", CSharpSymbolType.Comment), IgnoreRemaining); + } + + [Fact] + public void Next_Returns_Comment_Token_For_Entire_Multi_Line_Comment() + { + TestTokenizer("/* Foo\nBar\nBaz */", new CSharpSymbol(0, 0, 0, "/* Foo\nBar\nBaz */", CSharpSymbolType.Comment)); + } + + [Fact] + public void Multi_Line_Comment_Is_Terminated_By_End_Sequence() + { + TestTokenizer("/* Foo\nBar\nBaz */a", new CSharpSymbol(0, 0, 0, "/* Foo\nBar\nBaz */", CSharpSymbolType.Comment), IgnoreRemaining); + } + + [Fact] + public void Unterminated_Multi_Line_Comment_Captures_To_EOF() + { + TestTokenizer("/* Foo\nBar\nBaz", new CSharpSymbol(0, 0, 0, "/* Foo\nBar\nBaz", CSharpSymbolType.Comment), IgnoreRemaining); + } + + [Fact] + public void Nested_Multi_Line_Comments_Terminated_At_First_End_Sequence() + { + TestTokenizer("/* Foo/*\nBar\nBaz*/ */", new CSharpSymbol(0, 0, 0, "/* Foo/*\nBar\nBaz*/", CSharpSymbolType.Comment), IgnoreRemaining); + } + + [Fact] + public void Nested_Multi_Line_Comments_Terminated_At_Full_End_Sequence() + { + TestTokenizer("/* Foo\nBar\nBaz* */", new CSharpSymbol(0, 0, 0, "/* Foo\nBar\nBaz* */", CSharpSymbolType.Comment), IgnoreRemaining); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpTokenizerIdentifierTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpTokenizerIdentifierTest.cs new file mode 100644 index 0000000000..8b2e2718da --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpTokenizerIdentifierTest.cs @@ -0,0 +1,170 @@ +// 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.Legacy +{ + internal class CSharpTokenizerIdentifierTest : CSharpTokenizerTestBase + { + [Fact] + public void Simple_Identifier_Is_Recognized() + { + TestTokenizer("foo", new CSharpSymbol(0, 0, 0, "foo", CSharpSymbolType.Identifier)); + } + + [Fact] + public void Identifier_Starting_With_Underscore_Is_Recognized() + { + TestTokenizer("_foo", new CSharpSymbol(0, 0, 0, "_foo", CSharpSymbolType.Identifier)); + } + + [Fact] + public void Identifier_Can_Contain_Digits() + { + TestTokenizer("foo4", new CSharpSymbol(0, 0, 0, "foo4", CSharpSymbolType.Identifier)); + } + + [Fact] + public void Identifier_Can_Start_With_Titlecase_Letter() + { + TestTokenizer("ῼfoo", new CSharpSymbol(0, 0, 0, "ῼfoo", CSharpSymbolType.Identifier)); + } + + [Fact] + public void Identifier_Can_Start_With_Letter_Modifier() + { + TestTokenizer("ᵊfoo", new CSharpSymbol(0, 0, 0, "ᵊfoo", CSharpSymbolType.Identifier)); + } + + [Fact] + public void Identifier_Can_Start_With_Other_Letter() + { + TestTokenizer("ƻfoo", new CSharpSymbol(0, 0, 0, "ƻfoo", CSharpSymbolType.Identifier)); + } + + [Fact] + public void Identifier_Can_Start_With_Number_Letter() + { + TestTokenizer("Ⅽool", new CSharpSymbol(0, 0, 0, "Ⅽool", CSharpSymbolType.Identifier)); + } + + [Fact] + public void Identifier_Can_Contain_Non_Spacing_Mark() + { + TestTokenizer("foo\u0300", new CSharpSymbol(0, 0, 0, "foo\u0300", CSharpSymbolType.Identifier)); + } + + [Fact] + public void Identifier_Can_Contain_Spacing_Combining_Mark() + { + TestTokenizer("fooः", new CSharpSymbol(0, 0, 0, "fooः", CSharpSymbolType.Identifier)); + } + + [Fact] + public void Identifier_Can_Contain_Non_English_Digit() + { + TestTokenizer("foo١", new CSharpSymbol(0, 0, 0, "foo١", CSharpSymbolType.Identifier)); + } + + [Fact] + public void Identifier_Can_Contain_Connector_Punctuation() + { + TestTokenizer("foo‿bar", new CSharpSymbol(0, 0, 0, "foo‿bar", CSharpSymbolType.Identifier)); + } + + [Fact] + public void Identifier_Can_Contain_Format_Character() + { + TestTokenizer("foo؃bar", new CSharpSymbol(0, 0, 0, "foo؃bar", CSharpSymbolType.Identifier)); + } + + [Fact] + public void Keywords_Are_Recognized_As_Keyword_Tokens() + { + TestKeyword("abstract", CSharpKeyword.Abstract); + TestKeyword("byte", CSharpKeyword.Byte); + TestKeyword("class", CSharpKeyword.Class); + TestKeyword("delegate", CSharpKeyword.Delegate); + TestKeyword("event", CSharpKeyword.Event); + TestKeyword("fixed", CSharpKeyword.Fixed); + TestKeyword("if", CSharpKeyword.If); + TestKeyword("internal", CSharpKeyword.Internal); + TestKeyword("new", CSharpKeyword.New); + TestKeyword("override", CSharpKeyword.Override); + TestKeyword("readonly", CSharpKeyword.Readonly); + TestKeyword("short", CSharpKeyword.Short); + TestKeyword("struct", CSharpKeyword.Struct); + TestKeyword("try", CSharpKeyword.Try); + TestKeyword("unsafe", CSharpKeyword.Unsafe); + TestKeyword("volatile", CSharpKeyword.Volatile); + TestKeyword("as", CSharpKeyword.As); + TestKeyword("do", CSharpKeyword.Do); + TestKeyword("is", CSharpKeyword.Is); + TestKeyword("params", CSharpKeyword.Params); + TestKeyword("ref", CSharpKeyword.Ref); + TestKeyword("switch", CSharpKeyword.Switch); + TestKeyword("ushort", CSharpKeyword.Ushort); + TestKeyword("while", CSharpKeyword.While); + TestKeyword("case", CSharpKeyword.Case); + TestKeyword("const", CSharpKeyword.Const); + TestKeyword("explicit", CSharpKeyword.Explicit); + TestKeyword("float", CSharpKeyword.Float); + TestKeyword("null", CSharpKeyword.Null); + TestKeyword("sizeof", CSharpKeyword.Sizeof); + TestKeyword("typeof", CSharpKeyword.Typeof); + TestKeyword("implicit", CSharpKeyword.Implicit); + TestKeyword("private", CSharpKeyword.Private); + TestKeyword("this", CSharpKeyword.This); + TestKeyword("using", CSharpKeyword.Using); + TestKeyword("extern", CSharpKeyword.Extern); + TestKeyword("return", CSharpKeyword.Return); + TestKeyword("stackalloc", CSharpKeyword.Stackalloc); + TestKeyword("uint", CSharpKeyword.Uint); + TestKeyword("base", CSharpKeyword.Base); + TestKeyword("catch", CSharpKeyword.Catch); + TestKeyword("continue", CSharpKeyword.Continue); + TestKeyword("double", CSharpKeyword.Double); + TestKeyword("for", CSharpKeyword.For); + TestKeyword("in", CSharpKeyword.In); + TestKeyword("lock", CSharpKeyword.Lock); + TestKeyword("object", CSharpKeyword.Object); + TestKeyword("protected", CSharpKeyword.Protected); + TestKeyword("static", CSharpKeyword.Static); + TestKeyword("false", CSharpKeyword.False); + TestKeyword("public", CSharpKeyword.Public); + TestKeyword("sbyte", CSharpKeyword.Sbyte); + TestKeyword("throw", CSharpKeyword.Throw); + TestKeyword("virtual", CSharpKeyword.Virtual); + TestKeyword("decimal", CSharpKeyword.Decimal); + TestKeyword("else", CSharpKeyword.Else); + TestKeyword("operator", CSharpKeyword.Operator); + TestKeyword("string", CSharpKeyword.String); + TestKeyword("ulong", CSharpKeyword.Ulong); + TestKeyword("bool", CSharpKeyword.Bool); + TestKeyword("char", CSharpKeyword.Char); + TestKeyword("default", CSharpKeyword.Default); + TestKeyword("foreach", CSharpKeyword.Foreach); + TestKeyword("long", CSharpKeyword.Long); + TestKeyword("void", CSharpKeyword.Void); + TestKeyword("enum", CSharpKeyword.Enum); + TestKeyword("finally", CSharpKeyword.Finally); + TestKeyword("int", CSharpKeyword.Int); + TestKeyword("out", CSharpKeyword.Out); + TestKeyword("sealed", CSharpKeyword.Sealed); + TestKeyword("true", CSharpKeyword.True); + TestKeyword("goto", CSharpKeyword.Goto); + TestKeyword("unchecked", CSharpKeyword.Unchecked); + TestKeyword("interface", CSharpKeyword.Interface); + TestKeyword("break", CSharpKeyword.Break); + TestKeyword("checked", CSharpKeyword.Checked); + TestKeyword("namespace", CSharpKeyword.Namespace); + TestKeyword("when", CSharpKeyword.When); + } + + private void TestKeyword(string keyword, CSharpKeyword keywordType) + { + TestTokenizer(keyword, new CSharpSymbol(0, 0, 0, keyword, CSharpSymbolType.Keyword) { Keyword = keywordType }); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpTokenizerLiteralTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpTokenizerLiteralTest.cs new file mode 100644 index 0000000000..7565fad137 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpTokenizerLiteralTest.cs @@ -0,0 +1,285 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class CSharpTokenizerLiteralTest : CSharpTokenizerTestBase + { + [Fact] + public void Simple_Integer_Literal_Is_Recognized() + { + TestSingleToken("01189998819991197253", CSharpSymbolType.IntegerLiteral); + } + + [Fact] + public void Integer_Type_Suffix_Is_Recognized() + { + TestSingleToken("42U", CSharpSymbolType.IntegerLiteral); + TestSingleToken("42u", CSharpSymbolType.IntegerLiteral); + + TestSingleToken("42L", CSharpSymbolType.IntegerLiteral); + TestSingleToken("42l", CSharpSymbolType.IntegerLiteral); + + TestSingleToken("42UL", CSharpSymbolType.IntegerLiteral); + TestSingleToken("42Ul", CSharpSymbolType.IntegerLiteral); + + TestSingleToken("42uL", CSharpSymbolType.IntegerLiteral); + TestSingleToken("42ul", CSharpSymbolType.IntegerLiteral); + + TestSingleToken("42LU", CSharpSymbolType.IntegerLiteral); + TestSingleToken("42Lu", CSharpSymbolType.IntegerLiteral); + + TestSingleToken("42lU", CSharpSymbolType.IntegerLiteral); + TestSingleToken("42lu", CSharpSymbolType.IntegerLiteral); + } + + [Fact] + public void Trailing_Letter_Is_Not_Part_Of_Integer_Literal_If_Not_Type_Sufix() + { + TestTokenizer("42a", new CSharpSymbol(0, 0, 0, "42", CSharpSymbolType.IntegerLiteral), IgnoreRemaining); + } + + [Fact] + public void Simple_Hex_Literal_Is_Recognized() + { + TestSingleToken("0x0123456789ABCDEF", CSharpSymbolType.IntegerLiteral); + } + + [Fact] + public void Integer_Type_Suffix_Is_Recognized_In_Hex_Literal() + { + TestSingleToken("0xDEADBEEFU", CSharpSymbolType.IntegerLiteral); + TestSingleToken("0xDEADBEEFu", CSharpSymbolType.IntegerLiteral); + + TestSingleToken("0xDEADBEEFL", CSharpSymbolType.IntegerLiteral); + TestSingleToken("0xDEADBEEFl", CSharpSymbolType.IntegerLiteral); + + TestSingleToken("0xDEADBEEFUL", CSharpSymbolType.IntegerLiteral); + TestSingleToken("0xDEADBEEFUl", CSharpSymbolType.IntegerLiteral); + + TestSingleToken("0xDEADBEEFuL", CSharpSymbolType.IntegerLiteral); + TestSingleToken("0xDEADBEEFul", CSharpSymbolType.IntegerLiteral); + + TestSingleToken("0xDEADBEEFLU", CSharpSymbolType.IntegerLiteral); + TestSingleToken("0xDEADBEEFLu", CSharpSymbolType.IntegerLiteral); + + TestSingleToken("0xDEADBEEFlU", CSharpSymbolType.IntegerLiteral); + TestSingleToken("0xDEADBEEFlu", CSharpSymbolType.IntegerLiteral); + } + + [Fact] + public void Trailing_Letter_Is_Not_Part_Of_Hex_Literal_If_Not_Type_Sufix() + { + TestTokenizer("0xDEADBEEFz", new CSharpSymbol(0, 0, 0, "0xDEADBEEF", CSharpSymbolType.IntegerLiteral), IgnoreRemaining); + } + + [Fact] + public void Dot_Followed_By_Non_Digit_Is_Not_Part_Of_Real_Literal() + { + TestTokenizer("3.a", new CSharpSymbol(0, 0, 0, "3", CSharpSymbolType.IntegerLiteral), IgnoreRemaining); + } + + [Fact] + public void Simple_Real_Literal_Is_Recognized() + { + TestTokenizer("3.14159", new CSharpSymbol(0, 0, 0, "3.14159", CSharpSymbolType.RealLiteral)); + } + + [Fact] + public void Real_Literal_Between_Zero_And_One_Is_Recognized() + { + TestTokenizer(".14159", new CSharpSymbol(0, 0, 0, ".14159", CSharpSymbolType.RealLiteral)); + } + + [Fact] + public void Integer_With_Real_Type_Suffix_Is_Recognized() + { + TestSingleToken("42F", CSharpSymbolType.RealLiteral); + TestSingleToken("42f", CSharpSymbolType.RealLiteral); + TestSingleToken("42D", CSharpSymbolType.RealLiteral); + TestSingleToken("42d", CSharpSymbolType.RealLiteral); + TestSingleToken("42M", CSharpSymbolType.RealLiteral); + TestSingleToken("42m", CSharpSymbolType.RealLiteral); + } + + [Fact] + public void Integer_With_Exponent_Is_Recognized() + { + TestSingleToken("1e10", CSharpSymbolType.RealLiteral); + TestSingleToken("1E10", CSharpSymbolType.RealLiteral); + TestSingleToken("1e+10", CSharpSymbolType.RealLiteral); + TestSingleToken("1E+10", CSharpSymbolType.RealLiteral); + TestSingleToken("1e-10", CSharpSymbolType.RealLiteral); + TestSingleToken("1E-10", CSharpSymbolType.RealLiteral); + } + + [Fact] + public void Real_Number_With_Type_Suffix_Is_Recognized() + { + TestSingleToken("3.14F", CSharpSymbolType.RealLiteral); + TestSingleToken("3.14f", CSharpSymbolType.RealLiteral); + TestSingleToken("3.14D", CSharpSymbolType.RealLiteral); + TestSingleToken("3.14d", CSharpSymbolType.RealLiteral); + TestSingleToken("3.14M", CSharpSymbolType.RealLiteral); + TestSingleToken("3.14m", CSharpSymbolType.RealLiteral); + } + + [Fact] + public void Real_Number_With_Exponent_Is_Recognized() + { + TestSingleToken("3.14E10", CSharpSymbolType.RealLiteral); + TestSingleToken("3.14e10", CSharpSymbolType.RealLiteral); + TestSingleToken("3.14E+10", CSharpSymbolType.RealLiteral); + TestSingleToken("3.14e+10", CSharpSymbolType.RealLiteral); + TestSingleToken("3.14E-10", CSharpSymbolType.RealLiteral); + TestSingleToken("3.14e-10", CSharpSymbolType.RealLiteral); + } + + [Fact] + public void Real_Number_With_Exponent_And_Type_Suffix_Is_Recognized() + { + TestSingleToken("3.14E+10F", CSharpSymbolType.RealLiteral); + } + + [Fact] + public void Single_Character_Literal_Is_Recognized() + { + TestSingleToken("'f'", CSharpSymbolType.CharacterLiteral); + } + + [Fact] + public void Multi_Character_Literal_Is_Recognized() + { + TestSingleToken("'foo'", CSharpSymbolType.CharacterLiteral); + } + + [Fact] + public void Character_Literal_Is_Terminated_By_EOF_If_Unterminated() + { + TestSingleToken("'foo bar", CSharpSymbolType.CharacterLiteral); + } + + [Fact] + public void Character_Literal_Not_Terminated_By_Escaped_Quote() + { + TestSingleToken("'foo\\'bar'", CSharpSymbolType.CharacterLiteral); + } + + [Fact] + public void Character_Literal_Is_Terminated_By_EOL_If_Unterminated() + { + TestTokenizer("'foo\n", new CSharpSymbol(0, 0, 0, "'foo", CSharpSymbolType.CharacterLiteral), IgnoreRemaining); + } + + [Fact] + public void Character_Literal_Terminated_By_EOL_Even_When_Last_Char_Is_Slash() + { + TestTokenizer("'foo\\\n", new CSharpSymbol(0, 0, 0, "'foo\\", CSharpSymbolType.CharacterLiteral), IgnoreRemaining); + } + + [Fact] + public void Character_Literal_Terminated_By_EOL_Even_When_Last_Char_Is_Slash_And_Followed_By_Stuff() + { + TestTokenizer("'foo\\\nflarg", new CSharpSymbol(0, 0, 0, "'foo\\", CSharpSymbolType.CharacterLiteral), IgnoreRemaining); + } + + [Fact] + public void Character_Literal_Terminated_By_CRLF_Even_When_Last_Char_Is_Slash() + { + TestTokenizer("'foo\\" + Environment.NewLine, new CSharpSymbol(0, 0, 0, "'foo\\", CSharpSymbolType.CharacterLiteral), IgnoreRemaining); + } + + [Fact] + public void Character_Literal_Terminated_By_CRLF_Even_When_Last_Char_Is_Slash_And_Followed_By_Stuff() + { + TestTokenizer($"'foo\\{Environment.NewLine}flarg", new CSharpSymbol(0, 0, 0, "'foo\\", CSharpSymbolType.CharacterLiteral), IgnoreRemaining); + } + + [Fact] + public void Character_Literal_Allows_Escaped_Escape() + { + TestTokenizer("'foo\\\\'blah", new CSharpSymbol(0, 0, 0, "'foo\\\\'", CSharpSymbolType.CharacterLiteral), IgnoreRemaining); + } + + [Fact] + public void String_Literal_Is_Recognized() + { + TestSingleToken("\"foo\"", CSharpSymbolType.StringLiteral); + } + + [Fact] + public void String_Literal_Is_Terminated_By_EOF_If_Unterminated() + { + TestSingleToken("\"foo bar", CSharpSymbolType.StringLiteral); + } + + [Fact] + public void String_Literal_Not_Terminated_By_Escaped_Quote() + { + TestSingleToken("\"foo\\\"bar\"", CSharpSymbolType.StringLiteral); + } + + [Fact] + public void String_Literal_Is_Terminated_By_EOL_If_Unterminated() + { + TestTokenizer("\"foo\n", new CSharpSymbol(0, 0, 0, "\"foo", CSharpSymbolType.StringLiteral), IgnoreRemaining); + } + + [Fact] + public void String_Literal_Terminated_By_EOL_Even_When_Last_Char_Is_Slash() + { + TestTokenizer("\"foo\\\n", new CSharpSymbol(0, 0, 0, "\"foo\\", CSharpSymbolType.StringLiteral), IgnoreRemaining); + } + + [Fact] + public void String_Literal_Terminated_By_EOL_Even_When_Last_Char_Is_Slash_And_Followed_By_Stuff() + { + TestTokenizer("\"foo\\\nflarg", new CSharpSymbol(0, 0, 0, "\"foo\\", CSharpSymbolType.StringLiteral), IgnoreRemaining); + } + + [Fact] + public void String_Literal_Terminated_By_CRLF_Even_When_Last_Char_Is_Slash() + { + TestTokenizer("\"foo\\" + Environment.NewLine, new CSharpSymbol(0, 0, 0, "\"foo\\", CSharpSymbolType.StringLiteral), IgnoreRemaining); + } + + [Fact] + public void String_Literal_Terminated_By_CRLF_Even_When_Last_Char_Is_Slash_And_Followed_By_Stuff() + { + TestTokenizer($"\"foo\\{Environment.NewLine}flarg", new CSharpSymbol(0, 0, 0, "\"foo\\", CSharpSymbolType.StringLiteral), IgnoreRemaining); + } + + [Fact] + public void String_Literal_Allows_Escaped_Escape() + { + TestTokenizer("\"foo\\\\\"blah", new CSharpSymbol(0, 0, 0, "\"foo\\\\\"", CSharpSymbolType.StringLiteral), IgnoreRemaining); + } + + [Fact] + public void Verbatim_String_Literal_Can_Contain_Newlines() + { + TestSingleToken("@\"foo\nbar\nbaz\"", CSharpSymbolType.StringLiteral); + } + + [Fact] + public void Verbatim_String_Literal_Not_Terminated_By_Escaped_Double_Quote() + { + TestSingleToken("@\"foo\"\"bar\"", CSharpSymbolType.StringLiteral); + } + + [Fact] + public void Verbatim_String_Literal_Is_Terminated_By_Slash_Double_Quote() + { + TestTokenizer("@\"foo\\\"bar\"", new CSharpSymbol(0, 0, 0, "@\"foo\\\"", CSharpSymbolType.StringLiteral), IgnoreRemaining); + } + + [Fact] + public void Verbatim_String_Literal_Is_Terminated_By_EOF() + { + TestSingleToken("@\"foo", CSharpSymbolType.StringLiteral); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpTokenizerOperatorsTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpTokenizerOperatorsTest.cs new file mode 100644 index 0000000000..468aae09b3 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpTokenizerOperatorsTest.cs @@ -0,0 +1,296 @@ +// 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.Legacy +{ + internal class CSharpTokenizerOperatorsTest : CSharpTokenizerTestBase + { + [Fact] + public void LeftBrace_Is_Recognized() + { + TestSingleToken("{", CSharpSymbolType.LeftBrace); + } + + [Fact] + public void Plus_Is_Recognized() + { + TestSingleToken("+", CSharpSymbolType.Plus); + } + + [Fact] + public void Assign_Is_Recognized() + { + TestSingleToken("=", CSharpSymbolType.Assign); + } + + [Fact] + public void Arrow_Is_Recognized() + { + TestSingleToken("->", CSharpSymbolType.Arrow); + } + + [Fact] + public void AndAssign_Is_Recognized() + { + TestSingleToken("&=", CSharpSymbolType.AndAssign); + } + + [Fact] + public void RightBrace_Is_Recognized() + { + TestSingleToken("}", CSharpSymbolType.RightBrace); + } + + [Fact] + public void Minus_Is_Recognized() + { + TestSingleToken("-", CSharpSymbolType.Minus); + } + + [Fact] + public void LessThan_Is_Recognized() + { + TestSingleToken("<", CSharpSymbolType.LessThan); + } + + [Fact] + public void Equals_Is_Recognized() + { + TestSingleToken("==", CSharpSymbolType.Equals); + } + + [Fact] + public void OrAssign_Is_Recognized() + { + TestSingleToken("|=", CSharpSymbolType.OrAssign); + } + + [Fact] + public void LeftBracket_Is_Recognized() + { + TestSingleToken("[", CSharpSymbolType.LeftBracket); + } + + [Fact] + public void Star_Is_Recognized() + { + TestSingleToken("*", CSharpSymbolType.Star); + } + + [Fact] + public void GreaterThan_Is_Recognized() + { + TestSingleToken(">", CSharpSymbolType.GreaterThan); + } + + [Fact] + public void NotEqual_Is_Recognized() + { + TestSingleToken("!=", CSharpSymbolType.NotEqual); + } + + [Fact] + public void XorAssign_Is_Recognized() + { + TestSingleToken("^=", CSharpSymbolType.XorAssign); + } + + [Fact] + public void RightBracket_Is_Recognized() + { + TestSingleToken("]", CSharpSymbolType.RightBracket); + } + + [Fact] + public void Slash_Is_Recognized() + { + TestSingleToken("/", CSharpSymbolType.Slash); + } + + [Fact] + public void QuestionMark_Is_Recognized() + { + TestSingleToken("?", CSharpSymbolType.QuestionMark); + } + + [Fact] + public void LessThanEqual_Is_Recognized() + { + TestSingleToken("<=", CSharpSymbolType.LessThanEqual); + } + + [Fact] + public void LeftShift_Is_Not_Specially_Recognized() + { + TestTokenizer("<<", + new CSharpSymbol(new SourceLocation(0, 0, 0), "<", CSharpSymbolType.LessThan), + new CSharpSymbol(new SourceLocation(1, 0, 1), "<", CSharpSymbolType.LessThan)); + } + + [Fact] + public void LeftParen_Is_Recognized() + { + TestSingleToken("(", CSharpSymbolType.LeftParenthesis); + } + + [Fact] + public void Modulo_Is_Recognized() + { + TestSingleToken("%", CSharpSymbolType.Modulo); + } + + [Fact] + public void NullCoalesce_Is_Recognized() + { + TestSingleToken("??", CSharpSymbolType.NullCoalesce); + } + + [Fact] + public void GreaterThanEqual_Is_Recognized() + { + TestSingleToken(">=", CSharpSymbolType.GreaterThanEqual); + } + + [Fact] + public void EqualGreaterThan_Is_Recognized() + { + TestSingleToken("=>", CSharpSymbolType.GreaterThanEqual); + } + + [Fact] + public void RightParen_Is_Recognized() + { + TestSingleToken(")", CSharpSymbolType.RightParenthesis); + } + + [Fact] + public void And_Is_Recognized() + { + TestSingleToken("&", CSharpSymbolType.And); + } + + [Fact] + public void DoubleColon_Is_Recognized() + { + TestSingleToken("::", CSharpSymbolType.DoubleColon); + } + + [Fact] + public void PlusAssign_Is_Recognized() + { + TestSingleToken("+=", CSharpSymbolType.PlusAssign); + } + + [Fact] + public void Semicolon_Is_Recognized() + { + TestSingleToken(";", CSharpSymbolType.Semicolon); + } + + [Fact] + public void Tilde_Is_Recognized() + { + TestSingleToken("~", CSharpSymbolType.Tilde); + } + + [Fact] + public void DoubleOr_Is_Recognized() + { + TestSingleToken("||", CSharpSymbolType.DoubleOr); + } + + [Fact] + public void ModuloAssign_Is_Recognized() + { + TestSingleToken("%=", CSharpSymbolType.ModuloAssign); + } + + [Fact] + public void Colon_Is_Recognized() + { + TestSingleToken(":", CSharpSymbolType.Colon); + } + + [Fact] + public void Not_Is_Recognized() + { + TestSingleToken("!", CSharpSymbolType.Not); + } + + [Fact] + public void DoubleAnd_Is_Recognized() + { + TestSingleToken("&&", CSharpSymbolType.DoubleAnd); + } + + [Fact] + public void DivideAssign_Is_Recognized() + { + TestSingleToken("/=", CSharpSymbolType.DivideAssign); + } + + [Fact] + public void Comma_Is_Recognized() + { + TestSingleToken(",", CSharpSymbolType.Comma); + } + + [Fact] + public void Xor_Is_Recognized() + { + TestSingleToken("^", CSharpSymbolType.Xor); + } + + [Fact] + public void Decrement_Is_Recognized() + { + TestSingleToken("--", CSharpSymbolType.Decrement); + } + + [Fact] + public void MultiplyAssign_Is_Recognized() + { + TestSingleToken("*=", CSharpSymbolType.MultiplyAssign); + } + + [Fact] + public void Dot_Is_Recognized() + { + TestSingleToken(".", CSharpSymbolType.Dot); + } + + [Fact] + public void Or_Is_Recognized() + { + TestSingleToken("|", CSharpSymbolType.Or); + } + + [Fact] + public void Increment_Is_Recognized() + { + TestSingleToken("++", CSharpSymbolType.Increment); + } + + [Fact] + public void MinusAssign_Is_Recognized() + { + TestSingleToken("-=", CSharpSymbolType.MinusAssign); + } + + [Fact] + public void RightShift_Is_Not_Specially_Recognized() + { + TestTokenizer(">>", + new CSharpSymbol(0, 0, 0, ">", CSharpSymbolType.GreaterThan), + new CSharpSymbol(1, 0, 1, ">", CSharpSymbolType.GreaterThan)); + } + + [Fact] + public void Hash_Is_Recognized() + { + TestSingleToken("#", CSharpSymbolType.Hash); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpTokenizerTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpTokenizerTest.cs new file mode 100644 index 0000000000..e5e09daddf --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpTokenizerTest.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 Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class CSharpTokenizerTest : CSharpTokenizerTestBase + { + [Fact] + public void Next_Returns_Null_When_EOF_Reached() + { + TestTokenizer(""); + } + + [Fact] + public void Next_Returns_Newline_Token_For_Single_CR() + { + TestTokenizer("\r\ra", + new CSharpSymbol(0, 0, 0, "\r", CSharpSymbolType.NewLine), + new CSharpSymbol(1, 1, 0, "\r", CSharpSymbolType.NewLine), + IgnoreRemaining); + } + + [Fact] + public void Next_Returns_Newline_Token_For_Single_LF() + { + TestTokenizer("\n\na", + new CSharpSymbol(0, 0, 0, "\n", CSharpSymbolType.NewLine), + new CSharpSymbol(1, 1, 0, "\n", CSharpSymbolType.NewLine), + IgnoreRemaining); + } + + [Fact] + public void Next_Returns_Newline_Token_For_Single_NEL() + { + // NEL: Unicode "Next Line" U+0085 + TestTokenizer("\u0085\u0085a", + new CSharpSymbol(0, 0, 0, "\u0085", CSharpSymbolType.NewLine), + new CSharpSymbol(1, 1, 0, "\u0085", CSharpSymbolType.NewLine), + IgnoreRemaining); + } + + [Fact] + public void Next_Returns_Newline_Token_For_Single_Line_Separator() + { + // Unicode "Line Separator" U+2028 + TestTokenizer("\u2028\u2028a", + new CSharpSymbol(0, 0, 0, "\u2028", CSharpSymbolType.NewLine), + new CSharpSymbol(1, 1, 0, "\u2028", CSharpSymbolType.NewLine), + IgnoreRemaining); + } + + [Fact] + public void Next_Returns_Newline_Token_For_Single_Paragraph_Separator() + { + // Unicode "Paragraph Separator" U+2029 + TestTokenizer("\u2029\u2029a", + new CSharpSymbol(0, 0, 0, "\u2029", CSharpSymbolType.NewLine), + new CSharpSymbol(1, 1, 0, "\u2029", CSharpSymbolType.NewLine), + IgnoreRemaining); + } + + [Fact] + public void Next_Returns_Single_Newline_Token_For_CRLF() + { + TestTokenizer("\r\n\r\na", + new CSharpSymbol(0, 0, 0, "\r\n", CSharpSymbolType.NewLine), + new CSharpSymbol(2, 1, 0, "\r\n", CSharpSymbolType.NewLine), + IgnoreRemaining); + } + + [Fact] + public void Next_Returns_Token_For_Whitespace_Characters() + { + TestTokenizer(" \f\t\u000B \n ", + new CSharpSymbol(0, 0, 0, " \f\t\u000B ", CSharpSymbolType.WhiteSpace), + new CSharpSymbol(5, 0, 5, "\n", CSharpSymbolType.NewLine), + new CSharpSymbol(6, 1, 0, " ", CSharpSymbolType.WhiteSpace)); + } + + [Fact] + public void Transition_Is_Recognized() + { + TestSingleToken("@", CSharpSymbolType.Transition); + } + + [Fact] + public void Transition_Is_Recognized_As_SingleCharacter() + { + TestTokenizer("@(", + new CSharpSymbol(0, 0, 0, "@", CSharpSymbolType.Transition), + new CSharpSymbol(1, 0, 1, "(", CSharpSymbolType.LeftParenthesis)); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpTokenizerTestBase.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpTokenizerTestBase.cs new file mode 100644 index 0000000000..7c7743dbcc --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpTokenizerTestBase.cs @@ -0,0 +1,25 @@ +// 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.Legacy +{ + internal abstract class CSharpTokenizerTestBase : TokenizerTestBase + { + private static CSharpSymbol _ignoreRemaining = new CSharpSymbol(0, 0, 0, string.Empty, CSharpSymbolType.Unknown); + + protected override CSharpSymbol IgnoreRemaining + { + get { return _ignoreRemaining; } + } + + protected override Tokenizer CreateTokenizer(ITextDocument source) + { + return new CSharpTokenizer(source); + } + + protected void TestSingleToken(string text, CSharpSymbolType expectedSymbolType) + { + TestTokenizer(text, new CSharpSymbol(0, 0, 0, text, expectedSymbolType)); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpVerbatimBlockTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpVerbatimBlockTest.cs new file mode 100644 index 0000000000..62d363a51f --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpVerbatimBlockTest.cs @@ -0,0 +1,138 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class CSharpVerbatimBlockTest : CsHtmlCodeParserTestBase + { + private const string TestExtraKeyword = "model"; + + [Fact] + public void VerbatimBlock() + { + ParseBlockTest("@{ foo(); }", + new StatementBlock( + Factory.CodeTransition(), + Factory.MetaCode("{") + .Accepts(AcceptedCharacters.None), + Factory.Code(" foo(); ") + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + Factory.MetaCode("}") + .Accepts(AcceptedCharacters.None) + )); + } + + [Fact] + public void InnerImplicitExpressionWithOnlySingleAtOutputsZeroLengthCodeSpan() + { + ParseBlockTest("{@}", + new StatementBlock( + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.EmptyCSharp() + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.EmptyCSharp().AsImplicitExpression(KeywordSet, acceptTrailingDot: true).Accepts(AcceptedCharacters.NonWhiteSpace) + ), + Factory.EmptyCSharp().AsStatement(), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + designTime: true, + expectedErrors: new[] + { + new RazorError( + LegacyResources.FormatParseError_Unexpected_Character_At_Start_Of_CodeBlock_CS("}"), + new SourceLocation(2, 0, 2), + length: 1) + }); + } + + [Fact] + public void InnerImplicitExpressionDoesNotAcceptDotAfterAt() + { + ParseBlockTest("{@.}", + new StatementBlock( + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.EmptyCSharp() + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.EmptyCSharp().AsImplicitExpression(KeywordSet, acceptTrailingDot: true).Accepts(AcceptedCharacters.NonWhiteSpace) + ), + Factory.Code(".").AsStatement(), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + designTime: true, + expectedErrors: new[] + { + new RazorError( + LegacyResources.FormatParseError_Unexpected_Character_At_Start_Of_CodeBlock_CS("."), + new SourceLocation(2, 0, 2), + length: 1) + }); + } + + [Fact] + public void InnerImplicitExpressionWithOnlySingleAtAcceptsSingleSpaceOrNewlineAtDesignTime() + { + ParseBlockTest("{" + Environment.NewLine + + " @" + Environment.NewLine + + "}", + new StatementBlock( + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.Code(Environment.NewLine + " ") + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.EmptyCSharp().AsImplicitExpression(KeywordSet, acceptTrailingDot: true).Accepts(AcceptedCharacters.NonWhiteSpace) + ), + Factory.Code(Environment.NewLine).AsStatement(), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + /* designTimeParser */ true, + new RazorError( + LegacyResources.ParseError_Unexpected_WhiteSpace_At_Start_Of_CodeBlock_CS, + new SourceLocation(6 + Environment.NewLine.Length, 1, 5), + Environment.NewLine.Length)); + } + + [Fact] + public void InnerImplicitExpressionDoesNotAcceptTrailingNewlineInRunTimeMode() + { + ParseBlockTest("{@foo." + Environment.NewLine + + "}", + new StatementBlock( + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.EmptyCSharp() + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.Code("foo.").AsImplicitExpression(KeywordSet, acceptTrailingDot: true).Accepts(AcceptedCharacters.NonWhiteSpace)), + Factory.Code(Environment.NewLine).AsStatement(), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None))); + } + + [Fact] + public void InnerImplicitExpressionAcceptsTrailingNewlineInDesignTimeMode() + { + ParseBlockTest("{@foo." + Environment.NewLine + + "}", + new StatementBlock( + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.EmptyCSharp() + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.Code("foo.").AsImplicitExpression(KeywordSet, acceptTrailingDot: true).Accepts(AcceptedCharacters.NonWhiteSpace)), + Factory.Code(Environment.NewLine).AsStatement(), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + designTime: true); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpWhitespaceHandlingTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpWhitespaceHandlingTest.cs new file mode 100644 index 0000000000..718436c3f6 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CSharpWhitespaceHandlingTest.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; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class CSharpWhitespaceHandlingTest : CsHtmlMarkupParserTestBase + { + [Fact] + public void StatementBlockDoesNotAcceptTrailingNewlineIfNewlinesAreSignificantToAncestor() + { + ParseBlockTest("@: @if (true) { }" + Environment.NewLine + + "}", + new MarkupBlock( + Factory.MarkupTransition() + .Accepts(AcceptedCharacters.None), + Factory.MetaMarkup(":", HtmlSymbolType.Colon), + Factory.Markup(" ") + .With(new SpanEditHandler( + CSharpLanguageCharacteristics.Instance.TokenizeString, + AcceptedCharacters.Any)), + new StatementBlock( + Factory.CodeTransition() + .Accepts(AcceptedCharacters.None), + Factory.Code("if (true) { }") + .AsStatement() + ), + Factory.Markup(Environment.NewLine) + .Accepts(AcceptedCharacters.None))); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CodeParserTestBase.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CodeParserTestBase.cs new file mode 100644 index 0000000000..34650fc33b --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CodeParserTestBase.cs @@ -0,0 +1,75 @@ +// 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.Legacy +{ + internal abstract class CodeParserTestBase : ParserTestBase + { + protected abstract ISet KeywordSet { get; } + + protected override RazorSyntaxTree ParseBlock(string document, bool designTime) + { + return ParseCodeBlock(document, designTime); + } + + protected void ImplicitExpressionTest(string input, params RazorError[] errors) + { + ImplicitExpressionTest(input, AcceptedCharacters.NonWhiteSpace, errors); + } + + protected void ImplicitExpressionTest(string input, AcceptedCharacters acceptedCharacters, params RazorError[] errors) + { + ImplicitExpressionTest(input, input, acceptedCharacters, errors); + } + + protected void ImplicitExpressionTest(string input, string expected, params RazorError[] errors) + { + ImplicitExpressionTest(input, expected, AcceptedCharacters.NonWhiteSpace, errors); + } + + protected override void SingleSpanBlockTest(string document, BlockType blockType, SpanKind spanType, AcceptedCharacters acceptedCharacters = AcceptedCharacters.Any) + { + SingleSpanBlockTest(document, blockType, spanType, acceptedCharacters, expectedError: null); + } + + protected override void SingleSpanBlockTest(string document, string spanContent, BlockType blockType, SpanKind spanType, AcceptedCharacters acceptedCharacters = AcceptedCharacters.Any) + { + SingleSpanBlockTest(document, spanContent, blockType, spanType, acceptedCharacters, expectedErrors: null); + } + + protected override void SingleSpanBlockTest(string document, BlockType blockType, SpanKind spanType, params RazorError[] expectedError) + { + SingleSpanBlockTest(document, document, blockType, spanType, expectedError); + } + + protected override void SingleSpanBlockTest(string document, string spanContent, BlockType blockType, SpanKind spanType, params RazorError[] expectedErrors) + { + SingleSpanBlockTest(document, spanContent, blockType, spanType, AcceptedCharacters.Any, expectedErrors ?? new RazorError[0]); + } + + protected override void SingleSpanBlockTest(string document, BlockType blockType, SpanKind spanType, AcceptedCharacters acceptedCharacters, params RazorError[] expectedError) + { + SingleSpanBlockTest(document, document, blockType, spanType, acceptedCharacters, expectedError); + } + + protected override void SingleSpanBlockTest(string document, string spanContent, BlockType blockType, SpanKind spanType, AcceptedCharacters acceptedCharacters, params RazorError[] expectedErrors) + { + var b = CreateSimpleBlockAndSpan(spanContent, blockType, spanType, acceptedCharacters); + ParseBlockTest(document, b, expectedErrors ?? new RazorError[0]); + } + + protected void ImplicitExpressionTest(string input, string expected, AcceptedCharacters acceptedCharacters, params RazorError[] errors) + { + var factory = CreateSpanFactory(); + ParseBlockTest(SyntaxConstants.TransitionString + input, + new ExpressionBlock( + factory.CodeTransition(), + factory.Code(expected) + .AsImplicitExpression(KeywordSet) + .Accepts(acceptedCharacters)), + errors); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CsHtmlCodeParserTestBase.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CsHtmlCodeParserTestBase.cs new file mode 100644 index 0000000000..0e7586ed6c --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CsHtmlCodeParserTestBase.cs @@ -0,0 +1,20 @@ +// 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.Legacy +{ + internal abstract class CsHtmlCodeParserTestBase : CodeParserTestBase + { + protected override ISet KeywordSet + { + get { return CSharpCodeParser.DefaultKeywords; } + } + + protected override BlockFactory CreateBlockFactory() + { + return new BlockFactory(Factory ?? CreateSpanFactory()); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CsHtmlMarkupParserTestBase.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CsHtmlMarkupParserTestBase.cs new file mode 100644 index 0000000000..e92ea0a374 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/CsHtmlMarkupParserTestBase.cs @@ -0,0 +1,20 @@ +// 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.Legacy +{ + internal abstract class CsHtmlMarkupParserTestBase : MarkupParserTestBase + { + protected override ISet KeywordSet + { + get { return CSharpCodeParser.DefaultKeywords; } + } + + protected override BlockFactory CreateBlockFactory() + { + return new BlockFactory(Factory ?? CreateSpanFactory()); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/DisposableActionTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/DisposableActionTest.cs new file mode 100644 index 0000000000..6044b34568 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/DisposableActionTest.cs @@ -0,0 +1,25 @@ +// 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.Legacy +{ + internal class DisposableActionTest + { + [Fact] + public void ActionIsExecutedOnDispose() + { + // Arrange + var called = false; + var action = new DisposableAction(() => { called = true; }); + + // Act + action.Dispose(); + + // Assert + Assert.True(called, "The action was not run when the DisposableAction was disposed"); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/ErrorCollector.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/ErrorCollector.cs new file mode 100644 index 0000000000..31cc9afd0a --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/ErrorCollector.cs @@ -0,0 +1,57 @@ +// 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.Text; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class ErrorCollector + { + private StringBuilder _message = new StringBuilder(); + private int _indent = 0; + + public bool Success { get; private set; } + + public string Message + { + get { return _message.ToString(); } + } + + public ErrorCollector() + { + Success = true; + } + + public void AddError(string msg, params object[] args) + { + Append("F", msg, args); + Success = false; + } + + public void AddMessage(string msg, params object[] args) + { + Append("P", msg, args); + } + + public IDisposable Indent() + { + _indent++; + return new DisposableAction(Unindent); + } + + public void Unindent() + { + _indent--; + } + + private void Append(string prefix, string msg, object[] args) + { + _message.Append(prefix); + _message.Append(":"); + _message.Append(new String('\t', _indent)); + _message.AppendFormat(msg, args); + _message.AppendLine(); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/ExceptionHelpers.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/ExceptionHelpers.cs new file mode 100644 index 0000000000..982bce0b72 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/ExceptionHelpers.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; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + public static class ExceptionHelpers + { + public static void ValidateArgumentException(string parameterName, string expectedMessage, ArgumentException exception) + { + Assert.Equal(string.Format("{0}{1}Parameter name: {2}", expectedMessage, Environment.NewLine, parameterName), exception.Message); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/HtmlBlockTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/HtmlBlockTest.cs new file mode 100644 index 0000000000..638b5f754f --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/HtmlBlockTest.cs @@ -0,0 +1,639 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class HtmlBlockTest : CsHtmlMarkupParserTestBase + { + [Fact] + public void ParseBlockHandlesOpenAngleAtEof() + { + ParseDocumentTest("@{" + Environment.NewLine + + "<", + new MarkupBlock( + Factory.EmptyHtml(), + new StatementBlock( + Factory.CodeTransition(), + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.Code(Environment.NewLine) + .AsStatement() + .AutoCompleteWith("}"), + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("<"))))), + new RazorError( + LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF( + LegacyResources.BlockName_Code, "}", "{"), + new SourceLocation(1, 0, 1), + length: 1)); + } + + [Fact] + public void ParseBlockHandlesOpenAngleWithProperTagFollowingIt() + { + ParseDocumentTest("@{" + Environment.NewLine + + "<" + Environment.NewLine + + "", + new MarkupBlock( + Factory.EmptyHtml(), + new StatementBlock( + Factory.CodeTransition(), + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.Code(Environment.NewLine) + .AsStatement() + .AutoCompleteWith("}"), + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("<" + Environment.NewLine)) + ), + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)) + ), + Factory.EmptyCSharp().AsStatement() + ) + ), + designTime: true, + expectedErrors: new[] + { + new RazorError( + LegacyResources.FormatParseError_UnexpectedEndTag("html"), + new SourceLocation(5 + Environment.NewLine.Length * 2, 2, 2), + length: 4), + new RazorError( + LegacyResources.FormatParseError_Expected_EndOfBlock_Before_EOF("code", "}", "{"), + new SourceLocation(1, 0, 1), + length: 1) + }); + } + + [Fact] + public void TagWithoutCloseAngleDoesNotTerminateBlock() + { + ParseBlockTest("< " + Environment.NewLine + + " ", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup($"< {Environment.NewLine} "))), + designTime: true, + expectedErrors: new RazorError( + LegacyResources.FormatParseError_UnfinishedTag(string.Empty), + new SourceLocation(1, 0, 1), + length: 1)); + } + + [Fact] + public void ParseBlockAllowsStartAndEndTagsToDifferInCase() + { + ParseBlockTest("
  • Foo

  • ", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("
  • ").Accepts(AcceptedCharacters.None)), + new MarkupTagBlock( + Factory.Markup("

    ").Accepts(AcceptedCharacters.None)), + Factory.Markup("Foo"), + new MarkupTagBlock( + Factory.Markup("

    ").Accepts(AcceptedCharacters.None)), + new MarkupTagBlock( + Factory.Markup("
  • ").Accepts(AcceptedCharacters.None)) + )); + } + + [Fact] + public void ParseBlockReadsToEndOfLineIfFirstCharacterAfterTransitionIsColon() + { + ParseBlockTest("@:
  • Foo Bar Baz" + Environment.NewLine + + "bork", + new MarkupBlock( + Factory.MarkupTransition(), + Factory.MetaMarkup(":", HtmlSymbolType.Colon), + Factory.Markup("
  • Foo Bar Baz" + Environment.NewLine) + .With(new SpanEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString, AcceptedCharacters.None)) + )); + } + + [Fact] + public void ParseBlockStopsParsingSingleLineBlockAtEOFIfNoEOLReached() + { + ParseBlockTest("@:foo bar", + new MarkupBlock( + Factory.MarkupTransition(), + Factory.MetaMarkup(":", HtmlSymbolType.Colon), + Factory.Markup(@"foo bar") + .With(new SpanEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString)) + )); + } + + [Fact] + public void ParseBlockStopsAtMatchingCloseTagToStartTag() + { + ParseBlockTest("", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)) + )); + } + + [Fact] + public void ParseBlockParsesUntilMatchingEndTagIfFirstNonWhitespaceCharacterIsStartTag() + { + ParseBlockTest("", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)) + )); + } + + [Fact] + public void ParseBlockAllowsUnclosedTagsAsLongAsItCanRecoverToAnExpectedEndTag() + { + ParseBlockTest("", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)) + )); + } + + [Fact] + public void ParseBlockWithSelfClosingTagJustEmitsTag() + { + ParseBlockTest("", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)) + )); + } + + [Fact] + public void ParseBlockCanHandleSelfClosingTagsWithinBlock() + { + ParseBlockTest("", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)) + )); + } + + [Fact] + public void ParseBlockSupportsTagsWithAttributes() + { + ParseBlockTest("", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("(" bar=\"", 4, 0, 4), new LocationTagged("\"", 13, 0, 13)), + Factory.Markup(" bar=\"").With(SpanChunkGenerator.Null), + Factory.Markup("baz").With(new LiteralAttributeChunkGenerator(new LocationTagged(string.Empty, 10, 0, 10), new LocationTagged("baz", 10, 0, 10))), + Factory.Markup("\"").With(SpanChunkGenerator.Null)), + Factory.Markup(">").Accepts(AcceptedCharacters.None)), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + new MarkupTagBlock( + Factory.Markup("(" zoop=", 24, 0, 24), new LocationTagged(string.Empty, 34, 0, 34)), + Factory.Markup(" zoop=").With(SpanChunkGenerator.Null), + Factory.Markup("zork").With(new LiteralAttributeChunkGenerator(new LocationTagged(string.Empty, 30, 0, 30), new LocationTagged("zork", 30, 0, 30)))), + Factory.Markup("/>").Accepts(AcceptedCharacters.None)), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)))); + } + + [Fact] + public void ParseBlockAllowsCloseAngleBracketInAttributeValueIfDoubleQuoted() + { + ParseBlockTest("\" />", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + new MarkupTagBlock( + Factory.Markup("(" baz=\"", 9, 0, 9), new LocationTagged("\"", 16, 0, 16)), + Factory.Markup(" baz=\"").With(SpanChunkGenerator.Null), + Factory.Markup(">").With(new LiteralAttributeChunkGenerator(new LocationTagged(string.Empty, 15, 0, 15), new LocationTagged(">", 15, 0, 15))), + Factory.Markup("\"").With(SpanChunkGenerator.Null)), + Factory.Markup(" />").Accepts(AcceptedCharacters.None)), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)))); + } + + [Fact] + public void ParseBlockAllowsCloseAngleBracketInAttributeValueIfSingleQuoted() + { + ParseBlockTest("\' />", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + new MarkupTagBlock( + Factory.Markup("(" baz='", 9, 0, 9), new LocationTagged("'", 16, 0, 16)), + Factory.Markup(" baz='").With(SpanChunkGenerator.Null), + Factory.Markup(">").With(new LiteralAttributeChunkGenerator(new LocationTagged(string.Empty, 15, 0, 15), new LocationTagged(">", 15, 0, 15))), + Factory.Markup("'").With(SpanChunkGenerator.Null)), + Factory.Markup(" />").Accepts(AcceptedCharacters.None)), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)))); + } + + [Fact] + public void ParseBlockAllowsSlashInAttributeValueIfDoubleQuoted() + { + ParseBlockTest("", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + new MarkupTagBlock( + Factory.Markup("(" baz=\"", 9, 0, 9), new LocationTagged("\"", 16, 0, 16)), + Factory.Markup(" baz=\"").With(SpanChunkGenerator.Null), + Factory.Markup("/").With(new LiteralAttributeChunkGenerator(new LocationTagged(string.Empty, 15, 0, 15), new LocationTagged("/", 15, 0, 15))), + Factory.Markup("\"").With(SpanChunkGenerator.Null)), + Factory.Markup(">").Accepts(AcceptedCharacters.None)), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)))); + } + + [Fact] + public void ParseBlockAllowsSlashInAttributeValueIfSingleQuoted() + { + ParseBlockTest("", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + new MarkupTagBlock( + Factory.Markup("(" baz='", 9, 0, 9), new LocationTagged("'", 16, 0, 16)), + Factory.Markup(" baz='").With(SpanChunkGenerator.Null), + Factory.Markup("/").With(new LiteralAttributeChunkGenerator(new LocationTagged(string.Empty, 15, 0, 15), new LocationTagged("/", 15, 0, 15))), + Factory.Markup("'").With(SpanChunkGenerator.Null)), + Factory.Markup(">").Accepts(AcceptedCharacters.None)), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)))); + } + + [Fact] + public void ParseBlockTerminatesAtEOF() + { + ParseBlockTest("", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None))), + new RazorError( + LegacyResources.FormatParseError_MissingEndTag("foo"), + new SourceLocation(1, 0, 1), + length: 3)); + } + + [Fact] + public void ParseBlockSupportsCommentAsBlock() + { + SingleSpanBlockTest("", BlockType.Markup, SpanKind.Markup, acceptedCharacters: AcceptedCharacters.None); + } + + [Fact] + public void ParseBlockSupportsCommentWithinBlock() + { + ParseBlockTest("barbaz", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + Factory.Markup("bar"), + Factory.Markup("").Accepts(AcceptedCharacters.None), + Factory.Markup("baz"), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)))); + } + + public static TheoryData HtmlCommentSupportsMultipleDashesData + { + get + { + var factory = new SpanFactory(); + + return new TheoryData + { + { + "
    ", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("
    ").Accepts(AcceptedCharacters.None)), + factory.Markup("").Accepts(AcceptedCharacters.None), + new MarkupTagBlock( + factory.Markup("
    ").Accepts(AcceptedCharacters.None))) + }, + { + "
    ", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("
    ").Accepts(AcceptedCharacters.None)), + factory.Markup("").Accepts(AcceptedCharacters.None), + new MarkupTagBlock( + factory.Markup("
    ").Accepts(AcceptedCharacters.None))) + }, + { + "
    ", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("
    ").Accepts(AcceptedCharacters.None)), + factory.Markup("").Accepts(AcceptedCharacters.None), + new MarkupTagBlock( + factory.Markup("
    ").Accepts(AcceptedCharacters.None))) + }, + { + "
    ", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("
    ").Accepts(AcceptedCharacters.None)), + factory.Markup("").Accepts(AcceptedCharacters.None), + new MarkupTagBlock( + factory.Markup("
    ").Accepts(AcceptedCharacters.None))) + }, + }; + } + } + + [Theory] + [MemberData(nameof(HtmlCommentSupportsMultipleDashesData))] + public void HtmlCommentSupportsMultipleDashes(string documentContent, MarkupBlock expectedOutput) + { + ParseBlockTest(documentContent, expectedOutput); + } + + + [Fact] + public void ParseBlockProperlyBalancesCommentStartAndEndTags() + { + SingleSpanBlockTest("", BlockType.Markup, SpanKind.Markup, acceptedCharacters: AcceptedCharacters.None); + } + + [Fact] + public void ParseBlockTerminatesAtEOFWhenParsingComment() + { + SingleSpanBlockTest("", BlockType.Markup, SpanKind.Markup, acceptedCharacters: AcceptedCharacters.None); + } + + [Fact] + public void ParseBlockTerminatesCommentAtFirstOccurrenceOfEndSequence() + { + ParseBlockTest("-->", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + Factory.Markup("").Accepts(AcceptedCharacters.None), + Factory.Markup("-->"), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)))); + } + + [Fact] + public void ParseBlockTreatsMalformedTagsAsContent() + { + ParseBlockTest("", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None))), + new RazorError( + LegacyResources.FormatParseError_MissingEndTag("foo"), + new SourceLocation(1, 0, 1), + length: 3)); + } + + + [Fact] + public void ParseBlockParsesSGMLDeclarationAsEmptyTag() + { + ParseBlockTest("", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + Factory.Markup("").Accepts(AcceptedCharacters.None), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)))); + } + + [Fact] + public void ParseBlockTerminatesSGMLDeclarationAtFirstCloseAngle() + { + ParseBlockTest(" baz>", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + Factory.Markup("").Accepts(AcceptedCharacters.None), + Factory.Markup(" baz>"), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)))); + } + + [Fact] + public void ParseBlockParsesXMLProcessingInstructionAsEmptyTag() + { + ParseBlockTest("", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + Factory.Markup("").Accepts(AcceptedCharacters.None), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)))); + } + + [Fact] + public void ParseBlockTerminatesXMLProcessingInstructionAtQuestionMarkCloseAnglePair() + { + ParseBlockTest(" baz", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + Factory.Markup("").Accepts(AcceptedCharacters.None), + Factory.Markup(" baz"), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)))); + } + + [Fact] + public void ParseBlockDoesNotTerminateXMLProcessingInstructionAtCloseAngleUnlessPreceededByQuestionMark() + { + ParseBlockTest(" baz?>", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + Factory.Markup(" baz?>").Accepts(AcceptedCharacters.None), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)))); + } + + [Fact] + public void ParseBlockSupportsScriptTagsWithLessThanSignsInThem() + { + ParseBlockTest(@"", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)))); + } + + [Fact] + public void ParseBlockSupportsScriptTagsWithSpacedLessThanSignsInThem() + { + ParseBlockTest(@"", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)))); + } + + [Fact] + public void ParseBlockAcceptsEmptyTextTag() + { + ParseBlockTest("", + new MarkupBlock( + new MarkupTagBlock( + Factory.MarkupTransition("")) + )); + } + + [Fact] + public void ParseBlockAcceptsTextTagAsOuterTagButDoesNotRender() + { + ParseBlockTest("Foo Bar Baz zoop", + new MarkupBlock( + new MarkupTagBlock( + Factory.MarkupTransition("")), + Factory.Markup("Foo Bar ").Accepts(AcceptedCharacters.None), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + Factory.Markup(" Baz"), + new MarkupTagBlock( + Factory.MarkupTransition("")))); + } + + [Fact] + public void ParseBlockRendersLiteralTextTagIfDoubled() + { + ParseBlockTest("Foo Bar Baz zoop", + new MarkupBlock( + new MarkupTagBlock( + Factory.MarkupTransition("")), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + Factory.Markup("Foo Bar "), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + Factory.Markup(" Baz"), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + new MarkupTagBlock( + Factory.MarkupTransition("")))); + } + + [Fact] + public void ParseBlockDoesNotConsiderPsuedoTagWithinMarkupBlock() + { + ParseBlockTest("", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)) + )); + } + + [Fact] + public void ParseBlockStopsParsingMidEmptyTagIfEOFReached() + { + ParseBlockTest("
    Foo @if(true) {} Bar", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("
    ").Accepts(AcceptedCharacters.None)), + Factory.Markup("Foo "), + new StatementBlock( + Factory.CodeTransition(), + Factory.Code("if(true) {}").AsStatement()), + Factory.Markup(" Bar"), + new MarkupTagBlock( + Factory.Markup("
    ").Accepts(AcceptedCharacters.None)))); + } + + [Fact] + public void ParseBlockIgnoresTagsInContentsOfScriptTag() + { + ParseBlockTest(@"", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)))); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/HtmlDocumentTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/HtmlDocumentTest.cs new file mode 100644 index 0000000000..e36ad1fead --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/HtmlDocumentTest.cs @@ -0,0 +1,792 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class HtmlDocumentTest : CsHtmlMarkupParserTestBase + { + private static readonly TestFile Nested1000 = TestFile.Create("TestFiles/nested-1000.html"); + + [Fact] + public void ParseDocument_NestedCodeBlockWithMarkupSetsDotAsMarkup() + { + ParseDocumentTest("@if (true) { @if(false) {
    @something.
    } }", + new MarkupBlock( + Factory.EmptyHtml(), + new StatementBlock( + Factory.CodeTransition(), + Factory.Code("if (true) { ").AsStatement(), + new StatementBlock( + Factory.CodeTransition(), + Factory.Code("if(false) {").AsStatement(), + new MarkupBlock( + Factory.Markup(" "), + BlockFactory.MarkupTagBlock("
    ", AcceptedCharacters.None), + Factory.EmptyHtml(), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.Code("something") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: false) + .Accepts(AcceptedCharacters.NonWhiteSpace)), + Factory.Markup("."), + BlockFactory.MarkupTagBlock("
    ", AcceptedCharacters.None), + Factory.Markup(" ").Accepts(AcceptedCharacters.None)), + Factory.Code("}").AsStatement()), + Factory.Code(" }").AsStatement()))); + } + + [Fact] + public void ParseDocumentOutputsEmptyBlockWithEmptyMarkupSpanIfContentIsEmptyString() + { + ParseDocumentTest(string.Empty, new MarkupBlock(Factory.EmptyHtml())); + } + + [Fact] + public void ParseDocumentOutputsWhitespaceOnlyContentAsSingleWhitespaceMarkupSpan() + { + SingleSpanDocumentTest(" ", BlockType.Markup, SpanKind.Markup); + } + + [Fact] + public void ParseDocumentAcceptsSwapTokenAtEndOfFileAndOutputsZeroLengthCodeSpan() + { + ParseDocumentTest("@", + new MarkupBlock( + Factory.EmptyHtml(), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.EmptyCSharp() + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace)), + Factory.EmptyHtml()), + new RazorError( + LegacyResources.ParseError_Unexpected_EndOfFile_At_Start_Of_CodeBlock, + new SourceLocation(1, 0, 1), + length: 1)); + } + + [Fact] + public void ParseDocumentCorrectlyHandlesOddlySpacedHTMLElements() + { + ParseDocumentTest("

    Foo

    ", + new MarkupBlock( + BlockFactory.MarkupTagBlock("
    "), + new MarkupTagBlock( + Factory.Markup("(" class = '", 8, 0, 8), suffix: new LocationTagged("'", 21, 0, 21)), + Factory.Markup(" class = '").With(SpanChunkGenerator.Null), + Factory.Markup("bar").With(new LiteralAttributeChunkGenerator(prefix: new LocationTagged(string.Empty, 18, 0, 18), value: new LocationTagged("bar", 18, 0, 18))), + Factory.Markup("'").With(SpanChunkGenerator.Null)), + Factory.Markup(">")), + Factory.Markup(" Foo "), + BlockFactory.MarkupTagBlock("

    "), + BlockFactory.MarkupTagBlock("
    "))); + } + + [Fact] + public void ParseDocumentCorrectlyHandlesSingleLineOfMarkupWithEmbeddedStatement() + { + ParseDocumentTest("
    Foo @if(true) {} Bar
    ", + new MarkupBlock( + BlockFactory.MarkupTagBlock("
    "), + Factory.Markup("Foo "), + new StatementBlock( + Factory.CodeTransition(), + Factory.Code("if(true) {}").AsStatement()), + Factory.Markup(" Bar"), + BlockFactory.MarkupTagBlock("
    "))); + } + + [Fact] + public void ParseDocumentWithinSectionDoesNotCreateDocumentLevelSpan() + { + ParseDocumentTest("@section Foo {" + Environment.NewLine + + " " + Environment.NewLine + + "}", + new MarkupBlock( + Factory.EmptyHtml(), + new SectionBlock(new SectionChunkGenerator("Foo"), + Factory.CodeTransition(), + Factory.MetaCode("section Foo {") + .AutoCompleteWith(null, atEndOfSpan: true), + new MarkupBlock( + Factory.Markup(Environment.NewLine + " "), + BlockFactory.MarkupTagBlock(""), + BlockFactory.MarkupTagBlock(""), + Factory.Markup(Environment.NewLine)), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + Factory.EmptyHtml())); + } + + [Fact] + public void ParseDocumentParsesWholeContentAsOneSpanIfNoSwapCharacterEncountered() + { + SingleSpanDocumentTest("foo baz", BlockType.Markup, SpanKind.Markup); + } + + [Fact] + public void ParseDocumentHandsParsingOverToCodeParserWhenAtSignEncounteredAndEmitsOutput() + { + ParseDocumentTest("foo @bar baz", + new MarkupBlock( + Factory.Markup("foo "), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.Code("bar") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace)), + Factory.Markup(" baz"))); + } + + [Fact] + public void ParseDocumentEmitsAtSignAsMarkupIfAtEndOfFile() + { + ParseDocumentTest("foo @", + new MarkupBlock( + Factory.Markup("foo "), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.EmptyCSharp() + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace)), + Factory.EmptyHtml()), + new RazorError( + LegacyResources.ParseError_Unexpected_EndOfFile_At_Start_Of_CodeBlock, + new SourceLocation(5, 0, 5), + length: 1)); + } + + [Fact] + public void ParseDocumentEmitsCodeBlockIfFirstCharacterIsSwapCharacter() + { + ParseDocumentTest("@bar", + new MarkupBlock( + Factory.EmptyHtml(), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.Code("bar") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace)), + Factory.EmptyHtml())); + } + + [Fact] + public void ParseDocumentDoesNotSwitchToCodeOnEmailAddressInText() + { + SingleSpanDocumentTest("anurse@microsoft.com", BlockType.Markup, SpanKind.Markup); + } + + [Fact] + public void ParseDocumentDoesNotSwitchToCodeOnEmailAddressInAttribute() + { + ParseDocumentTest("Email me", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("(" href=\"", 2, 0, 2), new LocationTagged("\"", 36, 0, 36)), + Factory.Markup(" href=\"").With(SpanChunkGenerator.Null), + Factory.Markup("mailto:anurse@microsoft.com") + .With(new LiteralAttributeChunkGenerator(new LocationTagged(string.Empty, 9, 0, 9), new LocationTagged("mailto:anurse@microsoft.com", 9, 0, 9))), + Factory.Markup("\"").With(SpanChunkGenerator.Null)), + Factory.Markup(">")), + Factory.Markup("Email me"), + BlockFactory.MarkupTagBlock(""))); + } + + [Fact] + public void ParseDocumentDoesNotReturnErrorOnMismatchedTags() + { + ParseDocumentTest("Foo

    Baz", + new MarkupBlock( + Factory.Markup("Foo "), + BlockFactory.MarkupTagBlock("
    "), + BlockFactory.MarkupTagBlock("

    "), + BlockFactory.MarkupTagBlock("

    "), + BlockFactory.MarkupTagBlock("

    "), + Factory.Markup(" Baz"))); + } + + [Fact] + public void ParseDocumentReturnsOneMarkupSegmentIfNoCodeBlocksEncountered() + { + SingleSpanDocumentTest("Foo BazBar Bar", + new MarkupBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None), + Factory.Markup(" ").Accepts(AcceptedCharacters.None))); + } + + [Fact] + public void DocTypeTag() + { + ParseBlockTest(" foo", + new MarkupBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None), + Factory.Markup(" ").Accepts(AcceptedCharacters.None))); + } + + [Fact] + public void ProcessingInstructionTag() + { + ParseBlockTest(" foo", + new MarkupBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None), + Factory.Markup(" ").Accepts(AcceptedCharacters.None))); + } + + [Fact] + public void ElementTags() + { + ParseBlockTest("

    Foo

    Bar", + new MarkupBlock( + BlockFactory.MarkupTagBlock("

    ", AcceptedCharacters.None), + Factory.Markup("Foo"), + BlockFactory.MarkupTagBlock("

    ", AcceptedCharacters.None), + Factory.Markup(" ").Accepts(AcceptedCharacters.None))); + } + + [Fact] + public void TextTags() + { + ParseBlockTest("Foo}", + new MarkupBlock( + new MarkupTagBlock( + Factory.MarkupTransition("")), + Factory.Markup("Foo").Accepts(AcceptedCharacters.None), + new MarkupTagBlock( + Factory.MarkupTransition("")))); + } + + [Fact] + public void CDataTag() + { + ParseBlockTest(" Bar", + new MarkupBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None), + Factory.Markup(" ").Accepts(AcceptedCharacters.None))); + } + + [Fact] + public void ScriptTag() + { + ParseDocumentTest("", + new MarkupBlock( + BlockFactory.MarkupTagBlock(""))); + } + + [Fact] + public void ScriptTag_WithNestedMalformedTag() + { + ParseDocumentTest("", + new MarkupBlock( + BlockFactory.MarkupTagBlock(""))); + } + + [Fact] + public void ScriptTag_WithNestedEndTag() + { + ParseDocumentTest("", + new MarkupBlock( + BlockFactory.MarkupTagBlock(""))); + } + + [Fact] + public void ScriptTag_WithNestedBeginTag() + { + ParseDocumentTest("", + new MarkupBlock( + BlockFactory.MarkupTagBlock(""))); + } + + [Fact] + public void ScriptTag_WithNestedTag() + { + ParseDocumentTest("", + new MarkupBlock( + BlockFactory.MarkupTagBlock(""))); + } + + [Theory] + [MemberData("VoidElementNames")] + public void VoidElementFollowedByContent(string tagName) + { + ParseBlockTest("<" + tagName + ">foo", + new MarkupBlock( + BlockFactory.MarkupTagBlock("<" + tagName + ">", AcceptedCharacters.None))); + } + + [Theory] + [MemberData("VoidElementNames")] + public void VoidElementFollowedByOtherTag(string tagName) + { + ParseBlockTest("<" + tagName + ">foo", + new MarkupBlock( + BlockFactory.MarkupTagBlock("<" + tagName + ">", AcceptedCharacters.None))); + } + + [Theory] + [MemberData("VoidElementNames")] + public void VoidElementFollowedByCloseTag(string tagName) + { + ParseBlockTest("<" + tagName + "> foo", + new MarkupBlock( + BlockFactory.MarkupTagBlock("<" + tagName + ">", AcceptedCharacters.None), + Factory.Markup(" "), + BlockFactory.MarkupTagBlock("", AcceptedCharacters.None))); + } + + [Theory] + [MemberData("VoidElementNames")] + public void IncompleteVoidElementEndTag(string tagName) + { + ParseBlockTest("<" + tagName + ">", AcceptedCharacters.None), + BlockFactory.MarkupTagBlock("foo#@i

    ", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("

    ").Accepts(AcceptedCharacters.None)), + Factory.Markup("foo#"), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.Code("i").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace)), + new MarkupTagBlock( + Factory.Markup("

    ").Accepts(AcceptedCharacters.None)))); + } + + [Fact] + public void ParseBlockSwitchesToCodeWhenSwapCharacterEncounteredMidTag() + { + ParseBlockTest("", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)))); + } + + [Fact] + public void ParseBlockSwitchesToCodeWhenSwapCharacterEncounteredInAttributeValue() + { + ParseBlockTest("", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("(" bar=\"", 4, 0, 4), new LocationTagged("\"", 14, 0, 14)), + Factory.Markup(" bar=\"").With(SpanChunkGenerator.Null), + new MarkupBlock(new DynamicAttributeBlockChunkGenerator(new LocationTagged(string.Empty, 10, 0, 10), 10, 0, 10), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.Code("baz") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace))), + Factory.Markup("\"").With(SpanChunkGenerator.Null)), + Factory.Markup(" />").Accepts(AcceptedCharacters.None)))); + } + + [Fact] + public void ParseBlockSwitchesToCodeWhenSwapCharacterEncounteredInTagContent() + { + ParseBlockTest("@bar@boz", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + Factory.EmptyHtml(), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.Code("bar") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace)), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + Factory.EmptyHtml(), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.Code("boz") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace)), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)))); + } + + [Fact] + public void ParseBlockParsesCodeWithinSingleLineMarkup() + { + // TODO: Fix at a later date, HTML should be a tag block: https://github.com/aspnet/Razor/issues/101 + ParseBlockTest("@:
  • Foo @Bar Baz" + Environment.NewLine + + "bork", + new MarkupBlock( + Factory.MarkupTransition(), + Factory.MetaMarkup(":", HtmlSymbolType.Colon), + Factory.Markup("
  • Foo ").With(new SpanEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString)), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.Code("Bar") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace)), + Factory.Markup(" Baz" + Environment.NewLine) + .Accepts(AcceptedCharacters.None))); + } + + [Fact] + public void ParseBlockSupportsCodeWithinComment() + { + ParseBlockTest("", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + Factory.Markup("").Accepts(AcceptedCharacters.None), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)))); + } + + [Fact] + public void ParseBlockSupportsCodeWithinSGMLDeclaration() + { + ParseBlockTest("", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + Factory.Markup("").Accepts(AcceptedCharacters.None), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)))); + } + + [Fact] + public void ParseBlockSupportsCodeWithinCDataDeclaration() + { + ParseBlockTest("", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + Factory.Markup("").Accepts(AcceptedCharacters.None), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)))); + } + + [Fact] + public void ParseBlockSupportsCodeWithinXMLProcessingInstruction() + { + ParseBlockTest("", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + Factory.Markup("").Accepts(AcceptedCharacters.None), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)))); + } + + [Fact] + public void ParseBlockDoesNotSwitchToCodeOnEmailAddressInText() + { + ParseBlockTest("anurse@microsoft.com", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)), + Factory.Markup("anurse@microsoft.com"), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)))); + } + + [Fact] + public void ParseBlockDoesNotSwitchToCodeOnEmailAddressInAttribute() + { + ParseBlockTest("Email me", + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("(" href=\"", 2, 0, 2), new LocationTagged("\"", 36, 0, 36)), + Factory.Markup(" href=\"").With(SpanChunkGenerator.Null), + Factory.Markup("mailto:anurse@microsoft.com") + .With(new LiteralAttributeChunkGenerator(new LocationTagged(string.Empty, 9, 0, 9), new LocationTagged("mailto:anurse@microsoft.com", 9, 0, 9))), + Factory.Markup("\"").With(SpanChunkGenerator.Null)), + Factory.Markup(">").Accepts(AcceptedCharacters.None)), + Factory.Markup("Email me"), + new MarkupTagBlock( + Factory.Markup("").Accepts(AcceptedCharacters.None)))); + } + + [Fact] + public void ParseBlockGivesWhitespacePreceedingAtToCodeIfThereIsNoMarkupOnThatLine() + { + ParseBlockTest("
      " + Environment.NewLine + + " @foreach(var p in Products) {" + Environment.NewLine + + "
    • Product: @p.Name
    • " + Environment.NewLine + + " }" + Environment.NewLine + + "
    ", + new MarkupBlock( + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("
      ").Accepts(AcceptedCharacters.None)), + Factory.Markup(Environment.NewLine), + new StatementBlock( + Factory.Code(" ").AsStatement(), + Factory.CodeTransition(), + Factory.Code("foreach(var p in Products) {" + Environment.NewLine).AsStatement(), + new MarkupBlock( + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("
    • ").Accepts(AcceptedCharacters.None)), + Factory.Markup("Product: "), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.Code("p.Name") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace)), + new MarkupTagBlock( + Factory.Markup("
    • ").Accepts(AcceptedCharacters.None)), + Factory.Markup(Environment.NewLine).Accepts(AcceptedCharacters.None)), + Factory.Code(" }" + Environment.NewLine).AsStatement().Accepts(AcceptedCharacters.None)), + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("
    ").Accepts(AcceptedCharacters.None)))); + } + + [Fact] + public void ParseDocumentGivesWhitespacePreceedingAtToCodeIfThereIsNoMarkupOnThatLine() + { + ParseDocumentTest("
      " + Environment.NewLine + + " @foreach(var p in Products) {" + Environment.NewLine + + "
    • Product: @p.Name
    • " + Environment.NewLine + + " }" + Environment.NewLine + + "
    ", + new MarkupBlock( + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("
      ")), + Factory.Markup(Environment.NewLine), + new StatementBlock( + Factory.Code(" ").AsStatement(), + Factory.CodeTransition(), + Factory.Code("foreach(var p in Products) {" + Environment.NewLine).AsStatement(), + new MarkupBlock( + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("
    • ").Accepts(AcceptedCharacters.None)), + Factory.Markup("Product: "), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.Code("p.Name") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace)), + new MarkupTagBlock( + Factory.Markup("
    • ").Accepts(AcceptedCharacters.None)), + Factory.Markup(Environment.NewLine).Accepts(AcceptedCharacters.None)), + Factory.Code(" }" + Environment.NewLine).AsStatement().Accepts(AcceptedCharacters.None)), + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("
    ")))); + } + + [Fact] + public void SectionContextGivesWhitespacePreceedingAtToCodeIfThereIsNoMarkupOnThatLine() + { + ParseDocumentTest("@section foo {" + Environment.NewLine + + "
      " + Environment.NewLine + + " @foreach(var p in Products) {" + Environment.NewLine + + "
    • Product: @p.Name
    • " + Environment.NewLine + + " }" + Environment.NewLine + + "
    " + Environment.NewLine + + "}", + new MarkupBlock( + Factory.EmptyHtml(), + new SectionBlock(new SectionChunkGenerator("foo"), + Factory.CodeTransition(), + Factory.MetaCode("section foo {").AutoCompleteWith(null, atEndOfSpan: true), + new MarkupBlock( + Factory.Markup(Environment.NewLine + " "), + new MarkupTagBlock( + Factory.Markup("
      ")), + Factory.Markup(Environment.NewLine), + new StatementBlock( + Factory.Code(" ").AsStatement(), + Factory.CodeTransition(), + Factory.Code("foreach(var p in Products) {" + Environment.NewLine).AsStatement(), + new MarkupBlock( + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("
    • ").Accepts(AcceptedCharacters.None)), + Factory.Markup("Product: "), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.Code("p.Name") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace)), + new MarkupTagBlock( + Factory.Markup("
    • ").Accepts(AcceptedCharacters.None)), + Factory.Markup(Environment.NewLine).Accepts(AcceptedCharacters.None)), + Factory.Code(" }" + Environment.NewLine).AsStatement().Accepts(AcceptedCharacters.None)), + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("
    ")), + Factory.Markup(Environment.NewLine)), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + Factory.EmptyHtml())); + } + + [Fact] + public void CSharpCodeParserDoesNotAcceptLeadingOrTrailingWhitespaceInDesignMode() + { + ParseBlockTest("
      " + Environment.NewLine + + " @foreach(var p in Products) {" + Environment.NewLine + + "
    • Product: @p.Name
    • " + Environment.NewLine + + " }" + Environment.NewLine + + "
    ", + new MarkupBlock( + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("
      ").Accepts(AcceptedCharacters.None)), + Factory.Markup(Environment.NewLine + " "), + new StatementBlock( + Factory.CodeTransition(), + Factory.Code($"foreach(var p in Products) {{{Environment.NewLine} ").AsStatement(), + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("
    • ").Accepts(AcceptedCharacters.None)), + Factory.Markup("Product: "), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.Code("p.Name").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace)), + new MarkupTagBlock( + Factory.Markup("
    • ").Accepts(AcceptedCharacters.None))), + Factory.Code(Environment.NewLine + " }").AsStatement().Accepts(AcceptedCharacters.None)), + Factory.Markup(Environment.NewLine + " "), + new MarkupTagBlock( + Factory.Markup("
    ").Accepts(AcceptedCharacters.None))), + designTime: true); + } + + // Tests for "@@" escape sequence: + [Fact] + public void ParseBlockTreatsTwoAtSignsAsEscapeSequence() + { + HtmlParserTestUtils.RunSingleAtEscapeTest(ParseBlockTest); + } + + [Fact] + public void ParseBlockTreatsPairsOfAtSignsAsEscapeSequence() + { + HtmlParserTestUtils.RunMultiAtEscapeTest(ParseBlockTest); + } + + [Fact] + public void ParseDocumentTreatsTwoAtSignsAsEscapeSequence() + { + HtmlParserTestUtils.RunSingleAtEscapeTest(ParseDocumentTest, lastSpanAcceptedCharacters: AcceptedCharacters.Any); + } + + [Fact] + public void ParseDocumentTreatsPairsOfAtSignsAsEscapeSequence() + { + HtmlParserTestUtils.RunMultiAtEscapeTest(ParseDocumentTest, lastSpanAcceptedCharacters: AcceptedCharacters.Any); + } + + [Fact] + public void SectionBodyTreatsTwoAtSignsAsEscapeSequence() + { + ParseDocumentTest("@section Foo { @@bar }", + new MarkupBlock( + Factory.EmptyHtml(), + new SectionBlock(new SectionChunkGenerator("Foo"), + Factory.CodeTransition(), + Factory.MetaCode("section Foo {").AutoCompleteWith(null, atEndOfSpan: true), + new MarkupBlock( + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("")), + Factory.Markup("@").Hidden(), + Factory.Markup("@bar"), + new MarkupTagBlock( + Factory.Markup("")), + Factory.Markup(" ")), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + Factory.EmptyHtml())); + } + + [Fact] + public void SectionBodyTreatsPairsOfAtSignsAsEscapeSequence() + { + ParseDocumentTest("@section Foo { @@@@@bar }", + new MarkupBlock( + Factory.EmptyHtml(), + new SectionBlock(new SectionChunkGenerator("Foo"), + Factory.CodeTransition(), + Factory.MetaCode("section Foo {").AutoCompleteWith(null, atEndOfSpan: true), + new MarkupBlock( + Factory.Markup(" "), + new MarkupTagBlock( + Factory.Markup("")), + Factory.Markup("@").Hidden(), + Factory.Markup("@"), + Factory.Markup("@").Hidden(), + Factory.Markup("@"), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.Code("bar") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace)), + new MarkupTagBlock( + Factory.Markup("")), + Factory.Markup(" ")), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + Factory.EmptyHtml())); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/HtmlTokenizerTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/HtmlTokenizerTest.cs new file mode 100644 index 0000000000..a9872bdcd1 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/HtmlTokenizerTest.cs @@ -0,0 +1,160 @@ +// 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.Legacy +{ + internal class HtmlTokenizerTest : HtmlTokenizerTestBase + { + [Fact] + public void Next_Returns_Null_When_EOF_Reached() + { + TestTokenizer(""); + } + + [Fact] + public void Text_Is_Recognized() + { + TestTokenizer("foo-9309&smlkmb;::-3029022,.sdkq92384", + new HtmlSymbol(0, 0, 0, "foo-9309&smlkmb;::-3029022,.sdkq92384", HtmlSymbolType.Text)); + } + + [Fact] + public void Whitespace_Is_Recognized() + { + TestTokenizer(" \t\f ", + new HtmlSymbol(0, 0, 0, " \t\f ", HtmlSymbolType.WhiteSpace)); + } + + [Fact] + public void Newline_Is_Recognized() + { + TestTokenizer("\n\r\r\n", + new HtmlSymbol(0, 0, 0, "\n", HtmlSymbolType.NewLine), + new HtmlSymbol(1, 1, 0, "\r", HtmlSymbolType.NewLine), + new HtmlSymbol(2, 2, 0, "\r\n", HtmlSymbolType.NewLine)); + } + + [Fact] + public void Transition_Is_Not_Recognized_Mid_Text_If_Surrounded_By_Alphanumeric_Characters() + { + TestSingleToken("foo@bar", HtmlSymbolType.Text); + } + + [Fact] + public void OpenAngle_Is_Recognized() + { + TestSingleToken("<", HtmlSymbolType.OpenAngle); + } + + [Fact] + public void Bang_Is_Recognized() + { + TestSingleToken("!", HtmlSymbolType.Bang); + } + + [Fact] + public void Solidus_Is_Recognized() + { + TestSingleToken("/", HtmlSymbolType.ForwardSlash); + } + + [Fact] + public void QuestionMark_Is_Recognized() + { + TestSingleToken("?", HtmlSymbolType.QuestionMark); + } + + [Fact] + public void LeftBracket_Is_Recognized() + { + TestSingleToken("[", HtmlSymbolType.LeftBracket); + } + + [Fact] + public void CloseAngle_Is_Recognized() + { + TestSingleToken(">", HtmlSymbolType.CloseAngle); + } + + [Fact] + public void RightBracket_Is_Recognized() + { + TestSingleToken("]", HtmlSymbolType.RightBracket); + } + + [Fact] + public void Equals_Is_Recognized() + { + TestSingleToken("=", HtmlSymbolType.Equals); + } + + [Fact] + public void DoubleQuote_Is_Recognized() + { + TestSingleToken("\"", HtmlSymbolType.DoubleQuote); + } + + [Fact] + public void SingleQuote_Is_Recognized() + { + TestSingleToken("'", HtmlSymbolType.SingleQuote); + } + + [Fact] + public void Transition_Is_Recognized() + { + TestSingleToken("@", HtmlSymbolType.Transition); + } + + [Fact] + public void DoubleHyphen_Is_Recognized() + { + TestSingleToken("--", HtmlSymbolType.DoubleHyphen); + } + + [Fact] + public void SingleHyphen_Is_Not_Recognized() + { + TestSingleToken("-", HtmlSymbolType.Text); + } + + [Fact] + public void SingleHyphen_Mid_Text_Is_Not_Recognized_As_Separate_Token() + { + TestSingleToken("foo-bar", HtmlSymbolType.Text); + } + + [Fact] + public void Next_Ignores_Star_At_EOF_In_RazorComment() + { + TestTokenizer("@* Foo * Bar * Baz *", + new HtmlSymbol(0, 0, 0, "@", HtmlSymbolType.RazorCommentTransition), + new HtmlSymbol(1, 0, 1, "*", HtmlSymbolType.RazorCommentStar), + new HtmlSymbol(2, 0, 2, " Foo * Bar * Baz *", HtmlSymbolType.RazorComment)); + } + + [Fact] + public void Next_Ignores_Star_Without_Trailing_At() + { + TestTokenizer("@* Foo * Bar * Baz *@", + new HtmlSymbol(0, 0, 0, "@", HtmlSymbolType.RazorCommentTransition), + new HtmlSymbol(1, 0, 1, "*", HtmlSymbolType.RazorCommentStar), + new HtmlSymbol(2, 0, 2, " Foo * Bar * Baz ", HtmlSymbolType.RazorComment), + new HtmlSymbol(19, 0, 19, "*", HtmlSymbolType.RazorCommentStar), + new HtmlSymbol(20, 0, 20, "@", HtmlSymbolType.RazorCommentTransition)); + } + + [Fact] + public void Next_Returns_RazorComment_Token_For_Entire_Razor_Comment() + { + TestTokenizer("@* Foo Bar Baz *@", + new HtmlSymbol(0, 0, 0, "@", HtmlSymbolType.RazorCommentTransition), + new HtmlSymbol(1, 0, 1, "*", HtmlSymbolType.RazorCommentStar), + new HtmlSymbol(2, 0, 2, " Foo Bar Baz ", HtmlSymbolType.RazorComment), + new HtmlSymbol(15, 0, 15, "*", HtmlSymbolType.RazorCommentStar), + new HtmlSymbol(16, 0, 16, "@", HtmlSymbolType.RazorCommentTransition)); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/HtmlTokenizerTestBase.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/HtmlTokenizerTestBase.cs new file mode 100644 index 0000000000..57e86e6250 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/HtmlTokenizerTestBase.cs @@ -0,0 +1,25 @@ +// 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.Legacy +{ + internal abstract class HtmlTokenizerTestBase : TokenizerTestBase + { + private static HtmlSymbol _ignoreRemaining = new HtmlSymbol(0, 0, 0, string.Empty, HtmlSymbolType.Unknown); + + protected override HtmlSymbol IgnoreRemaining + { + get { return _ignoreRemaining; } + } + + protected override Tokenizer CreateTokenizer(ITextDocument source) + { + return new HtmlTokenizer(source); + } + + protected void TestSingleToken(string text, HtmlSymbolType expectedSymbolType) + { + TestTokenizer(text, new HtmlSymbol(0, 0, 0, text, expectedSymbolType)); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/LineTrackingStringBufferTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/LineTrackingStringBufferTest.cs new file mode 100644 index 0000000000..7c0aa66fad --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/LineTrackingStringBufferTest.cs @@ -0,0 +1,26 @@ +// 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.Legacy +{ + internal class LineTrackingStringBufferTest + { + [Fact] + public void CtorInitializesProperties() + { + var buffer = new LineTrackingStringBuffer(string.Empty); + Assert.Equal(0, buffer.Length); + } + + [Fact] + public void CharAtCorrectlyReturnsLocation() + { + var buffer = new LineTrackingStringBuffer("foo\rbar\nbaz\r\nbiz"); + LineTrackingStringBuffer.CharacterReference chr = buffer.CharAt(14); + Assert.Equal('i', chr.Character); + Assert.Equal(new SourceLocation(14, 3, 1), chr.Location); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/MarkupParserTestBase.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/MarkupParserTestBase.cs new file mode 100644 index 0000000000..b58b7e8ad4 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/MarkupParserTestBase.cs @@ -0,0 +1,20 @@ +// 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.Legacy +{ + internal abstract class MarkupParserTestBase : CodeParserTestBase + { + protected override RazorSyntaxTree ParseBlock(string document, bool designTime) + { + return ParseHtmlBlock(document, designTime); + } + + protected virtual void SingleSpanDocumentTest(string document, BlockType blockType, SpanKind spanType) + { + var b = CreateSimpleBlockAndSpan(document, blockType, spanType); + ParseDocumentTest(document, b); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/MiscUtils.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/MiscUtils.cs new file mode 100644 index 0000000000..544a539c3a --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/MiscUtils.cs @@ -0,0 +1,33 @@ +// 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; +#if DEBUG +using System.Diagnostics; +using System.Threading; +#endif +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + class MiscUtils + { + public const int TimeoutInSeconds = 1; + + public static void DoWithTimeoutIfNotDebugging(Func withTimeout) + { +#if DEBUG + if (Debugger.IsAttached) + { + withTimeout(Timeout.Infinite); + } + else + { +#endif + Assert.True(withTimeout((int)TimeSpan.FromSeconds(TimeoutInSeconds).TotalMilliseconds), "Timeout expired!"); +#if DEBUG + } +#endif + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/ParserTestBase.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/ParserTestBase.cs new file mode 100644 index 0000000000..4d5ecff4d1 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/ParserTestBase.cs @@ -0,0 +1,448 @@ +// 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.Diagnostics; +using System.Linq; +using System.Text; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal abstract class ParserTestBase + { + protected static Block IgnoreOutput = new IgnoreOutputBlock(); + + public SpanFactory Factory { get; private set; } + public BlockFactory BlockFactory { get; private set; } + + protected ParserTestBase() + { + Factory = CreateSpanFactory(); + BlockFactory = CreateBlockFactory(); + } + + protected abstract RazorSyntaxTree ParseBlock(string document, bool designTime); + + protected virtual RazorSyntaxTree ParseDocument(string document, bool designTime = false) + { + using (var reader = new SeekableTextReader(document)) + { + var parser = new RazorParser() + { + DesignTimeMode = designTime, + }; + + return parser.Parse((ITextDocument)reader); + } + } + + protected virtual RazorSyntaxTree ParseHtmlBlock(string document, bool designTime = false) + { + using (var reader = new SeekableTextReader(document)) + { + var context = new ParserContext(reader, designTime); + + var parser = new HtmlMarkupParser(context); + parser.CodeParser = new CSharpCodeParser(context) + { + HtmlParser = parser, + }; + + parser.ParseBlock(); + + var razorSyntaxTree = context.BuildRazorSyntaxTree(); + + return razorSyntaxTree; + } + } + + protected virtual RazorSyntaxTree ParseCodeBlock(string document, bool designTime = false) + { + using (var reader = new SeekableTextReader(document)) + { + var context = new ParserContext(reader, designTime); + + var parser = new CSharpCodeParser(context); + parser.HtmlParser = new HtmlMarkupParser(context) + { + CodeParser = parser, + }; + + parser.ParseBlock(); + + var razorSyntaxTree = context.BuildRazorSyntaxTree(); + + return razorSyntaxTree; + } + } + + protected SpanFactory CreateSpanFactory() + { + return new SpanFactory(); + } + + protected abstract BlockFactory CreateBlockFactory(); + + protected virtual void ParseBlockTest(string document) + { + ParseBlockTest(document, null, false, new RazorError[0]); + } + + protected virtual void ParseBlockTest(string document, bool designTime) + { + ParseBlockTest(document, null, designTime, new RazorError[0]); + } + + protected virtual void ParseBlockTest(string document, params RazorError[] expectedErrors) + { + ParseBlockTest(document, false, expectedErrors); + } + + protected virtual void ParseBlockTest(string document, bool designTime, params RazorError[] expectedErrors) + { + ParseBlockTest(document, null, designTime, expectedErrors); + } + + protected virtual void ParseBlockTest(string document, Block expectedRoot) + { + ParseBlockTest(document, expectedRoot, false, null); + } + + protected virtual void ParseBlockTest(string document, Block expectedRoot, bool designTime) + { + ParseBlockTest(document, expectedRoot, designTime, null); + } + + protected virtual void ParseBlockTest(string document, Block expectedRoot, params RazorError[] expectedErrors) + { + ParseBlockTest(document, expectedRoot, false, expectedErrors); + } + + protected virtual void ParseBlockTest(string document, Block expected, bool designTime, params RazorError[] expectedErrors) + { + var result = ParseBlock(document, designTime); + + if (!ReferenceEquals(expected, IgnoreOutput)) + { + EvaluateResults(result, expected, expectedErrors); + } + } + + protected virtual void SingleSpanBlockTest(string document, BlockType blockType, SpanKind spanType, AcceptedCharacters acceptedCharacters = AcceptedCharacters.Any) + { + SingleSpanBlockTest(document, blockType, spanType, acceptedCharacters, expectedError: null); + } + + protected virtual void SingleSpanBlockTest(string document, string spanContent, BlockType blockType, SpanKind spanType, AcceptedCharacters acceptedCharacters = AcceptedCharacters.Any) + { + SingleSpanBlockTest(document, spanContent, blockType, spanType, acceptedCharacters, expectedErrors: null); + } + + protected virtual void SingleSpanBlockTest(string document, BlockType blockType, SpanKind spanType, params RazorError[] expectedError) + { + SingleSpanBlockTest(document, document, blockType, spanType, expectedError); + } + + protected virtual void SingleSpanBlockTest(string document, string spanContent, BlockType blockType, SpanKind spanType, params RazorError[] expectedErrors) + { + SingleSpanBlockTest(document, spanContent, blockType, spanType, AcceptedCharacters.Any, expectedErrors ?? new RazorError[0]); + } + + protected virtual void SingleSpanBlockTest(string document, BlockType blockType, SpanKind spanType, AcceptedCharacters acceptedCharacters, params RazorError[] expectedError) + { + SingleSpanBlockTest(document, document, blockType, spanType, acceptedCharacters, expectedError); + } + + protected virtual void SingleSpanBlockTest(string document, string spanContent, BlockType blockType, SpanKind spanType, AcceptedCharacters acceptedCharacters, params RazorError[] expectedErrors) + { + var result = ParseBlock(document, designTime: false); + + var builder = new BlockBuilder(); + builder.Type = blockType; + var expected = ConfigureAndAddSpanToBlock(builder, Factory.Span(spanType, spanContent, spanType == SpanKind.Markup).Accepts(acceptedCharacters)); + + if (!ReferenceEquals(expected, IgnoreOutput)) + { + EvaluateResults(result, expected, expectedErrors); + } + } + + protected virtual void ParseDocumentTest(string document) + { + ParseDocumentTest(document, null, false); + } + + protected virtual void ParseDocumentTest(string document, Block expectedRoot) + { + ParseDocumentTest(document, expectedRoot, false, null); + } + + protected virtual void ParseDocumentTest(string document, Block expectedRoot, params RazorError[] expectedErrors) + { + ParseDocumentTest(document, expectedRoot, false, expectedErrors); + } + + protected virtual void ParseDocumentTest(string document, bool designTime) + { + ParseDocumentTest(document, null, designTime); + } + + protected virtual void ParseDocumentTest(string document, Block expectedRoot, bool designTime) + { + ParseDocumentTest(document, expectedRoot, designTime, null); + } + + protected virtual void ParseDocumentTest(string document, Block expectedRoot, bool designTime, params RazorError[] expectedErrors) + { + var result = ParseDocument(document, designTime); + + if (!ReferenceEquals(expectedRoot, IgnoreOutput)) + { + EvaluateResults(result, expectedRoot, expectedErrors); + } + } + + protected virtual RazorSyntaxTree RunParse( + string document, + Func parserActionSelector, + bool designTimeParser, + Func parserSelector = null, + ErrorSink errorSink = null) + { + throw null; + } + + protected virtual void RunParseTest( + string document, + Func parserActionSelector, + Block expectedRoot, IList expectedErrors, + bool designTimeParser, Func parserSelector = null) + { + throw null; + } + + [Conditional("PARSER_TRACE")] + private void WriteNode(int indent, SyntaxTreeNode node) + { + var content = node.ToString().Replace("\r", "\\r") + .Replace("\n", "\\n") + .Replace("{", "{{") + .Replace("}", "}}"); + if (indent > 0) + { + content = new String('.', indent * 2) + content; + } + WriteTraceLine(content); + var block = node as Block; + if (block != null) + { + foreach (SyntaxTreeNode child in block.Children) + { + WriteNode(indent + 1, child); + } + } + } + + public static void EvaluateResults(RazorSyntaxTree result, Block expectedRoot) + { + EvaluateResults(result, expectedRoot, null); + } + + public static void EvaluateResults(RazorSyntaxTree result, Block expectedRoot, IList expectedErrors) + { + EvaluateParseTree(result.Root, expectedRoot); + EvaluateRazorErrors(result.Diagnostics, expectedErrors); + } + + public static void EvaluateParseTree(Block actualRoot, Block expectedRoot) + { + // Evaluate the result + var collector = new ErrorCollector(); + + if (expectedRoot == null) + { + Assert.Null(actualRoot); + } + else + { + // Link all the nodes + expectedRoot.LinkNodes(); + Assert.NotNull(actualRoot); + EvaluateSyntaxTreeNode(collector, actualRoot, expectedRoot); + if (collector.Success) + { + WriteTraceLine("Parse Tree Validation Succeeded:" + Environment.NewLine + collector.Message); + } + else + { + Assert.True(false, Environment.NewLine + collector.Message); + } + } + } + + private static void EvaluateSyntaxTreeNode(ErrorCollector collector, SyntaxTreeNode actual, SyntaxTreeNode expected) + { + if (actual == null) + { + AddNullActualError(collector, actual, expected); + return; + } + + if (actual.IsBlock != expected.IsBlock) + { + AddMismatchError(collector, actual, expected); + } + else + { + if (expected.IsBlock) + { + EvaluateBlock(collector, (Block)actual, (Block)expected); + } + else + { + EvaluateSpan(collector, (Span)actual, (Span)expected); + } + } + } + + private static void EvaluateSpan(ErrorCollector collector, Span actual, Span expected) + { + if (!Equals(expected, actual)) + { + AddMismatchError(collector, actual, expected); + } + else + { + AddPassedMessage(collector, expected); + } + } + + private static void EvaluateBlock(ErrorCollector collector, Block actual, Block expected) + { + if (actual.Type != expected.Type || !expected.ChunkGenerator.Equals(actual.ChunkGenerator)) + { + AddMismatchError(collector, actual, expected); + } + else + { + AddPassedMessage(collector, expected); + using (collector.Indent()) + { + var expectedNodes = expected.Children.GetEnumerator(); + var actualNodes = actual.Children.GetEnumerator(); + while (expectedNodes.MoveNext()) + { + if (!actualNodes.MoveNext()) + { + collector.AddError("{0} - FAILED :: No more elements at this node", expectedNodes.Current); + } + else + { + EvaluateSyntaxTreeNode(collector, actualNodes.Current, expectedNodes.Current); + } + } + while (actualNodes.MoveNext()) + { + collector.AddError("End of Node - FAILED :: Found Node: {0}", actualNodes.Current); + } + } + } + } + + private static void AddPassedMessage(ErrorCollector collector, SyntaxTreeNode expected) + { + collector.AddMessage("{0} - PASSED", expected); + } + + private static void AddMismatchError(ErrorCollector collector, SyntaxTreeNode actual, SyntaxTreeNode expected) + { + collector.AddError("{0} - FAILED :: Actual: {1}", expected, actual); + } + + private static void AddNullActualError(ErrorCollector collector, SyntaxTreeNode actual, SyntaxTreeNode expected) + { + collector.AddError("{0} - FAILED :: Actual: << Null >>", expected); + } + + public static void EvaluateRazorErrors(IEnumerable actualErrors, IList expectedErrors) + { + var realCount = actualErrors.Count(); + + // Evaluate the errors + if (expectedErrors == null || expectedErrors.Count == 0) + { + Assert.True( + realCount == 0, + "Expected that no errors would be raised, but the following errors were:" + Environment.NewLine + FormatErrors(actualErrors)); + } + else + { + Assert.True( + expectedErrors.Count == realCount, + $"Expected that {expectedErrors.Count} errors would be raised, but {realCount} errors were." + + $"{Environment.NewLine}Expected Errors: {Environment.NewLine}{FormatErrors(expectedErrors)}" + + $"{Environment.NewLine}Actual Errors: {Environment.NewLine}{FormatErrors(actualErrors)}"); + Assert.Equal(expectedErrors, actualErrors); + } + WriteTraceLine("Expected Errors were raised:" + Environment.NewLine + FormatErrors(expectedErrors)); + } + + public static string FormatErrors(IEnumerable errors) + { + if (errors == null) + { + return "\t<< No Errors >>"; + } + + var builder = new StringBuilder(); + foreach (RazorError err in errors) + { + builder.AppendFormat("\t{0}", err); + builder.AppendLine(); + } + return builder.ToString(); + } + + [Conditional("PARSER_TRACE")] + private static void WriteTraceLine(string format, params object[] args) + { + Trace.WriteLine(string.Format(format, args)); + } + + protected virtual Block CreateSimpleBlockAndSpan(string spanContent, BlockType blockType, SpanKind spanType, AcceptedCharacters acceptedCharacters = AcceptedCharacters.Any) + { + var span = Factory.Span(spanType, spanContent, spanType == SpanKind.Markup).Accepts(acceptedCharacters); + var b = new BlockBuilder() + { + Type = blockType + }; + return ConfigureAndAddSpanToBlock(b, span); + } + + protected virtual Block ConfigureAndAddSpanToBlock(BlockBuilder block, SpanConstructor span) + { + switch (block.Type) + { + case BlockType.Markup: + span.With(new MarkupChunkGenerator()); + break; + case BlockType.Statement: + span.With(new StatementChunkGenerator()); + break; + case BlockType.Expression: + block.ChunkGenerator = new ExpressionChunkGenerator(); + span.With(new ExpressionChunkGenerator()); + break; + } + block.Children.Add(span); + return block.Build(); + } + + private class IgnoreOutputBlock : Block + { + public IgnoreOutputBlock() : base(BlockType.Template, new SyntaxTreeNode[0], null) { } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/RawTextSymbol.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/RawTextSymbol.cs new file mode 100644 index 0000000000..b0ab640a46 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/RawTextSymbol.cs @@ -0,0 +1,63 @@ +// 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.Globalization; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class RawTextSymbol : ISymbol + { + public SourceLocation Start { get; private set; } + public string Content { get; } + + public RawTextSymbol(SourceLocation start, string content) + { + Start = start; + Content = content; + } + + public override bool Equals(object obj) + { + var other = obj as RawTextSymbol; + return other != null && Equals(Start, other.Start) && Equals(Content, other.Content); + } + + internal bool EquivalentTo(ISymbol sym) + { + return Equals(Start, sym.Start) && Equals(Content, sym.Content); + } + + public override int GetHashCode() + { + // Hash code should include only immutable properties. + return Content == null ? 0 : Content.GetHashCode(); + } + + public void OffsetStart(SourceLocation documentStart) + { + Start = documentStart + Start; + } + + public void ChangeStart(SourceLocation newStart) + { + Start = newStart; + } + + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "{0} RAW - [{1}]", Start, Content); + } + + internal void CalculateStart(Span prev) + { + if (prev == null) + { + Start = SourceLocation.Zero; + } + else + { + Start = new SourceLocationTracker(prev.Start).UpdateLocation(prev.Content).CurrentLocation; + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/RazorErrorTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/RazorErrorTest.cs new file mode 100644 index 0000000000..5667f8e1d9 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/RazorErrorTest.cs @@ -0,0 +1,75 @@ +// 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 Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class RazorErrorTest + { + [Fact] + public void RazorError_CanBeSerialized() + { + // Arrange + var error = new RazorError( + message: "Testing", + location: new SourceLocation(absoluteIndex: 1, lineIndex: 2, characterIndex: 3), + length: 456); + var expectedSerializedError = + $"{{\"{nameof(RazorError.Message)}\":\"Testing\",\"{nameof(RazorError.Location)}\":{{\"" + + $"{nameof(SourceLocation.FilePath)}\":null,\"" + + $"{nameof(SourceLocation.AbsoluteIndex)}\":1,\"{nameof(SourceLocation.LineIndex)}\":2,\"" + + $"{nameof(SourceLocation.CharacterIndex)}\":3}},\"{nameof(RazorError.Length)}\":456}}"; + + // Act + var serializedError = JsonConvert.SerializeObject(error); + + // Assert + Assert.Equal(expectedSerializedError, serializedError, StringComparer.Ordinal); + } + + [Fact] + public void RazorError_WithFilePath_CanBeSerialized() + { + // Arrange + var error = new RazorError( + message: "Testing", + location: new SourceLocation("some-path", absoluteIndex: 1, lineIndex: 2, characterIndex: 56), + length: 3); + var expectedSerializedError = + $"{{\"{nameof(RazorError.Message)}\":\"Testing\",\"{nameof(RazorError.Location)}\":{{\"" + + $"{nameof(SourceLocation.FilePath)}\":\"some-path\",\"" + + $"{nameof(SourceLocation.AbsoluteIndex)}\":1,\"{nameof(SourceLocation.LineIndex)}\":2,\"" + + $"{nameof(SourceLocation.CharacterIndex)}\":56}},\"{nameof(RazorError.Length)}\":3}}"; + + // Act + var serializedError = JsonConvert.SerializeObject(error); + + // Assert + Assert.Equal(expectedSerializedError, serializedError, StringComparer.Ordinal); + } + + [Fact] + public void RazorError_CanBeDeserialized() + { + // Arrange + var error = new RazorError( + message: "Testing", + location: new SourceLocation("somepath", absoluteIndex: 1, lineIndex: 2, characterIndex: 3), + length: 456); + var serializedError = JsonConvert.SerializeObject(error); + + // Act + var deserializedError = JsonConvert.DeserializeObject(serializedError); + + // Assert + Assert.Equal("Testing", deserializedError.Message, StringComparer.Ordinal); + Assert.Equal(1, deserializedError.Location.AbsoluteIndex); + Assert.Equal(2, deserializedError.Location.LineIndex); + Assert.Equal(3, deserializedError.Location.CharacterIndex); + Assert.Equal(456, deserializedError.Length); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/RazorParserTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/RazorParserTest.cs new file mode 100644 index 0000000000..0bf60e5563 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/RazorParserTest.cs @@ -0,0 +1,66 @@ +// 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.IO; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class RazorParserTest + { + [Fact] + public void CanParseStuff() + { + + var parser = new RazorParser(); + var sourceDocument = TestRazorSourceDocument.CreateResource("TestFiles/Source/BasicMarkup.cshtml"); + var documentReader = sourceDocument.CreateReader(); + var output = parser.Parse(documentReader); + + Assert.NotNull(output); + } + + [Fact] + public void ParseMethodCallsParseDocumentOnMarkupParserAndReturnsResults() + { + var factory = new SpanFactory(); + + // Arrange + var parser = new RazorParser(); + + // Act/Assert + ParserTestBase.EvaluateResults(parser.Parse(new StringReader("foo @bar baz")), + new MarkupBlock( + factory.Markup("foo "), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("bar") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace)), + factory.Markup(" baz"))); + } + + [Fact] + public void ParseMethodUsesProvidedParserListenerIfSpecified() + { + var factory = new SpanFactory(); + + // Arrange + var parser = new RazorParser(); + + // Act + var results = parser.Parse(new StringReader("foo @bar baz")); + + // Assert + ParserTestBase.EvaluateResults(results, + new MarkupBlock( + factory.Markup("foo "), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("bar") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace)), + factory.Markup(" baz"))); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/SourceLocationTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/SourceLocationTest.cs new file mode 100644 index 0000000000..65dddb8d32 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/SourceLocationTest.cs @@ -0,0 +1,266 @@ +// 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.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class SourceLocationTest + { + [Fact] + public void ConstructorWithLineAndCharacterIndexSetsAssociatedProperties() + { + // Act + var loc = new SourceLocation(0, 42, 24); + + // Assert + Assert.Null(loc.FilePath); + Assert.Equal(0, loc.AbsoluteIndex); + Assert.Equal(42, loc.LineIndex); + Assert.Equal(24, loc.CharacterIndex); + } + + [Fact] + public void Constructor_SetsFilePathAndIndexes() + { + // Arrange + var filePath = "some-file-path"; + var absoluteIndex = 133; + var lineIndex = 23; + var characterIndex = 12; + + // Act + var sourceLocation = new SourceLocation(filePath, absoluteIndex, lineIndex, characterIndex); + + // Assert + Assert.Equal(filePath, sourceLocation.FilePath); + Assert.Equal(absoluteIndex, sourceLocation.AbsoluteIndex); + Assert.Equal(lineIndex, sourceLocation.LineIndex); + Assert.Equal(characterIndex, sourceLocation.CharacterIndex); + } + + [Theory] + [InlineData(null)] + [InlineData("some-file")] + public void GetHashCode_ReturnsSameValue_WhenEqual(string path) + { + // Arrange + var sourceLocationA = new SourceLocation(path, 10, 3, 4); + var sourceLocationB = new SourceLocation(path, 10, 3, 4); + var sourceLocationC = new SourceLocation(path, 10, 45, 8754); + + // Act + var hashCodeA = sourceLocationA.GetHashCode(); + var hashCodeB = sourceLocationB.GetHashCode(); + var hashCodeC = sourceLocationC.GetHashCode(); + + // Assert + Assert.Equal(hashCodeA, hashCodeB); + Assert.Equal(hashCodeA, hashCodeC); + } + + [Fact] + public void Equals_ReturnsTrue_FilePathsNullAndAbsoluteIndicesMatch() + { + // Arrange + var sourceLocationA = new SourceLocation(10, 3, 4); + var sourceLocationB = new SourceLocation(10, 45, 8754); + + // Act + var result = sourceLocationA.Equals(sourceLocationB); + + // Assert + Assert.True(result); + } + + [Fact] + public void Equals_ReturnsFalse_IfFilePathIsDifferent() + { + // Arrange + var sourceLocationA = new SourceLocation(10, 3, 4); + var sourceLocationB = new SourceLocation("different-file", 10, 3, 4); + + // Act + var result = sourceLocationA.Equals(sourceLocationB); + + // Assert + Assert.False(result); + } + + [Theory] + [InlineData(null)] + [InlineData("some-file")] + public void Equals_ReturnsTrue_IfFilePathAndIndexesAreSame(string path) + { + // Arrange + var sourceLocationA = new SourceLocation(path, 10, 3, 4); + var sourceLocationB = new SourceLocation(path, 10, 3, 4); + var sourceLocationC = new SourceLocation("different-path", 10, 3, 4); + + // Act + var result1 = sourceLocationA.Equals(sourceLocationB); + var result2 = sourceLocationA.Equals(sourceLocationC); + + // Assert + Assert.True(result1); + Assert.False(result2); + } + + [Fact] + public void Add_Throws_IfFilePathsDoNotMatch() + { + // Arrange + var sourceLocationA = new SourceLocation("a-path", 1, 1, 1); + var sourceLocationB = new SourceLocation("b-path", 1, 1, 1); + + // Act and Assert + ExceptionAssert.ThrowsArgument( + () => { var result = sourceLocationA + sourceLocationB; }, + "right", + $"Cannot perform '+' operations on 'SourceLocation' instances with different file paths."); + } + + [Theory] + [InlineData(null)] + [InlineData("same-path")] + public void Add_IgnoresCharacterIndexIfRightLineIndexIsNonZero(string path) + { + // Arrange + var sourceLocationA = new SourceLocation(path, 1, 2, 3); + var sourceLocationB = new SourceLocation(path, 4, 5, 6); + + // Act + var result = sourceLocationA + sourceLocationB; + + // Assert + Assert.Equal(path, result.FilePath); + Assert.Equal(5, result.AbsoluteIndex); + Assert.Equal(7, result.LineIndex); + Assert.Equal(6, result.CharacterIndex); + } + + [Theory] + [InlineData(null)] + [InlineData("same-path")] + public void Add_UsesCharacterIndexIfRightLineIndexIsZero(string path) + { + // Arrange + var sourceLocationA = new SourceLocation(path, 2, 5, 3); + var sourceLocationB = new SourceLocation(path, 4, 0, 6); + + // Act + var result = sourceLocationA + sourceLocationB; + + // Assert + Assert.Equal(path, result.FilePath); + Assert.Equal(6, result.AbsoluteIndex); + Assert.Equal(5, result.LineIndex); + Assert.Equal(9, result.CharacterIndex); + } + + [Fact] + public void Add_AllowsRightFilePathToBeNull_WhenLeftFilePathIsNonNull() + { + // Arrange + var left = new SourceLocation("left-path", 7, 1, 7); + var right = new SourceLocation(13, 1, 4); + + // Act + var result = left + right; + + // Assert + Assert.Equal(left.FilePath, result.FilePath); + Assert.Equal(20, result.AbsoluteIndex); + Assert.Equal(2, result.LineIndex); + Assert.Equal(4, result.CharacterIndex); + } + + [Fact] + public void Add_AllowsLeftFilePathToBeNull_WhenRightFilePathIsNonNull() + { + // Arrange + var left = new SourceLocation(4, 5, 6); + var right = new SourceLocation("right-path", 7, 8, 9); + + // Act + var result = left + right; + + // Assert + Assert.Equal(right.FilePath, result.FilePath); + Assert.Equal(11, result.AbsoluteIndex); + Assert.Equal(13, result.LineIndex); + Assert.Equal(9, result.CharacterIndex); + } + + [Fact] + public void Subtract_Throws_IfFilePathsDoNotMatch() + { + // Arrange + var sourceLocationA = new SourceLocation("a-path", 1, 1, 1); + var sourceLocationB = new SourceLocation("b-path", 1, 1, 1); + + // Act and Assert + ExceptionAssert.ThrowsArgument( + () => { var result = sourceLocationA - sourceLocationB; }, + "right", + "Cannot perform '-' operations on 'SourceLocation' instances with different file paths."); + } + + [Theory] + [InlineData(null)] + [InlineData("same-path")] + public void Subtract_UsesDifferenceOfCharacterIndexesIfLineIndexesAreSame(string path) + { + // Arrange + var sourceLocationA = new SourceLocation(path, 1, 5, 3); + var sourceLocationB = new SourceLocation(path, 5, 5, 6); + + // Act + var result = sourceLocationB - sourceLocationA; + + // Assert + Assert.Null(result.FilePath); + Assert.Equal(4, result.AbsoluteIndex); + Assert.Equal(0, result.LineIndex); + Assert.Equal(3, result.CharacterIndex); + } + + [Theory] + [InlineData(null)] + [InlineData("same-path")] + public void Subtract_UsesLeftCharacterIndexIfLineIndexesAreDifferent(string path) + { + // Arrange + var sourceLocationA = new SourceLocation(path, 2, 0, 3); + var sourceLocationB = new SourceLocation(path, 4, 5, 6); + + // Act + var result = sourceLocationB - sourceLocationA; + + // Assert + Assert.Null(result.FilePath); + Assert.Equal(2, result.AbsoluteIndex); + Assert.Equal(5, result.LineIndex); + Assert.Equal(6, result.CharacterIndex); + } + + [Theory] + [InlineData(null)] + [InlineData("path-to-file")] + public void Advance_PreservesSourceLocationFilePath(string path) + { + // Arrange + var sourceLocation = new SourceLocation(path, 15, 2, 8); + + // Act + var result = SourceLocation.Advance(sourceLocation, "Hello world"); + + // Assert + Assert.Equal(path, result.FilePath); + Assert.Equal(26, result.AbsoluteIndex); + Assert.Equal(2, result.LineIndex); + Assert.Equal(19, result.CharacterIndex); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/SourceLocationTrackerTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/SourceLocationTrackerTest.cs new file mode 100644 index 0000000000..5b68c1f1c3 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/SourceLocationTrackerTest.cs @@ -0,0 +1,196 @@ +// 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.Legacy +{ + internal class SourceLocationTrackerTest + { + private static readonly SourceLocation TestStartLocation = new SourceLocation(10, 42, 45); + + [Fact] + public void ConstructorSetsCurrentLocationToZero() + { + Assert.Equal(SourceLocation.Zero, new SourceLocationTracker().CurrentLocation); + } + + [Fact] + public void ConstructorWithSourceLocationSetsCurrentLocationToSpecifiedValue() + { + var loc = new SourceLocation(10, 42, 4); + Assert.Equal(loc, new SourceLocationTracker(loc).CurrentLocation); + } + + [Fact] + public void UpdateLocationAdvancesCorrectlyForMultiLineString() + { + // Arrange + var tracker = new SourceLocationTracker(TestStartLocation); + + // Act + tracker.UpdateLocation("foo\nbar\rbaz\r\nbox"); + + // Assert + Assert.Equal(26, tracker.CurrentLocation.AbsoluteIndex); + Assert.Equal(45, tracker.CurrentLocation.LineIndex); + Assert.Equal(3, tracker.CurrentLocation.CharacterIndex); + } + + [Fact] + public void UpdateLocationAdvancesAbsoluteIndexOnNonNewlineCharacter() + { + // Arrange + var tracker = new SourceLocationTracker(TestStartLocation); + + // Act + tracker.UpdateLocation('f', 'o'); + + // Assert + Assert.Equal(11, tracker.CurrentLocation.AbsoluteIndex); + } + + [Fact] + public void UpdateLocationAdvancesCharacterIndexOnNonNewlineCharacter() + { + // Arrange + var tracker = new SourceLocationTracker(TestStartLocation); + + // Act + tracker.UpdateLocation('f', 'o'); + + // Assert + Assert.Equal(46, tracker.CurrentLocation.CharacterIndex); + } + + [Fact] + public void UpdateLocationDoesNotAdvanceLineIndexOnNonNewlineCharacter() + { + // Arrange + var tracker = new SourceLocationTracker(TestStartLocation); + + // Act + tracker.UpdateLocation('f', 'o'); + + // Assert + Assert.Equal(42, tracker.CurrentLocation.LineIndex); + } + + [Fact] + public void UpdateLocationAdvancesLineIndexOnSlashN() + { + // Arrange + var tracker = new SourceLocationTracker(TestStartLocation); + + // Act + tracker.UpdateLocation('\n', 'o'); + + // Assert + Assert.Equal(43, tracker.CurrentLocation.LineIndex); + } + + [Fact] + public void UpdateLocationAdvancesAbsoluteIndexOnSlashN() + { + // Arrange + var tracker = new SourceLocationTracker(TestStartLocation); + + // Act + tracker.UpdateLocation('\n', 'o'); + + // Assert + Assert.Equal(11, tracker.CurrentLocation.AbsoluteIndex); + } + + [Fact] + public void UpdateLocationResetsCharacterIndexOnSlashN() + { + // Arrange + var tracker = new SourceLocationTracker(TestStartLocation); + + // Act + tracker.UpdateLocation('\n', 'o'); + + // Assert + Assert.Equal(0, tracker.CurrentLocation.CharacterIndex); + } + + [Fact] + public void UpdateLocationAdvancesLineIndexOnSlashRFollowedByNonNewlineCharacter() + { + // Arrange + var tracker = new SourceLocationTracker(TestStartLocation); + + // Act + tracker.UpdateLocation('\r', 'o'); + + // Assert + Assert.Equal(43, tracker.CurrentLocation.LineIndex); + } + + [Fact] + public void UpdateLocationAdvancesAbsoluteIndexOnSlashRFollowedByNonNewlineCharacter() + { + // Arrange + var tracker = new SourceLocationTracker(TestStartLocation); + + // Act + tracker.UpdateLocation('\r', 'o'); + + // Assert + Assert.Equal(11, tracker.CurrentLocation.AbsoluteIndex); + } + + [Fact] + public void UpdateLocationResetsCharacterIndexOnSlashRFollowedByNonNewlineCharacter() + { + // Arrange + var tracker = new SourceLocationTracker(TestStartLocation); + + // Act + tracker.UpdateLocation('\r', 'o'); + + // Assert + Assert.Equal(0, tracker.CurrentLocation.CharacterIndex); + } + + [Fact] + public void UpdateLocationDoesNotAdvanceLineIndexOnSlashRFollowedBySlashN() + { + // Arrange + var tracker = new SourceLocationTracker(TestStartLocation); + + // Act + tracker.UpdateLocation('\r', '\n'); + + // Assert + Assert.Equal(42, tracker.CurrentLocation.LineIndex); + } + + [Fact] + public void UpdateLocationAdvancesAbsoluteIndexOnSlashRFollowedBySlashN() + { + // Arrange + var tracker = new SourceLocationTracker(TestStartLocation); + + // Act + tracker.UpdateLocation('\r', '\n'); + + // Assert + Assert.Equal(11, tracker.CurrentLocation.AbsoluteIndex); + } + + [Fact] + public void UpdateLocationAdvancesCharacterIndexOnSlashRFollowedBySlashN() + { + // Arrange + var tracker = new SourceLocationTracker(TestStartLocation); + + // Act + tracker.UpdateLocation('\r', '\n'); + + // Assert + Assert.Equal(46, tracker.CurrentLocation.CharacterIndex); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/StringTextBuffer.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/StringTextBuffer.cs new file mode 100644 index 0000000000..5bf91b962f --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/StringTextBuffer.cs @@ -0,0 +1,53 @@ +// 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; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class StringTextBuffer : ITextBuffer, IDisposable + { + private string _buffer; + public bool Disposed { get; set; } + + public StringTextBuffer(string buffer) + { + _buffer = buffer; + } + + public int Length + { + get { return _buffer.Length; } + } + + public int Position { get; set; } + + public int Read() + { + if (Position >= _buffer.Length) + { + return -1; + } + return _buffer[Position++]; + } + + public int Peek() + { + if (Position >= _buffer.Length) + { + return -1; + } + return _buffer[Position]; + } + + public void Dispose() + { + Disposed = true; + } + + public object VersionToken + { + get { return _buffer; } + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TestFile.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TestFile.cs new file mode 100644 index 0000000000..72153403e4 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TestFile.cs @@ -0,0 +1,95 @@ +// 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.IO; +using System.Reflection; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class TestFile + { + public TestFile(string resourceName, Assembly assembly) + { + Assembly = assembly; + ResourceName = Assembly.GetName().Name + "." + resourceName.Replace('/', '.'); + } + + public Assembly Assembly { get; } + + public string ResourceName { get; } + + public static TestFile Create(string localResourceName) + { + return new TestFile(localResourceName, typeof(TestFile).GetTypeInfo().Assembly); + } + + public Stream OpenRead() + { + var stream = Assembly.GetManifestResourceStream(ResourceName); + if (stream == null) + { + Assert.True(false, string.Format("Manifest resource: {0} not found", ResourceName)); + } + + return stream; + } + + public bool Exists() + { + var resourceNames = Assembly.GetManifestResourceNames(); + foreach (var resourceName in resourceNames) + { + // Resource names are case-sensitive. + if (string.Equals(ResourceName, resourceName, StringComparison.Ordinal)) + { + return true; + } + } + + return false; + } + + public byte[] ReadAllBytes() + { + using (var stream = OpenRead()) + { + var buffer = new byte[stream.Length]; + stream.Read(buffer, 0, buffer.Length); + + return buffer; + } + } + + public string ReadAllText() + { + using (var reader = new StreamReader(OpenRead())) + { + // The .Replace() calls normalize line endings, in case you get \n instead of \r\n + // since all the unit tests rely on the assumption that the files will have \r\n endings. + return reader.ReadToEnd().Replace("\r", "").Replace("\n", "\r\n"); + } + } + + /// + /// Saves the file to the specified path. + /// + public void Save(string filePath) + { + var directory = Path.GetDirectoryName(filePath); + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + using (var outStream = File.Create(filePath)) + { + using (var inStream = OpenRead()) + { + inStream.CopyTo(outStream); + } + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TestSpanBuilder.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TestSpanBuilder.cs new file mode 100644 index 0000000000..ca6c593d13 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TestSpanBuilder.cs @@ -0,0 +1,431 @@ +// 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; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal static class SpanFactoryExtensions + { + public static UnclassifiedCodeSpanConstructor EmptyCSharp(this SpanFactory self) + { + return new UnclassifiedCodeSpanConstructor( + self.Span( + SpanKind.Code, + new CSharpSymbol(self.LocationTracker.CurrentLocation, string.Empty, CSharpSymbolType.Unknown))); + } + + public static SpanConstructor EmptyHtml(this SpanFactory self) + { + return self + .Span( + SpanKind.Markup, + new HtmlSymbol(self.LocationTracker.CurrentLocation, string.Empty, HtmlSymbolType.Unknown)) + .With(new MarkupChunkGenerator()); + } + + public static UnclassifiedCodeSpanConstructor Code(this SpanFactory self, string content) + { + return new UnclassifiedCodeSpanConstructor( + self.Span(SpanKind.Code, content, markup: false)); + } + + public static SpanConstructor CodeTransition(this SpanFactory self) + { + return self + .Span(SpanKind.Transition, SyntaxConstants.TransitionString, markup: false) + .Accepts(AcceptedCharacters.None); + } + + public static SpanConstructor CodeTransition(this SpanFactory self, string content) + { + return self.Span(SpanKind.Transition, content, markup: false).Accepts(AcceptedCharacters.None); + } + + public static SpanConstructor CodeTransition(this SpanFactory self, CSharpSymbolType type) + { + return self + .Span(SpanKind.Transition, SyntaxConstants.TransitionString, type) + .Accepts(AcceptedCharacters.None); + } + + public static SpanConstructor CodeTransition(this SpanFactory self, string content, CSharpSymbolType type) + { + return self.Span(SpanKind.Transition, content, type).Accepts(AcceptedCharacters.None); + } + + public static SpanConstructor MarkupTransition(this SpanFactory self) + { + return self + .Span(SpanKind.Transition, SyntaxConstants.TransitionString, markup: true) + .Accepts(AcceptedCharacters.None); + } + + public static SpanConstructor MarkupTransition(this SpanFactory self, string content) + { + return self.Span(SpanKind.Transition, content, markup: true).Accepts(AcceptedCharacters.None); + } + + public static SpanConstructor MarkupTransition(this SpanFactory self, HtmlSymbolType type) + { + return self + .Span(SpanKind.Transition, SyntaxConstants.TransitionString, type) + .Accepts(AcceptedCharacters.None); + } + + public static SpanConstructor MarkupTransition(this SpanFactory self, string content, HtmlSymbolType type) + { + return self.Span(SpanKind.Transition, content, type).Accepts(AcceptedCharacters.None); + } + + public static SpanConstructor MetaCode(this SpanFactory self, string content) + { + return self.Span(SpanKind.MetaCode, content, markup: false); + } + + public static SpanConstructor MetaCode(this SpanFactory self, string content, CSharpSymbolType type) + { + return self.Span(SpanKind.MetaCode, content, type); + } + + public static SpanConstructor MetaMarkup(this SpanFactory self, string content) + { + return self.Span(SpanKind.MetaCode, content, markup: true); + } + + public static SpanConstructor MetaMarkup(this SpanFactory self, string content, HtmlSymbolType type) + { + return self.Span(SpanKind.MetaCode, content, type); + } + + public static SpanConstructor Comment(this SpanFactory self, string content, CSharpSymbolType type) + { + return self.Span(SpanKind.Comment, content, type); + } + + public static SpanConstructor Comment(this SpanFactory self, string content, HtmlSymbolType type) + { + return self.Span(SpanKind.Comment, content, type); + } + + public static SpanConstructor BangEscape(this SpanFactory self) + { + return self + .Span(SpanKind.MetaCode, "!", markup: true) + .With(SpanChunkGenerator.Null) + .Accepts(AcceptedCharacters.None); + } + + public static SpanConstructor Markup(this SpanFactory self, string content) + { + return self.Span(SpanKind.Markup, content, markup: true).With(new MarkupChunkGenerator()); + } + + public static SpanConstructor Markup(this SpanFactory self, params string[] content) + { + return self.Span(SpanKind.Markup, content, markup: true).With(new MarkupChunkGenerator()); + } + + public static SpanConstructor CodeMarkup(this SpanFactory self, params string[] content) + { + return self + .Span(SpanKind.Code, content, markup: true) + .AsCodeMarkup(); + } + + public static SpanConstructor CSharpCodeMarkup(this SpanFactory self, string content) + { + return self.Code(content) + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true) + .AsCodeMarkup(); + } + + public static SpanConstructor AsCodeMarkup(this SpanConstructor self) + { + return self + .With(new ImplicitExpressionEditHandler( + (content) => SpanConstructor.TestTokenizer(content), + CSharpCodeParser.DefaultKeywords, + acceptTrailingDot: true)) + .With(new MarkupChunkGenerator()) + .Accepts(AcceptedCharacters.AnyExceptNewline); + } + + public static SourceLocation GetLocationAndAdvance(this SourceLocationTracker self, string content) + { + var ret = self.CurrentLocation; + self.UpdateLocation(content); + return ret; + } + } + + internal class SpanFactory + { + public SpanFactory() + { + LocationTracker = new SourceLocationTracker(); + + MarkupTokenizerFactory = doc => new HtmlTokenizer(doc); + CodeTokenizerFactory = doc => new CSharpTokenizer(doc); + } + + public Func MarkupTokenizerFactory { get; } + public Func CodeTokenizerFactory { get; } + public SourceLocationTracker LocationTracker { get; } + + + public SpanConstructor Span(SpanKind kind, string content, CSharpSymbolType type) + { + return CreateSymbolSpan(kind, content, st => new CSharpSymbol(st, content, type)); + } + + public SpanConstructor Span(SpanKind kind, string content, HtmlSymbolType type) + { + return CreateSymbolSpan(kind, content, st => new HtmlSymbol(st, content, type)); + } + + public SpanConstructor Span(SpanKind kind, string content, bool markup) + { + return new SpanConstructor(kind, Tokenize(new[] { content }, markup)); + } + + public SpanConstructor Span(SpanKind kind, string[] content, bool markup) + { + return new SpanConstructor(kind, Tokenize(content, markup)); + } + + public SpanConstructor Span(SpanKind kind, params ISymbol[] symbols) + { + return new SpanConstructor(kind, symbols); + } + + private SpanConstructor CreateSymbolSpan(SpanKind kind, string content, Func ctor) + { + var start = LocationTracker.CurrentLocation; + LocationTracker.UpdateLocation(content); + return new SpanConstructor(kind, new[] { ctor(start) }); + } + + public void Reset() + { + LocationTracker.CurrentLocation = SourceLocation.Zero; + } + + private IEnumerable Tokenize(IEnumerable contentFragments, bool markup) + { + return contentFragments.SelectMany(fragment => Tokenize(fragment, markup)); + } + + private IEnumerable Tokenize(string content, bool markup) + { + var tok = MakeTokenizer(markup, new SeekableTextReader(content)); + ISymbol sym; + ISymbol last = null; + while ((sym = tok.NextSymbol()) != null) + { + OffsetStart(sym, LocationTracker.CurrentLocation); + last = sym; + yield return sym; + } + LocationTracker.UpdateLocation(content); + } + + private ITokenizer MakeTokenizer(bool markup, SeekableTextReader seekableTextReader) + { + if (markup) + { + return MarkupTokenizerFactory(seekableTextReader); + } + else + { + return CodeTokenizerFactory(seekableTextReader); + } + } + + private void OffsetStart(ISymbol sym, SourceLocation sourceLocation) + { + sym.OffsetStart(sourceLocation); + } + } + + internal static class SpanConstructorExtensions + { + public static SpanConstructor Accepts(this SpanConstructor self, AcceptedCharacters accepted) + { + return self.With(eh => eh.AcceptedCharacters = accepted); + } + + public static SpanConstructor AutoCompleteWith(this SpanConstructor self, string autoCompleteString) + { + return AutoCompleteWith(self, autoCompleteString, atEndOfSpan: false); + } + + public static SpanConstructor AutoCompleteWith( + this SpanConstructor self, + string autoCompleteString, + bool atEndOfSpan) + { + return self.With(new AutoCompleteEditHandler( + (content) => SpanConstructor.TestTokenizer(content), + autoCompleteAtEndOfSpan: atEndOfSpan) + { + AutoCompleteString = autoCompleteString + }); + } + } + + internal class UnclassifiedCodeSpanConstructor + { + SpanConstructor _self; + + public UnclassifiedCodeSpanConstructor(SpanConstructor self) + { + _self = self; + } + + public SpanConstructor AsMetaCode() + { + _self.Builder.Kind = SpanKind.MetaCode; + return _self; + } + + public SpanConstructor AsStatement() + { + return _self.With(new StatementChunkGenerator()); + } + + public SpanConstructor AsExpression() + { + return _self.With(new ExpressionChunkGenerator()); + } + + public SpanConstructor AsImplicitExpression(ISet keywords) + { + return AsImplicitExpression(keywords, acceptTrailingDot: false); + } + + public SpanConstructor AsImplicitExpression(ISet keywords, bool acceptTrailingDot) + { + return _self + .With(new ImplicitExpressionEditHandler((content) => SpanConstructor.TestTokenizer(content), keywords, acceptTrailingDot)) + .With(new ExpressionChunkGenerator()); + } + + public SpanConstructor AsFunctionsBody() + { + return _self.With(new TypeMemberChunkGenerator()); + } + + public SpanConstructor AsNamespaceImport(string ns) + { + return _self.With(new AddImportChunkGenerator(ns)); + } + + public SpanConstructor Hidden() + { + return _self.With(SpanChunkGenerator.Null); + } + + public SpanConstructor AsBaseType(string baseType) + { + return _self + .With(new SetBaseTypeChunkGenerator(baseType)) + .Accepts(AcceptedCharacters.AnyExceptNewline); + } + + public SpanConstructor AsAddTagHelper(string lookupText) + { + return _self + .With(new AddTagHelperChunkGenerator(lookupText)) + .Accepts(AcceptedCharacters.AnyExceptNewline); + } + + public SpanConstructor AsRemoveTagHelper(string lookupText) + { + return _self + .With(new RemoveTagHelperChunkGenerator(lookupText)) + .Accepts(AcceptedCharacters.AnyExceptNewline); + } + + public SpanConstructor AsTagHelperPrefixDirective(string prefix) + { + return _self + .With(new TagHelperPrefixDirectiveChunkGenerator(prefix)) + .Accepts(AcceptedCharacters.AnyExceptNewline); + } + + public SpanConstructor As(ISpanChunkGenerator chunkGenerator) + { + return _self.With(chunkGenerator); + } + } + + internal class SpanConstructor + { + public SpanBuilder Builder { get; private set; } + + internal static IEnumerable TestTokenizer(string str) + { + yield return new RawTextSymbol(SourceLocation.Zero, str); + } + + public SpanConstructor(SpanKind kind, IEnumerable symbols) + { + Builder = new SpanBuilder(); + Builder.Kind = kind; + Builder.EditHandler = SpanEditHandler.CreateDefault((content) => SpanConstructor.TestTokenizer(content)); + foreach (ISymbol sym in symbols) + { + Builder.Accept(sym); + } + } + + private Span Build() + { + return Builder.Build(); + } + + public SpanConstructor As(SpanKind spanKind) + { + Builder.Kind = spanKind; + return this; + } + + public SpanConstructor With(ISpanChunkGenerator generator) + { + Builder.ChunkGenerator = generator; + return this; + } + + public SpanConstructor With(SpanEditHandler handler) + { + Builder.EditHandler = handler; + return this; + } + + public SpanConstructor With(Action generatorConfigurer) + { + generatorConfigurer(Builder.ChunkGenerator); + return this; + } + + public SpanConstructor With(Action handlerConfigurer) + { + handlerConfigurer(Builder.EditHandler); + return this; + } + + public static implicit operator Span(SpanConstructor self) + { + return self.Build(); + } + + public SpanConstructor Hidden() + { + Builder.ChunkGenerator = SpanChunkGenerator.Null; + return this; + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TextChangeTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TextChangeTest.cs new file mode 100644 index 0000000000..506aa74810 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TextChangeTest.cs @@ -0,0 +1,317 @@ +// 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 Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class TextChangeTest + { + [Fact] + public void ConstructorRequiresNonNegativeOldPosition() + { + var parameterName = "oldPosition"; + var exception = Assert.Throws(parameterName, () => new TextChange(-1, 0, new Mock().Object, 0, 0, new Mock().Object)); + ExceptionHelpers.ValidateArgumentException(parameterName, "Value must be greater than or equal to 0.", exception); + } + + [Fact] + public void ConstructorRequiresNonNegativeNewPosition() + { + var parameterName = "newPosition"; + var exception = Assert.Throws(parameterName, () => new TextChange(0, 0, new Mock().Object, -1, 0, new Mock().Object)); + ExceptionHelpers.ValidateArgumentException(parameterName, "Value must be greater than or equal to 0.", exception); + } + + [Fact] + public void ConstructorRequiresNonNegativeOldLength() + { + var parameterName = "oldLength"; + var exception = Assert.Throws(parameterName, () => new TextChange(0, -1, new Mock().Object, 0, 0, new Mock().Object)); + ExceptionHelpers.ValidateArgumentException(parameterName, "Value must be greater than or equal to 0.", exception); + } + + [Fact] + public void ConstructorRequiresNonNegativeNewLength() + { + var parameterName = "newLength"; + var exception = Assert.Throws(parameterName, () => new TextChange(0, 0, new Mock().Object, 0, -1, new Mock().Object)); + ExceptionHelpers.ValidateArgumentException(parameterName, "Value must be greater than or equal to 0.", exception); + } + + [Fact] + public void ConstructorInitializesProperties() + { + // Act + var oldBuffer = new Mock().Object; + var newBuffer = new Mock().Object; + var change = new TextChange(42, 24, oldBuffer, 1337, newBuffer); + + // Assert + Assert.Equal(42, change.OldPosition); + Assert.Equal(24, change.OldLength); + Assert.Equal(1337, change.NewLength); + Assert.Same(newBuffer, change.NewBuffer); + Assert.Same(oldBuffer, change.OldBuffer); + } + + [Fact] + public void TestIsDelete() + { + // Arrange + var oldBuffer = new Mock().Object; + var newBuffer = new Mock().Object; + var change = new TextChange(0, 1, oldBuffer, 0, newBuffer); + + // Assert + Assert.True(change.IsDelete); + } + + [Fact] + public void TestDeleteCreatesTheRightSizeChange() + { + // Arrange + var oldBuffer = new Mock().Object; + var newBuffer = new Mock().Object; + var change = new TextChange(0, 1, oldBuffer, 0, newBuffer); + + // Assert + Assert.Equal(0, change.NewText.Length); + Assert.Equal(1, change.OldText.Length); + } + + [Fact] + public void TestIsInsert() + { + // Arrange + var oldBuffer = new Mock().Object; + var newBuffer = new Mock().Object; + var change = new TextChange(0, 0, oldBuffer, 35, newBuffer); + + // Assert + Assert.True(change.IsInsert); + } + + [Fact] + public void TestInsertCreateTheRightSizeChange() + { + // Arrange + var oldBuffer = new Mock().Object; + var newBuffer = new Mock().Object; + var change = new TextChange(0, 0, oldBuffer, 1, newBuffer); + + // Assert + Assert.Equal(1, change.NewText.Length); + Assert.Equal(0, change.OldText.Length); + } + + [Fact] + public void TestIsReplace() + { + // Arrange + var oldBuffer = new Mock().Object; + var newBuffer = new Mock().Object; + var change = new TextChange(0, 5, oldBuffer, 10, newBuffer); + + // Assert + Assert.True(change.IsReplace); + } + + [Fact] + public void ReplaceCreatesTheRightSizeChange() + { + // Arrange + var oldBuffer = new Mock().Object; + var newBuffer = new Mock().Object; + var change = new TextChange(0, 5, oldBuffer, 10, newBuffer); + + // Assert + Assert.Equal(10, change.NewText.Length); + Assert.Equal(5, change.OldText.Length); + } + + [Fact] + public void ReplaceCreatesTheRightSizeChange1() + { + // Arrange + var oldBuffer = new Mock().Object; + var newBuffer = new Mock().Object; + var change = new TextChange(0, 5, oldBuffer, 1, newBuffer); + + // Assert + Assert.Equal(1, change.NewText.Length); + Assert.Equal(5, change.OldText.Length); + } + + [Fact] + public void OldTextReturnsOldSpanFromOldBuffer() + { + // Arrange + var newBuffer = new StringTextBuffer("test"); + var oldBuffer = new StringTextBuffer("text"); + var textChange = new TextChange(2, 1, oldBuffer, 1, newBuffer); + + // Act + var text = textChange.OldText; + + // Assert + Assert.Equal("x", text); + } + + [Fact] + public void OldTextReturnsOldSpanFromOldBuffer2() + { + // Arrange + var newBuffer = new StringTextBuffer("test"); + var oldBuffer = new StringTextBuffer("text"); + var textChange = new TextChange(2, 2, oldBuffer, 1, newBuffer); + + // Act + var text = textChange.OldText; + + // Assert + Assert.Equal("xt", text); + } + + [Fact] + public void NewTextWithInsertReturnsChangedTextFromBuffer() + { + // Arrange + var newBuffer = new StringTextBuffer("test"); + var oldBuffer = new StringTextBuffer(""); + var textChange = new TextChange(0, 0, oldBuffer, 3, newBuffer); + + // Act + var text = textChange.NewText; + var oldText = textChange.OldText; + + // Assert + Assert.Equal("tes", text); + Assert.Equal("", oldText); + } + + [Fact] + public void NewTextWithDeleteReturnsEmptyString() + { + // Arrange + var newBuffer = new StringTextBuffer("test"); + var oldBuffer = new StringTextBuffer(""); + var textChange = new TextChange(1, 1, oldBuffer, 0, newBuffer); + + // Act + var text = textChange.NewText; + + // Assert + Assert.Equal(string.Empty, text); + } + + [Fact] + public void NewTextWithReplaceReturnsChangedTextFromBuffer() + { + // Arrange + var newBuffer = new StringTextBuffer("test"); + var oldBuffer = new StringTextBuffer("tebb"); + var textChange = new TextChange(2, 2, oldBuffer, 1, newBuffer); + + // Act + var newText = textChange.NewText; + var oldText = textChange.OldText; + + // Assert + Assert.Equal("s", newText); + Assert.Equal("bb", oldText); + } + + [Fact] + public void ApplyChangeWithInsertedTextReturnsNewContentWithChangeApplied() + { + // Arrange + var newBuffer = new StringTextBuffer("test"); + var oldBuffer = new StringTextBuffer(""); + var textChange = new TextChange(0, 0, oldBuffer, 3, newBuffer); + + // Act + var text = textChange.ApplyChange("abcd", 0); + + // Assert + Assert.Equal("tesabcd", text); + } + + [Fact] + public void ApplyChangeWithRemovedTextReturnsNewContentWithChangeApplied() + { + // Arrange + var newBuffer = new StringTextBuffer("abcdefg"); + var oldBuffer = new StringTextBuffer(""); + var textChange = new TextChange(1, 1, oldBuffer, 0, newBuffer); + + // Act + var text = textChange.ApplyChange("abcdefg", 1); + + // Assert + Assert.Equal("bcdefg", text); + } + + [Fact] + public void ApplyChangeWithReplacedTextReturnsNewContentWithChangeApplied() + { + // Arrange + var newBuffer = new StringTextBuffer("abcdefg"); + var oldBuffer = new StringTextBuffer(""); + var textChange = new TextChange(1, 1, oldBuffer, 2, newBuffer); + + // Act + var text = textChange.ApplyChange("abcdefg", 1); + + // Assert + Assert.Equal("bcbcdefg", text); + } + + [Fact] + public void NormalizeFixesUpIntelliSenseStyleReplacements() + { + // Arrange + var newBuffer = new StringTextBuffer("Date."); + var oldBuffer = new StringTextBuffer("Date"); + var original = new TextChange(0, 4, oldBuffer, 5, newBuffer); + + // Act + var normalized = original.Normalize(); + + // Assert + Assert.Equal(new TextChange(4, 0, oldBuffer, 1, newBuffer), normalized); + } + + [Fact] + public void NormalizeDoesntAffectChangesWithoutCommonPrefixes() + { + // Arrange + var newBuffer = new StringTextBuffer("DateTime."); + var oldBuffer = new StringTextBuffer("Date."); + var original = new TextChange(0, 5, oldBuffer, 9, newBuffer); + + // Act + var normalized = original.Normalize(); + + // Assert + Assert.Equal(original, normalized); + } + + [Fact] + public void NormalizeDoesntAffectShrinkingReplacements() + { + // Arrange + var newBuffer = new StringTextBuffer("D"); + var oldBuffer = new StringTextBuffer("DateTime"); + var original = new TextChange(0, 8, oldBuffer, 1, newBuffer); + + // Act + var normalized = original.Normalize(); + + // Assert + Assert.Equal(original, normalized); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TextReaderExtensionsTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TextReaderExtensionsTest.cs new file mode 100644 index 0000000000..51a3d241a9 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TextReaderExtensionsTest.cs @@ -0,0 +1,113 @@ +// 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.IO; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class TextReaderExtensionsTest + { + [Fact] + public void ReadUntilWithCharReadsAllTextUpToSpecifiedCharacterButNotPast() + { + RunReaderTest("foo bar baz @biz", "foo bar baz ", '@', r => r.ReadUntil('@')); + } + + [Fact] + public void ReadUntilWithCharWithInclusiveFlagReadsAllTextUpToSpecifiedCharacterButNotPastIfInclusiveFalse() + { + RunReaderTest("foo bar baz @biz", "foo bar baz ", '@', r => r.ReadUntil('@', inclusive: false)); + } + + [Fact] + public void ReadUntilWithCharWithInclusiveFlagReadsAllTextUpToAndIncludingSpecifiedCharacterIfInclusiveTrue() + { + RunReaderTest("foo bar baz @biz", "foo bar baz @", 'b', r => r.ReadUntil('@', inclusive: true)); + } + + [Fact] + public void ReadUntilWithCharReadsToEndIfSpecifiedCharacterNotFound() + { + RunReaderTest("foo bar baz", "foo bar baz", -1, r => r.ReadUntil('@')); + } + + [Fact] + public void ReadUntilWithMultipleTerminatorsReadsUntilAnyTerminatorIsFound() + { + RunReaderTest("", " r.ReadUntil('/', '>')); + } + + [Fact] + public void ReadUntilWithMultipleTerminatorsHonorsInclusiveFlagWhenFalse() + { + // NOTE: Using named parameters would be difficult here, hence the inline comment + RunReaderTest("", " r.ReadUntil(/* inclusive */ false, '/', '>')); + } + + [Fact] + public void ReadUntilWithMultipleTerminatorsHonorsInclusiveFlagWhenTrue() + { + // NOTE: Using named parameters would be difficult here, hence the inline comment + RunReaderTest("", "', r => r.ReadUntil(/* inclusive */ true, '/', '>')); + } + + [Fact] + public void ReadUntilWithPredicateStopsWhenPredicateIsTrue() + { + RunReaderTest("foo bar baz 0 zoop zork zoink", "foo bar baz ", '0', r => r.ReadUntil(c => Char.IsDigit(c))); + } + + [Fact] + public void ReadUntilWithPredicateHonorsInclusiveFlagWhenFalse() + { + RunReaderTest("foo bar baz 0 zoop zork zoink", "foo bar baz ", '0', r => r.ReadUntil(c => Char.IsDigit(c), inclusive: false)); + } + + [Fact] + public void ReadUntilWithPredicateHonorsInclusiveFlagWhenTrue() + { + RunReaderTest("foo bar baz 0 zoop zork zoink", "foo bar baz 0", ' ', r => r.ReadUntil(c => Char.IsDigit(c), inclusive: true)); + } + + [Fact] + public void ReadWhileWithPredicateStopsWhenPredicateIsFalse() + { + RunReaderTest("012345a67890", "012345", 'a', r => r.ReadWhile(c => Char.IsDigit(c))); + } + + [Fact] + public void ReadWhileWithPredicateHonorsInclusiveFlagWhenFalse() + { + RunReaderTest("012345a67890", "012345", 'a', r => r.ReadWhile(c => Char.IsDigit(c), inclusive: false)); + } + + [Fact] + public void ReadWhileWithPredicateHonorsInclusiveFlagWhenTrue() + { + RunReaderTest("012345a67890", "012345a", '6', r => r.ReadWhile(c => Char.IsDigit(c), inclusive: true)); + } + + private static void RunReaderTest(string testString, string expectedOutput, int expectedPeek, Func action) + { + // Arrange + var reader = new StringReader(testString); + + // Act + var read = action(reader); + + // Assert + Assert.Equal(expectedOutput, read); + + if (expectedPeek == -1) + { + Assert.True(reader.Peek() == -1, "Expected that the reader would be positioned at the end of the input stream"); + } + else + { + Assert.Equal((char)expectedPeek, (char)reader.Peek()); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TokenizerLookaheadTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TokenizerLookaheadTest.cs new file mode 100644 index 0000000000..16662cdbdb --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TokenizerLookaheadTest.cs @@ -0,0 +1,121 @@ +// 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.IO; +using System.Text; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal class TokenizerLookaheadTest : HtmlTokenizerTestBase + { + [Fact] + public void Lookahead_MaintainsExistingBufferWhenRejected() + { + // Arrange + var tokenizer = new ExposedTokenizer("01234"); + tokenizer.Buffer.Append("pre-existing values"); + + // Act + var result = tokenizer.Lookahead("0x", takeIfMatch: true, caseSensitive: true); + + // Assert + Assert.False(result); + Assert.Equal("pre-existing values", tokenizer.Buffer.ToString(), StringComparer.Ordinal); + } + + [Fact] + public void Lookahead_AddsToExistingBufferWhenSuccessfulAndTakeIfMatchIsTrue() + { + // Arrange + var tokenizer = new ExposedTokenizer("0x1234"); + tokenizer.Buffer.Append("pre-existing values"); + + // Act + var result = tokenizer.Lookahead("0x", takeIfMatch: true, caseSensitive: true); + + // Assert + Assert.True(result); + Assert.Equal("pre-existing values0x", tokenizer.Buffer.ToString(), StringComparer.Ordinal); + } + + [Fact] + public void Lookahead_MaintainsExistingBufferWhenSuccessfulAndTakeIfMatchIsFalse() + { + // Arrange + var tokenizer = new ExposedTokenizer("0x1234"); + tokenizer.Buffer.Append("pre-existing values"); + + // Act + var result = tokenizer.Lookahead("0x", takeIfMatch: false, caseSensitive: true); + + // Assert + Assert.True(result); + Assert.Equal("pre-existing values", tokenizer.Buffer.ToString(), StringComparer.Ordinal); + } + + private class ExposedTokenizer : Tokenizer + { + public ExposedTokenizer(string input) + : base(new SeekableTextReader(new StringReader(input))) + { + } + + public new StringBuilder Buffer + { + get + { + return base.Buffer; + } + } + + public override CSharpSymbolType RazorCommentStarType + { + get + { + throw new NotImplementedException(); + } + } + + public override CSharpSymbolType RazorCommentTransitionType + { + get + { + throw new NotImplementedException(); + } + } + + public override CSharpSymbolType RazorCommentType + { + get + { + throw new NotImplementedException(); + } + } + + protected override int StartState + { + get + { + throw new NotImplementedException(); + } + } + + protected override CSharpSymbol CreateSymbol( + SourceLocation start, + string content, + CSharpSymbolType type, + IReadOnlyList errors) + { + throw new NotImplementedException(); + } + + protected override StateResult Dispatch() + { + throw new NotImplementedException(); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TokenizerTestBase.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TokenizerTestBase.cs new file mode 100644 index 0000000000..14896c4562 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Legacy/TokenizerTestBase.cs @@ -0,0 +1,76 @@ +// 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.Diagnostics; +using System.IO; +using System.Text; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Legacy +{ + internal abstract class TokenizerTestBase + where TSymbolType : struct + where TSymbol : SymbolBase + { + protected abstract TSymbol IgnoreRemaining { get; } + protected abstract Tokenizer CreateTokenizer(ITextDocument source); + + protected void TestTokenizer(string input, params TSymbol[] expectedSymbols) + { + // Arrange + var success = true; + var output = new StringBuilder(); + using (StringReader reader = new StringReader(input)) + { + using (SeekableTextReader source = new SeekableTextReader(reader)) + { + Tokenizer tokenizer = CreateTokenizer(source); + var counter = 0; + TSymbol current = null; + while ((current = tokenizer.NextSymbol()) != null) + { + if (counter >= expectedSymbols.Length) + { + output.AppendLine(string.Format("F: Expected: << Nothing >>; Actual: {0}", current)); + success = false; + } + else if (ReferenceEquals(expectedSymbols[counter], IgnoreRemaining)) + { + output.AppendLine(string.Format("P: Ignored {0}", current)); + } + else + { + if (!Equals(expectedSymbols[counter], current)) + { + output.AppendLine(string.Format("F: Expected: {0}; Actual: {1}", expectedSymbols[counter], current)); + success = false; + } + else + { + output.AppendLine(string.Format("P: Expected: {0}", expectedSymbols[counter])); + } + counter++; + } + } + if (counter < expectedSymbols.Length && !ReferenceEquals(expectedSymbols[counter], IgnoreRemaining)) + { + success = false; + for (; counter < expectedSymbols.Length; counter++) + { + output.AppendLine(string.Format("F: Expected: {0}; Actual: << None >>", expectedSymbols[counter])); + } + } + } + } + Assert.True(success, Environment.NewLine + output.ToString()); + WriteTraceLine(output.Replace("{", "{{").Replace("}", "}}").ToString()); + } + + [Conditional("PARSER_TRACE")] + private static void WriteTraceLine(string format, params object[] args) + { + Trace.WriteLine(string.Format(format, args)); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorSyntaxTreeTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorSyntaxTreeTest.cs new file mode 100644 index 0000000000..50ea54a893 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorSyntaxTreeTest.cs @@ -0,0 +1,24 @@ +// 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.Test +{ + public class RazorSyntaxTreeTest + { + [Fact] + public void Parse_CanParseEmptyDocument() + { + // Arrange + var source = TestRazorSourceDocument.Create(string.Empty); + + // Act + var syntaxTree = RazorSyntaxTree.Parse(source); + + // Assert + Assert.NotNull(syntaxTree); + Assert.Empty(syntaxTree.Diagnostics); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/TestFiles/Source/BasicMarkup.cshtml b/test/Microsoft.AspNetCore.Razor.Evolution.Test/TestFiles/Source/BasicMarkup.cshtml new file mode 100644 index 0000000000..384a7a6deb --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/TestFiles/Source/BasicMarkup.cshtml @@ -0,0 +1,8 @@ + + + This is the title. + + + Link to Contoso. + + \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/TestFiles/nested-1000.html b/test/Microsoft.AspNetCore.Razor.Evolution.Test/TestFiles/nested-1000.html new file mode 100644 index 0000000000..3c35bdbcbe --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/TestFiles/nested-1000.html @@ -0,0 +1,2002 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/TestRazorSourceDocument.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/TestRazorSourceDocument.cs index 0fb1b37056..8eaf24670b 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/TestRazorSourceDocument.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/TestRazorSourceDocument.cs @@ -3,11 +3,27 @@ using System.IO; using System.Text; +using Microsoft.AspNetCore.Razor.Evolution.Legacy; namespace Microsoft.AspNetCore.Razor.Evolution { internal class TestRazorSourceDocument : DefaultRazorSourceDocument { + public static RazorSourceDocument CreateResource(string path, Encoding encoding = null) + { + var file = TestFile.Create(path); + + var stream = new MemoryStream(); + using (var input = file.OpenRead()) + { + input.CopyTo(stream); + } + + stream.Seek(0L, SeekOrigin.Begin); + + return new TestRazorSourceDocument(stream, encoding ?? Encoding.UTF8, path); + } + public static MemoryStream CreateContent(string content = "Hello, World!", Encoding encoding = null) { var stream = new MemoryStream(); diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/project.json b/test/Microsoft.AspNetCore.Razor.Evolution.Test/project.json index 725e4d54a0..015b9c5f90 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/project.json +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/project.json @@ -3,6 +3,7 @@ "dependencies": { "dotnet-test-xunit": "2.2.0-*", "Microsoft.AspNetCore.Razor.Evolution": "1.1.0-*", + "Microsoft.AspNetCore.Testing": "1.1.0-*", "Moq": "4.6.36-*", "xunit": "2.2.0-*" },