【问题标题】:Replacing nested nodes in a Roslyn SyntaxTree替换 Roslyn SyntaxTree 中的嵌套节点
【发布时间】:2019-03-05 16:49:48
【问题描述】:

作为自定义编译过程的一部分,我将替换 SyntaxTree 中的各个节点以生成有效的 C#。当被替换的节点嵌套时会出现问题,因为所有类型的不变性意味着一旦一个节点被换出,其层次结构中就不再存在相等性。

已经有a similar question on SO,但它似乎针对的是旧版本的 Roslyn,并且依赖于一些现在是私有的方法。我已经有一个SyntaxTree 和一个SemanticModel,但到目前为止我还不需要Documents、Projects 或Solutions,所以我一直在犹豫要不要走这条路。

假设我有以下字符串public void Test() { cosh(x); },我想将其转换为public void Test() { MathNet.Numerics.Trig.Cosh(__resolver["x"]); }

我第一次尝试使用ReplaceNodes() 失败了,因为一旦进行了替换,树就会发生足够的变化,导致第二次比较失败。所以只替换了coshx 保持不变:

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();
}

我很欣赏在这种情况下可能无事可做,ReplaceNodes 根本无法处理嵌套替换。


根据上面链接中的答案,我切换到SyntaxVisitor,它完全没有做任何事情。我的重写方法永远不会被调用,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);
  }
}

问题:CSharpSyntaxVisitor 是正确的方法吗?如果是这样,如何让它发挥作用?


George Alexandria 提供的答案,首先调用基本的 Visit 方法至关重要,否则 SemanticModel 将无法再使用。这是适合我的 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() 是您需要的吗,但是您应该从深度替换节点,因此在当前深度级别中,您将只有一个更改用于比较。为了实现它(深度一阶重写)Roslyn 有一个CSharpSyntaxRewriter,并且在上面的链接中,您发布的@JoshVarty 指向CSharpSyntaxRewriter。你也可以重写你的第一个例子,保存交换订单并保存一个中间SyntaxTree,它会起作用。
  • @GeorgeAlexandria 我的第二次尝试使用 CSharpSyntaxRewriter,但它似乎根本不起作用。我将尝试修改 ReplaceNodes 方法来迭代我所有的交换对并首先执行最深嵌套的对。谢谢你的想法。
  • 不,您第二次尝试使用自定义CSharpSyntaxVisitor&lt;SyntaxNode&gt;,通过CSharpSyntaxVisitor 的设计不会深入。因此,当您调用var newRoot = replacer.Visit(root); 时,您实际上调用了VisitCompilationUnitRootSyntax(),仅此而已。而不是,CSharpSyntaxRewriter 转到子节点。
  • @GeorgeAlexandria 啊!重写者与访客。就是这样,谢谢!如果您将其发布为答案,我可以接受。

标签: c# roslyn abstract-syntax-tree


【解决方案1】:

ReplaceNode() 是您需要的吗,但是您应该从深度替换节点,因此在当前深度级别中,您将只有一个更改用于比较。

您可以重写您的第一个示例,保存交换订单并保存中间SyntaxTree,它会起作用。但是 Roslyn 内置了深度一阶重写的实现——CSharpSyntaxRewriter,并且在您发布的链接中@JoshVarty 指向CSharpSyntaxRewriter

您的第二个示例不起作用,因为您使用了自定义 CSharpSyntaxVisitor&lt;SyntaxNode&gt;,它不会通过设计深入,并且当您调用 replacer.Visit(root); 时,您只调用 VisitCompilationUnit(...) 而没有别的。取而代之的是,CSharpSyntaxRewriter 转到子节点并为所有子节点调用 Visit*() 方法。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-01-24
    • 2015-09-27
    • 2017-04-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多