diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/ParentChunkGenerator.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/ParentChunkGenerator.cs index 0d57311757..09a8c94f28 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/ParentChunkGenerator.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/ParentChunkGenerator.cs @@ -47,10 +47,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy public void Accept(ParserVisitor visitor, Block block) { - for (var i = 0; i < block.Children.Count; i++) - { - block.Children[i].Accept(visitor); - } + visitor.VisitDefault(block); } } } diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/ParserVisitor.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/ParserVisitor.cs index 108fcf5d21..fe6031046e 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/ParserVisitor.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/ParserVisitor.cs @@ -5,7 +5,12 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy { internal abstract class ParserVisitor { - protected virtual void VisitDefault(Block block) + public virtual void Visit(SyntaxTreeNode node) + { + node.Accept(this); + } + + public virtual void VisitDefault(Block block) { for (var i = 0; i < block.Children.Count; i++) { @@ -13,6 +18,10 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy } } + public virtual void VisitDefault(Span span) + { + } + public virtual void VisitBlock(Block block) { if (block.ChunkGenerator != null) @@ -44,35 +53,6 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy VisitDefault(block); } - public virtual void VisitExpressionSpan(ExpressionChunkGenerator chunkGenerator, Span span) - { - } - - public virtual void VisitMarkupSpan(MarkupChunkGenerator chunkGenerator, Span span) - { - } - - public virtual void VisitImportSpan(AddImportChunkGenerator chunkGenerator, Span span) - { - } - - public virtual void VisitStatementSpan(StatementChunkGenerator chunkGenerator, Span span) - { - } - - public virtual void VisitLiteralAttributeSpan(LiteralAttributeChunkGenerator chunkGenerator, Span span) - { - } - - public virtual void VisitDirectiveToken(DirectiveTokenChunkGenerator chunkGenerator, Span block) - { - } - - public virtual void VisitDirectiveBlock(DirectiveChunkGenerator chunkGenerator, Block block) - { - VisitDefault(block); - } - public virtual void VisitTemplateBlock(TemplateBlockChunkGenerator chunkGenerator, Block block) { VisitDefault(block); @@ -88,16 +68,54 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy VisitDefault(block); } + public virtual void VisitDirectiveBlock(DirectiveChunkGenerator chunkGenerator, Block block) + { + VisitDefault(block); + } + + public virtual void VisitExpressionSpan(ExpressionChunkGenerator chunkGenerator, Span span) + { + VisitDefault(span); + } + + public virtual void VisitMarkupSpan(MarkupChunkGenerator chunkGenerator, Span span) + { + VisitDefault(span); + } + + public virtual void VisitImportSpan(AddImportChunkGenerator chunkGenerator, Span span) + { + VisitDefault(span); + } + + public virtual void VisitStatementSpan(StatementChunkGenerator chunkGenerator, Span span) + { + VisitDefault(span); + } + + public virtual void VisitLiteralAttributeSpan(LiteralAttributeChunkGenerator chunkGenerator, Span span) + { + VisitDefault(span); + } + + public virtual void VisitDirectiveToken(DirectiveTokenChunkGenerator chunkGenerator, Span span) + { + VisitDefault(span); + } + public virtual void VisitAddTagHelperSpan(AddTagHelperChunkGenerator chunkGenerator, Span span) { + VisitDefault(span); } public virtual void VisitRemoveTagHelperSpan(RemoveTagHelperChunkGenerator chunkGenerator, Span span) { + VisitDefault(span); } public virtual void VisitTagHelperPrefixDirectiveSpan(TagHelperPrefixDirectiveChunkGenerator chunkGenerator, Span span) { + VisitDefault(span); } } } diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/SpanChunkGenerator.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/SpanChunkGenerator.cs index 75defef805..4e15538609 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/SpanChunkGenerator.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/SpanChunkGenerator.cs @@ -32,6 +32,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy { public void Accept(ParserVisitor visitor, Span span) { + visitor.VisitDefault(span); } public void GenerateChunk(Span target, ChunkGeneratorContext context) diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/CSharpReservedWordsTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/CSharpReservedWordsTest.cs index 21a2dacf19..7248a798bc 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/CSharpReservedWordsTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/CSharpReservedWordsTest.cs @@ -7,6 +7,11 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy { public class CSharpReservedWordsTest : CsHtmlCodeParserTestBase { + public CSharpReservedWordsTest() + { + UseBaselineTests = true; + } + [Theory] [InlineData("namespace")] [InlineData("class")] diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/CSharpVerbatimBlockTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/CSharpVerbatimBlockTest.cs index 903e68f313..2a96d2dcd6 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/CSharpVerbatimBlockTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/CSharpVerbatimBlockTest.cs @@ -10,125 +10,45 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy { private const string TestExtraKeyword = "model"; + public CSharpVerbatimBlockTest() + { + UseBaselineTests = true; + } + [Fact] public void VerbatimBlock() { - ParseBlockTest("@{ foo(); }", - new StatementBlock( - Factory.CodeTransition(), - Factory.MetaCode("{") - .Accepts(AcceptedCharactersInternal.None), - Factory.Code(" foo(); ") - .AsStatement() - .AutoCompleteWith(autoCompleteString: null), - Factory.MetaCode("}") - .Accepts(AcceptedCharactersInternal.None) - )); + ParseBlockTest("@{ foo(); }"); } [Fact] public void InnerImplicitExpressionWithOnlySingleAtOutputsZeroLengthCodeSpan() { - ParseBlockTest("{@}", - new StatementBlock( - Factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None), - Factory.EmptyCSharp() - .AsStatement() - .AutoCompleteWith(autoCompleteString: null), - new ExpressionBlock( - Factory.CodeTransition(), - Factory.EmptyCSharp().AsImplicitExpression(KeywordSet, acceptTrailingDot: true).Accepts(AcceptedCharactersInternal.NonWhiteSpace) - ), - Factory.EmptyCSharp().AsStatement(), - Factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)), - designTime: true, - expectedErrors: new[] - { - RazorDiagnosticFactory.CreateParsing_UnexpectedCharacterAtStartOfCodeBlock( - new SourceSpan(new SourceLocation(2, 0, 2), contentLength: 1), - "}") - }); + ParseBlockTest("{@}"); } [Fact] public void InnerImplicitExpressionDoesNotAcceptDotAfterAt() { - ParseBlockTest("{@.}", - new StatementBlock( - Factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None), - Factory.EmptyCSharp() - .AsStatement() - .AutoCompleteWith(autoCompleteString: null), - new ExpressionBlock( - Factory.CodeTransition(), - Factory.EmptyCSharp().AsImplicitExpression(KeywordSet, acceptTrailingDot: true).Accepts(AcceptedCharactersInternal.NonWhiteSpace) - ), - Factory.Code(".").AsStatement(), - Factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)), - designTime: true, - expectedErrors: new[] - { - RazorDiagnosticFactory.CreateParsing_UnexpectedCharacterAtStartOfCodeBlock( - new SourceSpan(new SourceLocation(2, 0, 2), contentLength: 1), - ".") - }); + ParseBlockTest("{@.}"); } [Fact] public void InnerImplicitExpressionWithOnlySingleAtAcceptsSingleSpaceOrNewlineAtDesignTime() { - ParseBlockTest("{" + Environment.NewLine - + " @" + Environment.NewLine - + "}", - new StatementBlock( - Factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None), - Factory.Code(Environment.NewLine + " ") - .AsStatement() - .AutoCompleteWith(autoCompleteString: null), - new ExpressionBlock( - Factory.CodeTransition(), - Factory.EmptyCSharp().AsImplicitExpression(KeywordSet, acceptTrailingDot: true).Accepts(AcceptedCharactersInternal.NonWhiteSpace) - ), - Factory.Code(Environment.NewLine).AsStatement(), - Factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)), - /* designTimeParser */ true, - RazorDiagnosticFactory.CreateParsing_UnexpectedWhiteSpaceAtStartOfCodeBlock( - new SourceSpan(new SourceLocation(6 + Environment.NewLine.Length, 1, 5), Environment.NewLine.Length))); + ParseBlockTest("{" + Environment.NewLine + " @" + Environment.NewLine + "}", designTime: true); } [Fact] public void InnerImplicitExpressionDoesNotAcceptTrailingNewlineInRunTimeMode() { - ParseBlockTest("{@foo." + Environment.NewLine - + "}", - new StatementBlock( - Factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None), - Factory.EmptyCSharp() - .AsStatement() - .AutoCompleteWith(autoCompleteString: null), - new ExpressionBlock( - Factory.CodeTransition(), - Factory.Code("foo.").AsImplicitExpression(KeywordSet, acceptTrailingDot: true).Accepts(AcceptedCharactersInternal.NonWhiteSpace)), - Factory.Code(Environment.NewLine).AsStatement(), - Factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None))); + ParseBlockTest("{@foo." + Environment.NewLine + "}"); } [Fact] public void InnerImplicitExpressionAcceptsTrailingNewlineInDesignTimeMode() { - ParseBlockTest("{@foo." + Environment.NewLine - + "}", - new StatementBlock( - Factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None), - Factory.EmptyCSharp() - .AsStatement() - .AutoCompleteWith(autoCompleteString: null), - new ExpressionBlock( - Factory.CodeTransition(), - Factory.Code("foo.").AsImplicitExpression(KeywordSet, acceptTrailingDot: true).Accepts(AcceptedCharactersInternal.NonWhiteSpace)), - Factory.Code(Environment.NewLine).AsStatement(), - Factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)), - designTime: true); + ParseBlockTest("{@foo." + Environment.NewLine + "}", designTime: true); } } } diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/ParserTests/CSharpVerbatimBlockTest/InnerImplicitExpressionAcceptsTrailingNewlineInDesignTimeMode.syntaxtree.txt b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/ParserTests/CSharpVerbatimBlockTest/InnerImplicitExpressionAcceptsTrailingNewlineInDesignTimeMode.syntaxtree.txt new file mode 100644 index 0000000000..5ee8b370e0 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/ParserTests/CSharpVerbatimBlockTest/InnerImplicitExpressionAcceptsTrailingNewlineInDesignTimeMode.syntaxtree.txt @@ -0,0 +1,8 @@ +Statement block - NullParentChunkGenerator - 9 - (0:0,0) + MetaCode span - NullSpanChunkGenerator - SpanEditHandler;Accepts:None - { - (0:0,0) + Code span - StatementChunkGenerator - AutoCompleteEditHandler;Accepts:Any,AutoComplete:[];AtEOL - - (1:0,1) + Expression block - ExpressionChunkGenerator - 5 - (1:0,1) + Transition span - NullSpanChunkGenerator - SpanEditHandler;Accepts:None - @ - (1:0,1) + Code span - ExpressionChunkGenerator - ImplicitExpressionEditHandler;Accepts:NonWhiteSpace;ImplicitExpression[ATD];K14 - foo. - (2:0,2) + Code span - StatementChunkGenerator - SpanEditHandler;Accepts:Any - LF - (6:0,6) + MetaCode span - NullSpanChunkGenerator - SpanEditHandler;Accepts:None - } - (8:1,0) diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/ParserTests/CSharpVerbatimBlockTest/InnerImplicitExpressionDoesNotAcceptDotAfterAt.diagnostics.txt b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/ParserTests/CSharpVerbatimBlockTest/InnerImplicitExpressionDoesNotAcceptDotAfterAt.diagnostics.txt new file mode 100644 index 0000000000..5f542c7bcf --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/ParserTests/CSharpVerbatimBlockTest/InnerImplicitExpressionDoesNotAcceptDotAfterAt.diagnostics.txt @@ -0,0 +1 @@ +(1,3): Error RZ1005: "." is not valid at the start of a code block. Only identifiers, keywords, comments, "(" and "{" are valid. diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/ParserTests/CSharpVerbatimBlockTest/InnerImplicitExpressionDoesNotAcceptDotAfterAt.syntaxtree.txt b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/ParserTests/CSharpVerbatimBlockTest/InnerImplicitExpressionDoesNotAcceptDotAfterAt.syntaxtree.txt new file mode 100644 index 0000000000..d0857f86f0 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/ParserTests/CSharpVerbatimBlockTest/InnerImplicitExpressionDoesNotAcceptDotAfterAt.syntaxtree.txt @@ -0,0 +1,8 @@ +Statement block - NullParentChunkGenerator - 4 - (0:0,0) + MetaCode span - NullSpanChunkGenerator - SpanEditHandler;Accepts:None - { - (0:0,0) + Code span - StatementChunkGenerator - AutoCompleteEditHandler;Accepts:Any,AutoComplete:[];AtEOL - - (1:0,1) + Expression block - ExpressionChunkGenerator - 1 - (1:0,1) + Transition span - NullSpanChunkGenerator - SpanEditHandler;Accepts:None - @ - (1:0,1) + Code span - ExpressionChunkGenerator - ImplicitExpressionEditHandler;Accepts:NonWhiteSpace;ImplicitExpression[ATD];K14 - - (2:0,2) + Code span - StatementChunkGenerator - SpanEditHandler;Accepts:Any - . - (2:0,2) + MetaCode span - NullSpanChunkGenerator - SpanEditHandler;Accepts:None - } - (3:0,3) diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/ParserTests/CSharpVerbatimBlockTest/InnerImplicitExpressionDoesNotAcceptTrailingNewlineInRunTimeMode.syntaxtree.txt b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/ParserTests/CSharpVerbatimBlockTest/InnerImplicitExpressionDoesNotAcceptTrailingNewlineInRunTimeMode.syntaxtree.txt new file mode 100644 index 0000000000..5ee8b370e0 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/ParserTests/CSharpVerbatimBlockTest/InnerImplicitExpressionDoesNotAcceptTrailingNewlineInRunTimeMode.syntaxtree.txt @@ -0,0 +1,8 @@ +Statement block - NullParentChunkGenerator - 9 - (0:0,0) + MetaCode span - NullSpanChunkGenerator - SpanEditHandler;Accepts:None - { - (0:0,0) + Code span - StatementChunkGenerator - AutoCompleteEditHandler;Accepts:Any,AutoComplete:[];AtEOL - - (1:0,1) + Expression block - ExpressionChunkGenerator - 5 - (1:0,1) + Transition span - NullSpanChunkGenerator - SpanEditHandler;Accepts:None - @ - (1:0,1) + Code span - ExpressionChunkGenerator - ImplicitExpressionEditHandler;Accepts:NonWhiteSpace;ImplicitExpression[ATD];K14 - foo. - (2:0,2) + Code span - StatementChunkGenerator - SpanEditHandler;Accepts:Any - LF - (6:0,6) + MetaCode span - NullSpanChunkGenerator - SpanEditHandler;Accepts:None - } - (8:1,0) diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/ParserTests/CSharpVerbatimBlockTest/InnerImplicitExpressionWithOnlySingleAtAcceptsSingleSpaceOrNewlineAtDesignTime.diagnostics.txt b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/ParserTests/CSharpVerbatimBlockTest/InnerImplicitExpressionWithOnlySingleAtAcceptsSingleSpaceOrNewlineAtDesignTime.diagnostics.txt new file mode 100644 index 0000000000..a8f6dc0930 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/ParserTests/CSharpVerbatimBlockTest/InnerImplicitExpressionWithOnlySingleAtAcceptsSingleSpaceOrNewlineAtDesignTime.diagnostics.txt @@ -0,0 +1 @@ +(2,6): Error RZ1003: 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. diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/ParserTests/CSharpVerbatimBlockTest/InnerImplicitExpressionWithOnlySingleAtAcceptsSingleSpaceOrNewlineAtDesignTime.syntaxtree.txt b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/ParserTests/CSharpVerbatimBlockTest/InnerImplicitExpressionWithOnlySingleAtAcceptsSingleSpaceOrNewlineAtDesignTime.syntaxtree.txt new file mode 100644 index 0000000000..74caffc857 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/ParserTests/CSharpVerbatimBlockTest/InnerImplicitExpressionWithOnlySingleAtAcceptsSingleSpaceOrNewlineAtDesignTime.syntaxtree.txt @@ -0,0 +1,8 @@ +Statement block - NullParentChunkGenerator - 11 - (0:0,0) + MetaCode span - NullSpanChunkGenerator - SpanEditHandler;Accepts:None - { - (0:0,0) + Code span - StatementChunkGenerator - AutoCompleteEditHandler;Accepts:Any,AutoComplete:[];AtEOL - LF - (1:0,1) + Expression block - ExpressionChunkGenerator - 1 - (7:1,4) + Transition span - NullSpanChunkGenerator - SpanEditHandler;Accepts:None - @ - (7:1,4) + Code span - ExpressionChunkGenerator - ImplicitExpressionEditHandler;Accepts:NonWhiteSpace;ImplicitExpression[ATD];K14 - - (8:1,5) + Code span - StatementChunkGenerator - SpanEditHandler;Accepts:Any - LF - (8:1,5) + MetaCode span - NullSpanChunkGenerator - SpanEditHandler;Accepts:None - } - (10:2,0) diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/ParserTests/CSharpVerbatimBlockTest/InnerImplicitExpressionWithOnlySingleAtOutputsZeroLengthCodeSpan.diagnostics.txt b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/ParserTests/CSharpVerbatimBlockTest/InnerImplicitExpressionWithOnlySingleAtOutputsZeroLengthCodeSpan.diagnostics.txt new file mode 100644 index 0000000000..78ebc1884b --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/ParserTests/CSharpVerbatimBlockTest/InnerImplicitExpressionWithOnlySingleAtOutputsZeroLengthCodeSpan.diagnostics.txt @@ -0,0 +1 @@ +(1,3): Error RZ1005: "}" is not valid at the start of a code block. Only identifiers, keywords, comments, "(" and "{" are valid. diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/ParserTests/CSharpVerbatimBlockTest/InnerImplicitExpressionWithOnlySingleAtOutputsZeroLengthCodeSpan.syntaxtree.txt b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/ParserTests/CSharpVerbatimBlockTest/InnerImplicitExpressionWithOnlySingleAtOutputsZeroLengthCodeSpan.syntaxtree.txt new file mode 100644 index 0000000000..dcc86f7429 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/ParserTests/CSharpVerbatimBlockTest/InnerImplicitExpressionWithOnlySingleAtOutputsZeroLengthCodeSpan.syntaxtree.txt @@ -0,0 +1,8 @@ +Statement block - NullParentChunkGenerator - 3 - (0:0,0) + MetaCode span - NullSpanChunkGenerator - SpanEditHandler;Accepts:None - { - (0:0,0) + Code span - StatementChunkGenerator - AutoCompleteEditHandler;Accepts:Any,AutoComplete:[];AtEOL - - (1:0,1) + Expression block - ExpressionChunkGenerator - 1 - (1:0,1) + Transition span - NullSpanChunkGenerator - SpanEditHandler;Accepts:None - @ - (1:0,1) + Code span - ExpressionChunkGenerator - ImplicitExpressionEditHandler;Accepts:NonWhiteSpace;ImplicitExpression[ATD];K14 - - (2:0,2) + Code span - StatementChunkGenerator - SpanEditHandler;Accepts:Any - - (2:0,2) + MetaCode span - NullSpanChunkGenerator - SpanEditHandler;Accepts:None - } - (2:0,2) diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/ParserTests/CSharpVerbatimBlockTest/VerbatimBlock.syntaxtree.txt b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/ParserTests/CSharpVerbatimBlockTest/VerbatimBlock.syntaxtree.txt new file mode 100644 index 0000000000..fc2ac0edce --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/ParserTests/CSharpVerbatimBlockTest/VerbatimBlock.syntaxtree.txt @@ -0,0 +1,5 @@ +Statement block - NullParentChunkGenerator - 11 - (0:0,0) + Transition span - NullSpanChunkGenerator - SpanEditHandler;Accepts:None - @ - (0:0,0) + MetaCode span - NullSpanChunkGenerator - SpanEditHandler;Accepts:None - { - (1:0,1) + Code span - StatementChunkGenerator - AutoCompleteEditHandler;Accepts:Any,AutoComplete:[];AtEOL - foo(); - (2:0,2) + MetaCode span - NullSpanChunkGenerator - SpanEditHandler;Accepts:None - } - (10:0,10) diff --git a/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/IntializeTestFileAttribute.cs b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/IntializeTestFileAttribute.cs new file mode 100644 index 0000000000..b860241356 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/IntializeTestFileAttribute.cs @@ -0,0 +1,36 @@ +// 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.Reflection; +using Xunit; +using Xunit.Sdk; + +namespace Microsoft.AspNetCore.Razor.Language.Legacy +{ + public class IntializeTestFileAttribute : BeforeAfterTestAttribute + { + public override void Before(MethodInfo methodUnderTest) + { + if (typeof(ParserTestBase).GetTypeInfo().IsAssignableFrom(methodUnderTest.DeclaringType.GetTypeInfo())) + { + var typeName = methodUnderTest.DeclaringType.Name; + ParserTestBase.FileName = $"TestFiles/ParserTests/{typeName}/{methodUnderTest.Name}"; + ParserTestBase.IsTheory = false; + + if (methodUnderTest.GetCustomAttributes(typeof(TheoryAttribute), inherit: false).Length > 0) + { + ParserTestBase.IsTheory = true; + } + } + } + + public override void After(MethodInfo methodUnderTest) + { + if (typeof(ParserTestBase).GetTypeInfo().IsAssignableFrom(methodUnderTest.DeclaringType.GetTypeInfo())) + { + ParserTestBase.FileName = null; + ParserTestBase.IsTheory = false; + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/ParserTestBase.cs b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/ParserTestBase.cs index 4d80343ff3..4541d04e24 100644 --- a/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/ParserTestBase.cs +++ b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/ParserTestBase.cs @@ -4,20 +4,36 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; +using System.Reflection; +#if NET46 +using System.Runtime.Remoting; +using System.Runtime.Remoting.Messaging; +#else +using System.Threading; +#endif using System.Text; using Xunit; +using Xunit.Sdk; namespace Microsoft.AspNetCore.Razor.Language.Legacy { + [IntializeTestFile] public abstract class ParserTestBase { +#if !NET46 + private static readonly AsyncLocal _fileName = new AsyncLocal(); + private static readonly AsyncLocal _isTheory = new AsyncLocal(); +#endif + internal static Block IgnoreOutput = new IgnoreOutputBlock(); internal ParserTestBase() { Factory = CreateSpanFactory(); BlockFactory = CreateBlockFactory(); + TestProjectRoot = TestProject.GetProjectDirectory(GetType()); } /// @@ -30,6 +46,105 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy internal BlockFactory BlockFactory { get; private set; } +#if GENERATE_BASELINES + protected bool GenerateBaselines { get; set; } = true; +#else + protected bool GenerateBaselines { get; set; } = false; +#endif + + protected string TestProjectRoot { get; } + + protected bool UseBaselineTests { get; set; } + + // Used by the test framework to set the 'base' name for test files. + public static string FileName + { +#if NET46 + get + { + var handle = (ObjectHandle)CallContext.LogicalGetData("ParserTestBase_FileName"); + return (string)handle.Unwrap(); + } + set + { + CallContext.LogicalSetData("ParserTestBase_FileName", new ObjectHandle(value)); + } +#elif NETCOREAPP2_2 + get { return _fileName.Value; } + set { _fileName.Value = value; } +#endif + } + + public static bool IsTheory + { +#if NET46 + get + { + var handle = (ObjectHandle)CallContext.LogicalGetData("ParserTestBase_IsTheory"); + return (bool)handle.Unwrap(); + } + set + { + CallContext.LogicalSetData("ParserTestBase_IsTheory", new ObjectHandle(value)); + } +#elif NETCOREAPP2_2 + get { return _isTheory.Value; } + set { _isTheory.Value = value; } +#endif + } + + internal void AssertSyntaxTreeNodeMatchesBaseline(RazorSyntaxTree syntaxTree) + { + if (FileName == null) + { + var message = $"{nameof(AssertSyntaxTreeNodeMatchesBaseline)} should only be called from a parser test ({nameof(FileName)} is null)."; + throw new InvalidOperationException(message); + } + + var baselineFileName = Path.ChangeExtension(FileName, ".syntaxtree.txt"); + var baselineDiagnosticsFileName = Path.ChangeExtension(FileName, ".diagnostics.txt"); + + var root = syntaxTree.Root; + var diagnostics = syntaxTree.Diagnostics; + if (GenerateBaselines) + { + var baselineFullPath = Path.Combine(TestProjectRoot, baselineFileName); + File.WriteAllText(baselineFullPath, SyntaxTreeNodeSerializer.Serialize(root)); + + var baselineDiagnosticsFullPath = Path.Combine(TestProjectRoot, baselineDiagnosticsFileName); + var lines = diagnostics.Select(RazorDiagnosticSerializer.Serialize).ToArray(); + if (lines.Any()) + { + File.WriteAllLines(baselineDiagnosticsFullPath, lines); + } + else if (File.Exists(baselineDiagnosticsFullPath)) + { + File.Delete(baselineDiagnosticsFullPath); + } + + return; + } + + var stFile = TestFile.Create(baselineFileName, GetType().GetTypeInfo().Assembly); + if (!stFile.Exists()) + { + throw new XunitException($"The resource {baselineFileName} was not found."); + } + + var baseline = stFile.ReadAllText().Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + SyntaxTreeNodeVerifier.Verify(root, baseline); + + var baselineDiagnostics = string.Empty; + var diagnosticsFile = TestFile.Create(baselineDiagnosticsFileName, GetType().GetTypeInfo().Assembly); + if (diagnosticsFile.Exists()) + { + baselineDiagnostics = diagnosticsFile.ReadAllText(); + } + + var actualDiagnostics = string.Concat(diagnostics.Select(d => RazorDiagnosticSerializer.Serialize(d) + "\r\n")); + Assert.Equal(baselineDiagnostics, actualDiagnostics); + } + internal RazorSyntaxTree ParseBlock(string document, bool designTime) { return ParseBlock(RazorLanguageVersion.Latest, document, designTime); @@ -128,7 +243,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy { directives = directives ?? Array.Empty(); - var source = TestRazorSourceDocument.Create(document, filePath: null); + var source = TestRazorSourceDocument.Create(document, filePath: null, normalizeNewLines: UseBaselineTests); var options = CreateParserOptions(version, directives, designTime); var context = new ParserContext(source, options); @@ -222,6 +337,12 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy { var result = ParseBlock(version, document, directives, designTime); + if (UseBaselineTests && !IsTheory) + { + AssertSyntaxTreeNodeMatchesBaseline(result); + return; + } + if (FixupSpans) { SpancestryCorrector.Correct(expected); diff --git a/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/SyntaxTreeNodeSerializer.cs b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/SyntaxTreeNodeSerializer.cs new file mode 100644 index 0000000000..50a22e25a7 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/SyntaxTreeNodeSerializer.cs @@ -0,0 +1,48 @@ +// 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; + +namespace Microsoft.AspNetCore.Razor.Language.Legacy +{ + public static class SyntaxTreeNodeSerializer + { + internal static string Serialize(SyntaxTreeNode node) + { + using (var writer = new StringWriter()) + { + var walker = new Walker(writer); + walker.Visit(node); + + return writer.ToString(); + } + } + + private class Walker : SyntaxTreeNodeWalker + { + private readonly SyntaxTreeNodeWriter _visitor; + private readonly TextWriter _writer; + + public Walker(TextWriter writer) + { + _visitor = new SyntaxTreeNodeWriter(writer); + _writer = writer; + } + + public TextWriter Writer { get; } + + public override void Visit(SyntaxTreeNode node) + { + _visitor.Visit(node); + _writer.WriteLine(); + + if (node is Block block) + { + _visitor.Depth++; + base.VisitDefault(block); + _visitor.Depth--; + } + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/SyntaxTreeNodeVerifier.cs b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/SyntaxTreeNodeVerifier.cs new file mode 100644 index 0000000000..140fd58ed3 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/SyntaxTreeNodeVerifier.cs @@ -0,0 +1,276 @@ +// 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.Linq; +using System.Text; +using Xunit; +using Xunit.Sdk; + +namespace Microsoft.AspNetCore.Razor.Language.Legacy +{ + public static class SyntaxTreeNodeVerifier + { + internal static void Verify(SyntaxTreeNode node, string[] baseline) + { + var walker = new Walker(baseline); + walker.Visit(node); + walker.AssertReachedEndOfBaseline(); + } + + private class Walker : SyntaxTreeNodeWalker + { + private readonly string[] _baseline; + private readonly SyntaxTreeNodeWriter _visitor; + private readonly StringWriter _writer; + + private int _index; + + public Walker(string[] baseline) + { + _writer = new StringWriter(); + + _visitor = new SyntaxTreeNodeWriter(_writer); + _baseline = baseline; + } + + public TextWriter Writer { get; } + + public override void Visit(SyntaxTreeNode node) + { + var expected = _index < _baseline.Length ? _baseline[_index++] : null; + + // Write the node as text for comparison + _writer.GetStringBuilder().Clear(); + _visitor.Visit(node); + var actual = _writer.GetStringBuilder().ToString(); + + AssertNodeEquals(node, Ancestors, expected, actual); + + if (node is Block block) + { + _visitor.Depth++; + base.VisitDefault(block); + _visitor.Depth--; + } + } + + public void AssertReachedEndOfBaseline() + { + // Since we're walking the nodes of our generated code there's the chance that our baseline is longer. + Assert.True(_baseline.Length == _index, "Not all lines of the baseline were visited!"); + } + + private void AssertNodeEquals(SyntaxTreeNode node, IEnumerable ancestors, string expected, string actual) + { + if (string.Equals(expected, actual)) + { + // YAY!!! everything is great. + return; + } + + if (expected == null) + { + var message = "The node is missing from baseline."; + throw new SyntaxTreeNodeBaselineException(node, Ancestors.ToArray(), expected, actual, message); + } + + int charsVerified = 0; + AssertNestingEqual(node, ancestors, expected, actual, ref charsVerified); + AssertNameEqual(node, ancestors, expected, actual, ref charsVerified); + AssertDelimiter(node, expected, actual, true, ref charsVerified); + AssertLocationEqual(node, ancestors, expected, actual, ref charsVerified); + AssertDelimiter(node, expected, actual, false, ref charsVerified); + AssertContentEqual(node, ancestors, expected, actual, ref charsVerified); + + throw new InvalidOperationException("We can't figure out HOW these two things are different. This is a bug."); + } + + private void AssertNestingEqual(SyntaxTreeNode node, IEnumerable ancestors, string expected, string actual, ref int charsVerified) + { + var i = 0; + for (; i < expected.Length; i++) + { + if (expected[i] != ' ') + { + break; + } + } + + var failed = false; + var j = 0; + for (; j < i; j++) + { + if (actual.Length <= j || actual[j] != ' ') + { + failed = true; + break; + } + } + + if (actual.Length <= j + 1 || actual[j] == ' ') + { + failed = true; + } + + if (failed) + { + var message = "The node is at the wrong level of nesting. This usually means a child is missing."; + throw new SyntaxTreeNodeBaselineException(node, ancestors.ToArray(), expected, actual, message); + } + + charsVerified = j; + } + + private void AssertNameEqual(SyntaxTreeNode node, IEnumerable ancestors, string expected, string actual, ref int charsVerified) + { + var expectedName = GetName(expected, charsVerified); + var actualName = GetName(actual, charsVerified); + + if (!string.Equals(expectedName, actualName)) + { + var message = $"Node names are not equal."; + throw new SyntaxTreeNodeBaselineException(node, ancestors.ToArray(), expected, actual, message); + } + + charsVerified += expectedName.Length; + } + + // Either both strings need to have a delimiter next or neither should. + private void AssertDelimiter(SyntaxTreeNode node, string expected, string actual, bool required, ref int charsVerified) + { + if (charsVerified == expected.Length && required) + { + throw new InvalidOperationException($"Baseline text is not well-formed: '{expected}'."); + } + + if (charsVerified == actual.Length && required) + { + throw new InvalidOperationException($"Baseline text is not well-formed: '{actual}'."); + } + + if (charsVerified == expected.Length && charsVerified == actual.Length) + { + return; + } + + var expectedDelimiter = expected.IndexOf(" - ", charsVerified); + if (expectedDelimiter != charsVerified && expectedDelimiter != -1) + { + throw new InvalidOperationException($"Baseline text is not well-formed: '{actual}'."); + } + + var actualDelimiter = actual.IndexOf(" - ", charsVerified); + if (actualDelimiter != charsVerified && actualDelimiter != -1) + { + throw new InvalidOperationException($"Baseline text is not well-formed: '{actual}'."); + } + + Assert.Equal(expectedDelimiter, actualDelimiter); + + charsVerified += 3; + } + + private void AssertLocationEqual(SyntaxTreeNode node, IEnumerable ancestors, string expected, string actual, ref int charsVerified) + { + var expectedLocation = GetLocation(expected, charsVerified); + var actualLocation = GetLocation(actual, charsVerified); + + if (!string.Equals(expectedLocation, actualLocation)) + { + var message = $"Locations are not equal."; + throw new SyntaxTreeNodeBaselineException(node, ancestors.ToArray(), expected, actual, message); + } + + charsVerified += expectedLocation.Length; + } + + private void AssertContentEqual(SyntaxTreeNode node, IEnumerable ancestors, string expected, string actual, ref int charsVerified) + { + var expectedContent = GetContent(expected, charsVerified); + var actualContent = GetContent(actual, charsVerified); + + if (!string.Equals(expectedContent, actualContent)) + { + var message = $"Contents are not equal."; + throw new SyntaxTreeNodeBaselineException(node, ancestors.ToArray(), expected, actual, message); + } + + charsVerified += expectedContent.Length; + } + + private string GetName(string text, int start) + { + var delimiter = text.IndexOf(" - ", start); + if (delimiter == -1) + { + throw new InvalidOperationException($"Baseline text is not well-formed: '{text}'."); + } + + return text.Substring(start, delimiter - start); + } + + private string GetLocation(string text, int start) + { + var delimiter = text.IndexOf(" - ", start); + return delimiter == -1 ? text.Substring(start) : text.Substring(start, delimiter - start); + } + + private string GetContent(string text, int start) + { + return start == text.Length ? string.Empty : text.Substring(start); + } + + private class SyntaxTreeNodeBaselineException : XunitException + { + public SyntaxTreeNodeBaselineException(SyntaxTreeNode node, SyntaxTreeNode[] ancestors, string expected, string actual, string userMessage) + : base(Format(node, ancestors, expected, actual, userMessage)) + { + Node = node; + Expected = expected; + Actual = actual; + } + + public SyntaxTreeNode Node { get; } + + public string Actual { get; } + + public string Expected { get; } + + private static string Format(SyntaxTreeNode node, SyntaxTreeNode[] ancestors, string expected, string actual, string userMessage) + { + var builder = new StringBuilder(); + builder.AppendLine(userMessage); + builder.AppendLine(); + + if (expected != null) + { + builder.Append("Expected: "); + builder.AppendLine(expected); + } + + if (actual != null) + { + builder.Append("Actual: "); + builder.AppendLine(actual); + } + + if (ancestors != null) + { + builder.AppendLine(); + builder.AppendLine("Path:"); + + foreach (var ancestor in ancestors) + { + builder.AppendLine(ancestor.ToString()); + } + } + + return builder.ToString(); + } + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/SyntaxTreeNodeWalker.cs b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/SyntaxTreeNodeWalker.cs new file mode 100644 index 0000000000..93b84cd357 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/SyntaxTreeNodeWalker.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.Language.Legacy +{ + internal class SyntaxTreeNodeWalker : ParserVisitor + { + private readonly List _ancestors = new List(); + + protected IReadOnlyList Ancestors => _ancestors; + + protected SyntaxTreeNode Parent => _ancestors.Count > 0 ? _ancestors[0] : null; + + public override void VisitDefault(Block block) + { + var children = block.Children; + if (block.Children.Count == 0) + { + return; + } + + _ancestors.Insert(0, block); + + try + { + for (var i = 0; i < block.Children.Count; i++) + { + var child = children[i]; + Visit(child); + } + } + finally + { + _ancestors.RemoveAt(0); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/SyntaxTreeNodeWriter.cs b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/SyntaxTreeNodeWriter.cs new file mode 100644 index 0000000000..d6698e40c2 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/SyntaxTreeNodeWriter.cs @@ -0,0 +1,244 @@ +// 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; + +namespace Microsoft.AspNetCore.Razor.Language.Legacy +{ + internal class SyntaxTreeNodeWriter : ParserVisitor + { + private readonly TextWriter _writer; + + public int Depth { get; set; } + + public SyntaxTreeNodeWriter(TextWriter writer) + { + _writer = writer; + } + + public override void VisitDefault(Block block) + { + WriteBlock(block); + } + + public override void VisitDefault(Span span) + { + WriteSpan(span); + } + + public override void VisitTagHelperBlock(TagHelperChunkGenerator chunkGenerator, Block block) + { + WriteBlock(block); + + if (block is TagHelperBlock tagHelperBlock) + { + WriteSeparator(); + Write(tagHelperBlock.TagName); + WriteSeparator(); + Write(tagHelperBlock.TagMode); + + foreach (var descriptor in tagHelperBlock.Binding.Descriptors) + { + WriteSeparator(); + + // Get the type name without the namespace. + var typeName = descriptor.Name.Substring(descriptor.Name.LastIndexOf('.') + 1); + Write(typeName); + } + } + } + + public override void VisitAttributeBlock(AttributeBlockChunkGenerator chunkGenerator, Block block) + { + WriteBlock(block); + WriteSeparator(); + Write(chunkGenerator.Name); + WriteSeparator(); + WriteLocationTaggedString(chunkGenerator.Prefix); + WriteSeparator(); + WriteLocationTaggedString(chunkGenerator.Suffix); + } + + public override void VisitCommentBlock(RazorCommentChunkGenerator chunkGenerator, Block block) + { + WriteBlock(block); + } + + public override void VisitDirectiveBlock(DirectiveChunkGenerator chunkGenerator, Block block) + { + WriteBlock(block); + WriteSeparator(); + Write(chunkGenerator.Descriptor.Directive); + WriteSeparator(); + Write(chunkGenerator.Descriptor.Kind); + WriteSeparator(); + Write(chunkGenerator.Descriptor.Usage); + } + + public override void VisitDynamicAttributeBlock(DynamicAttributeBlockChunkGenerator chunkGenerator, Block block) + { + WriteBlock(block); + WriteSeparator(); + WriteLocationTaggedString(chunkGenerator.Prefix); + WriteSeparator(); + WriteSourceLocation(chunkGenerator.ValueStart); + } + + public override void VisitExpressionBlock(ExpressionChunkGenerator chunkGenerator, Block block) + { + WriteBlock(block); + } + + public override void VisitTemplateBlock(TemplateBlockChunkGenerator chunkGenerator, Block block) + { + WriteBlock(block); + } + + public override void VisitMarkupSpan(MarkupChunkGenerator chunkGenerator, Span span) + { + WriteSpan(span); + } + + public override void VisitAddTagHelperSpan(AddTagHelperChunkGenerator chunkGenerator, Span span) + { + WriteSpan(span); + WriteSeparator(); + Write(chunkGenerator.LookupText); + WriteSeparator(); + Write(chunkGenerator.DirectiveText); + WriteSeparator(); + Write(chunkGenerator.TypePattern); + WriteSeparator(); + Write(chunkGenerator.AssemblyName); + } + + public override void VisitExpressionSpan(ExpressionChunkGenerator chunkGenerator, Span span) + { + WriteSpan(span); + } + + public override void VisitImportSpan(AddImportChunkGenerator chunkGenerator, Span span) + { + WriteSpan(span); + WriteSeparator(); + Write(chunkGenerator.Namespace); + } + + public override void VisitLiteralAttributeSpan(LiteralAttributeChunkGenerator chunkGenerator, Span span) + { + WriteSpan(span); + WriteSeparator(); + WriteLocationTaggedString(chunkGenerator.Prefix); + WriteSeparator(); + WriteLocationTaggedString(chunkGenerator.Value); + } + + public override void VisitRemoveTagHelperSpan(RemoveTagHelperChunkGenerator chunkGenerator, Span span) + { + WriteSpan(span); + WriteSeparator(); + Write(chunkGenerator.LookupText); + WriteSeparator(); + Write(chunkGenerator.DirectiveText); + WriteSeparator(); + Write(chunkGenerator.TypePattern); + WriteSeparator(); + Write(chunkGenerator.AssemblyName); + } + + public override void VisitTagHelperPrefixDirectiveSpan(TagHelperPrefixDirectiveChunkGenerator chunkGenerator, Span span) + { + WriteSpan(span); + WriteSeparator(); + Write(chunkGenerator.Prefix); + WriteSeparator(); + Write(chunkGenerator.DirectiveText); + } + + public override void VisitStatementSpan(StatementChunkGenerator chunkGenerator, Span span) + { + WriteSpan(span); + } + + public override void VisitDirectiveToken(DirectiveTokenChunkGenerator chunkGenerator, Span span) + { + WriteSpan(span); + WriteSeparator(); + Write(chunkGenerator.Descriptor.Kind); + WriteSeparator(); + Write(chunkGenerator.Descriptor.Name); + WriteSeparator(); + Write($"Optional: {chunkGenerator.Descriptor.Optional}"); + } + + protected void WriteBlock(Block block) + { + WriteIndent(); + Write($"{block.Type} block"); + WriteSeparator(); + Write(block.ChunkGenerator.GetType().Name); + WriteSeparator(); + Write(block.Length); + WriteSeparator(); + WriteSourceLocation(block.Start); + } + + protected void WriteSpan(Span span) + { + WriteIndent(); + Write($"{span.Kind} span"); + WriteSeparator(); + Write(span.ChunkGenerator.GetType().Name); + WriteSeparator(); + Write(span.EditHandler); + WriteSeparator(); + Write(span.Content); + WriteSeparator(); + WriteSourceLocation(span.Start); + } + + protected void WriteSourceLocation(SourceLocation location) + { + Write(location); + } + + protected void WriteLocationTaggedString(LocationTagged item) + { + Write(item.ToString("F", null)); + } + + protected void WriteIndent() + { + for (var i = 0; i < Depth; i++) + { + for (var j = 0; j < 4; j++) + { + Write(' '); + } + } + } + + protected void WriteSeparator() + { + Write(" - "); + } + + protected void WriteNewLine() + { + _writer.WriteLine(); + } + + protected void Write(object value) + { + if (value is string stringValue) + { + stringValue = stringValue.Replace("\r\n", "LF"); + _writer.Write(stringValue); + return; + } + + _writer.Write(value); + } + } +} \ No newline at end of file