|
// 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.Threading;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
using Xunit;
namespace Microsoft.CodeAnalysis.CSharp.UnitTests
{
public class SyntaxPathTests
{
[Fact]
public void RecoverSingle()
{
var node = SyntaxFactory.IdentifierName(SyntaxFactory.Identifier("Hi"));
var path = new SyntaxPath(node);
Assert.True(path.TryResolve(node, out SyntaxNode recovered));
Assert.Equal(node, recovered);
}
[Fact]
public void FailFirstType()
{
var node = SyntaxFactory.IdentifierName(SyntaxFactory.Identifier("Hi"));
var path = new SyntaxPath(node);
Assert.False(path.TryResolve(SyntaxFactory.ParseExpression("Goo()"), out SyntaxNode _));
}
[Fact]
public void RecoverChild()
{
var node = SyntaxFactory.ParseExpression("Goo()");
var child = ((InvocationExpressionSyntax)node).ArgumentList;
var path = new SyntaxPath(child);
Assert.True(path.TryResolve(node, out SyntaxNode recovered));
Assert.Equal(child, recovered);
}
[Fact]
public void FailChildCount()
{
var root = SyntaxFactory.ParseExpression("Goo(a, b)");
var path = new SyntaxPath(((InvocationExpressionSyntax)root).ArgumentList.Arguments.Last());
var root2 = SyntaxFactory.ParseExpression("Goo(a)");
Assert.False(path.TryResolve(root2, out SyntaxNode _));
}
[Fact]
public void FailChildType()
{
var root = SyntaxFactory.ParseExpression("Goo(a)");
var path = new SyntaxPath(((InvocationExpressionSyntax)root).ArgumentList.Arguments.First().Expression);
var root2 = SyntaxFactory.ParseExpression("Goo(3)");
Assert.False(path.TryResolve(root2, out SyntaxNode _));
}
[Fact]
public void RecoverGeneric()
{
var root = SyntaxFactory.ParseExpression("Goo()");
var node = ((InvocationExpressionSyntax)root).ArgumentList;
var path = new SyntaxPath(node);
Assert.True(path.TryResolve(root, out ArgumentListSyntax recovered));
Assert.Equal(node, recovered);
}
[Fact]
public void TestRoot1()
{
var tree = SyntaxFactory.ParseSyntaxTree(string.Empty);
var root = tree.GetRoot();
var path = new SyntaxPath(root);
Assert.True(path.TryResolve(tree, CancellationToken.None, out SyntaxNode node));
Assert.Equal(root, node);
}
[Fact]
public void TestRoot2()
{
var text = SourceText.From(string.Empty);
var tree = SyntaxFactory.ParseSyntaxTree(string.Empty);
var root = tree.GetCompilationUnitRoot();
var path = new SyntaxPath(root);
var newText = text.WithChanges(new TextChange(new TextSpan(0, 0), "class C {}"));
var newTree = tree.WithChangedText(newText);
Assert.True(path.TryResolve(newTree, CancellationToken.None, out SyntaxNode node));
Assert.Equal(SyntaxKind.CompilationUnit, node.Kind());
}
[Fact]
public void TestRoot3()
{
var text = SourceText.From("class C {}");
var tree = SyntaxFactory.ParseSyntaxTree(text);
var root = tree.GetRoot();
var path = new SyntaxPath(root);
var newText = text.WithChanges(new TextChange(new TextSpan(0, text.Length), ""));
var newTree = tree.WithChangedText(newText);
Assert.True(path.TryResolve(newTree, CancellationToken.None, out SyntaxNode node));
Assert.Equal(SyntaxKind.CompilationUnit, node.Kind());
}
[Fact]
public void TestRoot4()
{
var text = "class C {}";
var tree = SyntaxFactory.ParseSyntaxTree(text);
var root = tree.GetRoot();
var path = new SyntaxPath(root);
tree = WithReplaceFirst(tree, "C", "D");
Assert.True(path.TryResolve(tree, CancellationToken.None, out SyntaxNode node));
Assert.Equal(SyntaxKind.CompilationUnit, node.Kind());
}
[Fact]
public void TestMethodBodyChange()
{
var text =
@"namespace N {
class C {
void M1() {
int i1;
}
void M2() {
int i2;
}
void M3() {
int i3;
}
}
}";
var tree = SyntaxFactory.ParseSyntaxTree(text);
var namespaceDecl = (NamespaceDeclarationSyntax)(tree.GetRoot() as CompilationUnitSyntax).Members[0];
var classDecl = (TypeDeclarationSyntax)namespaceDecl.Members[0];
var member1 = classDecl.Members[0];
var member2 = classDecl.Members[1];
var member3 = classDecl.Members[2];
var path1 = new SyntaxPath(member1);
var path2 = new SyntaxPath(member2);
var path3 = new SyntaxPath(member3);
tree = WithReplaceFirst(WithReplaceFirst(WithReplaceFirst(tree, "i1", "j1"), "i2", "j2"), "i3", "j3");
Assert.True(path1.TryResolve(tree, CancellationToken.None, out SyntaxNode n1));
Assert.True(path2.TryResolve(tree, CancellationToken.None, out SyntaxNode n2));
Assert.True(path3.TryResolve(tree, CancellationToken.None, out SyntaxNode n3));
Assert.Equal(SyntaxKind.MethodDeclaration, n1.Kind());
Assert.Equal("M1", ((MethodDeclarationSyntax)n1).Identifier.ValueText);
Assert.Equal(SyntaxKind.MethodDeclaration, n2.Kind());
Assert.Equal("M2", ((MethodDeclarationSyntax)n2).Identifier.ValueText);
Assert.Equal(SyntaxKind.MethodDeclaration, n3.Kind());
Assert.Equal("M3", ((MethodDeclarationSyntax)n3).Identifier.ValueText);
}
[Fact]
public void TestAddBase()
{
var text =
@"namespace N {
class C {
}
class D {
}
}";
var tree = SyntaxFactory.ParseSyntaxTree(text);
var namespaceDecl = (NamespaceDeclarationSyntax)(tree.GetRoot() as CompilationUnitSyntax).Members[0];
var class1 = (TypeDeclarationSyntax)namespaceDecl.Members[0];
var class2 = (TypeDeclarationSyntax)namespaceDecl.Members[1];
var path1 = new SyntaxPath(class1);
var path2 = new SyntaxPath(class2);
tree = WithReplaceFirst(tree, "C {", "C : Goo {");
Assert.True(path1.TryResolve(tree, CancellationToken.None, out SyntaxNode n1));
Assert.True(path2.TryResolve(tree, CancellationToken.None, out SyntaxNode n2));
Assert.Equal(SyntaxKind.ClassDeclaration, n1.Kind());
Assert.Equal("C", ((TypeDeclarationSyntax)n1).Identifier.ValueText);
Assert.Equal(SyntaxKind.ClassDeclaration, n2.Kind());
Assert.Equal("D", ((TypeDeclarationSyntax)n2).Identifier.ValueText);
}
[Fact]
public void TestAddFieldBefore()
{
var text =
@"namespace N {
class C {
void M1() {
int i1;
}
bool M2() {
int i2;
}
}
}";
var tree = SyntaxFactory.ParseSyntaxTree(text);
var namespaceDecl = (NamespaceDeclarationSyntax)((CompilationUnitSyntax)tree.GetRoot()).Members[0];
var classDecl = (TypeDeclarationSyntax)namespaceDecl.Members[0];
var member1 = classDecl.Members[0];
var member2 = classDecl.Members[1];
var path1 = new SyntaxPath(member1);
var path2 = new SyntaxPath(member2);
tree = WithReplaceFirst(tree, "bool", "int field; bool");
Assert.True(path1.TryResolve(tree, CancellationToken.None, out SyntaxNode n1));
Assert.True(path2.TryResolve(tree, CancellationToken.None, out SyntaxNode n2));
Assert.Equal(SyntaxKind.MethodDeclaration, n1.Kind());
Assert.Equal("M1", ((MethodDeclarationSyntax)n1).Identifier.ValueText);
Assert.Equal(SyntaxKind.MethodDeclaration, n2.Kind());
Assert.Equal("M2", ((MethodDeclarationSyntax)n2).Identifier.ValueText);
}
[Fact]
public void TestChangeType()
{
var text =
@"namespace N {
class C {
}
class D {
}
}";
var tree = SyntaxFactory.ParseSyntaxTree(text);
var namespaceDecl = (NamespaceDeclarationSyntax)((CompilationUnitSyntax)tree.GetRoot()).Members[0];
var class1 = (TypeDeclarationSyntax)namespaceDecl.Members[0];
var class2 = (TypeDeclarationSyntax)namespaceDecl.Members[1];
var path1 = new SyntaxPath(class1);
var path2 = new SyntaxPath(class2);
tree = WithReplaceFirst(tree, "class", "struct");
Assert.True(path1.TryResolve(tree, CancellationToken.None, out SyntaxNode n1));
Assert.False(path2.TryResolve(tree, CancellationToken.None, out SyntaxNode _));
Assert.Equal(SyntaxKind.ClassDeclaration, n1.Kind());
Assert.Equal("D", ((TypeDeclarationSyntax)n1).Identifier.ValueText);
}
[Fact]
public void TestChangeType1()
{
var text =
@"namespace N {
class C {
void Goo();
}
class D {
}
}";
var tree = SyntaxFactory.ParseSyntaxTree(text);
var namespaceDecl = (NamespaceDeclarationSyntax)((CompilationUnitSyntax)tree.GetRoot()).Members[0];
var class1 = (TypeDeclarationSyntax)namespaceDecl.Members[0];
var class2 = (TypeDeclarationSyntax)namespaceDecl.Members[1];
var method1 = class1.Members[0];
var path1 = new SyntaxPath(class1);
var path2 = new SyntaxPath(class2);
var path3 = new SyntaxPath(method1);
tree = WithReplaceFirst(tree, "class", "struct");
Assert.True(path1.TryResolve(tree, CancellationToken.None, out SyntaxNode n1));
Assert.False(path2.TryResolve(tree, CancellationToken.None, out SyntaxNode _));
Assert.False(path3.TryResolve(tree, CancellationToken.None, out SyntaxNode _));
Assert.Equal(SyntaxKind.ClassDeclaration, n1.Kind());
Assert.Equal("D", ((TypeDeclarationSyntax)n1).Identifier.ValueText);
}
[Fact]
public void TestWhitespace1()
{
var text =
@"namespace N {
class C {
}
class D {
}
}";
var text2 = text.Replace(" ", " ");
var tree = SyntaxFactory.ParseSyntaxTree(text);
var namespaceDecl = (NamespaceDeclarationSyntax)((CompilationUnitSyntax)tree.GetRoot()).Members[0];
var class1 = (TypeDeclarationSyntax)namespaceDecl.Members[0];
var class2 = (TypeDeclarationSyntax)namespaceDecl.Members[1];
var path1 = new SyntaxPath(class1);
var path2 = new SyntaxPath(class2);
tree = WithReplace(tree, 0, text.Length, text2);
Assert.True(path1.TryResolve(tree, CancellationToken.None, out SyntaxNode n1));
Assert.True(path2.TryResolve(tree, CancellationToken.None, out SyntaxNode n2));
Assert.Equal(SyntaxKind.ClassDeclaration, n1.Kind());
Assert.Equal("C", ((TypeDeclarationSyntax)n1).Identifier.ValueText);
Assert.Equal(SyntaxKind.ClassDeclaration, n2.Kind());
Assert.Equal("D", ((TypeDeclarationSyntax)n2).Identifier.ValueText);
}
[Fact]
public void TestComment()
{
var text =
@"namespace N {
class C {
}
struct D {
}
}";
var tree = SyntaxFactory.ParseSyntaxTree(text);
var namespaceDecl = (NamespaceDeclarationSyntax)((CompilationUnitSyntax)tree.GetRoot()).Members[0];
var class1 = (TypeDeclarationSyntax)namespaceDecl.Members[0];
var class2 = (TypeDeclarationSyntax)namespaceDecl.Members[1];
var path1 = new SyntaxPath(class1);
var path2 = new SyntaxPath(class2);
tree = WithReplaceFirst(WithReplaceFirst(tree, "class", "/* goo */ class"), "struct", "/* bar */ struct");
Assert.True(path1.TryResolve(tree, CancellationToken.None, out SyntaxNode n1));
Assert.True(path2.TryResolve(tree, CancellationToken.None, out SyntaxNode n2));
Assert.Equal(SyntaxKind.ClassDeclaration, n1.Kind());
Assert.Equal("C", ((TypeDeclarationSyntax)n1).Identifier.ValueText);
Assert.Equal(SyntaxKind.StructDeclaration, n2.Kind());
Assert.Equal("D", ((TypeDeclarationSyntax)n2).Identifier.ValueText);
}
[Fact]
public void TestPP1()
{
var text =
@"namespace N {
class C {
}
struct D {
}
}";
var text2 =
@"namespace N {
#if true
class C {
}
struct D {
}
#endif
}";
var tree = SyntaxFactory.ParseSyntaxTree(text);
var namespaceDecl = (NamespaceDeclarationSyntax)((CompilationUnitSyntax)tree.GetRoot()).Members[0];
var class1 = (TypeDeclarationSyntax)namespaceDecl.Members[0];
var class2 = (TypeDeclarationSyntax)namespaceDecl.Members[1];
var path1 = new SyntaxPath(class1);
var path2 = new SyntaxPath(class2);
tree = WithReplace(tree, 0, text.Length, text2);
Assert.True(path1.TryResolve(tree, CancellationToken.None, out SyntaxNode n1));
Assert.True(path2.TryResolve(tree, CancellationToken.None, out SyntaxNode n2));
Assert.Equal(SyntaxKind.ClassDeclaration, n1.Kind());
Assert.Equal("C", ((TypeDeclarationSyntax)n1).Identifier.ValueText);
Assert.Equal(SyntaxKind.StructDeclaration, n2.Kind());
Assert.Equal("D", ((TypeDeclarationSyntax)n2).Identifier.ValueText);
}
[Fact]
public void TestRemoveUsing()
{
var text = SourceText.From("using X; class C {}");
var tree = SyntaxFactory.ParseSyntaxTree(text);
var root = (CompilationUnitSyntax)tree.GetRoot();
var path = new SyntaxPath(root.Members[0]);
var newText = WithReplaceFirst(text, "using X;", "");
var newTree = tree.WithChangedText(newText);
Assert.True(path.TryResolve(newTree, CancellationToken.None, out SyntaxNode node));
Assert.Equal(SyntaxKind.ClassDeclaration, node.Kind());
}
internal static SourceText WithReplaceFirst(SourceText text, string oldText, string newText)
{
var oldFullText = text.ToString();
var offset = oldFullText.IndexOf(oldText, StringComparison.Ordinal);
var length = oldText.Length;
var span = new TextSpan(offset, length);
var newFullText = oldFullText[..offset] + newText + oldFullText[span.End..];
return SourceText.From(newFullText);
}
public static SyntaxTree WithReplaceFirst(SyntaxTree syntaxTree, string oldText, string newText)
{
return WithReplace(syntaxTree,
startIndex: 0,
oldText: oldText,
newText: newText);
}
public static SyntaxTree WithReplace(SyntaxTree syntaxTree, int offset, int length, string newText)
{
var oldFullText = syntaxTree.GetText();
var newFullText = oldFullText.WithChanges(new TextChange(new TextSpan(offset, length), newText));
return syntaxTree.WithChangedText(newFullText);
}
public static SyntaxTree WithReplace(SyntaxTree syntaxTree, int startIndex, string oldText, string newText)
{ // Use the offset to find the first element to replace at
return WithReplace(syntaxTree,
offset: syntaxTree.GetText().ToString().IndexOf(oldText, startIndex, StringComparison.Ordinal),
length: oldText.Length,
newText: newText);
}
}
}
|