Замена вложенных узлов в Roslyn SyntaxTree - PullRequest
0 голосов
/ 30 сентября 2018

Как часть пользовательского процесса компиляции, я заменяю различные узлы в SyntaxTree для генерации действительного C #.Проблема возникает, когда заменяемые узлы являются вложенными, поскольку неизменность всех типов означает, что как только один узел заменяется, в его иерархии больше нет равенства.

Уже существует aаналогичный вопрос для SO , однако он, похоже, нацелен на более старую версию Roslyn и полагается на некоторые методы, которые сейчас являются закрытыми.У меня уже есть SyntaxTree и SemanticModel, но пока мне не нужно Document с, Project с или Solution с, поэтому я не решался идти по этому маршруту.

Предположим, у меня есть следующая строка 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 правильный подход?И если да, то как можно заставить его работать?


Ответ, предоставленный Джорджем Александрией, жизненно важно, чтобы базовый метод 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 голосов
/ 30 сентября 2018

ReplaceNode() это то, что вам нужно, но вы должны заменить узел из глубины, чтобы на текущем уровне глубины у вас было только одно изменение для сравнения.

Вы можете переписать первый пример, сохранив порядок обмена и сохранив промежуточный SyntaxTree, и он будет работать.Но у Roslyn есть встроенная реализация глубокого переписывания первого порядка - CSharpSyntaxRewriter, и в ссылке, которую вы публикуете @JoshVarty, указано CSharpSyntaxRewriter.

Ваш второй пример не работает, потому что вы используете пользовательский CSharpSyntaxVisitor<SyntaxNode>, который не углубляется в desing, а когда вы вызываете replacer.Visit(root);, вы вызываете только VisitCompilationUnit(...) и ничего больше.Вместо этого CSharpSyntaxRewriter переходит к дочерним узлам и вызывает методы Visit*() для всех них.

...