// 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.Language.Legacy
{
public abstract class ParserTestBase
{
internal static Block IgnoreOutput = new IgnoreOutputBlock();
internal ParserTestBase()
{
Factory = CreateSpanFactory();
BlockFactory = CreateBlockFactory();
}
///
/// Set to true to autocorrect the locations of spans to appear in document order with no gaps.
/// Use this when spans were not created in document order.
///
protected bool FixupSpans { get; set; }
internal SpanFactory Factory { get; private set; }
internal BlockFactory BlockFactory { get; private set; }
internal RazorSyntaxTree ParseBlock(string document, bool designTime)
{
return ParseBlock(document, null, designTime);
}
internal abstract RazorSyntaxTree ParseBlock(string document, IEnumerable directives, bool designTime);
internal virtual RazorSyntaxTree ParseDocument(string document, bool designTime = false)
{
return ParseDocument(document, null, designTime);
}
internal virtual RazorSyntaxTree ParseDocument(string document, IEnumerable directives, bool designTime = false)
{
directives = directives ?? Array.Empty();
var source = TestRazorSourceDocument.Create(document, fileName: null);
var options = CreateParserOptions(directives, designTime);
var context = new ParserContext(source, options);
var codeParser = new CSharpCodeParser(directives, context);
var markupParser = new HtmlMarkupParser(context);
codeParser.HtmlParser = markupParser;
markupParser.CodeParser = codeParser;
markupParser.ParseDocument();
var root = context.Builder.Build();
var diagnostics = context.ErrorSink.Errors?.Select(error => RazorDiagnostic.Create(error));
var codeDocument = RazorCodeDocument.Create(source);
var syntaxTree = RazorSyntaxTree.Create(root, source, diagnostics, options);
codeDocument.SetSyntaxTree(syntaxTree);
var defaultDirectivePass = new DefaultDirectiveSyntaxTreePass();
syntaxTree = defaultDirectivePass.Execute(codeDocument, syntaxTree);
return syntaxTree;
}
internal virtual RazorSyntaxTree ParseHtmlBlock(string document, IEnumerable directives, bool designTime = false)
{
directives = directives ?? Array.Empty();
var source = TestRazorSourceDocument.Create(document, fileName: null);
var options = CreateParserOptions(directives, designTime);
var context = new ParserContext(source, options);
var parser = new HtmlMarkupParser(context);
parser.CodeParser = new CSharpCodeParser(directives, context)
{
HtmlParser = parser,
};
parser.ParseBlock();
var root = context.Builder.Build();
var diagnostics = context.ErrorSink.Errors?.Select(error => RazorDiagnostic.Create(error));
return RazorSyntaxTree.Create(root, source, diagnostics, options);
}
internal virtual RazorSyntaxTree ParseCodeBlock(string document, bool designTime = false)
{
return ParseCodeBlock(document, Enumerable.Empty(), designTime);
}
internal virtual RazorSyntaxTree ParseCodeBlock(
string document,
IEnumerable directives,
bool designTime)
{
directives = directives ?? Array.Empty();
var source = TestRazorSourceDocument.Create(document, fileName: null);
var options = CreateParserOptions(directives, designTime);
var context = new ParserContext(source, options);
var parser = new CSharpCodeParser(directives, context);
parser.HtmlParser = new HtmlMarkupParser(context)
{
CodeParser = parser,
};
parser.ParseBlock();
var root = context.Builder.Build();
var diagnostics = context.ErrorSink.Errors?.Select(error => RazorDiagnostic.Create(error));
return RazorSyntaxTree.Create(root, source, diagnostics, options);
}
internal SpanFactory CreateSpanFactory()
{
return new SpanFactory();
}
internal abstract BlockFactory CreateBlockFactory();
internal virtual void ParseBlockTest(string document)
{
ParseBlockTest(document, null, false, new RazorError[0]);
}
internal virtual void ParseBlockTest(string document, bool designTime)
{
ParseBlockTest(document, null, designTime, new RazorError[0]);
}
internal virtual void ParseBlockTest(string document, params RazorError[] expectedErrors)
{
ParseBlockTest(document, false, expectedErrors);
}
internal virtual void ParseBlockTest(string document, bool designTime, params RazorError[] expectedErrors)
{
ParseBlockTest(document, null, designTime, expectedErrors);
}
internal virtual void ParseBlockTest(string document, Block expectedRoot)
{
ParseBlockTest(document, expectedRoot, false, null);
}
internal virtual void ParseBlockTest(string document, IEnumerable directives, Block expectedRoot)
{
ParseBlockTest(document, directives, expectedRoot, false, null);
}
internal virtual void ParseBlockTest(string document, Block expectedRoot, bool designTime)
{
ParseBlockTest(document, expectedRoot, designTime, null);
}
internal virtual void ParseBlockTest(string document, Block expectedRoot, params RazorError[] expectedErrors)
{
ParseBlockTest(document, expectedRoot, false, expectedErrors);
}
internal virtual void ParseBlockTest(string document, IEnumerable directives, Block expectedRoot, params RazorError[] expectedErrors)
{
ParseBlockTest(document, directives, expectedRoot, false, expectedErrors);
}
internal virtual void ParseBlockTest(string document, Block expected, bool designTime, params RazorError[] expectedErrors)
{
ParseBlockTest(document, null, expected, designTime, expectedErrors);
}
internal virtual void ParseBlockTest(string document, IEnumerable directives, Block expected, bool designTime, params RazorError[] expectedErrors)
{
var result = ParseBlock(document, directives, designTime);
if (FixupSpans)
{
SpancestryCorrector.Correct(expected);
var span = expected.FindFirstDescendentSpan();
span.ChangeStart(SourceLocation.Zero);
}
SyntaxTreeVerifier.Verify(result);
SyntaxTreeVerifier.Verify(expected);
if (!ReferenceEquals(expected, IgnoreOutput))
{
EvaluateResults(result, expected, expectedErrors?.Select(error => RazorDiagnostic.Create(error)).ToList());
}
}
internal virtual void SingleSpanBlockTest(string document, BlockKindInternal blockKind, SpanKindInternal spanType, AcceptedCharactersInternal acceptedCharacters = AcceptedCharactersInternal.Any)
{
SingleSpanBlockTest(document, blockKind, spanType, acceptedCharacters, expectedError: null);
}
internal virtual void SingleSpanBlockTest(string document, string spanContent, BlockKindInternal blockKind, SpanKindInternal spanType, AcceptedCharactersInternal acceptedCharacters = AcceptedCharactersInternal.Any)
{
SingleSpanBlockTest(document, spanContent, blockKind, spanType, acceptedCharacters, expectedErrors: null);
}
internal virtual void SingleSpanBlockTest(string document, BlockKindInternal blockKind, SpanKindInternal spanType, params RazorError[] expectedError)
{
SingleSpanBlockTest(document, document, blockKind, spanType, expectedError);
}
internal virtual void SingleSpanBlockTest(string document, string spanContent, BlockKindInternal blockKind, SpanKindInternal spanType, params RazorError[] expectedErrors)
{
SingleSpanBlockTest(document, spanContent, blockKind, spanType, AcceptedCharactersInternal.Any, expectedErrors ?? new RazorError[0]);
}
internal virtual void SingleSpanBlockTest(string document, BlockKindInternal blockKind, SpanKindInternal spanType, AcceptedCharactersInternal acceptedCharacters, params RazorError[] expectedError)
{
SingleSpanBlockTest(document, document, blockKind, spanType, acceptedCharacters, expectedError);
}
internal virtual void SingleSpanBlockTest(string document, string spanContent, BlockKindInternal blockKind, SpanKindInternal spanType, AcceptedCharactersInternal acceptedCharacters, params RazorError[] expectedErrors)
{
var result = ParseBlock(document, designTime: false);
var builder = new BlockBuilder();
builder.Type = blockKind;
var expected = ConfigureAndAddSpanToBlock(builder, Factory.Span(spanType, spanContent, spanType == SpanKindInternal.Markup).Accepts(acceptedCharacters));
if (FixupSpans)
{
SpancestryCorrector.Correct(expected);
var span = expected.FindFirstDescendentSpan();
span.ChangeStart(SourceLocation.Zero);
}
SyntaxTreeVerifier.Verify(result);
SyntaxTreeVerifier.Verify(expected);
if (!ReferenceEquals(expected, IgnoreOutput))
{
EvaluateResults(result, expected, expectedErrors?.Select(error => RazorDiagnostic.Create(error)).ToList());
}
}
internal virtual void ParseDocumentTest(string document)
{
ParseDocumentTest(document, null, false);
}
internal virtual void ParseDocumentTest(string document, Block expectedRoot)
{
ParseDocumentTest(document, expectedRoot, false, null);
}
internal virtual void ParseDocumentTest(string document, Block expectedRoot, params RazorError[] expectedErrors)
{
ParseDocumentTest(document, expectedRoot, false, expectedErrors);
}
internal virtual void ParseDocumentTest(string document, IEnumerable directives, Block expected, params RazorError[] expectedErrors)
{
ParseDocumentTest(document, directives, expected, false, expectedErrors);
}
internal virtual void ParseDocumentTest(string document, bool designTime)
{
ParseDocumentTest(document, null, designTime);
}
internal virtual void ParseDocumentTest(string document, Block expectedRoot, bool designTime)
{
ParseDocumentTest(document, expectedRoot, designTime, null);
}
internal virtual void ParseDocumentTest(string document, Block expected, bool designTime, params RazorError[] expectedErrors)
{
ParseDocumentTest(document, null, expected, designTime, expectedErrors);
}
internal virtual void ParseDocumentTest(string document, IEnumerable directives, Block expected, bool designTime, params RazorError[] expectedErrors)
{
var result = ParseDocument(document, directives, designTime);
if (FixupSpans)
{
SpancestryCorrector.Correct(expected);
var span = expected.FindFirstDescendentSpan();
span.ChangeStart(SourceLocation.Zero);
}
SyntaxTreeVerifier.Verify(result);
SyntaxTreeVerifier.Verify(expected);
if (!ReferenceEquals(expected, IgnoreOutput))
{
EvaluateResults(result, expected, expectedErrors?.Select(error => RazorDiagnostic.Create(error)).ToList());
}
}
[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);
}
}
}
internal static void EvaluateResults(RazorSyntaxTree result, Block expectedRoot)
{
EvaluateResults(result, expectedRoot, null);
}
internal static void EvaluateResults(RazorSyntaxTree result, Block expectedRoot, IList expectedErrors)
{
EvaluateParseTree(result.Root, expectedRoot);
EvaluateRazorErrors(result.Diagnostics, expectedErrors);
}
internal 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 EvaluateTagHelperAttribute(
ErrorCollector collector,
TagHelperAttributeNode actual,
TagHelperAttributeNode expected)
{
if (actual.Name != expected.Name)
{
collector.AddError("{0} - FAILED :: Attribute names do not match", expected.Name);
}
else
{
collector.AddMessage("{0} - PASSED :: Attribute names match", expected.Name);
}
if (actual.AttributeStructure != expected.AttributeStructure)
{
collector.AddError("{0} - FAILED :: Attribute value styles do not match", expected.AttributeStructure.ToString());
}
else
{
collector.AddMessage("{0} - PASSED :: Attribute value style match", expected.AttributeStructure);
}
if (actual.AttributeStructure != AttributeStructure.Minimized)
{
EvaluateSyntaxTreeNode(collector, actual.Value, expected.Value);
}
}
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
{
if (actual is TagHelperBlock)
{
EvaluateTagHelperBlock(collector, actual as TagHelperBlock, expected as TagHelperBlock);
}
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 EvaluateTagHelperBlock(ErrorCollector collector, TagHelperBlock actual, TagHelperBlock expected)
{
if (expected == null)
{
AddMismatchError(collector, actual, expected);
}
else
{
if (!string.Equals(expected.TagName, actual.TagName, StringComparison.Ordinal))
{
collector.AddError(
"{0} - FAILED :: TagName mismatch for TagHelperBlock :: ACTUAL: {1}",
expected.TagName,
actual.TagName);
}
if (expected.TagMode != actual.TagMode)
{
collector.AddError(
$"{expected.TagMode} - FAILED :: {nameof(TagMode)} for {nameof(TagHelperBlock)} " +
$"{actual.TagName} :: ACTUAL: {actual.TagMode}");
}
var expectedAttributes = expected.Attributes.GetEnumerator();
var actualAttributes = actual.Attributes.GetEnumerator();
while (expectedAttributes.MoveNext())
{
if (!actualAttributes.MoveNext())
{
collector.AddError("{0} - FAILED :: No more attributes on this node", expectedAttributes.Current);
}
else
{
EvaluateTagHelperAttribute(collector, actualAttributes.Current, expectedAttributes.Current);
}
}
while (actualAttributes.MoveNext())
{
collector.AddError("End of Attributes - FAILED :: Found Attribute: {0}", actualAttributes.Current.Name);
}
}
}
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);
}
internal 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));
}
internal static string FormatErrors(IEnumerable errors)
{
if (errors == null)
{
return "\t<< No Errors >>";
}
var builder = new StringBuilder();
foreach (var error in errors)
{
builder.AppendFormat("\t{0}", error);
builder.AppendLine();
}
return builder.ToString();
}
[Conditional("PARSER_TRACE")]
private static void WriteTraceLine(string format, params object[] args)
{
Trace.WriteLine(string.Format(format, args));
}
internal virtual Block CreateSimpleBlockAndSpan(string spanContent, BlockKindInternal blockKind, SpanKindInternal spanType, AcceptedCharactersInternal acceptedCharacters = AcceptedCharactersInternal.Any)
{
var span = Factory.Span(spanType, spanContent, spanType == SpanKindInternal.Markup).Accepts(acceptedCharacters);
var b = new BlockBuilder()
{
Type = blockKind
};
return ConfigureAndAddSpanToBlock(b, span);
}
internal virtual Block ConfigureAndAddSpanToBlock(BlockBuilder block, SpanConstructor span)
{
switch (block.Type)
{
case BlockKindInternal.Markup:
span.With(new MarkupChunkGenerator());
break;
case BlockKindInternal.Statement:
span.With(new StatementChunkGenerator());
break;
case BlockKindInternal.Expression:
block.ChunkGenerator = new ExpressionChunkGenerator();
span.With(new ExpressionChunkGenerator());
break;
}
block.Children.Add(span);
return block.Build();
}
private static RazorParserOptions CreateParserOptions(IEnumerable directives, bool designTime)
{
if (designTime)
{
return RazorParserOptions.CreateDesignTime(ConfigureOptions);
}
else
{
return RazorParserOptions.Create(ConfigureOptions);
}
void ConfigureOptions(RazorParserOptionsBuilder builder)
{
foreach (var directive in directives)
{
builder.Directives.Add(directive);
}
}
}
private class IgnoreOutputBlock : Block
{
public IgnoreOutputBlock() : base(BlockKindInternal.Template, new SyntaxTreeNode[0], null) { }
}
// Corrects the parents and previous/next information for spans
internal class SpancestryCorrector : ParserVisitor
{
private SpancestryCorrector()
{
}
protected Block CurrentBlock { get; set; }
protected Span LastSpan { get; set; }
public static void Correct(Block block)
{
new SpancestryCorrector().VisitBlock(block);
}
public override void VisitBlock(Block block)
{
CurrentBlock = block;
base.VisitBlock(block);
}
public override void VisitSpan(Span span)
{
span.Parent = CurrentBlock;
span.Previous = LastSpan;
if (LastSpan != null)
{
LastSpan.Next = span;
}
LastSpan = span;
}
}
}
}