// 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; } } } }