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

时间:2018-09-30 09:18:17

标签: c# roslyn abstract-syntax-tree

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

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

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

我第一次尝试使用ReplaceNodes()失败了,因为一旦进行了一次替换,树就发生了足够的变化,导致第二次比较失败。因此,仅进行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();
}

我很高兴在这种情况下可能无需执行任何操作,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;
  }
}

1 个答案:

答案 0 :(得分:1)

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

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

您的第二个示例不起作用,因为您使用了自定义CSharpSyntaxVisitor<SyntaxNode>,而该replacer.Visit(root);并不会因设计而深入,而在调用VisitCompilationUnit(...)时,您只会调用CSharpSyntaxRewriter,而没有其他任何东西。相反,Visit*()转到子节点,并将为所有子节点调用window.MODULES.Flickr = async query => { const opts = { api_key: 'API_KEY' }; const photos = await window.fetch( `https://api.flickr.com/services/rest/?method=flickr.photos.search&api_key=${ opts.api_key }&text=${query}&format=json&nojsoncallback=1 ` ); const data = await photos.json(); const pictures = data.photos.photo; const images = []; pictures.forEach(picture => { return images.push({ id: picture.id, url: `https://farm${picture.farm}.staticflickr.com/${picture.server}/${ picture.id }_${picture.secret}_s.jpg`, title: picture.title }); }); return { query, images }; }; 方法。