How to get semantic model for renamed class without "not found to remove" exception

Viewed 953

Here is a script that

  1. Identifies the type information for a class using its semantic model
  2. Renames the class by replacing its Identifier token
  3. Identifies the type information for the renamed class using its semantic model
  4. Runs a SyntaxRewriter on the renamed class - the rewriter replaces all ocurrences of the old type info with the new type info.

(I hope) as minimally as possible:

static void Main(string[] args)
{
    // This class needs to be renamed. We want to use Roslyn and symbols,
    // because string-based replacement would change things that it's not supposed to.
    var someScript = "public class FooBar { public NotFooBar a; FooBar string b; public OtherNamespace.FooBar c; }";

    // Minimal Roslyn boilerplate
    var compilation = CSharpCompilation.Create("CodeGeneration")
        .WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
        .AddSyntaxTrees(CSharpSyntaxTree.ParseText(someScript));

    var tree = compilation.SyntaxTrees[0];
    var classDeclaration = tree.GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>().Single();
    var originalType = compilation.GetSemanticModel(classDeclaration.SyntaxTree).GetDeclaredSymbol(classDeclaration);
    var renamedClass = classDeclaration.ReplaceToken(
        classDeclaration.Identifier,
        SyntaxFactory.Identifier(classDeclaration.Identifier.Text.Replace("Bar", "Baz"))
    );
    var semanticModel = compilation.GetSemanticModel(renamedClass.SyntaxTree);
    var renamedType = semanticModel.GetDeclaredSymbol(renamedClass);
    var rewritten = new TestRewriter(semanticModel, originalType, renamedType).Visit(renamedClass);
    Console.Write(rewritten.GetText());
}

class TestRewriter : CSharpSyntaxRewriter
{
    private readonly SemanticModel _model;
    private readonly ITypeSymbol _replace;
    private readonly ITypeSymbol _replacement;

    public TestRewriter(SemanticModel model, ITypeSymbol replace, ITypeSymbol replacement)
    {
        _model = model;
        _replace = replace;
        _replacement = replacement;
    }

    public override SyntaxNode VisitIdentifierName(IdentifierNameSyntax node)
    {
        return base.VisitIdentifierName(
            _model.GetTypeInfo(node).Type.Equals(_replace)
                ? node.ReplaceToken(node.Identifier, SyntaxFactory.Identifier(_replacement.ToDisplayString()))
                : node
        );
    }
}

When running this program, compilation.GetSemanticModel(renamedClass.SyntaxTree) causes the following exception

System.ArgumentException: SyntaxTree public class FooBaz{ public NotFooBar a; FooBar string b; public OtherNamespace.FooBar c; } not found to remove

If, instead of getting a semantic model from the renamed class' tree, the semantic model from the original class' tree is used, then GetTypeInfo(node) will (as expected) throw an exception because node is not a part of the model's tree.

What step am I missing that would prevent the "not found to remove" exception?

1 Answers

The error that you receive is a result of you searching an element in a SyntaxTree which does not contain said element.

When you call classDeclaration.ReplaceToken you do not actually replace the token in the existing SyntaxTree but rather you generate a new structure which is unrelated to the existing one. When you try to get the semantic model from the renamedClass you use a SyntaxTree not within the Compilation which causes the code to throw an exception.

In order to fix this problem you can replace the node in the existing CompilationUnit and then replace the SyntaxTree in the Compilation. However, you will not be able to find the ClassDeclaration again, as it becomes a new object which does not correspond to the existing "renamedClass" Object.

In order to track the element you can use SyntaxAnnotations which allow you to add information to a node which can be retrieved later. This could look something along these lines:

    static void ReplaceClassName()
    {
        // This class needs to be renamed. We want to use Roslyn and symbols,
        // because string-based replacement would change things that it's not supposed to.
        var someScript = "public class FooBar { public NotFooBar a; FooBar b; public OtherNamespace.FooBar c; }";

        // Minimal Roslyn boilerplate
        var compilation = CSharpCompilation.Create("CodeGeneration")
            .WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
            .AddSyntaxTrees(CSharpSyntaxTree.ParseText(someScript));
        var oldSyntaxTree = compilation.SyntaxTrees[0];
        var root = oldSyntaxTree.GetRoot();
        var classDeclaration = root.DescendantNodes().OfType<ClassDeclarationSyntax>().Single();
        var originalType = compilation.GetSemanticModel(classDeclaration.SyntaxTree).GetDeclaredSymbol(classDeclaration);
        var renamedClass = classDeclaration.ReplaceToken(
            classDeclaration.Identifier,
            SyntaxFactory.Identifier(classDeclaration.Identifier.Text.Replace("Bar", "Baz"))
        ).WithAdditionalAnnotations(new SyntaxAnnotation("ReplacedClass"));
        root = root.ReplaceNode(classDeclaration, renamedClass);
        compilation = compilation.ReplaceSyntaxTree(oldSyntaxTree, renamedClass.SyntaxTree);
        renamedClass = compilation.SyntaxTrees[0].GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>().FirstOrDefault(curr => curr.GetAnnotations("ReplacedClass").Any());
        var semanticModel = compilation.GetSemanticModel(compilation.SyntaxTrees[0]);
        var renamedType = semanticModel.GetDeclaredSymbol(renamedClass);
        var rewritten = new TestRewriter(semanticModel, originalType, renamedType).Visit(renamedClass);
        Console.Write(rewritten.GetText());
    }

Sadly this will not actually resolve your problem. While you now have a new SemanticModel available, you will not be able to compare the TypeInfo of two different Models as you do in your SyntaxRewriter.

In order to retain your Type information I would advise you to use the SyntaxRewriter to change both the IdentifierName as well as the ClassDeclarationSyntax, as the SyntaxRewriter will visit the nodes first and replace them only afterwards. This could be implemented as follows:

static void ReplaceClassName()
{
    // This class needs to be renamed. We want to use Roslyn and symbols,
    // because string-based replacement would change things that it's not supposed to.
    var someScript = "public class FooBar { public NotFooBar a; FooBar b; public OtherNamespace.FooBar c; }";

    // Minimal Roslyn boilerplate
    var compilation = CSharpCompilation.Create("CodeGeneration")
        .WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
        .AddSyntaxTrees(CSharpSyntaxTree.ParseText(someScript));
    var oldSyntaxTree = compilation.SyntaxTrees[0];
    var root = oldSyntaxTree.GetRoot();
    var classDeclaration = root.DescendantNodes().OfType<ClassDeclarationSyntax>().FirstOrDefault();
    var semanticModel = compilation.GetSemanticModel(classDeclaration.SyntaxTree);
    var originalType = semanticModel.GetDeclaredSymbol(classDeclaration);
    var rewritten = new TestRewriter(semanticModel, originalType).Visit(root);
    Console.Write(rewritten.GetText());
}

class TestRewriter : CSharpSyntaxRewriter
{
    private readonly SemanticModel _model;
    private readonly ITypeSymbol _replace;
    public TestRewriter(SemanticModel model, ITypeSymbol replace)
    {
        _model = model;
        _replace = replace;
    }

    public override SyntaxNode VisitIdentifierName(IdentifierNameSyntax node)
    {
        node = base.VisitIdentifierName(node) as IdentifierNameSyntax;
        if (_model.GetTypeInfo(node).Type.Equals(_replace))
        {
            var oldNode = node;
            node = node.ReplaceToken(

                        node.Identifier,
                        SyntaxFactory.Identifier(node.Identifier.Text.Replace("Bar", "Baz"))
                ).WithLeadingTrivia(oldNode.GetLeadingTrivia()).WithTrailingTrivia(oldNode.GetTrailingTrivia());
        }
        return node;
    }

    public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node)
    {
        var oldNode = node;
        node = base.VisitClassDeclaration(node) as ClassDeclarationSyntax;
        if (_model.GetDeclaredSymbol(oldNode).Equals(_replace))
        {
            return node.WithIdentifier(SyntaxFactory.Identifier(node.Identifier.Text.Replace("Bar", "Baz")));
        }
        return node;
    }
}

If you actually have a solution available you could possibly use the Renamer class which should do all the work for you.

Related