// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; using Microsoft.AspNetCore.Mvc.Razor.Extensions; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Legacy; using Microsoft.VisualStudio.Test; using Microsoft.VisualStudio.Text; using Xunit; namespace Microsoft.VisualStudio.Editor.Razor { public class RazorSyntaxTreePartialParserTest : PartialParserTestBase { private const string NewLine = "\r\n"; public static TheoryData TagHelperPartialParseRejectData { get { return new TheoryData { CreateInsertionChange("

", 2, " "), CreateInsertionChange("

", 6, " "), CreateInsertionChange("

", 12, " "), CreateInsertionChange("

", 12, "ibute"), CreateInsertionChange("

", 2, " before"), }; } } [Theory] [MemberData(nameof(TagHelperPartialParseRejectData))] public void TagHelperTagBodiesRejectPartialChanges(object objectEdit) { // Arrange var edit = (TestEdit)objectEdit; var builder = TagHelperDescriptorBuilder.Create("PTagHelper", "TestAssembly"); builder.SetTypeName("PTagHelper"); builder.TagMatchingRule(rule => rule.TagName = "p"); var descriptors = new[] { builder.Build() }; var projectEngine = CreateProjectEngine(tagHelpers: descriptors); var projectItem = new TestRazorProjectItem("Index.cshtml") { Content = edit.OldSnapshot.GetText() }; var codeDocument = projectEngine.Process(projectItem); var syntaxTree = codeDocument.GetSyntaxTree(); var parser = new RazorSyntaxTreePartialParser(syntaxTree); // Act var result = parser.Parse(edit.Change); // Assert Assert.Equal(PartialParseResultInternal.Rejected, result); } public static TheoryData TagHelperAttributeAcceptData { get { // change, (Block)expectedDocument, partialParseResult return new TheoryData { { CreateInsertionChange("

", 22, "."), PartialParseResultInternal.Accepted | PartialParseResultInternal.Provisional }, { CreateInsertionChange("

", 21, "."), PartialParseResultInternal.Accepted }, { CreateInsertionChange("

", 25, "."), PartialParseResultInternal.Accepted }, { CreateInsertionChange("

", 34, "."), PartialParseResultInternal.Accepted | PartialParseResultInternal.Provisional }, { CreateInsertionChange("

