简体   繁体   中英

Replacing nested nodes in a Roslyn SyntaxTree

As part of a custom compilation process I'm replacing various nodes in a SyntaxTree in order to generate valid C#. The problem occurs when replaced nodes are nested, as the immutability of all the types mean that as soon as one node it swapped out, there is no equality any more in its hierarchy.

There is already a similar question on SO , however it seems to target an older version of Roslyn and relying on some methods which are private now. I already have a SyntaxTree and a SemanticModel , but so far I have not needed Document s, Project s or Solution s, so I've been hesitant going down that route.

Let's assume I have the following string public void Test() { cosh(x); } public void Test() { cosh(x); } which I want to convert into public void Test() { MathNet.Numerics.Trig.Cosh(__resolver["x"]); } public void Test() { MathNet.Numerics.Trig.Cosh(__resolver["x"]); }

My first attempt using ReplaceNodes() failed because as soon as one replacement is made, the tree changes sufficiently for the second comparison to fail. So only the cosh replacement is made, the x is left the same:

public static void TestSyntaxReplace()
{
  const string code = "public void Test() { cosh(x); }";
  var tree = CSharpSyntaxTree.ParseText(code);
  var root = tree.GetRoot();
  var swap = new Dictionary<SyntaxNode, SyntaxNode>();

  foreach (var node in root.DescendantNodes())
    if (node is InvocationExpressionSyntax oldInvocation)
    {
      var newExpression = ParseExpression("MathNet.Numerics.Trig.Cosh");
      var newInvocation = InvocationExpression(newExpression, oldInvocation.ArgumentList);
      swap.Add(node, newInvocation);
    }

  foreach (var node in root.DescendantNodes())
    if (node is IdentifierNameSyntax identifier)
      if (identifier.ToString() == "x")
      {
        var resolver = IdentifierName("__resolver");
        var literal = LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(identifier.ToString()));
        var argument = BracketedArgumentList(SingletonSeparatedList(Argument(literal)));
        var resolverCall = ElementAccessExpression(resolver, argument);
        swap.Add(node, resolverCall);
      }

  root = root.ReplaceNodes(swap.Keys, (n1, n2) => swap[n1]);
  var newCode = root.ToString();
}

I appreciate that there's probably nothing to be done in this case, ReplaceNodes simply isn't up to handling nested replacements.


Based on the answer in the above link, I switched to SyntaxVisitor , which utterly fails to do anything at all. My overridden methods are never called, and the Visit() method returns a null node:

public static void TestSyntaxVisitor()
{
  const string code = "public void Test() { cosh(x); }";
  var tree = CSharpSyntaxTree.ParseText(code);
  var root = tree.GetRoot();

  var replacer = new NodeReplacer();
  var newRoot = replacer.Visit(root); // This just returns null.
  var newCode = newRoot.ToString();
}
private sealed class NodeReplacer : CSharpSyntaxVisitor<SyntaxNode>
{
  public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax node)
  {
    if (node.ToString().Contains("cosh"))
    {
      var newExpression = ParseExpression("MathNet.Numerics.Trig.Cosh");
      node = InvocationExpression(newExpression, node.ArgumentList);
    }
    return base.VisitInvocationExpression(node);
  }

  public override SyntaxNode VisitIdentifierName(IdentifierNameSyntax node)
  {
    if (node.ToString() == "x")
    {
      var resolver = IdentifierName("__resolver");
      var literal = LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(node.ToString()));
      var argument = BracketedArgumentList(SingletonSeparatedList(Argument(literal)));
      return ElementAccessExpression(resolver, argument);
    }

    return base.VisitIdentifierName(node);
  }
}

Question: Is CSharpSyntaxVisitor the correct approach? And if so, how does one make it work?


Answer as provided by George Alexandria, it is vital that the base Visit method is called first, otherwise the SemanticModel can no longer be used. This is the SyntaxRewriter which works for me:

private sealed class NonCsNodeRewriter : CSharpSyntaxRewriter
{
  private readonly SemanticModel _model;
  public NonCsNodeRewriter(SemanticModel model)
  {
    _model = model;
  }

  public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax node)
  {
    var invocation = (InvocationExpressionSyntax)base.VisitInvocationExpression(node);
    var symbol = _model.GetSymbolInfo(node);
    if (symbol.Symbol == null)
      if (!symbol.CandidateSymbols.Any())
      {
        var methodName = node.Expression.ToString();
        if (_methodMap.TryGetValue(methodName, out var mapped))
          return InvocationExpression(mapped, invocation.ArgumentList);
      }

    return invocation;
  }
  public override SyntaxNode VisitIdentifierName(IdentifierNameSyntax node)
  {
    var identifier = base.VisitIdentifierName(node);
    var symbol = _model.GetSymbolInfo(node);
    if (symbol.Symbol == null)
      if (!symbol.CandidateSymbols.Any())
      {
        // Do not replace unknown methods, only unknown variables.
        if (node.Parent.IsKind(SyntaxKind.InvocationExpression))
          return identifier;

        return CreateResolverIndexer(node.Identifier);
      }
    return identifier;
  }

  private static SyntaxNode CreateResolverIndexer(SyntaxToken token)
  {
    var literal = LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(token.ToString()));
    var argument = BracketedArgumentList(SingletonSeparatedList(Argument(literal)));
    var indexer = ElementAccessExpression(IdentifierName("__resolver"), argument);
    return indexer;
  }
}

ReplaceNode() is it that you need, but you should replace node from the depth so in the current depth level you will have only a one changing for the comparison.

You can rewrite you first example, saving the swapping order and saving an intermediate SyntaxTree , and it will work. But Roslyn has build-in implementation of deep-first order rewriting – CSharpSyntaxRewriter and in the link that you post @JoshVarty pointed to CSharpSyntaxRewriter .

You second example doesn't work because you use the custom CSharpSyntaxVisitor<SyntaxNode> that doesn't go to deep by desing and when you invoke replacer.Visit(root); you only invoke the VisitCompilationUnit(...) and nothing else. Instead of, CSharpSyntaxRewriter goes to the child nodes and will invoke Visit*() methods for all of them.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM