Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/dotnet/aspnetcore.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common')
-rw-r--r--src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/CodeGeneration/TestCodeRenderingContext.cs99
-rw-r--r--src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntegrationTestBase.cs321
-rw-r--r--src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntermediateNodeSerializer.cs46
-rw-r--r--src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntermediateNodeVerifier.cs275
-rw-r--r--src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntermediateNodeWriter.cs306
-rw-r--r--src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntializeTestFileAttribute.cs28
-rw-r--r--src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/RazorDiagnosticSerializer.cs13
-rw-r--r--src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/SourceMappingsSerializer.cs48
-rw-r--r--src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Intermediate/IntermediateNodeAssert.cs495
-rw-r--r--src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/BlockFactory.cs99
-rw-r--r--src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/BlockTypes.cs230
-rw-r--r--src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/ErrorCollector.cs57
-rw-r--r--src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/ParserTestBase.cs696
-rw-r--r--src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/RawTextSymbol.cs55
-rw-r--r--src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/TestSpanBuilder.cs443
-rw-r--r--src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/RazorEngineBuilderExtensions.cs71
-rw-r--r--src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/RazorProjectEngineBuilderExtensions.cs70
-rw-r--r--src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/SyntaxTreeVerifier.cs42
-rw-r--r--src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestBoundAttributeDescriptorBuilderExtensions.cs123
-rw-r--r--src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestFile.cs89
-rw-r--r--src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestProject.cs28
-rw-r--r--src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorCodeDocument.cs27
-rw-r--r--src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorProjectFileSystem.cs41
-rw-r--r--src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorProjectItem.cs49
-rw-r--r--src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorSourceDocument.cs113
-rw-r--r--src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRequiredAttributeDescriptorBuilderExtensions.cs69
-rw-r--r--src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestTagHelperDescriptorBuilderExtensions.cs122
-rw-r--r--src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestTagHelperFeature.cs27
-rw-r--r--src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestTagMatchingRuleDescriptorBuilderExtensions.cs72
-rw-r--r--src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Microsoft.AspNetCore.Razor.Test.Common.csproj21
-rw-r--r--src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Properties/AssemblyInfo.cs12
-rw-r--r--src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/TestCompilation.cs71
32 files changed, 4258 insertions, 0 deletions
diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/CodeGeneration/TestCodeRenderingContext.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/CodeGeneration/TestCodeRenderingContext.cs
new file mode 100644
index 0000000000..2791dbcf62
--- /dev/null
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/CodeGeneration/TestCodeRenderingContext.cs
@@ -0,0 +1,99 @@
+// 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.Razor.Language.Intermediate;
+
+namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration
+{
+ public static class TestCodeRenderingContext
+ {
+ public static CodeRenderingContext CreateDesignTime(
+ string newLineString = null,
+ string suppressUniqueIds = "test",
+ RazorSourceDocument source = null,
+ IntermediateNodeWriter nodeWriter = null)
+ {
+ var codeWriter = new CodeWriter();
+ var documentNode = new DocumentIntermediateNode();
+ var options = RazorCodeGenerationOptions.CreateDesignTimeDefault();
+
+ if (source == null)
+ {
+ source = TestRazorSourceDocument.Create();
+ }
+
+ var codeDocument = RazorCodeDocument.Create(source);
+ if (newLineString != null)
+ {
+ codeDocument.Items[CodeRenderingContext.NewLineString] = newLineString;
+ }
+
+ if (suppressUniqueIds != null)
+ {
+ codeDocument.Items[CodeRenderingContext.SuppressUniqueIds] = suppressUniqueIds;
+ }
+
+ if (nodeWriter == null)
+ {
+ nodeWriter = new DesignTimeNodeWriter();
+ }
+
+ var context = new DefaultCodeRenderingContext(codeWriter, nodeWriter, codeDocument, documentNode, options);
+ context.Visitor = new RenderChildrenVisitor(context);
+
+ return context;
+ }
+
+ public static CodeRenderingContext CreateRuntime(
+ string newLineString = null,
+ string suppressUniqueIds = "test",
+ RazorSourceDocument source = null,
+ IntermediateNodeWriter nodeWriter = null)
+ {
+ var codeWriter = new CodeWriter();
+ var documentNode = new DocumentIntermediateNode();
+ var options = RazorCodeGenerationOptions.CreateDefault();
+
+ if (source == null)
+ {
+ source = TestRazorSourceDocument.Create();
+ }
+
+ var codeDocument = RazorCodeDocument.Create(source);
+ if (newLineString != null)
+ {
+ codeDocument.Items[CodeRenderingContext.NewLineString] = newLineString;
+ }
+
+ if (suppressUniqueIds != null)
+ {
+ codeDocument.Items[CodeRenderingContext.SuppressUniqueIds] = suppressUniqueIds;
+ }
+
+ if (nodeWriter == null)
+ {
+ nodeWriter = new RuntimeNodeWriter();
+ }
+
+ var context = new DefaultCodeRenderingContext(codeWriter, nodeWriter, codeDocument, documentNode, options);
+ context.Visitor = new RenderChildrenVisitor(context);
+
+ return context;
+ }
+
+ private class RenderChildrenVisitor : IntermediateNodeVisitor
+ {
+ private readonly CodeRenderingContext _context;
+ public RenderChildrenVisitor(CodeRenderingContext context)
+ {
+ _context = context;
+ }
+
+ public override void VisitDefault(IntermediateNode node)
+ {
+ _context.CodeWriter.WriteLine("Render Children");
+ }
+ }
+
+ }
+}
diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntegrationTestBase.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntegrationTestBase.cs
new file mode 100644
index 0000000000..0d6dd241ce
--- /dev/null
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntegrationTestBase.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 System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+#if NET46
+using System.Runtime.Remoting;
+using System.Runtime.Remoting.Messaging;
+#else
+using System.Threading;
+#endif
+using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
+using Microsoft.AspNetCore.Razor.Language.Intermediate;
+using Xunit;
+using Xunit.Sdk;
+
+namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests
+{
+ [IntializeTestFile]
+ public abstract class IntegrationTestBase
+ {
+#if !NET46
+ private static readonly AsyncLocal<string> _fileName = new AsyncLocal<string>();
+#endif
+
+ protected IntegrationTestBase()
+ {
+ TestProjectRoot = TestProject.GetProjectDirectory(GetType());
+ }
+
+#if GENERATE_BASELINES
+ protected bool GenerateBaselines { get; set; } = true;
+#else
+ protected bool GenerateBaselines { get; set; } = false;
+#endif
+
+ protected string TestProjectRoot { get; }
+
+ // 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("IntegrationTestBase_FileName");
+ return (string)handle.Unwrap();
+ }
+ set
+ {
+ CallContext.LogicalSetData("IntegrationTestBase_FileName", new ObjectHandle(value));
+ }
+#elif NETCOREAPP2_0 || NETCOREAPP2_1
+ get { return _fileName.Value; }
+ set { _fileName.Value = value; }
+#endif
+ }
+
+ protected virtual RazorProjectEngine CreateProjectEngine() => CreateProjectEngine(configure: null);
+
+ protected virtual RazorProjectEngine CreateProjectEngine(Action<RazorProjectEngineBuilder> configure)
+ {
+ if (FileName == null)
+ {
+ var message = $"{nameof(CreateProjectEngine)} should only be called from an integration test, ({nameof(FileName)} is null).";
+ throw new InvalidOperationException(message);
+ }
+
+ var assembly = GetType().GetTypeInfo().Assembly;
+ var projectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, IntegrationTestFileSystem.Default, b =>
+ {
+ configure?.Invoke(b);
+
+ var existingImportFeature = b.Features.OfType<IImportProjectFeature>().Single();
+ b.SetImportFeature(new IntegrationTestImportFeature(assembly, existingImportFeature));
+ });
+ var testProjectEngine = new IntegrationTestProjectEngine(projectEngine);
+
+ return testProjectEngine;
+ }
+
+ protected virtual RazorProjectItem CreateProjectItem()
+ {
+ if (FileName == null)
+ {
+ var message = $"{nameof(CreateProjectItem)} should only be called from an integration test, ({nameof(FileName)} is null).";
+ throw new InvalidOperationException(message);
+ }
+
+ var suffixIndex = FileName.LastIndexOf("_");
+ var normalizedFileName = suffixIndex == -1 ? FileName : FileName.Substring(0, suffixIndex);
+ var sourceFileName = Path.ChangeExtension(normalizedFileName, ".cshtml");
+ var testFile = TestFile.Create(sourceFileName, GetType().GetTypeInfo().Assembly);
+ if (!testFile.Exists())
+ {
+ throw new XunitException($"The resource {sourceFileName} was not found.");
+ }
+ var fileContent = testFile.ReadAllText();
+ var normalizedContent = NormalizeNewLines(fileContent);
+
+ var projectItem = new TestRazorProjectItem(sourceFileName)
+ {
+ Content = normalizedContent,
+ };
+
+ return projectItem;
+ }
+
+ protected void AssertDocumentNodeMatchesBaseline(DocumentIntermediateNode document)
+ {
+ if (FileName == null)
+ {
+ var message = $"{nameof(AssertDocumentNodeMatchesBaseline)} should only be called from an integration test ({nameof(FileName)} is null).";
+ throw new InvalidOperationException(message);
+ }
+
+ var baselineFileName = Path.ChangeExtension(FileName, ".ir.txt");
+
+ if (GenerateBaselines)
+ {
+ var baselineFullPath = Path.Combine(TestProjectRoot, baselineFileName);
+ File.WriteAllText(baselineFullPath, IntermediateNodeSerializer.Serialize(document));
+ return;
+ }
+
+ var irFile = TestFile.Create(baselineFileName, GetType().GetTypeInfo().Assembly);
+ if (!irFile.Exists())
+ {
+ throw new XunitException($"The resource {baselineFileName} was not found.");
+ }
+
+ var baseline = irFile.ReadAllText().Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
+ IntermediateNodeVerifier.Verify(document, baseline);
+ }
+
+ protected void AssertCSharpDocumentMatchesBaseline(RazorCSharpDocument document)
+ {
+ if (FileName == null)
+ {
+ var message = $"{nameof(AssertCSharpDocumentMatchesBaseline)} should only be called from an integration test ({nameof(FileName)} is null).";
+ throw new InvalidOperationException(message);
+ }
+
+ var baselineFileName = Path.ChangeExtension(FileName, ".codegen.cs");
+ var baselineDiagnosticsFileName = Path.ChangeExtension(FileName, ".diagnostics.txt");
+
+ if (GenerateBaselines)
+ {
+ var baselineFullPath = Path.Combine(TestProjectRoot, baselineFileName);
+ File.WriteAllText(baselineFullPath, document.GeneratedCode);
+
+ var baselineDiagnosticsFullPath = Path.Combine(TestProjectRoot, baselineDiagnosticsFileName);
+ var lines = document.Diagnostics.Select(RazorDiagnosticSerializer.Serialize).ToArray();
+ if (lines.Any())
+ {
+ File.WriteAllLines(baselineDiagnosticsFullPath, lines);
+ }
+ else if (File.Exists(baselineDiagnosticsFullPath))
+ {
+ File.Delete(baselineDiagnosticsFullPath);
+ }
+
+ return;
+ }
+
+ var codegenFile = TestFile.Create(baselineFileName, GetType().GetTypeInfo().Assembly);
+ if (!codegenFile.Exists())
+ {
+ throw new XunitException($"The resource {baselineFileName} was not found.");
+ }
+
+ var baseline = codegenFile.ReadAllText();
+
+ // Normalize newlines to match those in the baseline.
+ var actual = document.GeneratedCode.Replace("\r", "").Replace("\n", "\r\n");
+ Assert.Equal(baseline, actual);
+
+ var baselineDiagnostics = string.Empty;
+ var diagnosticsFile = TestFile.Create(baselineDiagnosticsFileName, GetType().GetTypeInfo().Assembly);
+ if (diagnosticsFile.Exists())
+ {
+ baselineDiagnostics = diagnosticsFile.ReadAllText();
+ }
+
+ var actualDiagnostics = string.Concat(document.Diagnostics.Select(d => RazorDiagnosticSerializer.Serialize(d) + "\r\n"));
+ Assert.Equal(baselineDiagnostics, actualDiagnostics);
+ }
+
+ protected void AssertSourceMappingsMatchBaseline(RazorCodeDocument document)
+ {
+ if (FileName == null)
+ {
+ var message = $"{nameof(AssertSourceMappingsMatchBaseline)} should only be called from an integration test ({nameof(FileName)} is null).";
+ throw new InvalidOperationException(message);
+ }
+
+ var csharpDocument = document.GetCSharpDocument();
+ Assert.NotNull(csharpDocument);
+
+ var baselineFileName = Path.ChangeExtension(FileName, ".mappings.txt");
+ var serializedMappings = SourceMappingsSerializer.Serialize(csharpDocument, document.Source);
+
+ if (GenerateBaselines)
+ {
+ var baselineFullPath = Path.Combine(TestProjectRoot, baselineFileName);
+ File.WriteAllText(baselineFullPath, serializedMappings);
+ return;
+ }
+
+ var testFile = TestFile.Create(baselineFileName, GetType().GetTypeInfo().Assembly);
+ if (!testFile.Exists())
+ {
+ throw new XunitException($"The resource {baselineFileName} was not found.");
+ }
+
+ var baseline = testFile.ReadAllText();
+
+ // Normalize newlines to match those in the baseline.
+ var actual = serializedMappings.Replace("\r", "").Replace("\n", "\r\n");
+
+ Assert.Equal(baseline, actual);
+ }
+
+ private static string NormalizeNewLines(string content)
+ {
+ return Regex.Replace(content, "(?<!\r)\n", "\r\n", RegexOptions.None, TimeSpan.FromSeconds(10));
+ }
+
+ private class IntegrationTestProjectEngine : DefaultRazorProjectEngine
+ {
+ public IntegrationTestProjectEngine(
+ RazorProjectEngine innerEngine)
+ : base(innerEngine.Configuration, innerEngine.Engine, innerEngine.FileSystem, innerEngine.ProjectFeatures)
+ {
+ }
+
+ protected override void ProcessCore(RazorCodeDocument codeDocument)
+ {
+ // This will ensure that we're not putting any randomly generated data in a baseline.
+ codeDocument.Items[CodeRenderingContext.SuppressUniqueIds] = "test";
+
+ // This is to make tests work cross platform.
+ codeDocument.Items[CodeRenderingContext.NewLineString] = "\r\n";
+
+ base.ProcessCore(codeDocument);
+ }
+ }
+
+ private class IntegrationTestImportFeature : RazorProjectEngineFeatureBase, IImportProjectFeature
+ {
+ private Assembly _assembly;
+ private IImportProjectFeature _existingImportFeature;
+
+ public IntegrationTestImportFeature(Assembly assembly, IImportProjectFeature existingImportFeature)
+ {
+ _assembly = assembly;
+ _existingImportFeature = existingImportFeature;
+ }
+
+ protected override void OnInitialized()
+ {
+ _existingImportFeature.ProjectEngine = ProjectEngine;
+ }
+
+ public IReadOnlyList<RazorProjectItem> GetImports(RazorProjectItem projectItem)
+ {
+ var imports = new List<RazorProjectItem>();
+
+ while (true)
+ {
+ var importsFileName = Path.ChangeExtension(projectItem.FilePathWithoutExtension + "_Imports" + imports.Count.ToString(), ".cshtml");
+ var importsFile = TestFile.Create(importsFileName, _assembly);
+ if (!importsFile.Exists())
+ {
+ break;
+ }
+
+ var importContent = importsFile.ReadAllText();
+ var normalizedContent = NormalizeNewLines(importContent);
+ var importItem = new TestRazorProjectItem(importsFileName)
+ {
+ Content = normalizedContent
+ };
+ imports.Add(importItem);
+ }
+
+ imports.AddRange(_existingImportFeature.GetImports(projectItem));
+
+ return imports;
+ }
+ }
+
+ private class IntegrationTestFileSystem : RazorProjectFileSystem
+ {
+ public static IntegrationTestFileSystem Default = new IntegrationTestFileSystem();
+
+ private IntegrationTestFileSystem()
+ {
+ }
+
+ public override IEnumerable<RazorProjectItem> EnumerateItems(string basePath)
+ {
+ return Enumerable.Empty<RazorProjectItem>();
+ }
+
+ public override RazorProjectItem GetItem(string path)
+ {
+ return new NotFoundProjectItem(string.Empty, path);
+ }
+
+ public override IEnumerable<RazorProjectItem> FindHierarchicalItems(string basePath, string path, string fileName)
+ {
+ return Enumerable.Empty<RazorProjectItem>();
+ }
+ }
+ }
+}
diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntermediateNodeSerializer.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntermediateNodeSerializer.cs
new file mode 100644
index 0000000000..891a4849ed
--- /dev/null
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntermediateNodeSerializer.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.IO;
+using Microsoft.AspNetCore.Razor.Language.Intermediate;
+
+namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests
+{
+ public static class IntermediateNodeSerializer
+ {
+ public static string Serialize(IntermediateNode node)
+ {
+ using (var writer = new StringWriter())
+ {
+ var walker = new Walker(writer);
+ walker.Visit(node);
+
+ return writer.ToString();
+ }
+ }
+
+ private class Walker : IntermediateNodeWalker
+ {
+ private readonly IntermediateNodeWriter _visitor;
+ private readonly TextWriter _writer;
+
+ public Walker(TextWriter writer)
+ {
+ _visitor = new IntermediateNodeWriter(writer);
+ _writer = writer;
+ }
+
+ public TextWriter Writer { get; }
+
+ public override void VisitDefault(IntermediateNode node)
+ {
+ _visitor.Visit(node);
+ _writer.WriteLine();
+
+ _visitor.Depth++;
+ base.VisitDefault(node);
+ _visitor.Depth--;
+ }
+ }
+ }
+}
diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntermediateNodeVerifier.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntermediateNodeVerifier.cs
new file mode 100644
index 0000000000..a75da6a2fc
--- /dev/null
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntermediateNodeVerifier.cs
@@ -0,0 +1,275 @@
+// 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 Microsoft.AspNetCore.Razor.Language.Intermediate;
+using Xunit;
+using Xunit.Sdk;
+
+namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests
+{
+ public static class IntermediateNodeVerifier
+ {
+ public static void Verify(IntermediateNode node, string[] baseline)
+ {
+ var walker = new Walker(baseline);
+ walker.Visit(node);
+ walker.AssertReachedEndOfBaseline();
+ }
+
+ private class Walker : IntermediateNodeWalker
+ {
+ private readonly string[] _baseline;
+ private readonly IntermediateNodeWriter _visitor;
+ private readonly StringWriter _writer;
+
+ private int _index;
+
+ public Walker(string[] baseline)
+ {
+ _writer = new StringWriter();
+
+ _visitor = new IntermediateNodeWriter(_writer);
+ _baseline = baseline;
+
+ }
+
+ public TextWriter Writer { get; }
+
+ public override void VisitDefault(IntermediateNode 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);
+
+ _visitor.Depth++;
+ base.VisitDefault(node);
+ _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(IntermediateNode node, IEnumerable<IntermediateNode> 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 IntermediateNodeBaselineException(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(IntermediateNode node, IEnumerable<IntermediateNode> 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 IntermediateNodeBaselineException(node, ancestors.ToArray(), expected, actual, message);
+ }
+
+ charsVerified = j;
+ }
+
+ private void AssertNameEqual(IntermediateNode node, IEnumerable<IntermediateNode> 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 IntermediateNodeBaselineException(node, ancestors.ToArray(), expected, actual, message);
+ }
+
+ charsVerified += expectedName.Length;
+ }
+
+ // Either both strings need to have a delimiter next or neither should.
+ private void AssertDelimiter(IntermediateNode 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(IntermediateNode node, IEnumerable<IntermediateNode> 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 IntermediateNodeBaselineException(node, ancestors.ToArray(), expected, actual, message);
+ }
+
+ charsVerified += expectedLocation.Length;
+ }
+
+ private void AssertContentEqual(IntermediateNode node, IEnumerable<IntermediateNode> 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 IntermediateNodeBaselineException(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 IntermediateNodeBaselineException : XunitException
+ {
+ public IntermediateNodeBaselineException(IntermediateNode node, IntermediateNode[] ancestors, string expected, string actual, string userMessage)
+ : base(Format(node, ancestors, expected, actual, userMessage))
+ {
+ Node = node;
+ Expected = expected;
+ Actual = actual;
+ }
+
+ public IntermediateNode Node { get; }
+
+ public string Actual { get; }
+
+ public string Expected { get; }
+
+ private static string Format(IntermediateNode node, IntermediateNode[] 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();
+ }
+ }
+ }
+ }
+}
diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntermediateNodeWriter.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntermediateNodeWriter.cs
new file mode 100644
index 0000000000..4329c0d02d
--- /dev/null
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntermediateNodeWriter.cs
@@ -0,0 +1,306 @@
+// 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.Security.Cryptography;
+using System.Text;
+using Microsoft.AspNetCore.Razor.Language.Extensions;
+using Microsoft.AspNetCore.Razor.Language.Intermediate;
+
+namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests
+{
+ // Serializes single IR nodes (shallow).
+ public class IntermediateNodeWriter :
+ IntermediateNodeVisitor,
+ IExtensionIntermediateNodeVisitor<SectionIntermediateNode>
+ {
+ private readonly TextWriter _writer;
+
+ public IntermediateNodeWriter(TextWriter writer)
+ {
+ _writer = writer;
+ }
+
+ public int Depth { get; set; }
+
+ public override void VisitDefault(IntermediateNode node)
+ {
+ WriteBasicNode(node);
+ }
+
+ public override void VisitClassDeclaration(ClassDeclarationIntermediateNode node)
+ {
+ var entries = new List<string>()
+ {
+ string.Join(" ", node.Modifiers),
+ node.ClassName,
+ node.BaseType,
+ string.Join(", ", node.Interfaces ?? Array.Empty<string>())
+ };
+
+ // Avoid adding the type parameters to the baseline if they aren't present.
+ if (node.TypeParameters != null && node.TypeParameters.Count > 0)
+ {
+ entries.Add(string.Join(", ", node.TypeParameters));
+ }
+
+ WriteContentNode(node, entries.ToArray());
+ }
+
+ public override void VisitCSharpExpressionAttributeValue(CSharpExpressionAttributeValueIntermediateNode node)
+ {
+ WriteContentNode(node, node.Prefix);
+ }
+
+ public override void VisitCSharpCodeAttributeValue(CSharpCodeAttributeValueIntermediateNode node)
+ {
+ WriteContentNode(node, node.Prefix);
+ }
+
+ public override void VisitToken(IntermediateToken node)
+ {
+ WriteContentNode(node, node.Kind.ToString(), node.Content);
+ }
+
+ public override void VisitMalformedDirective(MalformedDirectiveIntermediateNode node)
+ {
+ WriteContentNode(node, node.DirectiveName);
+ }
+
+ public override void VisitDirective(DirectiveIntermediateNode node)
+ {
+ WriteContentNode(node, node.DirectiveName);
+ }
+
+ public override void VisitDirectiveToken(DirectiveTokenIntermediateNode node)
+ {
+ WriteContentNode(node, node.Content);
+ }
+
+ public override void VisitFieldDeclaration(FieldDeclarationIntermediateNode node)
+ {
+ WriteContentNode(node, string.Join(" ", node.Modifiers), node.FieldType, node.FieldName);
+ }
+
+ public override void VisitHtmlAttribute(HtmlAttributeIntermediateNode node)
+ {
+ WriteContentNode(node, node.Prefix, node.Suffix);
+ }
+
+ public override void VisitHtmlAttributeValue(HtmlAttributeValueIntermediateNode node)
+ {
+ WriteContentNode(node, node.Prefix);
+ }
+
+ public override void VisitNamespaceDeclaration(NamespaceDeclarationIntermediateNode node)
+ {
+ WriteContentNode(node, node.Content);
+ }
+
+ public override void VisitMethodDeclaration(MethodDeclarationIntermediateNode node)
+ {
+ WriteContentNode(node, string.Join(" ", node.Modifiers), node.ReturnType, node.MethodName);
+ }
+
+ public override void VisitUsingDirective(UsingDirectiveIntermediateNode node)
+ {
+ WriteContentNode(node, node.Content);
+ }
+
+ public override void VisitTagHelper(TagHelperIntermediateNode node)
+ {
+ WriteContentNode(node, node.TagName, string.Format("{0}.{1}", nameof(TagMode), node.TagMode));
+ }
+
+ public override void VisitTagHelperProperty(TagHelperPropertyIntermediateNode node)
+ {
+ WriteContentNode(node, node.AttributeName, node.BoundAttribute.DisplayName, string.Format("HtmlAttributeValueStyle.{0}", node.AttributeStructure));
+ }
+
+ public override void VisitTagHelperHtmlAttribute(TagHelperHtmlAttributeIntermediateNode node)
+ {
+ WriteContentNode(node, node.AttributeName, string.Format("HtmlAttributeValueStyle.{0}", node.AttributeStructure));
+ }
+
+ public override void VisitExtension(ExtensionIntermediateNode node)
+ {
+ switch (node)
+ {
+ case PreallocatedTagHelperHtmlAttributeIntermediateNode n:
+ WriteContentNode(n, n.VariableName);
+ break;
+ case PreallocatedTagHelperHtmlAttributeValueIntermediateNode n:
+ WriteContentNode(n, n.VariableName, n.AttributeName, n.Value, string.Format("HtmlAttributeValueStyle.{0}", n.AttributeStructure));
+ break;
+ case PreallocatedTagHelperPropertyIntermediateNode n:
+ WriteContentNode(n, n.VariableName, n.AttributeName, n.PropertyName);
+ break;
+ case PreallocatedTagHelperPropertyValueIntermediateNode n:
+ WriteContentNode(n, n.VariableName, n.AttributeName, n.Value, string.Format("HtmlAttributeValueStyle.{0}", n.AttributeStructure));
+ break;
+ case DefaultTagHelperCreateIntermediateNode n:
+ WriteContentNode(n, n.TypeName);
+ break;
+ case DefaultTagHelperExecuteIntermediateNode n:
+ WriteBasicNode(n);
+ break;
+ case DefaultTagHelperHtmlAttributeIntermediateNode n:
+ WriteContentNode(n, n.AttributeName, string.Format("HtmlAttributeValueStyle.{0}", n.AttributeStructure));
+ break;
+ case DefaultTagHelperPropertyIntermediateNode n:
+ WriteContentNode(n, n.AttributeName, n.BoundAttribute.DisplayName, string.Format("HtmlAttributeValueStyle.{0}", n.AttributeStructure));
+ break;
+ case DefaultTagHelperRuntimeIntermediateNode n:
+ WriteBasicNode(n);
+ break;
+ default:
+ base.VisitExtension(node);
+ break;
+ }
+ }
+
+ public void VisitExtension(SectionIntermediateNode node)
+ {
+ WriteContentNode(node, node.SectionName);
+ }
+
+ protected void WriteBasicNode(IntermediateNode node)
+ {
+ WriteIndent();
+ WriteName(node);
+ WriteSeparator();
+ WriteSourceRange(node);
+ }
+
+ protected void WriteContentNode(IntermediateNode node, params string[] content)
+ {
+ WriteIndent();
+ WriteName(node);
+ WriteSeparator();
+ WriteSourceRange(node);
+
+ for (var i = 0; i < content.Length; i++)
+ {
+ WriteSeparator();
+ WriteContent(content[i]);
+ }
+ }
+
+ protected void WriteIndent()
+ {
+ for (var i = 0; i < Depth; i++)
+ {
+ for (var j = 0; j < 4; j++)
+ {
+ _writer.Write(' ');
+ }
+ }
+ }
+
+ protected void WriteSeparator()
+ {
+ _writer.Write(" - ");
+ }
+
+ protected void WriteNewLine()
+ {
+ _writer.WriteLine();
+ }
+
+ protected void WriteName(IntermediateNode node)
+ {
+ var typeName = node.GetType().Name;
+ if (typeName.EndsWith("IntermediateNode"))
+ {
+ _writer.Write(typeName.Substring(0, typeName.Length - "IntermediateNode".Length));
+ }
+ else
+ {
+ _writer.Write(typeName);
+ }
+ }
+
+ protected void WriteSourceRange(IntermediateNode node)
+ {
+ if (node.Source != null)
+ {
+ WriteSourceRange(node.Source.Value);
+ }
+ }
+
+ protected void WriteSourceRange(SourceSpan sourceRange)
+ {
+ _writer.Write("(");
+ _writer.Write(sourceRange.AbsoluteIndex);
+ _writer.Write(":");
+ _writer.Write(sourceRange.LineIndex);
+ _writer.Write(",");
+ _writer.Write(sourceRange.CharacterIndex);
+ _writer.Write(" [");
+ _writer.Write(sourceRange.Length);
+ _writer.Write("] ");
+
+ if (sourceRange.FilePath != null)
+ {
+ var fileName = sourceRange.FilePath.Substring(sourceRange.FilePath.LastIndexOf('/') + 1);
+ _writer.Write(fileName);
+ }
+
+ _writer.Write(")");
+ }
+
+ protected void WriteDiagnostics(IntermediateNode node)
+ {
+ if (node.HasDiagnostics)
+ {
+ _writer.Write("| ");
+ for (var i = 0; i < node.Diagnostics.Count; i++)
+ {
+ var diagnostic = node.Diagnostics[i];
+ _writer.Write("{");
+ WriteSourceRange(diagnostic.Span);
+ _writer.Write(": ");
+ _writer.Write(diagnostic.Severity);
+ _writer.Write(" ");
+ _writer.Write(diagnostic.Id);
+ _writer.Write(": ");
+
+ // Purposefully not writing out the entire message to ensure readable IR and because messages
+ // can span multiple lines. Not using string.GetHashCode because we can't have any collisions.
+ using (var md5 = MD5.Create())
+ {
+ var diagnosticMessage = diagnostic.GetMessage();
+ var messageBytes = Encoding.UTF8.GetBytes(diagnosticMessage);
+ var messageHash = md5.ComputeHash(messageBytes);
+ var stringHashBuilder = new StringBuilder();
+
+ for (var j = 0; j < messageHash.Length; j++)
+ {
+ stringHashBuilder.Append(messageHash[j].ToString("x2"));
+ }
+
+ var stringHash = stringHashBuilder.ToString();
+ _writer.Write(stringHash);
+ }
+ _writer.Write("} ");
+ }
+ }
+ }
+
+ protected void WriteContent(string content)
+ {
+ if (content == null)
+ {
+ return;
+ }
+
+ // We explicitly escape newlines in node content so that the IR can be compared line-by-line. The escaped
+ // newline cannot be platform specific so we need to drop the windows \r.
+ // Also, escape our separator so we can search for ` - `to find delimiters.
+ _writer.Write(content.Replace("\r", string.Empty).Replace("\n", "\\n").Replace(" - ", "\\-"));
+ }
+ }
+}
diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntializeTestFileAttribute.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntializeTestFileAttribute.cs
new file mode 100644
index 0000000000..0b1c78a53e
--- /dev/null
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntializeTestFileAttribute.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.
+
+using System.Reflection;
+using Xunit.Sdk;
+
+namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests
+{
+ public class IntializeTestFileAttribute : BeforeAfterTestAttribute
+ {
+ public override void Before(MethodInfo methodUnderTest)
+ {
+ if (typeof(IntegrationTestBase).GetTypeInfo().IsAssignableFrom(methodUnderTest.DeclaringType.GetTypeInfo()))
+ {
+ var typeName = methodUnderTest.DeclaringType.Name;
+ IntegrationTestBase.FileName = $"TestFiles/IntegrationTests/{typeName}/{methodUnderTest.Name}";
+ }
+ }
+
+ public override void After(MethodInfo methodUnderTest)
+ {
+ if (typeof(IntegrationTestBase).GetTypeInfo().IsAssignableFrom(methodUnderTest.DeclaringType.GetTypeInfo()))
+ {
+ IntegrationTestBase.FileName = null;
+ }
+ }
+ }
+}
diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/RazorDiagnosticSerializer.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/RazorDiagnosticSerializer.cs
new file mode 100644
index 0000000000..bb92350872
--- /dev/null
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/RazorDiagnosticSerializer.cs
@@ -0,0 +1,13 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Microsoft.AspNetCore.Razor.Language
+{
+ public class RazorDiagnosticSerializer
+ {
+ public static string Serialize(RazorDiagnostic diagnostic)
+ {
+ return diagnostic.ToString();
+ }
+ }
+}
diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/SourceMappingsSerializer.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/SourceMappingsSerializer.cs
new file mode 100644
index 0000000000..40d95625f4
--- /dev/null
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/SourceMappingsSerializer.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.Text;
+using Microsoft.AspNetCore.Razor.Language.Legacy;
+
+namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests
+{
+ public static class SourceMappingsSerializer
+ {
+ public static string Serialize(RazorCSharpDocument csharpDocument, RazorSourceDocument sourceDocument)
+ {
+ var builder = new StringBuilder();
+ var charBuffer = new char[sourceDocument.Length];
+ sourceDocument.CopyTo(0, charBuffer, 0, sourceDocument.Length);
+ var sourceContent = new string(charBuffer);
+
+ for (var i = 0; i < csharpDocument.SourceMappings.Count; i++)
+ {
+ var sourceMapping = csharpDocument.SourceMappings[i];
+
+ builder.Append("Source Location: ");
+ AppendMappingLocation(builder, sourceMapping.OriginalSpan, sourceContent);
+
+ builder.Append("Generated Location: ");
+ AppendMappingLocation(builder, sourceMapping.GeneratedSpan, csharpDocument.GeneratedCode);
+
+ builder.AppendLine();
+ }
+
+ return builder.ToString();
+ }
+
+ private static void AppendMappingLocation(StringBuilder builder, SourceSpan location, string content)
+ {
+ builder
+ .AppendLine(location.ToString())
+ .Append("|");
+
+ for (var i = 0; i < location.Length; i++)
+ {
+ builder.Append(content[location.AbsoluteIndex + i]);
+ }
+
+ builder.AppendLine("|");
+ }
+ }
+}
diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Intermediate/IntermediateNodeAssert.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Intermediate/IntermediateNodeAssert.cs
new file mode 100644
index 0000000000..b4fc6f105e
--- /dev/null
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Intermediate/IntermediateNodeAssert.cs
@@ -0,0 +1,495 @@
+// 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.Text;
+using Microsoft.AspNetCore.Razor.Language.Extensions;
+using Microsoft.AspNetCore.Razor.Language.Legacy;
+using Xunit;
+using Xunit.Sdk;
+
+namespace Microsoft.AspNetCore.Razor.Language.Intermediate
+{
+ public static class IntermediateNodeAssert
+ {
+ public static TNode SingleChild<TNode>(IntermediateNode node)
+ {
+ if (node.Children.Count == 0)
+ {
+ throw new IntermediateNodeAssertException(node, "The node has no children.");
+ }
+ else if (node.Children.Count > 1)
+ {
+ throw new IntermediateNodeAssertException(node, node.Children, "The node has multiple children");
+ }
+
+ var child = node.Children[0];
+ return Assert.IsType<TNode>(child);
+ }
+
+ public static void NoChildren(IntermediateNode node)
+ {
+ if (node.Children.Count > 0)
+ {
+ throw new IntermediateNodeAssertException(node, node.Children, "The node has children.");
+ }
+ }
+
+ public static void Children(IntermediateNode node, params Action<IntermediateNode>[] validators)
+ {
+ var i = 0;
+ for (; i < validators.Length; i++)
+ {
+ if (node.Children.Count == i)
+ {
+ throw new IntermediateNodeAssertException(node, node.Children, $"The node only has {node.Children.Count} children.");
+ }
+
+ try
+ {
+ validators[i].Invoke(node.Children[i]);
+ }
+ catch (XunitException e)
+ {
+ throw new IntermediateNodeAssertException(node, node.Children, $"Failed while validating node {node.Children[i]} at {i}.", e);
+ }
+ }
+
+ if (i < node.Children.Count)
+ {
+ throw new IntermediateNodeAssertException(node, node.Children, $"The node has extra child {node.Children[i]} at {i}.");
+ }
+ }
+
+ public static void AnnotationEquals(IntermediateNode node, object value)
+ {
+ AnnotationEquals(node, value, value);
+ }
+
+ public static void AnnotationEquals(IntermediateNode node, object key, object value)
+ {
+ try
+ {
+ Assert.NotNull(node);
+ Assert.Equal(value, node.Annotations[key]);
+ }
+ catch (XunitException e)
+ {
+ throw new IntermediateNodeAssertException(node, node.Children, e.Message, e);
+ }
+ }
+
+ public static void HasAnnotation(IntermediateNode node, object key)
+ {
+ try
+ {
+ Assert.NotNull(node);
+ Assert.NotNull(node.Annotations[key]);
+ }
+ catch (XunitException e)
+ {
+ throw new IntermediateNodeAssertException(node, node.Children, e.Message, e);
+ }
+ }
+
+ public static void Html(string expected, IntermediateNode node)
+ {
+ try
+ {
+ var html = Assert.IsType<HtmlContentIntermediateNode>(node);
+ var content = new StringBuilder();
+ for (var i = 0; i < html.Children.Count; i++)
+ {
+ var token = Assert.IsType<IntermediateToken>(html.Children[i]);
+ Assert.Equal(TokenKind.Html, token.Kind);
+ content.Append(token.Content);
+ }
+
+ Assert.Equal(expected, content.ToString());
+ }
+ catch (XunitException e)
+ {
+ throw new IntermediateNodeAssertException(node, node.Children, e.Message, e);
+ }
+ }
+
+ public static void CSharpCode(string expected, IntermediateNode node)
+ {
+ try
+ {
+ var statement = Assert.IsType<CSharpCodeIntermediateNode>(node);
+ var content = new StringBuilder();
+ for (var i = 0; i < statement.Children.Count; i++)
+ {
+ var token = Assert.IsType<IntermediateToken>(statement.Children[i]);
+ Assert.Equal(TokenKind.CSharp, token.Kind);
+ content.Append(token.Content);
+ }
+
+ Assert.Equal(expected, content.ToString());
+ }
+ catch (XunitException e)
+ {
+ throw new IntermediateNodeAssertException(node, node.Children, e.Message, e);
+ }
+ }
+
+ public static void Directive(string expectedName, IntermediateNode node, params Action<IntermediateNode>[] childValidators)
+ {
+ try
+ {
+ var directive = Assert.IsType<DirectiveIntermediateNode>(node);
+ Assert.Equal(expectedName, directive.DirectiveName);
+ }
+ catch (XunitException e)
+ {
+ throw new IntermediateNodeAssertException(node, node.Children, e.Message, e);
+ }
+
+ Children(node, childValidators);
+ }
+
+ public static void DirectiveToken(DirectiveTokenKind expectedKind, string expectedContent, IntermediateNode node)
+ {
+ try
+ {
+ var token = Assert.IsType<DirectiveTokenIntermediateNode>(node);
+ Assert.Equal(expectedKind, token.DirectiveToken.Kind);
+ Assert.Equal(expectedContent, token.Content);
+ }
+ catch (XunitException e)
+ {
+ throw new IntermediateNodeAssertException(node, node.Children, e.Message, e);
+ }
+ }
+
+ public static void Using(string expected, IntermediateNode node)
+ {
+ try
+ {
+ var @using = Assert.IsType<UsingDirectiveIntermediateNode>(node);
+ Assert.Equal(expected, @using.Content);
+ }
+ catch (XunitException e)
+ {
+ throw new IntermediateNodeAssertException(node, node.Children, e.Message, e);
+ }
+ }
+
+ public static void ConditionalAttribute(
+ string prefix,
+ string name,
+ string suffix,
+ IntermediateNode node,
+ params Action<IntermediateNode>[] valueValidators)
+ {
+ var attribute = Assert.IsType<HtmlAttributeIntermediateNode>(node);
+
+ try
+ {
+ Assert.Equal(prefix, attribute.Prefix);
+ Assert.Equal(name, attribute.AttributeName);
+ Assert.Equal(suffix, attribute.Suffix);
+
+ Children(attribute, valueValidators);
+ }
+ catch (XunitException e)
+ {
+ throw new IntermediateNodeAssertException(attribute, attribute.Children, e.Message, e);
+ }
+ }
+
+ public static void CSharpExpressionAttributeValue(string prefix, string expected, IntermediateNode node)
+ {
+ var attributeValue = Assert.IsType<CSharpExpressionAttributeValueIntermediateNode>(node);
+
+ try
+ {
+ var content = new StringBuilder();
+ for (var i = 0; i < attributeValue.Children.Count; i++)
+ {
+ var token = Assert.IsType<IntermediateToken>(attributeValue.Children[i]);
+ Assert.True(token.IsCSharp);
+ content.Append(token.Content);
+ }
+
+ Assert.Equal(prefix, attributeValue.Prefix);
+ Assert.Equal(expected, content.ToString());
+ }
+ catch (XunitException e)
+ {
+ throw new IntermediateNodeAssertException(attributeValue, attributeValue.Children, e.Message, e);
+ }
+ }
+
+ public static void LiteralAttributeValue(string prefix, string expected, IntermediateNode node)
+ {
+ var attributeValue = Assert.IsType<HtmlAttributeValueIntermediateNode>(node);
+
+ try
+ {
+ var content = new StringBuilder();
+ for (var i = 0; i < attributeValue.Children.Count; i++)
+ {
+ var token = Assert.IsType<IntermediateToken>(attributeValue.Children[i]);
+ Assert.True(token.IsHtml);
+ content.Append(token.Content);
+ }
+
+ Assert.Equal(prefix, attributeValue.Prefix);
+ Assert.Equal(expected, content.ToString());
+ }
+ catch (XunitException e)
+ {
+ throw new IntermediateNodeAssertException(attributeValue, e.Message);
+ }
+ }
+
+ public static void CSharpExpression(string expected, IntermediateNode node)
+ {
+ try
+ {
+ var cSharp = Assert.IsType<CSharpExpressionIntermediateNode>(node);
+
+ var content = new StringBuilder();
+ for (var i = 0; i < cSharp.Children.Count; i++)
+ {
+ var token = Assert.IsType<IntermediateToken>(cSharp.Children[i]);
+ Assert.Equal(TokenKind.CSharp, token.Kind);
+ content.Append(token.Content);
+ }
+
+ Assert.Equal(expected, content.ToString());
+ }
+ catch (XunitException e)
+ {
+ throw new IntermediateNodeAssertException(node, node.Children, e.Message, e);
+ }
+ }
+
+ public static void BeginInstrumentation(string expected, IntermediateNode node)
+ {
+ try
+ {
+ var beginNode = Assert.IsType<CSharpCodeIntermediateNode>(node);
+ var content = new StringBuilder();
+ for (var i = 0; i < beginNode.Children.Count; i++)
+ {
+ var token = Assert.IsType<IntermediateToken>(beginNode.Children[i]);
+ Assert.True(token.IsCSharp);
+ content.Append(token.Content);
+ }
+
+ Assert.Equal($"BeginContext({expected});", content.ToString());
+ }
+ catch (XunitException e)
+ {
+ throw new IntermediateNodeAssertException(node, node.Children, e.Message, e);
+ }
+ }
+
+ public static void EndInstrumentation(IntermediateNode node)
+ {
+ try
+ {
+ var endNode = Assert.IsType<CSharpCodeIntermediateNode>(node);
+ var content = new StringBuilder();
+ for (var i = 0; i < endNode.Children.Count; i++)
+ {
+ var token = Assert.IsType<IntermediateToken>(endNode.Children[i]);
+ Assert.Equal(TokenKind.CSharp, token.Kind);
+ content.Append(token.Content);
+ }
+ Assert.Equal("EndContext();", content.ToString());
+ }
+ catch (XunitException e)
+ {
+ throw new IntermediateNodeAssertException(node, node.Children, e.Message, e);
+ }
+ }
+
+ internal static void TagHelperFieldDeclaration(IntermediateNode node, string typeFullName)
+ {
+ try
+ {
+ var fieldNode = Assert.IsType<FieldDeclarationIntermediateNode>(node);
+ Assert.Equal(typeFullName, fieldNode.FieldType);
+ }
+ catch (XunitException e)
+ {
+ throw new IntermediateNodeAssertException(node, e.Message);
+ }
+ }
+
+ internal static void PreallocatedTagHelperPropertyValue(
+ IntermediateNode node,
+ string attributeName,
+ string value,
+ AttributeStructure valueStyle)
+ {
+ var propertyValueNode = Assert.IsType<PreallocatedTagHelperPropertyValueIntermediateNode>(node);
+
+ try
+ {
+ Assert.Equal(attributeName, propertyValueNode.AttributeName);
+ Assert.Equal(value, propertyValueNode.Value);
+ Assert.Equal(valueStyle, propertyValueNode.AttributeStructure);
+ }
+ catch (XunitException e)
+ {
+ throw new IntermediateNodeAssertException(propertyValueNode, e.Message);
+ }
+ }
+
+ internal static void TagHelper(string tagName, TagMode tagMode, IEnumerable<TagHelperDescriptor> tagHelpers, IntermediateNode node, params Action<IntermediateNode>[] childValidators)
+ {
+ var tagHelperNode = Assert.IsType<TagHelperIntermediateNode>(node);
+
+ try
+ {
+ Assert.Equal(tagName, tagHelperNode.TagName);
+ Assert.Equal(tagMode, tagHelperNode.TagMode);
+
+ Assert.Equal(tagHelpers, tagHelperNode.TagHelpers, TagHelperDescriptorComparer.CaseSensitive);
+ }
+ catch (XunitException e)
+ {
+ throw new IntermediateNodeAssertException(tagHelperNode, e.Message);
+ }
+
+ Children(node, childValidators);
+ }
+
+ internal static void TagHelperHtmlAttribute(
+ string name,
+ AttributeStructure valueStyle,
+ IntermediateNode node,
+ params Action<IntermediateNode>[] valueValidators)
+ {
+ var tagHelperHtmlAttribute = Assert.IsType<TagHelperHtmlAttributeIntermediateNode>(node);
+
+ try
+ {
+ Assert.Equal(name, tagHelperHtmlAttribute.AttributeName);
+ Assert.Equal(valueStyle, tagHelperHtmlAttribute.AttributeStructure);
+ Children(tagHelperHtmlAttribute, valueValidators);
+ }
+ catch (XunitException e)
+ {
+ throw new IntermediateNodeAssertException(tagHelperHtmlAttribute, tagHelperHtmlAttribute.Children, e.Message, e);
+ }
+ }
+
+ internal static void SetPreallocatedTagHelperProperty(IntermediateNode node, string attributeName, string propertyName)
+ {
+ var setPreallocatedTagHelperProperty = Assert.IsType<PreallocatedTagHelperPropertyIntermediateNode>(node);
+
+ try
+ {
+ Assert.Equal(attributeName, setPreallocatedTagHelperProperty.AttributeName);
+ Assert.Equal(propertyName, setPreallocatedTagHelperProperty.PropertyName);
+ }
+ catch (XunitException e)
+ {
+ throw new IntermediateNodeAssertException(setPreallocatedTagHelperProperty, e.Message);
+ }
+ }
+
+ internal static void SetTagHelperProperty(
+ string name,
+ string propertyName,
+ AttributeStructure valueStyle,
+ IntermediateNode node,
+ params Action<IntermediateNode>[] valueValidators)
+ {
+ var propertyNode = Assert.IsType<TagHelperPropertyIntermediateNode>(node);
+
+ try
+ {
+ Assert.Equal(name, propertyNode.AttributeName);
+ Assert.Equal(propertyName, propertyNode.BoundAttribute.GetPropertyName());
+ Assert.Equal(valueStyle, propertyNode.AttributeStructure);
+ Children(propertyNode, valueValidators);
+ }
+ catch (XunitException e)
+ {
+ throw new IntermediateNodeAssertException(propertyNode, propertyNode.Children, e.Message, e);
+ }
+ }
+
+ private class IntermediateNodeAssertException : XunitException
+ {
+ public IntermediateNodeAssertException(IntermediateNode node, string userMessage)
+ : base(Format(node, null, null, userMessage))
+ {
+ Node = node;
+ }
+
+ public IntermediateNodeAssertException(IntermediateNode node, IEnumerable<IntermediateNode> nodes, string userMessage)
+ : base(Format(node, null, nodes, userMessage))
+ {
+ Node = node;
+ Nodes = nodes;
+ }
+
+ public IntermediateNodeAssertException(
+ IntermediateNode node,
+ IEnumerable<IntermediateNode> nodes,
+ string userMessage,
+ Exception innerException)
+ : base(Format(node, null, nodes, userMessage), innerException)
+ {
+ }
+
+ public IntermediateNodeAssertException(
+ IntermediateNode node,
+ IntermediateNode[] ancestors,
+ IEnumerable<IntermediateNode> nodes,
+ string userMessage,
+ Exception innerException)
+ : base(Format(node, ancestors, nodes, userMessage), innerException)
+ {
+ }
+
+ public IntermediateNode Node { get; }
+
+ public IEnumerable<IntermediateNode> Nodes { get; }
+
+ private static string Format(IntermediateNode node, IntermediateNode[] ancestors, IEnumerable<IntermediateNode> nodes, string userMessage)
+ {
+ var builder = new StringBuilder();
+ builder.AppendLine(userMessage);
+ builder.AppendLine();
+
+ if (nodes != null)
+ {
+ builder.AppendLine("Nodes:");
+
+ foreach (var n in nodes)
+ {
+ builder.AppendLine(n.ToString());
+ }
+
+ builder.AppendLine();
+ }
+
+
+ builder.AppendLine("Path:");
+
+ if (ancestors != null)
+ {
+ builder.AppendLine();
+ builder.AppendLine("Path:");
+
+ foreach (var ancestor in ancestors)
+ {
+ builder.AppendLine(ancestor.ToString());
+ }
+ }
+
+ return builder.ToString();
+ }
+ }
+ }
+}
diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/BlockFactory.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/BlockFactory.cs
new file mode 100644
index 0000000000..2551fee917
--- /dev/null
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/BlockFactory.cs
@@ -0,0 +1,99 @@
+// 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.Language.Legacy
+{
+ internal class BlockFactory
+ {
+ private SpanFactory _factory;
+
+ public BlockFactory(SpanFactory factory)
+ {
+ _factory = factory;
+ }
+
+ public Block EscapedMarkupTagBlock(string prefix, string suffix)
+ {
+ return EscapedMarkupTagBlock(prefix, suffix, AcceptedCharactersInternal.Any);
+ }
+
+ public Block EscapedMarkupTagBlock(string prefix, string suffix, params SyntaxTreeNode[] children)
+ {
+ return EscapedMarkupTagBlock(prefix, suffix, AcceptedCharactersInternal.Any, children);
+ }
+
+ public Block EscapedMarkupTagBlock(
+ string prefix,
+ string suffix,
+ AcceptedCharactersInternal acceptedCharacters,
+ params SyntaxTreeNode[] children)
+ {
+ var newChildren = new List<SyntaxTreeNode>(
+ 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, AcceptedCharactersInternal.Any);
+ }
+
+ public Block MarkupTagBlock(string content, AcceptedCharactersInternal acceptedCharacters)
+ {
+ return new MarkupTagBlock(
+ _factory.Markup(content).Accepts(acceptedCharacters)
+ );
+ }
+
+ public HtmlCommentBlock HtmlCommentBlock(string content)
+ {
+ return HtmlCommentBlock(_factory, f => new SyntaxTreeNode[] { f.Markup(content).Accepts(AcceptedCharactersInternal.WhiteSpace) });
+ }
+
+ public static HtmlCommentBlock HtmlCommentBlock(SpanFactory factory, Func<SpanFactory, IEnumerable<SyntaxTreeNode>> nodesBuilder = null)
+ {
+ var nodes = new List<SyntaxTreeNode>();
+ nodes.Add(factory.Markup("<!--").Accepts(AcceptedCharactersInternal.None));
+ if (nodesBuilder != null)
+ {
+ nodes.AddRange(nodesBuilder(factory));
+ }
+ nodes.Add(factory.Markup("-->").Accepts(AcceptedCharactersInternal.None));
+
+ return new HtmlCommentBlock(nodes.ToArray());
+ }
+
+ public Block TagHelperBlock(
+ string tagName,
+ TagMode tagMode,
+ SourceLocation start,
+ Block startTag,
+ SyntaxTreeNode[] children,
+ Block endTag)
+ {
+ var builder = new TagHelperBlockBuilder(
+ tagName,
+ tagMode,
+ attributes: new List<TagHelperAttributeNode>(),
+ children: children)
+ {
+ Start = start,
+ SourceStartTag = startTag,
+ SourceEndTag = endTag
+ };
+
+ return builder.Build();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/BlockTypes.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/BlockTypes.cs
new file mode 100644
index 0000000000..56deb6b460
--- /dev/null
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/BlockTypes.cs
@@ -0,0 +1,230 @@
+// 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
+{
+ // The product code doesn't need this, but having subclasses for the block types makes tests much cleaner :)
+
+ internal class StatementBlock : Block
+ {
+ private const BlockKindInternal ThisBlockKind = BlockKindInternal.Statement;
+
+ public StatementBlock(IParentChunkGenerator chunkGenerator, IReadOnlyList<SyntaxTreeNode> children)
+ : base(ThisBlockKind, children, chunkGenerator)
+ {
+ }
+
+ public StatementBlock(IParentChunkGenerator chunkGenerator, params SyntaxTreeNode[] children)
+ : this(chunkGenerator, (IReadOnlyList<SyntaxTreeNode>)children)
+ {
+ }
+
+ public StatementBlock(params SyntaxTreeNode[] children)
+ : this(ParentChunkGenerator.Null, children)
+ {
+ }
+ }
+
+ internal class DirectiveBlock : Block
+ {
+ private const BlockKindInternal ThisBlockKind = BlockKindInternal.Directive;
+
+ public DirectiveBlock(IParentChunkGenerator chunkGenerator, IReadOnlyList<SyntaxTreeNode> children)
+ : base(ThisBlockKind, children, chunkGenerator)
+ {
+ }
+
+ public DirectiveBlock(IParentChunkGenerator chunkGenerator, params SyntaxTreeNode[] children)
+ : this(chunkGenerator, (IReadOnlyList<SyntaxTreeNode>)children)
+ {
+ }
+
+ public DirectiveBlock(params SyntaxTreeNode[] children)
+ : this(ParentChunkGenerator.Null, children)
+ {
+ }
+ }
+
+ internal class ExpressionBlock : Block
+ {
+ private const BlockKindInternal ThisBlockKind = BlockKindInternal.Expression;
+
+ public ExpressionBlock(IParentChunkGenerator chunkGenerator, IReadOnlyList<SyntaxTreeNode> children)
+ : base(ThisBlockKind, children, chunkGenerator)
+ {
+ }
+
+ public ExpressionBlock(IParentChunkGenerator chunkGenerator, params SyntaxTreeNode[] children)
+ : this(chunkGenerator, (IReadOnlyList<SyntaxTreeNode>)children)
+ {
+ }
+
+ public ExpressionBlock(params SyntaxTreeNode[] children)
+ : this(new ExpressionChunkGenerator(), children)
+ {
+ }
+ }
+
+ internal class MarkupTagBlock : Block
+ {
+ private const BlockKindInternal ThisBlockKind = BlockKindInternal.Tag;
+
+ public MarkupTagBlock(params SyntaxTreeNode[] children)
+ : base(ThisBlockKind, children, ParentChunkGenerator.Null)
+ {
+ }
+ }
+
+ internal class MarkupBlock : Block
+ {
+ private const BlockKindInternal ThisBlockKind = BlockKindInternal.Markup;
+
+ public MarkupBlock(
+ BlockKindInternal BlockKind,
+ IParentChunkGenerator chunkGenerator,
+ IReadOnlyList<SyntaxTreeNode> children)
+ : base(BlockKind, children, chunkGenerator)
+ {
+ }
+
+ public MarkupBlock(IParentChunkGenerator chunkGenerator, IReadOnlyList<SyntaxTreeNode> children)
+ : this(ThisBlockKind, chunkGenerator, children)
+ {
+ }
+
+ public MarkupBlock(IParentChunkGenerator chunkGenerator, params SyntaxTreeNode[] children)
+ : this(chunkGenerator, (IReadOnlyList<SyntaxTreeNode>)children)
+ {
+ }
+
+ public MarkupBlock(params SyntaxTreeNode[] children)
+ : this(ParentChunkGenerator.Null, children)
+ {
+ }
+ }
+
+ internal class MarkupTagHelperBlock : TagHelperBlock
+ {
+ public MarkupTagHelperBlock(string tagName)
+ : this(tagName, tagMode: TagMode.StartTagAndEndTag, attributes: new List<TagHelperAttributeNode>())
+ {
+ }
+
+ public MarkupTagHelperBlock(string tagName, TagMode tagMode)
+ : this(tagName, tagMode, new List<TagHelperAttributeNode>())
+ {
+ }
+
+ public MarkupTagHelperBlock(
+ string tagName,
+ IList<TagHelperAttributeNode> attributes)
+ : this(tagName, TagMode.StartTagAndEndTag, attributes, children: new SyntaxTreeNode[0])
+ {
+ }
+
+ public MarkupTagHelperBlock(
+ string tagName,
+ TagMode tagMode,
+ IList<TagHelperAttributeNode> attributes)
+ : this(tagName, tagMode, attributes, new SyntaxTreeNode[0])
+ {
+ }
+
+ public MarkupTagHelperBlock(string tagName, params SyntaxTreeNode[] children)
+ : this(
+ tagName,
+ TagMode.StartTagAndEndTag,
+ attributes: new List<TagHelperAttributeNode>(),
+ children: children)
+ {
+ }
+
+ public MarkupTagHelperBlock(string tagName, TagMode tagMode, params SyntaxTreeNode[] children)
+ : this(tagName, tagMode, new List<TagHelperAttributeNode>(), children)
+ {
+ }
+
+ public MarkupTagHelperBlock(
+ string tagName,
+ IList<TagHelperAttributeNode> attributes,
+ params SyntaxTreeNode[] children)
+ : base(new TagHelperBlockBuilder(
+ tagName,
+ TagMode.StartTagAndEndTag,
+ attributes: attributes,
+ children: children))
+ {
+ }
+
+ public MarkupTagHelperBlock(
+ string tagName,
+ TagMode tagMode,
+ IList<TagHelperAttributeNode> attributes,
+ params SyntaxTreeNode[] children)
+ : base(new TagHelperBlockBuilder(tagName, tagMode, attributes, children))
+ {
+ }
+ }
+
+ internal class TemplateBlock : Block
+ {
+ private const BlockKindInternal ThisBlockKind = BlockKindInternal.Template;
+
+ public TemplateBlock(IParentChunkGenerator chunkGenerator, IReadOnlyList<SyntaxTreeNode> children)
+ : base(ThisBlockKind, children, chunkGenerator)
+ {
+ }
+
+ public TemplateBlock(IParentChunkGenerator chunkGenerator, params SyntaxTreeNode[] children)
+ : this(chunkGenerator, (IReadOnlyList<SyntaxTreeNode>)children)
+ {
+ }
+
+ public TemplateBlock(params SyntaxTreeNode[] children)
+ : this(new TemplateBlockChunkGenerator(), children)
+ {
+ }
+
+ public TemplateBlock(IReadOnlyList<SyntaxTreeNode> children)
+ : this(new TemplateBlockChunkGenerator(), children)
+ {
+ }
+ }
+
+ internal class CommentBlock : Block
+ {
+ private const BlockKindInternal ThisBlockKind = BlockKindInternal.Comment;
+
+ public CommentBlock(IParentChunkGenerator chunkGenerator, IReadOnlyList<SyntaxTreeNode> children)
+ : base(ThisBlockKind, children, chunkGenerator)
+ {
+ }
+
+ public CommentBlock(IParentChunkGenerator chunkGenerator, params SyntaxTreeNode[] children)
+ : this(chunkGenerator, (IReadOnlyList<SyntaxTreeNode>)children)
+ {
+ }
+
+ public CommentBlock(params SyntaxTreeNode[] children)
+ : this(new RazorCommentChunkGenerator(), children)
+ {
+ }
+
+ public CommentBlock(IReadOnlyList<SyntaxTreeNode> children)
+ : this(new RazorCommentChunkGenerator(), children)
+ {
+ }
+ }
+
+ internal class HtmlCommentBlock : Block
+ {
+ private const BlockKindInternal ThisBlockKind = BlockKindInternal.HtmlComment;
+
+ public HtmlCommentBlock(params SyntaxTreeNode[] children)
+ : base(ThisBlockKind, children, ParentChunkGenerator.Null)
+ {
+ }
+ }
+}
diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/ErrorCollector.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/ErrorCollector.cs
new file mode 100644
index 0000000000..13da12b61a
--- /dev/null
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/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.Language.Legacy
+{
+ public 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/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/ParserTestBase.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/ParserTestBase.cs
new file mode 100644
index 0000000000..4d80343ff3
--- /dev/null
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/ParserTestBase.cs
@@ -0,0 +1,696 @@
+// 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();
+ }
+
+ /// <summary>
+ /// 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.
+ /// </summary>
+ 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(RazorLanguageVersion.Latest, document, designTime);
+ }
+
+ internal RazorSyntaxTree ParseBlock(RazorLanguageVersion version, string document, bool designTime)
+ {
+ return ParseBlock(version, document, null, designTime);
+ }
+
+ internal RazorSyntaxTree ParseBlock(string document, IEnumerable<DirectiveDescriptor> directives, bool designTime)
+ {
+ return ParseBlock(RazorLanguageVersion.Latest, document, directives, designTime);
+ }
+
+ internal abstract RazorSyntaxTree ParseBlock(RazorLanguageVersion version, string document, IEnumerable<DirectiveDescriptor> directives, bool designTime);
+
+ internal RazorSyntaxTree ParseDocument(string document, bool designTime = false)
+ {
+ return ParseDocument(RazorLanguageVersion.Latest, document, designTime);
+ }
+
+ internal RazorSyntaxTree ParseDocument(RazorLanguageVersion version, string document, bool designTime = false)
+ {
+ return ParseDocument(version, document, null, designTime);
+ }
+
+ internal RazorSyntaxTree ParseDocument(string document, IEnumerable<DirectiveDescriptor> directives, bool designTime = false)
+ {
+ return ParseDocument(RazorLanguageVersion.Latest, document, directives, designTime);
+ }
+
+ internal virtual RazorSyntaxTree ParseDocument(RazorLanguageVersion version, string document, IEnumerable<DirectiveDescriptor> directives, bool designTime = false)
+ {
+ directives = directives ?? Array.Empty<DirectiveDescriptor>();
+
+ var source = TestRazorSourceDocument.Create(document, filePath: null);
+
+ var options = CreateParserOptions(version, 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;
+
+ 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(RazorLanguageVersion version, string document, IEnumerable<DirectiveDescriptor> directives, bool designTime = false)
+ {
+ directives = directives ?? Array.Empty<DirectiveDescriptor>();
+
+ var source = TestRazorSourceDocument.Create(document, filePath: null);
+ var options = CreateParserOptions(version, 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;
+
+ return RazorSyntaxTree.Create(root, source, diagnostics, options);
+ }
+
+ internal RazorSyntaxTree ParseCodeBlock(string document, bool designTime = false)
+ {
+ return ParseCodeBlock(RazorLanguageVersion.Latest, document, Enumerable.Empty<DirectiveDescriptor>(), designTime);
+ }
+
+ internal virtual RazorSyntaxTree ParseCodeBlock(
+ RazorLanguageVersion version,
+ string document,
+ IEnumerable<DirectiveDescriptor> directives,
+ bool designTime)
+ {
+ directives = directives ?? Array.Empty<DirectiveDescriptor>();
+
+ var source = TestRazorSourceDocument.Create(document, filePath: null);
+ var options = CreateParserOptions(version, 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;
+
+ 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 RazorDiagnostic[0]);
+ }
+
+ internal virtual void ParseBlockTest(string document, bool designTime)
+ {
+ ParseBlockTest(document, null, designTime, new RazorDiagnostic[0]);
+ }
+
+ internal virtual void ParseBlockTest(string document, params RazorDiagnostic[] expectedErrors)
+ {
+ ParseBlockTest(document, false, expectedErrors);
+ }
+
+ internal virtual void ParseBlockTest(string document, bool designTime, params RazorDiagnostic[] expectedErrors)
+ {
+ ParseBlockTest(document, null, designTime, expectedErrors);
+ }
+
+ internal virtual void ParseBlockTest(RazorLanguageVersion version, string document, Block expectedRoot)
+ {
+ ParseBlockTest(version, document, expectedRoot, false, null);
+ }
+
+ internal virtual void ParseBlockTest(string document, Block expectedRoot)
+ {
+ ParseBlockTest(document, expectedRoot, false, null);
+ }
+
+ internal virtual void ParseBlockTest(string document, IEnumerable<DirectiveDescriptor> 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 RazorDiagnostic[] expectedErrors)
+ {
+ ParseBlockTest(document, expectedRoot, false, expectedErrors);
+ }
+
+ internal virtual void ParseBlockTest(string document, IEnumerable<DirectiveDescriptor> directives, Block expectedRoot, params RazorDiagnostic[] expectedErrors)
+ {
+ ParseBlockTest(document, directives, expectedRoot, false, expectedErrors);
+ }
+
+ internal virtual void ParseBlockTest(string document, Block expected, bool designTime, params RazorDiagnostic[] expectedErrors)
+ {
+ ParseBlockTest(document, null, expected, designTime, expectedErrors);
+ }
+
+ internal virtual void ParseBlockTest(RazorLanguageVersion version, string document, Block expected, bool designTime, params RazorDiagnostic[] expectedErrors)
+ {
+ ParseBlockTest(version, document, null, expected, designTime, expectedErrors);
+ }
+
+ internal virtual void ParseBlockTest(string document, IEnumerable<DirectiveDescriptor> directives, Block expected, bool designTime, params RazorDiagnostic[] expectedErrors)
+ {
+ ParseBlockTest(RazorLanguageVersion.Latest, document, directives, expected, designTime, expectedErrors);
+ }
+
+ internal virtual void ParseBlockTest(RazorLanguageVersion version, string document, IEnumerable<DirectiveDescriptor> directives, Block expected, bool designTime, params RazorDiagnostic[] expectedErrors)
+ {
+ var result = ParseBlock(version, 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);
+ }
+ }
+
+ 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 RazorDiagnostic[] expectedError)
+ {
+ SingleSpanBlockTest(document, document, blockKind, spanType, expectedError);
+ }
+
+ internal virtual void SingleSpanBlockTest(string document, string spanContent, BlockKindInternal blockKind, SpanKindInternal spanType, params RazorDiagnostic[] expectedErrors)
+ {
+ SingleSpanBlockTest(document, spanContent, blockKind, spanType, AcceptedCharactersInternal.Any, expectedErrors ?? new RazorDiagnostic[0]);
+ }
+
+ internal virtual void SingleSpanBlockTest(string document, BlockKindInternal blockKind, SpanKindInternal spanType, AcceptedCharactersInternal acceptedCharacters, params RazorDiagnostic[] expectedError)
+ {
+ SingleSpanBlockTest(document, document, blockKind, spanType, acceptedCharacters, expectedError);
+ }
+
+ internal virtual void SingleSpanBlockTest(string document, string spanContent, BlockKindInternal blockKind, SpanKindInternal spanType, AcceptedCharactersInternal acceptedCharacters, params RazorDiagnostic[] 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);
+ }
+ }
+
+ 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 RazorDiagnostic[] expectedErrors)
+ {
+ ParseDocumentTest(document, expectedRoot, false, expectedErrors);
+ }
+
+ internal virtual void ParseDocumentTest(string document, IEnumerable<DirectiveDescriptor> directives, Block expected, params RazorDiagnostic[] 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 RazorDiagnostic[] expectedErrors)
+ {
+ ParseDocumentTest(document, null, expected, designTime, expectedErrors);
+ }
+
+ internal virtual void ParseDocumentTest(string document, IEnumerable<DirectiveDescriptor> directives, Block expected, bool designTime, params RazorDiagnostic[] 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);
+ }
+ }
+
+ [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<RazorDiagnostic> 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<RazorDiagnostic> actualErrors, IList<RazorDiagnostic> 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<RazorDiagnostic> 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(
+ RazorLanguageVersion version,
+ IEnumerable<DirectiveDescriptor> directives,
+ bool designTime)
+ {
+ return new DefaultRazorParserOptions(
+ directives.ToArray(),
+ designTime,
+ parseLeadingDirectives: false,
+ version: version);
+ }
+
+ 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;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/RawTextSymbol.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/RawTextSymbol.cs
new file mode 100644
index 0000000000..e2c3ccff64
--- /dev/null
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/RawTextSymbol.cs
@@ -0,0 +1,55 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Razor.Language.Legacy
+{
+ internal class RawTextSymbol : ISymbol
+ {
+ public RawTextSymbol(SourceLocation start, string content)
+ {
+ Start = start;
+ Content = content;
+ }
+
+ public SourceLocation Start { get; private set; }
+ public string Content { get; }
+ public Span Parent { get; set; }
+
+ 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 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/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/TestSpanBuilder.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/TestSpanBuilder.cs
new file mode 100644
index 0000000000..ed7b3aadde
--- /dev/null
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/TestSpanBuilder.cs
@@ -0,0 +1,443 @@
+// 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.Language.Legacy
+{
+ internal static class SpanFactoryExtensions
+ {
+ public static UnclassifiedCodeSpanConstructor EmptyCSharp(this SpanFactory self)
+ {
+ return new UnclassifiedCodeSpanConstructor(
+ self.Span(
+ SpanKindInternal.Code,
+ new CSharpSymbol(string.Empty, CSharpSymbolType.Unknown)));
+ }
+
+ public static SpanConstructor EmptyHtml(this SpanFactory self)
+ {
+ return self
+ .Span(
+ SpanKindInternal.Markup,
+ new HtmlSymbol(string.Empty, HtmlSymbolType.Unknown))
+ .With(new MarkupChunkGenerator());
+ }
+
+ public static UnclassifiedCodeSpanConstructor Code(this SpanFactory self, string content)
+ {
+ return new UnclassifiedCodeSpanConstructor(
+ self.Span(SpanKindInternal.Code, content, markup: false));
+ }
+
+ public static SpanConstructor CodeTransition(this SpanFactory self)
+ {
+ return self
+ .Span(SpanKindInternal.Transition, SyntaxConstants.TransitionString, markup: false)
+ .Accepts(AcceptedCharactersInternal.None);
+ }
+
+ public static SpanConstructor CodeTransition(this SpanFactory self, string content)
+ {
+ return self.Span(SpanKindInternal.Transition, content, markup: false).Accepts(AcceptedCharactersInternal.None);
+ }
+
+ public static SpanConstructor CodeTransition(this SpanFactory self, CSharpSymbolType type)
+ {
+ return self
+ .Span(SpanKindInternal.Transition, SyntaxConstants.TransitionString, type)
+ .Accepts(AcceptedCharactersInternal.None);
+ }
+
+ public static SpanConstructor CodeTransition(this SpanFactory self, string content, CSharpSymbolType type)
+ {
+ return self.Span(SpanKindInternal.Transition, content, type).Accepts(AcceptedCharactersInternal.None);
+ }
+
+ public static SpanConstructor MarkupTransition(this SpanFactory self)
+ {
+ return self
+ .Span(SpanKindInternal.Transition, SyntaxConstants.TransitionString, markup: true)
+ .Accepts(AcceptedCharactersInternal.None);
+ }
+
+ public static SpanConstructor MarkupTransition(this SpanFactory self, string content)
+ {
+ return self.Span(SpanKindInternal.Transition, content, markup: true).Accepts(AcceptedCharactersInternal.None);
+ }
+
+ public static SpanConstructor MarkupTransition(this SpanFactory self, HtmlSymbolType type)
+ {
+ return self
+ .Span(SpanKindInternal.Transition, SyntaxConstants.TransitionString, type)
+ .Accepts(AcceptedCharactersInternal.None);
+ }
+
+ public static SpanConstructor MarkupTransition(this SpanFactory self, string content, HtmlSymbolType type)
+ {
+ return self.Span(SpanKindInternal.Transition, content, type).Accepts(AcceptedCharactersInternal.None);
+ }
+
+ public static SpanConstructor MetaCode(this SpanFactory self, string content)
+ {
+ return self.Span(SpanKindInternal.MetaCode, content, markup: false);
+ }
+
+ public static SpanConstructor MetaCode(this SpanFactory self, string content, CSharpSymbolType type)
+ {
+ return self.Span(SpanKindInternal.MetaCode, content, type);
+ }
+
+ public static SpanConstructor MetaMarkup(this SpanFactory self, string content)
+ {
+ return self.Span(SpanKindInternal.MetaCode, content, markup: true);
+ }
+
+ public static SpanConstructor MetaMarkup(this SpanFactory self, string content, HtmlSymbolType type)
+ {
+ return self.Span(SpanKindInternal.MetaCode, content, type);
+ }
+
+ public static SpanConstructor Comment(this SpanFactory self, string content, CSharpSymbolType type)
+ {
+ return self.Span(SpanKindInternal.Comment, content, type);
+ }
+
+ public static SpanConstructor Comment(this SpanFactory self, string content, HtmlSymbolType type)
+ {
+ return self.Span(SpanKindInternal.Comment, content, type);
+ }
+
+ public static SpanConstructor BangEscape(this SpanFactory self)
+ {
+ return self
+ .Span(SpanKindInternal.MetaCode, "!", markup: true)
+ .With(SpanChunkGenerator.Null)
+ .Accepts(AcceptedCharactersInternal.None);
+ }
+
+ public static SpanConstructor Markup(this SpanFactory self, string content)
+ {
+ return self.Span(SpanKindInternal.Markup, content, markup: true).With(new MarkupChunkGenerator());
+ }
+
+ public static SpanConstructor Markup(this SpanFactory self, params string[] content)
+ {
+ return self.Span(SpanKindInternal.Markup, content, markup: true).With(new MarkupChunkGenerator());
+ }
+
+ public static SpanConstructor CodeMarkup(this SpanFactory self, params string[] content)
+ {
+ return self
+ .Span(SpanKindInternal.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(AcceptedCharactersInternal.AnyExceptNewline);
+ }
+
+ public static SpanConstructor AsDirectiveToken(this SpanConstructor self, DirectiveTokenDescriptor descriptor)
+ {
+ return self
+ .With(new DirectiveTokenChunkGenerator(descriptor))
+ .With(new DirectiveTokenEditHandler((content) => SpanConstructor.TestTokenizer(content)))
+ .Accepts(AcceptedCharactersInternal.NonWhiteSpace);
+ }
+
+ 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<ITextDocument, HtmlTokenizer> MarkupTokenizerFactory { get; }
+ public Func<ITextDocument, CSharpTokenizer> CodeTokenizerFactory { get; }
+ public SourceLocationTracker LocationTracker { get; }
+
+
+ public SpanConstructor Span(SpanKindInternal kind, string content, CSharpSymbolType type)
+ {
+ return CreateSymbolSpan(kind, content, () => new CSharpSymbol(content, type));
+ }
+
+ public SpanConstructor Span(SpanKindInternal kind, string content, HtmlSymbolType type)
+ {
+ return CreateSymbolSpan(kind, content, () => new HtmlSymbol(content, type));
+ }
+
+ public SpanConstructor Span(SpanKindInternal kind, string content, bool markup)
+ {
+ return new SpanConstructor(kind, LocationTracker.CurrentLocation, Tokenize(new[] { content }, markup));
+ }
+
+ public SpanConstructor Span(SpanKindInternal kind, string[] content, bool markup)
+ {
+ return new SpanConstructor(kind, LocationTracker.CurrentLocation, Tokenize(content, markup));
+ }
+
+ public SpanConstructor Span(SpanKindInternal kind, params ISymbol[] symbols)
+ {
+ var start = LocationTracker.CurrentLocation;
+ foreach (var symbol in symbols)
+ {
+ LocationTracker.UpdateLocation(symbol.Content);
+ }
+
+ return new SpanConstructor(kind, start, symbols);
+ }
+
+ private SpanConstructor CreateSymbolSpan(SpanKindInternal kind, string content, Func<ISymbol> ctor)
+ {
+ var start = LocationTracker.CurrentLocation;
+ LocationTracker.UpdateLocation(content);
+
+ return new SpanConstructor(kind, start, new[] { ctor() });
+ }
+
+ public void Reset()
+ {
+ LocationTracker.CurrentLocation = SourceLocation.Zero;
+ }
+
+ private IEnumerable<ISymbol> Tokenize(IEnumerable<string> contentFragments, bool markup)
+ {
+ return contentFragments.SelectMany(fragment => Tokenize(fragment, markup));
+ }
+
+ private IEnumerable<ISymbol> Tokenize(string content, bool markup)
+ {
+ var tokenizer = MakeTokenizer(markup, new SeekableTextReader(content, filePath: null));
+ ISymbol symbol;
+ ISymbol last = null;
+
+ while ((symbol = tokenizer.NextSymbol()) != null)
+ {
+ last = symbol;
+ yield return symbol;
+ }
+
+ LocationTracker.UpdateLocation(content);
+ }
+
+ private ITokenizer MakeTokenizer(bool markup, SeekableTextReader seekableTextReader)
+ {
+ if (markup)
+ {
+ return MarkupTokenizerFactory(seekableTextReader);
+ }
+ else
+ {
+ return CodeTokenizerFactory(seekableTextReader);
+ }
+ }
+ }
+
+ internal static class SpanConstructorExtensions
+ {
+ public static SpanConstructor Accepts(this SpanConstructor self, AcceptedCharactersInternal 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 = SpanKindInternal.MetaCode;
+ return _self;
+ }
+
+ public SpanConstructor AsStatement()
+ {
+ return _self.With(new StatementChunkGenerator());
+ }
+
+ public SpanConstructor AsExpression()
+ {
+ return _self.With(new ExpressionChunkGenerator());
+ }
+
+ public SpanConstructor AsImplicitExpression(ISet<string> keywords)
+ {
+ return AsImplicitExpression(keywords, acceptTrailingDot: false);
+ }
+
+ public SpanConstructor AsImplicitExpression(ISet<string> keywords, bool acceptTrailingDot)
+ {
+ return _self
+ .With(new ImplicitExpressionEditHandler((content) => SpanConstructor.TestTokenizer(content), keywords, acceptTrailingDot))
+ .With(new ExpressionChunkGenerator());
+ }
+
+ public SpanConstructor AsNamespaceImport(string ns)
+ {
+ return _self.With(new AddImportChunkGenerator(ns));
+ }
+
+ public SpanConstructor Hidden()
+ {
+ return _self.With(SpanChunkGenerator.Null);
+ }
+
+ public SpanConstructor AsAddTagHelper(
+ string lookupText,
+ string directiveText,
+ string typePattern = null,
+ string assemblyName = null,
+ params RazorDiagnostic[] errors)
+ {
+ var diagnostics = errors.ToList();
+ return _self
+ .With(new AddTagHelperChunkGenerator(lookupText, directiveText, typePattern, assemblyName, diagnostics))
+ .Accepts(AcceptedCharactersInternal.AnyExceptNewline);
+ }
+
+ public SpanConstructor AsRemoveTagHelper(
+ string lookupText,
+ string directiveText,
+ string typePattern = null,
+ string assemblyName = null,
+ params RazorDiagnostic[] errors)
+ {
+ var diagnostics = errors.ToList();
+ return _self
+ .With(new RemoveTagHelperChunkGenerator(lookupText, directiveText, typePattern, assemblyName, diagnostics))
+ .Accepts(AcceptedCharactersInternal.AnyExceptNewline);
+ }
+
+ public SpanConstructor AsTagHelperPrefixDirective(string prefix, string directiveText, params RazorDiagnostic[] errors)
+ {
+ var diagnostics = errors.ToList();
+ return _self
+ .With(new TagHelperPrefixDirectiveChunkGenerator(prefix, directiveText, diagnostics))
+ .Accepts(AcceptedCharactersInternal.AnyExceptNewline);
+ }
+
+ public SpanConstructor As(ISpanChunkGenerator chunkGenerator)
+ {
+ return _self.With(chunkGenerator);
+ }
+ }
+
+ internal class SpanConstructor
+ {
+ public SpanBuilder Builder { get; private set; }
+
+ internal static IEnumerable<ISymbol> TestTokenizer(string str)
+ {
+ yield return new RawTextSymbol(SourceLocation.Zero, str);
+ }
+
+ public SpanConstructor(SpanKindInternal kind, SourceLocation location, IEnumerable<ISymbol> symbols)
+ {
+ Builder = new SpanBuilder(location);
+ 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(SpanKindInternal 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<ISpanChunkGenerator> generatorConfigurer)
+ {
+ generatorConfigurer(Builder.ChunkGenerator);
+ return this;
+ }
+
+ public SpanConstructor With(Action<SpanEditHandler> 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/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/RazorEngineBuilderExtensions.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/RazorEngineBuilderExtensions.cs
new file mode 100644
index 0000000000..3d7977ea2d
--- /dev/null
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/RazorEngineBuilderExtensions.cs
@@ -0,0 +1,71 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Razor.Language.IntegrationTests;
+using Microsoft.AspNetCore.Razor.Language.Intermediate;
+
+namespace Microsoft.AspNetCore.Razor.Language
+{
+ [Obsolete("This class is obsolete and will be removed in a future version.")]
+ public static class RazorEngineBuilderExtensions
+ {
+ public static IRazorEngineBuilder AddTagHelpers(this IRazorEngineBuilder builder, params TagHelperDescriptor[] tagHelpers)
+ {
+ return AddTagHelpers(builder, (IEnumerable<TagHelperDescriptor>)tagHelpers);
+ }
+
+ public static IRazorEngineBuilder AddTagHelpers(this IRazorEngineBuilder builder, IEnumerable<TagHelperDescriptor> tagHelpers)
+ {
+ var feature = (TestTagHelperFeature)builder.Features.OfType<ITagHelperFeature>().FirstOrDefault();
+ if (feature == null)
+ {
+ feature = new TestTagHelperFeature();
+ builder.Features.Add(feature);
+ }
+
+ feature.TagHelpers.AddRange(tagHelpers);
+ return builder;
+ }
+
+ public static IRazorEngineBuilder ConfigureDocumentClassifier(this IRazorEngineBuilder builder)
+ {
+ var feature = builder.Features.OfType<DefaultDocumentClassifierPassFeature>().FirstOrDefault();
+ if (feature == null)
+ {
+ feature = new DefaultDocumentClassifierPassFeature();
+ builder.Features.Add(feature);
+ }
+
+ feature.ConfigureNamespace.Clear();
+ feature.ConfigureClass.Clear();
+ feature.ConfigureMethod.Clear();
+
+ feature.ConfigureNamespace.Add((RazorCodeDocument codeDocument, NamespaceDeclarationIntermediateNode node) =>
+ {
+ node.Content = "Microsoft.AspNetCore.Razor.Language.IntegrationTests.TestFiles";
+ });
+
+ feature.ConfigureClass.Add((RazorCodeDocument codeDocument, ClassDeclarationIntermediateNode node) =>
+ {
+ node.ClassName = IntegrationTestBase.FileName.Replace('/', '_');
+ node.Modifiers.Clear();
+ node.Modifiers.Add("public");
+ });
+
+ feature.ConfigureMethod.Add((RazorCodeDocument codeDocument, MethodDeclarationIntermediateNode node) =>
+ {
+ node.Modifiers.Clear();
+ node.Modifiers.Add("public");
+ node.Modifiers.Add("async");
+ node.MethodName = "ExecuteAsync";
+ node.ReturnType = typeof(Task).FullName;
+ });
+
+ return builder;
+ }
+ }
+}
diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/RazorProjectEngineBuilderExtensions.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/RazorProjectEngineBuilderExtensions.cs
new file mode 100644
index 0000000000..bcea84e918
--- /dev/null
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/RazorProjectEngineBuilderExtensions.cs
@@ -0,0 +1,70 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Razor.Language.IntegrationTests;
+using Microsoft.AspNetCore.Razor.Language.Intermediate;
+
+namespace Microsoft.AspNetCore.Razor.Language
+{
+ public static class RazorProjectEngineBuilderExtensions
+ {
+ public static RazorProjectEngineBuilder AddTagHelpers(this RazorProjectEngineBuilder builder, params TagHelperDescriptor[] tagHelpers)
+ {
+ return AddTagHelpers(builder, (IEnumerable<TagHelperDescriptor>)tagHelpers);
+ }
+
+ public static RazorProjectEngineBuilder AddTagHelpers(this RazorProjectEngineBuilder builder, IEnumerable<TagHelperDescriptor> tagHelpers)
+ {
+ var feature = (TestTagHelperFeature)builder.Features.OfType<ITagHelperFeature>().FirstOrDefault();
+ if (feature == null)
+ {
+ feature = new TestTagHelperFeature();
+ builder.Features.Add(feature);
+ }
+
+ feature.TagHelpers.AddRange(tagHelpers);
+ return builder;
+ }
+
+ public static RazorProjectEngineBuilder ConfigureDocumentClassifier(this RazorProjectEngineBuilder builder)
+ {
+ var feature = builder.Features.OfType<DefaultDocumentClassifierPassFeature>().FirstOrDefault();
+ if (feature == null)
+ {
+ feature = new DefaultDocumentClassifierPassFeature();
+ builder.Features.Add(feature);
+ }
+
+ feature.ConfigureNamespace.Clear();
+ feature.ConfigureClass.Clear();
+ feature.ConfigureMethod.Clear();
+
+ feature.ConfigureNamespace.Add((RazorCodeDocument codeDocument, NamespaceDeclarationIntermediateNode node) =>
+ {
+ node.Content = "Microsoft.AspNetCore.Razor.Language.IntegrationTests.TestFiles";
+ });
+
+ feature.ConfigureClass.Add((RazorCodeDocument codeDocument, ClassDeclarationIntermediateNode node) =>
+ {
+ node.ClassName = IntegrationTestBase.FileName.Replace('/', '_');
+ node.Modifiers.Clear();
+ node.Modifiers.Add("public");
+ });
+
+ feature.ConfigureMethod.Add((RazorCodeDocument codeDocument, MethodDeclarationIntermediateNode node) =>
+ {
+ node.Modifiers.Clear();
+ node.Modifiers.Add("public");
+ node.Modifiers.Add("async");
+ node.MethodName = "ExecuteAsync";
+ node.ReturnType = typeof(Task).FullName;
+ });
+
+ return builder;
+ }
+ }
+}
diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/SyntaxTreeVerifier.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/SyntaxTreeVerifier.cs
new file mode 100644
index 0000000000..2e5f86d1cc
--- /dev/null
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/SyntaxTreeVerifier.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 System;
+using Microsoft.AspNetCore.Razor.Language.Legacy;
+
+namespace Microsoft.AspNetCore.Razor.Language
+{
+ // Verifies recursively that a syntax tree has no gaps in terms of position/location.
+ internal class SyntaxTreeVerifier : ParserVisitor
+ {
+ private readonly SourceLocationTracker _tracker = new SourceLocationTracker(SourceLocation.Zero);
+
+ private SyntaxTreeVerifier()
+ {
+ }
+
+ public static void Verify(RazorSyntaxTree syntaxTree)
+ {
+ Verify(syntaxTree.Root);
+ }
+
+ public static void Verify(Block block)
+ {
+ new SyntaxTreeVerifier().VisitBlock(block);
+ }
+
+ public override void VisitSpan(Span span)
+ {
+ var start = span.Start;
+ if (!start.Equals(_tracker.CurrentLocation))
+ {
+ throw new InvalidOperationException($"Span starting at {span.Start} should start at {_tracker.CurrentLocation} - {span} ");
+ }
+
+ for (var i = 0; i < span.Symbols.Count; i++)
+ {
+ _tracker.UpdateLocation(span.Symbols[i].Content);
+ }
+ }
+ }
+}
diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestBoundAttributeDescriptorBuilderExtensions.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestBoundAttributeDescriptorBuilderExtensions.cs
new file mode 100644
index 0000000000..23987d03bf
--- /dev/null
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestBoundAttributeDescriptorBuilderExtensions.cs
@@ -0,0 +1,123 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.AspNetCore.Razor.Language
+{
+ public static class TestBoundAttributeDescriptorBuilderExtensions
+ {
+ public static BoundAttributeDescriptorBuilder Name(this BoundAttributeDescriptorBuilder builder, string name)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.Name = name;
+
+ return builder;
+ }
+
+ public static BoundAttributeDescriptorBuilder TypeName(this BoundAttributeDescriptorBuilder builder, string typeName)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.TypeName = typeName;
+
+ return builder;
+ }
+
+ public static BoundAttributeDescriptorBuilder PropertyName(this BoundAttributeDescriptorBuilder builder, string propertyName)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.SetPropertyName(propertyName);
+
+ return builder;
+ }
+
+ public static BoundAttributeDescriptorBuilder DisplayName(this BoundAttributeDescriptorBuilder builder, string displayName)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.DisplayName = displayName;
+
+ return builder;
+ }
+
+ public static BoundAttributeDescriptorBuilder AsEnum(this BoundAttributeDescriptorBuilder builder)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.IsEnum = true;
+
+ return builder;
+ }
+
+ public static BoundAttributeDescriptorBuilder AsDictionaryAttribute(
+ this BoundAttributeDescriptorBuilder builder,
+ string attributeNamePrefix,
+ string valueTypeName)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.IsDictionary = true;
+ builder.IndexerAttributeNamePrefix = attributeNamePrefix;
+ builder.IndexerValueTypeName = valueTypeName;
+
+ return builder;
+ }
+
+ public static BoundAttributeDescriptorBuilder Documentation(this BoundAttributeDescriptorBuilder builder, string documentation)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.Documentation = documentation;
+
+ return builder;
+ }
+
+ public static BoundAttributeDescriptorBuilder AddMetadata(this BoundAttributeDescriptorBuilder builder, string key, string value)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.Metadata[key] = value;
+
+ return builder;
+ }
+
+ public static BoundAttributeDescriptorBuilder AddDiagnostic(this BoundAttributeDescriptorBuilder builder, RazorDiagnostic diagnostic)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.Diagnostics.Add(diagnostic);
+
+ return builder;
+ }
+ }
+}
diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestFile.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestFile.cs
new file mode 100644
index 0000000000..c04b9243ff
--- /dev/null
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestFile.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 System;
+using System.IO;
+using System.Reflection;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Razor.Language
+{
+ public class TestFile
+ {
+ private 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 resourceName, Type type)
+ {
+ return new TestFile(resourceName, type.GetTypeInfo().Assembly);
+ }
+
+ public static TestFile Create(string resourceName, Assembly assembly)
+ {
+ return new TestFile(resourceName, 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 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");
+ }
+ }
+
+ /// <summary>
+ /// Saves the file to the specified path.
+ /// </summary>
+ 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/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestProject.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestProject.cs
new file mode 100644
index 0000000000..4cc8d85d61
--- /dev/null
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestProject.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.
+
+using System;
+using System.IO;
+using Microsoft.AspNetCore.Testing;
+
+namespace Microsoft.AspNetCore.Razor.Language
+{
+ public static class TestProject
+ {
+ public static string GetProjectDirectory(Type type)
+ {
+ var solutionDir = TestPathUtilities.GetSolutionRootDirectory("Razor");
+
+ var assemblyName = type.Assembly.GetName().Name;
+ var projectDirectory = Path.Combine(solutionDir, "test", assemblyName);
+ if (!Directory.Exists(projectDirectory))
+ {
+ throw new InvalidOperationException(
+$@"Could not locate project directory for type {type.FullName}.
+Directory probe path: {projectDirectory}.");
+ }
+
+ return projectDirectory;
+ }
+ }
+}
diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorCodeDocument.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorCodeDocument.cs
new file mode 100644
index 0000000000..4a6430d618
--- /dev/null
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorCodeDocument.cs
@@ -0,0 +1,27 @@
+// 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
+{
+ public static class TestRazorCodeDocument
+ {
+ public static RazorCodeDocument CreateEmpty()
+ {
+ var source = TestRazorSourceDocument.Create(content: string.Empty);
+ return new DefaultRazorCodeDocument(source, imports: null);
+ }
+
+ public static RazorCodeDocument Create(string content)
+ {
+ var source = TestRazorSourceDocument.Create(content);
+ return new DefaultRazorCodeDocument(source, imports: null);
+ }
+
+ public static RazorCodeDocument Create(RazorSourceDocument source, IEnumerable<RazorSourceDocument> imports)
+ {
+ return new DefaultRazorCodeDocument(source, imports);
+ }
+ }
+}
diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorProjectFileSystem.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorProjectFileSystem.cs
new file mode 100644
index 0000000000..f16fff0274
--- /dev/null
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorProjectFileSystem.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 System.Collections.Generic;
+using System.Linq;
+
+namespace Microsoft.AspNetCore.Razor.Language
+{
+ internal class TestRazorProjectFileSystem : DefaultRazorProjectFileSystem
+ {
+ public new static RazorProjectFileSystem Empty = new TestRazorProjectFileSystem();
+
+ private readonly Dictionary<string, RazorProjectItem> _lookup;
+
+ public TestRazorProjectFileSystem()
+ : this(new RazorProjectItem[0])
+ {
+ }
+
+ public TestRazorProjectFileSystem(IList<RazorProjectItem> items) : base("/")
+ {
+ _lookup = items.ToDictionary(item => item.FilePath);
+ }
+
+ public override IEnumerable<RazorProjectItem> EnumerateItems(string basePath)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override RazorProjectItem GetItem(string path)
+ {
+ if (!_lookup.TryGetValue(path, out var value))
+ {
+ value = new NotFoundProjectItem("", path);
+ }
+
+ return value;
+ }
+ }
+}
diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorProjectItem.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorProjectItem.cs
new file mode 100644
index 0000000000..d4f03fbf0d
--- /dev/null
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorProjectItem.cs
@@ -0,0 +1,49 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.IO;
+using System.Text;
+
+namespace Microsoft.AspNetCore.Razor.Language
+{
+ public class TestRazorProjectItem : RazorProjectItem
+ {
+ public TestRazorProjectItem(
+ string filePath,
+ string physicalPath = null,
+ string relativePhysicalPath = null,
+ string basePath = "/")
+ {
+ FilePath = filePath;
+ PhysicalPath = physicalPath;
+ RelativePhysicalPath = relativePhysicalPath;
+ BasePath = basePath;
+ }
+
+ public override string BasePath { get; }
+
+ public override string FilePath { get; }
+
+ public override string PhysicalPath { get; }
+
+ public override string RelativePhysicalPath { get; }
+
+ public override bool Exists { get; } = true;
+
+ public string Content { get; set; } = "Default content";
+
+ public override Stream Read()
+ {
+ // Act like a file and have a UTF8 BOM.
+ var preamble = Encoding.UTF8.GetPreamble();
+ var contentBytes = Encoding.UTF8.GetBytes(Content);
+ var buffer = new byte[preamble.Length + contentBytes.Length];
+ preamble.CopyTo(buffer, 0);
+ contentBytes.CopyTo(buffer, preamble.Length);
+
+ var stream = new MemoryStream(buffer);
+
+ return stream;
+ }
+ }
+}
diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorSourceDocument.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorSourceDocument.cs
new file mode 100644
index 0000000000..e731b03594
--- /dev/null
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorSourceDocument.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 System.Reflection;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace Microsoft.AspNetCore.Razor.Language
+{
+ public static class TestRazorSourceDocument
+ {
+ public static RazorSourceDocument CreateResource(string resourcePath, Type type, Encoding encoding = null, bool normalizeNewLines = false)
+ {
+ return CreateResource(resourcePath, type.GetTypeInfo().Assembly, encoding, normalizeNewLines);
+ }
+
+ public static RazorSourceDocument CreateResource(string resourcePath, Assembly assembly, Encoding encoding = null, bool normalizeNewLines = false)
+ {
+ var file = TestFile.Create(resourcePath, assembly);
+
+ using (var input = file.OpenRead())
+ using (var reader = new StreamReader(input))
+ {
+ var content = reader.ReadToEnd();
+ if (normalizeNewLines)
+ {
+ content = NormalizeNewLines(content);
+ }
+
+ var properties = new RazorSourceDocumentProperties(resourcePath, resourcePath);
+ return new StringSourceDocument(content, encoding ?? Encoding.UTF8, properties);
+ }
+ }
+
+ public static RazorSourceDocument CreateResource(
+ string path,
+ Assembly assembly,
+ Encoding encoding,
+ RazorSourceDocumentProperties properties,
+ bool normalizeNewLines = false)
+ {
+ var file = TestFile.Create(path, assembly);
+
+ using (var input = file.OpenRead())
+ using (var reader = new StreamReader(input))
+ {
+ var content = reader.ReadToEnd();
+ if (normalizeNewLines)
+ {
+ content = NormalizeNewLines(content);
+ }
+
+ return new StringSourceDocument(content, encoding ?? Encoding.UTF8, properties);
+ }
+ }
+
+ public static MemoryStream CreateStreamContent(string content = "Hello, World!", Encoding encoding = null, bool normalizeNewLines = false)
+ {
+ var stream = new MemoryStream();
+ encoding = encoding ?? Encoding.UTF8;
+ using (var writer = new StreamWriter(stream, encoding, bufferSize: 1024, leaveOpen: true))
+ {
+ if (normalizeNewLines)
+ {
+ content = NormalizeNewLines(content);
+ }
+
+ writer.Write(content);
+ }
+
+ stream.Seek(0L, SeekOrigin.Begin);
+
+ return stream;
+ }
+
+ public static RazorSourceDocument Create(
+ string content = "Hello, world!",
+ Encoding encoding = null,
+ bool normalizeNewLines = false,
+ string filePath = "test.cshtml",
+ string relativePath = "test.cshtml")
+ {
+ if (normalizeNewLines)
+ {
+ content = NormalizeNewLines(content);
+ }
+
+ var properties = new RazorSourceDocumentProperties(filePath, relativePath);
+ return new StringSourceDocument(content, encoding ?? Encoding.UTF8, properties);
+ }
+
+ public static RazorSourceDocument Create(
+ string content,
+ RazorSourceDocumentProperties properties,
+ Encoding encoding = null,
+ bool normalizeNewLines = false)
+ {
+ if (normalizeNewLines)
+ {
+ content = NormalizeNewLines(content);
+ }
+
+ return new StringSourceDocument(content, encoding ?? Encoding.UTF8, properties);
+ }
+
+ private static string NormalizeNewLines(string content)
+ {
+ return Regex.Replace(content, "(?<!\r)\n", "\r\n", RegexOptions.None, TimeSpan.FromSeconds(10));
+ }
+ }
+}
diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRequiredAttributeDescriptorBuilderExtensions.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRequiredAttributeDescriptorBuilderExtensions.cs
new file mode 100644
index 0000000000..da638e1750
--- /dev/null
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRequiredAttributeDescriptorBuilderExtensions.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;
+
+namespace Microsoft.AspNetCore.Razor.Language
+{
+ public static class TestRequiredAttributeDescriptorBuilderExtensions
+ {
+ public static RequiredAttributeDescriptorBuilder Name(this RequiredAttributeDescriptorBuilder builder, string name)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.Name = name;
+
+ return builder;
+ }
+
+ public static RequiredAttributeDescriptorBuilder NameComparisonMode(
+ this RequiredAttributeDescriptorBuilder builder,
+ RequiredAttributeDescriptor.NameComparisonMode nameComparison)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.NameComparisonMode = nameComparison;
+
+ return builder;
+ }
+
+ public static RequiredAttributeDescriptorBuilder Value(this RequiredAttributeDescriptorBuilder builder, string value)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.Value = value;
+
+ return builder;
+ }
+
+ public static RequiredAttributeDescriptorBuilder ValueComparisonMode(
+ this RequiredAttributeDescriptorBuilder builder,
+ RequiredAttributeDescriptor.ValueComparisonMode valueComparison)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.ValueComparisonMode = valueComparison;
+
+ return builder;
+ }
+
+ public static RequiredAttributeDescriptorBuilder AddDiagnostic(this RequiredAttributeDescriptorBuilder builder, RazorDiagnostic diagnostic)
+ {
+ builder.Diagnostics.Add(diagnostic);
+
+ return builder;
+ }
+ }
+}
diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestTagHelperDescriptorBuilderExtensions.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestTagHelperDescriptorBuilderExtensions.cs
new file mode 100644
index 0000000000..5bc81a7792
--- /dev/null
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestTagHelperDescriptorBuilderExtensions.cs
@@ -0,0 +1,122 @@
+// 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.Language
+{
+ public static class TestTagHelperDescriptorBuilderExtensions
+ {
+ public static TagHelperDescriptorBuilder TypeName(this TagHelperDescriptorBuilder builder, string typeName)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.SetTypeName(typeName);
+
+ return builder;
+ }
+
+ public static TagHelperDescriptorBuilder DisplayName(this TagHelperDescriptorBuilder builder, string displayName)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.DisplayName = displayName;
+
+ return builder;
+ }
+
+ public static TagHelperDescriptorBuilder AllowChildTag(this TagHelperDescriptorBuilder builder, string allowedChild)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.AllowChildTag(childTagBuilder => childTagBuilder.Name = allowedChild);
+
+ return builder;
+ }
+
+ public static TagHelperDescriptorBuilder TagOutputHint(this TagHelperDescriptorBuilder builder, string hint)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.TagOutputHint = hint;
+
+ return builder;
+ }
+
+ public static TagHelperDescriptorBuilder Documentation(this TagHelperDescriptorBuilder builder, string documentation)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.Documentation = documentation;
+
+ return builder;
+ }
+
+ public static TagHelperDescriptorBuilder AddMetadata(this TagHelperDescriptorBuilder builder, string key, string value)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.Metadata[key] = value;
+
+ return builder;
+ }
+
+ public static TagHelperDescriptorBuilder AddDiagnostic(this TagHelperDescriptorBuilder builder, RazorDiagnostic diagnostic)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.Diagnostics.Add(diagnostic);
+
+ return builder;
+ }
+
+ public static TagHelperDescriptorBuilder BoundAttributeDescriptor(
+ this TagHelperDescriptorBuilder builder,
+ Action<BoundAttributeDescriptorBuilder> configure)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.BindAttribute(configure);
+
+ return builder;
+ }
+
+ public static TagHelperDescriptorBuilder TagMatchingRuleDescriptor(
+ this TagHelperDescriptorBuilder builder,
+ Action<TagMatchingRuleDescriptorBuilder> configure)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.TagMatchingRule(configure);
+
+ return builder;
+ }
+ }
+}
diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestTagHelperFeature.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestTagHelperFeature.cs
new file mode 100644
index 0000000000..ad4039007e
--- /dev/null
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestTagHelperFeature.cs
@@ -0,0 +1,27 @@
+// 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
+{
+ public class TestTagHelperFeature : RazorEngineFeatureBase, ITagHelperFeature
+ {
+ public TestTagHelperFeature()
+ {
+ TagHelpers = new List<TagHelperDescriptor>();
+ }
+
+ public TestTagHelperFeature(IEnumerable<TagHelperDescriptor> tagHelpers)
+ {
+ TagHelpers = new List<TagHelperDescriptor>(tagHelpers);
+ }
+
+ public List<TagHelperDescriptor> TagHelpers { get; }
+
+ public IReadOnlyList<TagHelperDescriptor> GetDescriptors()
+ {
+ return TagHelpers.ToArray();
+ }
+ }
+}
diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestTagMatchingRuleDescriptorBuilderExtensions.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestTagMatchingRuleDescriptorBuilderExtensions.cs
new file mode 100644
index 0000000000..f052818fe6
--- /dev/null
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestTagMatchingRuleDescriptorBuilderExtensions.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;
+
+namespace Microsoft.AspNetCore.Razor.Language
+{
+ public static class TestTagMatchingRuleDescriptorBuilderExtensions
+ {
+ public static TagMatchingRuleDescriptorBuilder RequireTagName(this TagMatchingRuleDescriptorBuilder builder, string tagName)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.TagName = tagName;
+
+ return builder;
+ }
+
+ public static TagMatchingRuleDescriptorBuilder RequireParentTag(this TagMatchingRuleDescriptorBuilder builder, string parentTag)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.ParentTag = parentTag;
+
+ return builder;
+ }
+
+ public static TagMatchingRuleDescriptorBuilder RequireTagStructure(this TagMatchingRuleDescriptorBuilder builder, TagStructure tagStructure)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.TagStructure = tagStructure;
+
+ return builder;
+ }
+
+ public static TagMatchingRuleDescriptorBuilder AddDiagnostic(this TagMatchingRuleDescriptorBuilder builder, RazorDiagnostic diagnostic)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.Diagnostics.Add(diagnostic);
+
+ return builder;
+ }
+
+ public static TagMatchingRuleDescriptorBuilder RequireAttributeDescriptor(
+ this TagMatchingRuleDescriptorBuilder builder,
+ Action<RequiredAttributeDescriptorBuilder> configure)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.Attribute(configure);
+
+ return builder;
+ }
+ }
+}
diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Microsoft.AspNetCore.Razor.Test.Common.csproj b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Microsoft.AspNetCore.Razor.Test.Common.csproj
new file mode 100644
index 0000000000..cf35d8f8d7
--- /dev/null
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Microsoft.AspNetCore.Razor.Test.Common.csproj
@@ -0,0 +1,21 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <!-- To generate baselines, run tests with /p:GenerateBaselines=true -->
+ <DefineConstants Condition="'$(GenerateBaselines)'=='true'">$(DefineConstants);GENERATE_BASELINES</DefineConstants>
+ <DefineConstants>$(DefineConstants);__RemoveThisBitTo__GENERATE_BASELINES</DefineConstants>
+ <TargetFrameworks>netcoreapp2.1;netcoreapp2.0;net46</TargetFrameworks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Razor.Language\Microsoft.AspNetCore.Razor.Language.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.AspNetCore.Testing" Version="$(MicrosoftAspNetCoreTestingPackageVersion)" />
+ <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="$(MicrosoftCodeAnalysisCSharpPackageVersion)" />
+ <PackageReference Include="Microsoft.Extensions.DependencyModel" Version="$(MicrosoftExtensionsDependencyModelPackageVersion)" />
+ <PackageReference Include="xunit" Version="$(XunitPackageVersion)" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Properties/AssemblyInfo.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..576c4e4e44
--- /dev/null
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/Properties/AssemblyInfo.cs
@@ -0,0 +1,12 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Runtime.CompilerServices;
+[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Razor.Extensions.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
+[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
+[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Razor.Language.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
+[assembly: InternalsVisibleTo("Microsoft.CodeAnalysis.Razor.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
+[assembly: InternalsVisibleTo("Microsoft.CodeAnalysis.Razor.Workspaces.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
+[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Editor.Razor.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
+[assembly: InternalsVisibleTo("Microsoft.VisualStudio.LanguageServices.Razor.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
+[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/TestCompilation.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/TestCompilation.cs
new file mode 100644
index 0000000000..ffa7fd66d4
--- /dev/null
+++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/TestCompilation.cs
@@ -0,0 +1,71 @@
+// 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.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.Extensions.DependencyModel;
+using Xunit;
+
+namespace Microsoft.CodeAnalysis
+{
+ public static class TestCompilation
+ {
+ private static readonly ConcurrentDictionary<Assembly, IEnumerable<MetadataReference>> _referenceCache =
+ new ConcurrentDictionary<Assembly, IEnumerable<MetadataReference>>();
+
+ public static IEnumerable<MetadataReference> GetMetadataReferences(Assembly assembly)
+ {
+ var dependencyContext = DependencyContext.Load(assembly);
+
+ var metadataReferences = dependencyContext.CompileLibraries
+ .SelectMany(l => l.ResolveReferencePaths())
+ .Select(assemblyPath => MetadataReference.CreateFromFile(assemblyPath))
+ .ToArray();
+
+ return metadataReferences;
+ }
+
+ public static string AssemblyName => "TestAssembly";
+
+ public static CSharpCompilation Create(Assembly assembly, SyntaxTree syntaxTree = null)
+ {
+ IEnumerable<SyntaxTree> syntaxTrees = null;
+
+ if (syntaxTree != null)
+ {
+ syntaxTrees = new[] { syntaxTree };
+ }
+
+ if (!_referenceCache.TryGetValue(assembly, out IEnumerable<MetadataReference> metadataReferences))
+ {
+ metadataReferences = GetMetadataReferences(assembly);
+ _referenceCache.TryAdd(assembly, metadataReferences);
+ }
+
+ var compilation = CSharpCompilation.Create(AssemblyName, syntaxTrees, metadataReferences, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
+
+ EnsureValidCompilation(compilation);
+
+ return compilation;
+ }
+
+ private static void EnsureValidCompilation(CSharpCompilation compilation)
+ {
+ using (var stream = new MemoryStream())
+ {
+ var emitResult = compilation
+ .WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
+ .Emit(stream);
+ var diagnostics = string.Join(
+ Environment.NewLine,
+ emitResult.Diagnostics.Select(d => CSharpDiagnosticFormatter.Instance.Format(d)));
+ Assert.True(emitResult.Success, $"Compilation is invalid : {Environment.NewLine}{diagnostics}");
+ }
+ }
+ }
+}