", 29, "."), PartialParseResultInternal.Accepted | PartialParseResultInternal.Provisional }, }; } } [Theory] [MemberData(nameof(TagHelperAttributeAcceptData))] public void TagHelperAttributesAreLocatedAndAcceptChangesCorrectly(object editObject, object partialParseResultObject) { // Arrange var edit = (TestEdit)editObject; var partialParseResult = (PartialParseResultInternal)partialParseResultObject; var builder = TagHelperDescriptorBuilder.Create("PTagHelper", "Test"); builder.SetTypeName("PTagHelper"); builder.TagMatchingRule(rule => rule.TagName = "p"); builder.BindAttribute(attribute => { attribute.Name = "obj-attr"; attribute.TypeName = typeof(object).FullName; attribute.SetPropertyName("ObjectAttribute"); }); builder.BindAttribute(attribute => { attribute.Name = "str-attr"; attribute.TypeName = typeof(string).FullName; attribute.SetPropertyName("StringAttribute"); }); var descriptors = new[] { builder.Build() }; var projectEngine = CreateProjectEngine(tagHelpers: descriptors); var sourceDocument = new TestRazorProjectItem("Index.cshtml") { Content = edit.OldSnapshot.GetText() }; var codeDocument = projectEngine.Process(sourceDocument); var syntaxTree = codeDocument.GetSyntaxTree(); var parser = new RazorSyntaxTreePartialParser(syntaxTree); // Act var result = parser.Parse(edit.Change); // Assert Assert.Equal(partialParseResult, result); } [Fact] public void ImpExprAcceptsInnerInsertionsInStatementBlock() { // Arrange var changed = new StringTextSnapshot("@{" + NewLine + " @DateTime..Now" + NewLine + "}"); var old = new StringTextSnapshot("@{" + NewLine + " @DateTime.Now" + NewLine + "}"); // Act and Assert RunPartialParseTest(new TestEdit(17, 0, old, 1, changed, ".")); } [Fact] public void ImpExprAcceptsInnerInsertions() { // Arrange var changed = new StringTextSnapshot("foo @DateTime..Now baz"); var old = new StringTextSnapshot("foo @DateTime.Now baz"); // Act and Assert RunPartialParseTest(new TestEdit(13, 0, old, 1, changed, "."), additionalFlags: PartialParseResultInternal.Provisional); } [Fact] public void ImpExprAcceptsWholeIdentifierReplacement() { // Arrange var old = new StringTextSnapshot("foo @date baz"); var changed = new StringTextSnapshot("foo @DateTime baz"); // Act and Assert RunPartialParseTest(new TestEdit(5, 4, old, 8, changed, "DateTime")); } [Fact] public void ImpExprRejectsWholeIdentifierReplacementToKeyword() { // Arrange var old = new StringTextSnapshot("foo @date baz"); var changed = new StringTextSnapshot("foo @if baz"); var edit = new TestEdit(5, 4, old, 2, changed, "if"); // Act & Assert RunPartialParseRejectionTest(edit); } [Fact] public void ImpExprRejectsWholeIdentifierReplacementToDirective() { // Arrange var old = new StringTextSnapshot("foo @date baz"); var changed = new StringTextSnapshot("foo @inherits baz"); var edit = new TestEdit(5, 4, old, 8, changed, "inherits"); // Act & Assert RunPartialParseRejectionTest(edit, PartialParseResultInternal.SpanContextChanged); } [Fact] public void ImpExprAcceptsPrefixIdentifierReplacements_SingleSymbol() { // Arrange var old = new StringTextSnapshot("foo @dTime baz"); var changed = new StringTextSnapshot("foo @DateTime baz"); // Act and Assert RunPartialParseTest(new TestEdit(5, 1, old, 4, changed, "Date")); } [Fact] public void ImpExprAcceptsPrefixIdentifierReplacements_MultipleSymbols() { // Arrange var old = new StringTextSnapshot("foo @dTime.Now baz"); var changed = new StringTextSnapshot("foo @DateTime.Now baz"); // Act and Assert RunPartialParseTest(new TestEdit(5, 1, old, 4, changed, "Date")); } [Fact] public void ImpExprAcceptsSuffixIdentifierReplacements_SingleSymbol() { // Arrange var old = new StringTextSnapshot("foo @Datet baz"); var changed = new StringTextSnapshot("foo @DateTime baz"); // Act and Assert RunPartialParseTest(new TestEdit(9, 1, old, 4, changed, "Time")); } [Fact] public void ImpExprAcceptsSuffixIdentifierReplacements_MultipleSymbols() { // Arrange var old = new StringTextSnapshot("foo @DateTime.n baz"); var changed = new StringTextSnapshot("foo @DateTime.Now baz"); // Act and Assert RunPartialParseTest(new TestEdit(14, 1, old, 3, changed, "Now")); } [Fact] public void ImpExprAcceptsSurroundedIdentifierReplacements() { // Arrange var old = new StringTextSnapshot("foo @DateTime.n.ToString() baz"); var changed = new StringTextSnapshot("foo @DateTime.Now.ToString() baz"); // Act and Assert RunPartialParseTest(new TestEdit(14, 1, old, 3, changed, "Now")); } [Fact] public void ImpExprProvisionallyAcceptsDeleteOfIdentifierPartsIfDotRemains() { var changed = new StringTextSnapshot("foo @User. baz"); var old = new StringTextSnapshot("foo @User.Name baz"); RunPartialParseTest(new TestEdit(10, 4, old, 0, changed, string.Empty), additionalFlags: PartialParseResultInternal.Provisional); } [Fact] public void ImpExprAcceptsDeleteOfIdentifierPartsIfSomeOfIdentifierRemains() { var changed = new StringTextSnapshot("foo @Us baz"); var old = new StringTextSnapshot("foo @User baz"); RunPartialParseTest(new TestEdit(7, 2, old, 0, changed, string.Empty)); } [Fact] public void ImpExprProvisionalForMultipleInsertionIfItCausesIdentifierExpansionAndTrailingDot() { // ImpExprProvisionallyAcceptsMultipleInsertionIfItCausesIdentifierExpansionAndTrailingDot var changed = new StringTextSnapshot("foo @User. baz"); var old = new StringTextSnapshot("foo @U baz"); RunPartialParseTest(new TestEdit(6, 0, old, 4, changed, "ser."), additionalFlags: PartialParseResultInternal.Provisional); } [Fact] public void ImpExprAcceptsMultipleInsertionIfItOnlyCausesIdentifierExpansion() { var changed = new StringTextSnapshot("foo @barbiz baz"); var old = new StringTextSnapshot("foo @bar baz"); RunPartialParseTest(new TestEdit(8, 0, old, 3, changed, "biz")); } [Fact] public void ImpExprAcceptsIdentifierExpansionAtEndOfNonWhitespaceCharacters() { var changed = new StringTextSnapshot("@{" + NewLine + " @food" + NewLine + "}"); var old = new StringTextSnapshot("@{" + NewLine + " @foo" + NewLine + "}"); RunPartialParseTest(new TestEdit(10 + NewLine.Length, 0, old, 1, changed, "d")); } [Fact] public void ImpExprAcceptsIdentifierAfterDotAtEndOfNonWhitespaceCharacters() { var changed = new StringTextSnapshot("@{" + NewLine + " @foo.d" + NewLine + "}"); var old = new StringTextSnapshot("@{" + NewLine + " @foo." + NewLine + "}"); RunPartialParseTest(new TestEdit(11 + NewLine.Length, 0, old, 1, changed, "d")); } [Fact] public void ImpExprAcceptsDotAtEndOfNonWhitespaceCharacters() { var changed = new StringTextSnapshot("@{" + NewLine + " @foo." + NewLine + "}"); var old = new StringTextSnapshot("@{" + NewLine + " @foo" + NewLine + "}"); RunPartialParseTest(new TestEdit(10 + NewLine.Length, 0, old, 1, changed, ".")); } [Fact] public void ImpExprProvisionallyAcceptsDotAfterIdentifierInMarkup() { var changed = new StringTextSnapshot("foo @foo. bar"); var old = new StringTextSnapshot("foo @foo bar"); RunPartialParseTest(new TestEdit(8, 0, old, 1, changed, "."), additionalFlags: PartialParseResultInternal.Provisional); } [Fact] public void ImpExprAcceptsAdditionalIdentifierCharactersIfEndOfSpanIsIdentifier() { var changed = new StringTextSnapshot("foo @foob bar"); var old = new StringTextSnapshot("foo @foo bar"); RunPartialParseTest(new TestEdit(8, 0, old, 1, changed, "b")); } [Fact] public void ImpExprAcceptsAdditionalIdentifierStartCharactersIfEndOfSpanIsDot() { var changed = new StringTextSnapshot("@{@foo.b}"); var old = new StringTextSnapshot("@{@foo.}"); RunPartialParseTest(new TestEdit(7, 0, old, 1, changed, "b")); } [Fact] public void ImpExprAcceptsDotIfTrailingDotsAreAllowed() { var changed = new StringTextSnapshot("@{@foo.}"); var old = new StringTextSnapshot("@{@foo}"); RunPartialParseTest(new TestEdit(6, 0, old, 1, changed, ".")); } private void RunPartialParseRejectionTest(TestEdit edit, PartialParseResultInternal additionalFlags = 0) { var templateEngine = CreateProjectEngine(); var document = TestRazorCodeDocument.Create(edit.OldSnapshot.GetText()); templateEngine.Engine.Process(document); var syntaxTree = document.GetSyntaxTree(); var parser = new RazorSyntaxTreePartialParser(syntaxTree); var result = parser.Parse(edit.Change); Assert.Equal(PartialParseResultInternal.Rejected | additionalFlags, result); } private void RunPartialParseTest(TestEdit edit, PartialParseResultInternal additionalFlags = 0) { var templateEngine = CreateProjectEngine(); var document = TestRazorCodeDocument.Create(edit.OldSnapshot.GetText()); templateEngine.Engine.Process(document); var syntaxTree = document.GetSyntaxTree(); var parser = new RazorSyntaxTreePartialParser(syntaxTree); var result = parser.Parse(edit.Change); Assert.Equal(PartialParseResultInternal.Accepted | additionalFlags, result); var newSource = TestRazorSourceDocument.Create(edit.NewSnapshot.GetText()); var newSyntaxTree = RazorSyntaxTree.Create(parser.ModifiedSyntaxTreeRoot, newSource, parser.OriginalSyntaxTree.Diagnostics, parser.OriginalSyntaxTree.Options); BaselineTest(newSyntaxTree); } private static TestEdit CreateInsertionChange(string initialText, int insertionLocation, string insertionText) { var changedText = initialText.Insert(insertionLocation, insertionText); var sourceChange = new SourceChange(insertionLocation, 0, insertionText); var oldSnapshot = new StringTextSnapshot(initialText); var changedSnapshot = new StringTextSnapshot(changedText); return new TestEdit(sourceChange, oldSnapshot, changedSnapshot); } private static RazorProjectEngine CreateProjectEngine( string path = "C:\\This\\Path\\Is\\Just\\For\\Line\\Pragmas.cshtml", IEnumerable tagHelpers = null) { var fileSystem = new TestRazorProjectFileSystem(); var projectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, fileSystem, builder => { RazorExtensions.Register(builder); builder.AddDefaultImports("@addTagHelper *, Test"); if (tagHelpers != null) { builder.AddTagHelpers(tagHelpers); } }); return projectEngine; } } }