简体   繁体   English

替换Roslyn语法树中的嵌套节点

[英]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#. 作为自定义编译过程的一部分,我将替换SyntaxTree中的各个节点以生成有效的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. 关于SO ,已经存在一个类似的问题 ,但是它似乎针对的是Roslyn的较旧版本,并依赖于现在私有的某些方法。 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. 我已经有了SyntaxTreeSemanticModel ,但是到目前为止,我并不需要DocumentProjectSolution ,所以我一直犹豫要走那条路线。

Let's assume I have the following string public void Test() { cosh(x); } 假设我有以下字符串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() { cosh(x); } ,我想将其转换为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. 我第一次使用ReplaceNodes()尝试失败了,因为一旦进行了一次替换,树的变化就足以使第二次比较失败。 So only the cosh replacement is made, the x is left the same: 因此,仅进行cosh替换, x保持不变:

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. 我很欣赏在这种情况下可能无需执行任何操作, ReplaceNodes根本无法处理嵌套的替换。


Based on the answer in the above link, I switched to SyntaxVisitor , which utterly fails to do anything at all. 根据以上链接中的答案,我切换到SyntaxVisitor ,它根本无法做任何事情。 My overridden methods are never called, and the Visit() method returns a null node: 我的重写方法从不调用, Visit()方法返回一个空节点:

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? 问题: CSharpSyntaxVisitor是否正确? 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. 回答由George Alexandria提供的,至关重要的是首先调用基本的Visit方法,否则将无法再使用SemanticModel。 This is the SyntaxRewriter which works for me: 这是对我有用的SyntaxRewriter:

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. 是否需要ReplaceNode() ,但应该从深度替换节点,因此在当前深度级别中,只有一个更改可用于比较。

You can rewrite you first example, saving the swapping order and saving an intermediate SyntaxTree , and it will work. 您可以重写第一个示例,保存交换顺序并保存中间的SyntaxTree ,它将起作用。 But Roslyn has build-in implementation of deep-first order rewriting – CSharpSyntaxRewriter and in the link that you post @JoshVarty pointed to CSharpSyntaxRewriter . 但是Roslyn具有深度一阶重写的CSharpSyntaxRewriter实现– CSharpSyntaxRewriter并且在您发布@JoshVarty的链接中指向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); 第二个示例不起作用,因为您使用了自定义CSharpSyntaxVisitor<SyntaxNode> ,该自定义CSharpSyntaxVisitor<SyntaxNode>设计和调用replacer.Visit(root);来深入了解replacer.Visit(root); you only invoke the VisitCompilationUnit(...) and nothing else. 您只需调用VisitCompilationUnit(...) Instead of, CSharpSyntaxRewriter goes to the child nodes and will invoke Visit*() methods for all of them. 取而代之的是, CSharpSyntaxRewriter转到子节点,并将为所有子节点调用Visit*()方法。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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