Search code examples
c#.netroslyn

Roslyn Analyzer + Code Fix - Replacing Node with Descendant Node


I've written an analyzer that will flag certain methods if they're not marked as async.

I'm now implementing a code fixer that will make them async.

It'll also either add an await expression, or it'll remove a Task.FromResult call and just return the object directly.

To do the latter, I'm doing this:

var expressionSyntax = returnStatement.ChildNodes().OfType<ExpressionSyntax>().First()!;

var firstInnerExpression = expressionSyntax.ChildNodes().OfType<ArgumentListSyntax>().First().Arguments.First().Expression;

var newReturnStatement = returnStatement.ReplaceNode(expressionSyntax, firstInnerExpression);
                
newMethodDeclarationSyntax = newMethodDeclarationSyntax.ReplaceNode(returnStatement, newReturnStatement);

In the debugger, I can see my old return statement is:

ReturnStatementSyntax ReturnStatement return Task.FromResult<string?>("Foo!");

My new return statement is:

ReturnStatementSyntax ReturnStatement return "Foo!";

However when I call the method.ReplaceNode, it doesn't error, but it also doesn't update to the new return statement.

Does anyone know why?

Thanks


Solution

  • It sounds like you make several node replacements in your code. Just in the snippet there are already two calls to the SyntaxNode.ReplaceNode method.

    Public data structures in Roslyn are immutable. Similar to strings when you modify a syntax tree with node replacement you will receive a new node which have a new different SyntaxTree. If you have some syntax nodes found before the first replacement, then these nodes are part of the old syntax tree.

    To make this more obvious I usually call replace on the root node and store the result in a new variable like this:

        var newRoot = oldRoot.ReplaceNode(oldNode, newGeneratedNode);
    

    Attempting to combine nodes with different syntax trees won't be successful. With the second call to ReplaceNode one of the nodes just won't be found in the syntax tree of another. No nodes found to replace, no error, everything is OK from the API point of view.

    There are several approaches to this issue. I usually use the following two:

    1. The most simple approach is to attempt to reduce your calls to ReplaceNode to just one. Prepare the new node as much as you can, add everything needed there. Maybe replace not the node you were originally going to replace, but it's ancestor.
    2. The previous approach frequently is enough but sometimes you have to call ReplaceNode several times. In this case I usually use the TrackNodes functionality. The idea is that you can find some nodes before any modifications, explicitly tell Roslyn to mark them, so they could be found in new modified syntax trees. Here is a simple example:
    SyntaxNode someNode, anotherNode; //assume that you have two nodes to modify
    
    //This is also a modification, the trackingRoot has different syntax tree
    var trackingRoot = root.TrackNodes(someNode, anotherNode); 
    var someNode = trackingRoot.GetCurrentNode(someNode); // node has to be found in the new syntax tree 
    
    if (someNode != null)
    {
       var newSomeNode = Modify(someNode);
       trackingRoot = trackingRoot.ReplaceNode(someNode, newSomeNode);
    }
    
    var anotherNode= trackingRoot.GetCurrentNode(anotherNode); // node has to be found in the new syntax tree 
    
    if (anotherNode!= null)
    {
       var newAnotherNode = Modify(anotherNode);
       trackingRoot = trackingRoot.ReplaceNode(anotherNode, newAnotherNode);
    }
    

    You can find an example of such code fix in the open source project I maintain: https://github.com/Acumatica/Acuminator/blob/dev/src/Acuminator/Acuminator.Analyzers/StaticAnalysis/NonPublicExtensions/NonPublicExtensionFix.cs#L66-L74

    For cases of many edits there are more complex APIs like CSharpSyntaxRewriter or DocumentEditor. Describing them all will make this answer huge. If you are interested here is a link to good series of blog posts describing them: https://joshvarty.com/2015/08/18/learn-roslyn-now-part-12-the-documenteditor/