// 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 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 { 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 { var factory = new SpanFactory(); // 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 ImplicitExpressionAcceptsInnerInsertionsInStatementBlock() { // Arrange var factory = new SpanFactory(); var changed = new StringTextSnapshot("@{" + Environment.NewLine + " @DateTime..Now" + Environment.NewLine + "}"); var old = new StringTextSnapshot("@{" + Environment.NewLine + " @DateTime.Now" + Environment.NewLine + "}"); // Act and Assert RunPartialParseTest(new TestEdit(17, 0, old, 1, changed, "."), new MarkupBlock( factory.EmptyHtml(), new StatementBlock( factory.CodeTransition(), factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None), factory.Code(Environment.NewLine + " ") .AsStatement() .AutoCompleteWith(autoCompleteString: null), new ExpressionBlock( factory.CodeTransition(), factory.Code("DateTime..Now") .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true) .Accepts(AcceptedCharactersInternal.NonWhitespace)), factory.Code(Environment.NewLine).AsStatement(), factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)), factory.EmptyHtml())); } [Fact] public void ImplicitExpressionAcceptsInnerInsertions() { // Arrange var factory = new SpanFactory(); 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, "."), new MarkupBlock( factory.Markup("foo "), new ExpressionBlock( factory.CodeTransition(), factory.Code("DateTime..Now").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhitespace)), factory.Markup(" baz")), additionalFlags: PartialParseResultInternal.Provisional); } [Fact] public void ImplicitExpressionAcceptsWholeIdentifierReplacement() { // Arrange var factory = new SpanFactory(); 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"), new MarkupBlock( factory.Markup("foo "), new ExpressionBlock( factory.CodeTransition(), factory.Code("DateTime").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhitespace)), factory.Markup(" baz"))); } [Fact] public void ImplicitExpressionRejectsWholeIdentifierReplacementToKeyword() { // 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 ImplicitExpressionRejectsWholeIdentifierReplacementToDirective() { // 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 ImplicitExpressionAcceptsPrefixIdentifierReplacements_SingleSymbol() { // Arrange var factory = new SpanFactory(); 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"), new MarkupBlock( factory.Markup("foo "), new ExpressionBlock( factory.CodeTransition(), factory.Code("DateTime").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhitespace)), factory.Markup(" baz"))); } [Fact] public void ImplicitExpressionAcceptsPrefixIdentifierReplacements_MultipleSymbols() { // Arrange var factory = new SpanFactory(); 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"), new MarkupBlock( factory.Markup("foo "), new ExpressionBlock( factory.CodeTransition(), factory.Code("DateTime.Now").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhitespace)), factory.Markup(" baz"))); } [Fact] public void ImplicitExpressionAcceptsSuffixIdentifierReplacements_SingleSymbol() { // Arrange var factory = new SpanFactory(); 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"), new MarkupBlock( factory.Markup("foo "), new ExpressionBlock( factory.CodeTransition(), factory.Code("DateTime").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhitespace)), factory.Markup(" baz"))); } [Fact] public void ImplicitExpressionAcceptsSuffixIdentifierReplacements_MultipleSymbols() { // Arrange var factory = new SpanFactory(); 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"), new MarkupBlock( factory.Markup("foo "), new ExpressionBlock( factory.CodeTransition(), factory.Code("DateTime.Now").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhitespace)), factory.Markup(" baz"))); } [Fact] public void ImplicitExpressionAcceptsSurroundedIdentifierReplacements() { // Arrange var factory = new SpanFactory(); 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"), new MarkupBlock( factory.Markup("foo "), new ExpressionBlock( factory.CodeTransition(), factory.Code("DateTime.Now.ToString()").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhitespace)), factory.Markup(" baz"))); } [Fact] public void ImplicitExpressionProvisionallyAcceptsDeleteOfIdentifierPartsIfDotRemains() { var factory = new SpanFactory(); 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), new MarkupBlock( factory.Markup("foo "), new ExpressionBlock( factory.CodeTransition(), factory.Code("User.").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhitespace)), factory.Markup(" baz")), additionalFlags: PartialParseResultInternal.Provisional); } [Fact] public void ImplicitExpressionAcceptsDeleteOfIdentifierPartsIfSomeOfIdentifierRemains() { var factory = new SpanFactory(); var changed = new StringTextSnapshot("foo @Us baz"); var old = new StringTextSnapshot("foo @User baz"); RunPartialParseTest(new TestEdit(7, 2, old, 0, changed, string.Empty), new MarkupBlock( factory.Markup("foo "), new ExpressionBlock( factory.CodeTransition(), factory.Code("Us").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhitespace)), factory.Markup(" baz"))); } [Fact] public void ImplicitExpressionProvisionallyAcceptsMultipleInsertionIfItCausesIdentifierExpansionAndTrailingDot() { var factory = new SpanFactory(); var changed = new StringTextSnapshot("foo @User. baz"); var old = new StringTextSnapshot("foo @U baz"); RunPartialParseTest(new TestEdit(6, 0, old, 4, changed, "ser."), new MarkupBlock( factory.Markup("foo "), new ExpressionBlock( factory.CodeTransition(), factory.Code("User.").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhitespace)), factory.Markup(" baz")), additionalFlags: PartialParseResultInternal.Provisional); } [Fact] public void ImplicitExpressionAcceptsMultipleInsertionIfItOnlyCausesIdentifierExpansion() { var factory = new SpanFactory(); var changed = new StringTextSnapshot("foo @barbiz baz"); var old = new StringTextSnapshot("foo @bar baz"); RunPartialParseTest(new TestEdit(8, 0, old, 3, changed, "biz"), new MarkupBlock( factory.Markup("foo "), new ExpressionBlock( factory.CodeTransition(), factory.Code("barbiz").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhitespace)), factory.Markup(" baz"))); } [Fact] public void ImplicitExpressionAcceptsIdentifierExpansionAtEndOfNonWhitespaceCharacters() { var factory = new SpanFactory(); var changed = new StringTextSnapshot("@{" + Environment.NewLine + " @food" + Environment.NewLine + "}"); var old = new StringTextSnapshot("@{" + Environment.NewLine + " @foo" + Environment.NewLine + "}"); RunPartialParseTest(new TestEdit(10 + Environment.NewLine.Length, 0, old, 1, changed, "d"), new MarkupBlock( factory.EmptyHtml(), new StatementBlock( factory.CodeTransition(), factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None), factory.Code(Environment.NewLine + " ") .AsStatement() .AutoCompleteWith(autoCompleteString: null), new ExpressionBlock( factory.CodeTransition(), factory.Code("food") .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true) .Accepts(AcceptedCharactersInternal.NonWhitespace)), factory.Code(Environment.NewLine).AsStatement(), factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)), factory.EmptyHtml())); } [Fact] public void ImplicitExpressionAcceptsIdentifierAfterDotAtEndOfNonWhitespaceCharacters() { var factory = new SpanFactory(); var changed = new StringTextSnapshot("@{" + Environment.NewLine + " @foo.d" + Environment.NewLine + "}"); var old = new StringTextSnapshot("@{" + Environment.NewLine + " @foo." + Environment.NewLine + "}"); RunPartialParseTest(new TestEdit(11 + Environment.NewLine.Length, 0, old, 1, changed, "d"), new MarkupBlock( factory.EmptyHtml(), new StatementBlock( factory.CodeTransition(), factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None), factory.Code(Environment.NewLine + " ") .AsStatement() .AutoCompleteWith(autoCompleteString: null), new ExpressionBlock( factory.CodeTransition(), factory.Code("foo.d") .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true) .Accepts(AcceptedCharactersInternal.NonWhitespace)), factory.Code(Environment.NewLine).AsStatement(), factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)), factory.EmptyHtml())); } [Fact] public void ImplicitExpressionAcceptsDotAtEndOfNonWhitespaceCharacters() { var factory = new SpanFactory(); var changed = new StringTextSnapshot("@{" + Environment.NewLine + " @foo." + Environment.NewLine + "}"); var old = new StringTextSnapshot("@{" + Environment.NewLine + " @foo" + Environment.NewLine + "}"); RunPartialParseTest(new TestEdit(10 + Environment.NewLine.Length, 0, old, 1, changed, "."), new MarkupBlock( factory.EmptyHtml(), new StatementBlock( factory.CodeTransition(), factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None), factory.Code(Environment.NewLine + " ") .AsStatement() .AutoCompleteWith(autoCompleteString: null), new ExpressionBlock( factory.CodeTransition(), factory.Code(@"foo.") .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true) .Accepts(AcceptedCharactersInternal.NonWhitespace)), factory.Code(Environment.NewLine).AsStatement(), factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)), factory.EmptyHtml())); } [Fact] public void ImplicitExpressionProvisionallyAcceptsDotAfterIdentifierInMarkup() { var factory = new SpanFactory(); var changed = new StringTextSnapshot("foo @foo. bar"); var old = new StringTextSnapshot("foo @foo bar"); RunPartialParseTest(new TestEdit(8, 0, old, 1, changed, "."), new MarkupBlock( factory.Markup("foo "), new ExpressionBlock( factory.CodeTransition(), factory.Code("foo.") .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) .Accepts(AcceptedCharactersInternal.NonWhitespace)), factory.Markup(" bar")), additionalFlags: PartialParseResultInternal.Provisional); } [Fact] public void ImplicitExpressionAcceptsAdditionalIdentifierCharactersIfEndOfSpanIsIdentifier() { var factory = new SpanFactory(); var changed = new StringTextSnapshot("foo @foob bar"); var old = new StringTextSnapshot("foo @foo bar"); RunPartialParseTest(new TestEdit(8, 0, old, 1, changed, "b"), new MarkupBlock( factory.Markup("foo "), new ExpressionBlock( factory.CodeTransition(), factory.Code("foob") .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) .Accepts(AcceptedCharactersInternal.NonWhitespace)), factory.Markup(" bar"))); } [Fact] public void ImplicitExpressionAcceptsAdditionalIdentifierStartCharactersIfEndOfSpanIsDot() { var factory = new SpanFactory(); var changed = new StringTextSnapshot("@{@foo.b}"); var old = new StringTextSnapshot("@{@foo.}"); RunPartialParseTest(new TestEdit(7, 0, old, 1, changed, "b"), new MarkupBlock( factory.EmptyHtml(), new StatementBlock( factory.CodeTransition(), factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None), factory.EmptyCSharp() .AsStatement() .AutoCompleteWith(autoCompleteString: null), new ExpressionBlock( factory.CodeTransition(), factory.Code("foo.b") .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true) .Accepts(AcceptedCharactersInternal.NonWhitespace)), factory.EmptyCSharp().AsStatement(), factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)), factory.EmptyHtml())); } [Fact] public void ImplicitExpressionAcceptsDotIfTrailingDotsAreAllowed() { var factory = new SpanFactory(); var changed = new StringTextSnapshot("@{@foo.}"); var old = new StringTextSnapshot("@{@foo}"); RunPartialParseTest(new TestEdit(6, 0, old, 1, changed, "."), new MarkupBlock( factory.EmptyHtml(), new StatementBlock( factory.CodeTransition(), factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None), factory.EmptyCSharp() .AsStatement() .AutoCompleteWith(autoCompleteString: null), new ExpressionBlock( factory.CodeTransition(), factory.Code("foo.") .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true) .Accepts(AcceptedCharactersInternal.NonWhitespace)), factory.EmptyCSharp().AsStatement(), factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)), factory.EmptyHtml())); } 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 static void RunPartialParseTest(TestEdit edit, Block expectedTree, 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); ParserTestBase.EvaluateParseTree(parser.SyntaxTreeRoot, expectedTree); } 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; } } }