|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable disable
using System;
using System.Linq;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Test.Utilities;
using Xunit;
namespace Microsoft.CodeAnalysis.CSharp.UnitTests
{
public class SyntaxDiffingTests
{
[Fact]
public void TestDiffEmptyVersusClass()
{
var oldTree = SyntaxFactory.ParseSyntaxTree("");
var newTree = SyntaxFactory.ParseSyntaxTree("class C { }");
// it should be all new
var spans = newTree.GetChangedSpans(oldTree);
Assert.NotNull(spans);
Assert.Equal(1, spans.Count);
Assert.Equal(newTree.GetCompilationUnitRoot().FullSpan, spans[0]);
var changes = newTree.GetChanges(oldTree);
Assert.NotNull(changes);
Assert.Equal(1, changes.Count);
Assert.Equal(new TextSpan(0, 0), changes[0].Span);
Assert.Equal("class C { }", changes[0].NewText);
}
[Fact]
public void TestDiffClassWithNameChanged()
{
var oldTree = SyntaxFactory.ParseSyntaxTree("class A { }");
var newTree = SyntaxFactory.ParseSyntaxTree("class B { }");
// since most tokens are automatically interned we should see only the name tokens change
var spans = newTree.GetChangedSpans(oldTree);
Assert.NotNull(spans);
Assert.Equal(1, spans.Count);
var decl = (TypeDeclarationSyntax)(newTree.GetCompilationUnitRoot()).Members[0];
Assert.Equal(decl.Identifier.Span, spans[0]);
var changes = newTree.GetChanges(oldTree);
Assert.NotNull(changes);
Assert.Equal(1, changes.Count);
Assert.Equal(new TextSpan(6, 1), changes[0].Span);
Assert.Equal("B", changes[0].NewText);
}
[Fact]
public void TestDiffTwoClassesWithBothNamesChanged()
{
var oldTree = SyntaxFactory.ParseSyntaxTree("class A { } class B { }");
var newTree = SyntaxFactory.ParseSyntaxTree("class C { } class D { }");
// since most tokens are automatically interned we should see only the name tokens change
var spans = newTree.GetChangedSpans(oldTree);
Assert.NotNull(spans);
Assert.Equal(2, spans.Count);
var decl1 = (TypeDeclarationSyntax)(newTree.GetCompilationUnitRoot()).Members[0];
Assert.Equal(decl1.Identifier.Span, spans[0]);
var decl2 = (TypeDeclarationSyntax)(newTree.GetCompilationUnitRoot()).Members[1];
Assert.Equal(decl2.Identifier.Span, spans[1]);
var changes = newTree.GetChanges(oldTree);
Assert.NotNull(changes);
Assert.Equal(2, changes.Count);
Assert.Equal(new TextSpan(6, 1), changes[0].Span);
Assert.Equal("C", changes[0].NewText);
Assert.Equal(new TextSpan(18, 1), changes[1].Span);
Assert.Equal("D", changes[1].NewText);
}
[Fact]
public void TestDiffClassWithNewClassStarted()
{
var oldTree = SyntaxFactory.ParseSyntaxTree("class A { }");
var newTree = oldTree.WithInsertAt(0, "class ");
var spans = newTree.GetChangedSpans(oldTree);
Assert.NotNull(spans);
Assert.Equal(1, spans.Count);
Assert.Equal(new TextSpan(0, 6), spans[0]);
var changes = newTree.GetChanges(oldTree);
Assert.NotNull(changes);
Assert.Equal(1, changes.Count);
Assert.Equal(new TextSpan(0, 0), changes[0].Span);
Assert.Equal("class ", changes[0].NewText);
}
[Fact]
public void TestDiffClassWithNewClassStarted2()
{
var oldTree = SyntaxFactory.ParseSyntaxTree("class A { }");
var newTree = oldTree.WithInsertAt(0, "class A ");
var spans = newTree.GetChangedSpans(oldTree);
Assert.NotNull(spans);
Assert.Equal(1, spans.Count);
Assert.Equal(new TextSpan(0, 8), spans[0]);
var changes = newTree.GetChanges(oldTree);
Assert.NotNull(changes);
Assert.Equal(1, changes.Count);
Assert.Equal(new TextSpan(0, 0), changes[0].Span);
Assert.Equal("class A ", changes[0].NewText);
}
[Fact]
public void TestDiffClassWithNewClassStarted3()
{
var oldTree = SyntaxFactory.ParseSyntaxTree("class A { }");
var newTree = oldTree.WithInsertAt(0, "class A { }");
// new tree appears to have two duplicate (similar) copies of the same declarations (indistinguishable)
var spans = newTree.GetChangedSpans(oldTree);
Assert.NotNull(spans);
Assert.Equal(1, spans.Count);
Assert.Equal(new TextSpan(11, 11), spans[0]); // its going to pick one of the two spans.
var changes = newTree.GetChanges(oldTree);
Assert.NotNull(changes);
Assert.Equal(1, changes.Count);
Assert.Equal(new TextSpan(11, 0), changes[0].Span);
Assert.Equal("class A { }", changes[0].NewText);
}
[Fact]
public void TestDiffClassWithNewClassStarted4()
{
var oldTree = SyntaxFactory.ParseSyntaxTree("class A { }");
var newTree = oldTree.WithInsertAt(0, "class A { } ");
// new tree appears to have two almost duplicate (similar) copies of the same declarations, except the
// second (original) one is a closer match
var spans = newTree.GetChangedSpans(oldTree);
Assert.NotNull(spans);
Assert.Equal(1, spans.Count);
Assert.Equal(new TextSpan(10, 12), spans[0]);
var changes = newTree.GetChanges(oldTree);
Assert.NotNull(changes);
Assert.Equal(1, changes.Count);
Assert.Equal(new TextSpan(10, 0), changes[0].Span);
Assert.Equal("} class A { ", changes[0].NewText);
}
[Fact]
public void TestDiffClassWithNewNamespaceEnclosing()
{
var oldTree = SyntaxFactory.ParseSyntaxTree("class A { }");
var newTree = oldTree.WithInsertAt(0, "namespace N { ");
var spans = newTree.GetChangedSpans(oldTree);
Assert.NotNull(spans);
Assert.Equal(1, spans.Count);
Assert.Equal(new TextSpan(0, 14), spans[0]);
var changes = newTree.GetChanges(oldTree);
Assert.NotNull(changes);
Assert.Equal(1, changes.Count);
Assert.Equal(new TextSpan(0, 0), changes[0].Span);
Assert.Equal("namespace N { ", changes[0].NewText);
}
[Fact]
public void TestDiffClassWithNewMemberInserted()
{
var oldTree = SyntaxFactory.ParseSyntaxTree("class A { }");
var newTree = oldTree.WithInsertAt(10, "int X; ");
var spans = newTree.GetChangedSpans(oldTree);
Assert.NotNull(spans);
Assert.Equal(1, spans.Count);
Assert.Equal(new TextSpan(10, 7), spans[0]); // int X;
var changes = newTree.GetChanges(oldTree);
Assert.NotNull(changes);
Assert.Equal(1, changes.Count);
Assert.Equal(new TextSpan(10, 0), changes[0].Span);
Assert.Equal("int X; ", changes[0].NewText);
}
[Fact]
public void TestDiffClassWithMemberRemoved()
{
var oldTree = SyntaxFactory.ParseSyntaxTree("class A { int X; }");
var newTree = oldTree.WithRemoveAt(10, 7);
var spans = newTree.GetChangedSpans(oldTree);
Assert.NotNull(spans);
Assert.Equal(0, spans.Count);
var changes = newTree.GetChanges(oldTree);
Assert.NotNull(changes);
Assert.Equal(1, changes.Count);
Assert.Equal(new TextSpan(10, 7), changes[0].Span);
Assert.Equal("", changes[0].NewText);
}
[Fact]
public void TestDiffClassWithMemberRemovedDeep()
{
var oldTree = SyntaxFactory.ParseSyntaxTree("namespace N { class A { int X; } }");
var newTree = oldTree.WithRemoveAt(24, 7);
var spans = newTree.GetChangedSpans(oldTree);
Assert.NotNull(spans);
Assert.Equal(0, spans.Count);
var changes = newTree.GetChanges(oldTree);
Assert.NotNull(changes);
Assert.Equal(1, changes.Count);
Assert.Equal(new TextSpan(24, 7), changes[0].Span);
Assert.Equal("", changes[0].NewText);
}
[Fact]
public void TestDiffClassWithMemberNameRemoved()
{
var oldTree = SyntaxFactory.ParseSyntaxTree("class A { int X; }");
var newTree = oldTree.WithRemoveAt(14, 1);
var spans = newTree.GetChangedSpans(oldTree);
Assert.NotNull(spans);
Assert.Equal(0, spans.Count);
var changes = newTree.GetChanges(oldTree);
Assert.NotNull(changes);
Assert.Equal(1, changes.Count);
Assert.Equal(new TextSpan(14, 1), changes[0].Span);
Assert.Equal("", changes[0].NewText);
}
[Fact]
public void TestDiffClassChangedToStruct()
{
var oldTree = SyntaxFactory.ParseSyntaxTree("namespace N { class A { int X; } }");
var newTree = oldTree.WithReplaceFirst("class", "struct");
var spans = newTree.GetChangedSpans(oldTree);
Assert.NotNull(spans);
Assert.Equal(1, spans.Count);
Assert.Equal(new TextSpan(14, 6), spans[0]); // 'struct'
var changes = newTree.GetChanges(oldTree);
Assert.NotNull(changes);
Assert.Equal(1, changes.Count);
Assert.Equal(new TextSpan(14, 5), changes[0].Span);
Assert.Equal("struct", changes[0].NewText);
}
[Fact, WorkItem(463, "https://github.com/dotnet/roslyn/issues/463")]
public void TestQualifyWithThis()
{
var original = @"
class C
{
int Sign;
void F()
{
string x = @""namespace Namespace
{
class Type
{
void Goo()
{
int x = 1 "" + Sign + @"" "" + Sign + @""3;
}
}
}
"";
}
}";
var oldTree = SyntaxFactory.ParseSyntaxTree(original);
var root = oldTree.GetRoot();
var indexText = "Sign +";
// Expected behavior: Qualifying identifier 'Sign' with 'this.' and doing a diff between trees
// should return a single text change with 'this.' as added text.
// Works as expected for last index
var index = original.LastIndexOf(indexText, StringComparison.Ordinal);
TestQualifyWithThisCore(root, index);
// Doesn't work as expected for first index.
// It returns 2 changes with add followed by delete,
// causing the 2 isolated edits of adding "this." to seem conflicting edits, even though they are not.
// See https://github.com/dotnet/roslyn/issues/320 for details.
index = original.IndexOf(indexText, StringComparison.Ordinal);
TestQualifyWithThisCore(root, index);
}
private void TestQualifyWithThisCore(SyntaxNode root, int index)
{
var oldTree = root.SyntaxTree;
var span = new TextSpan(index, 4);
var node = root.FindNode(span, getInnermostNodeForTie: true) as SimpleNameSyntax;
Assert.NotNull(node);
Assert.Equal("Sign", node.Identifier.ValueText);
var leadingTrivia = node.GetLeadingTrivia();
var newNode = SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
SyntaxFactory.ThisExpression(),
node.WithoutLeadingTrivia())
.WithLeadingTrivia(leadingTrivia);
var newRoot = root.ReplaceNode(node, newNode);
var newTree = newRoot.SyntaxTree;
var changes = newTree.GetChanges(oldTree);
Assert.NotNull(changes);
Assert.Equal(1, changes.Count);
Assert.Equal("this.", changes[0].NewText);
}
[Fact, WorkItem(463, "https://github.com/dotnet/roslyn/issues/463")]
public void TestReplaceWithBuiltInType()
{
var original = @"
using System;
using System.Collections.Generic;
public class TestClass
{
public void TestMethod()
{
var dictionary = new Dictionary<Object, Object>();
dictionary[new Object()] = new Object();
}
}";
var oldTree = SyntaxFactory.ParseSyntaxTree(original);
var root = oldTree.GetRoot();
var indexText = "Object";
// Expected behavior: Replacing identifier 'Object' with 'object' and doing a diff between trees
// should return a single text change for character replace.
// Works as expected for first index
var index = original.IndexOf(indexText, StringComparison.Ordinal);
TestReplaceWithBuiltInTypeCore(root, index);
// Works as expected for last index
index = original.LastIndexOf(indexText, StringComparison.Ordinal);
TestReplaceWithBuiltInTypeCore(root, index);
// Doesn't work as expected for the third index.
// It returns 2 changes with add followed by delete,
// causing the 2 isolated edits to seem conflicting edits, even though they are not.
// See https://github.com/dotnet/roslyn/issues/320 for details.
indexText = "Object()";
index = original.IndexOf(indexText, StringComparison.Ordinal);
TestReplaceWithBuiltInTypeCore(root, index);
}
private void TestReplaceWithBuiltInTypeCore(SyntaxNode root, int index)
{
var oldTree = root.SyntaxTree;
var span = new TextSpan(index, 6);
var node = root.FindNode(span, getInnermostNodeForTie: true) as SimpleNameSyntax;
Assert.NotNull(node);
Assert.Equal("Object", node.Identifier.ValueText);
var leadingTrivia = node.GetLeadingTrivia();
var newNode = SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.ObjectKeyword))
.WithLeadingTrivia(leadingTrivia);
var newRoot = root.ReplaceNode(node, newNode);
var newTree = newRoot.SyntaxTree;
var changes = newTree.GetChanges(oldTree);
Assert.NotNull(changes);
Assert.Equal(1, changes.Count);
Assert.Equal("o", changes[0].NewText);
}
}
}
|