替换 Roslyn SyntaxTree 中的嵌套节点
Replacing nested nodes in a Roslyn SyntaxTree
作为自定义编译过程的一部分,我将替换 SyntaxTree 中的各种节点以生成有效的 C#。当替换节点嵌套时会出现问题,因为所有类型的不变性意味着一旦一个节点被换出,其层次结构中就不再存在相等性。
已经有 a similar question on SO,但它似乎针对旧版本的 Roslyn 并依赖于一些现在私有的方法。我已经有一个 SyntaxTree
和一个 SemanticModel
,但到目前为止我还不需要 Document
s、Project
s 或 Solution
s,所以我已经犹豫着走那条路。
假设我有以下字符串 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
根本无法处理嵌套替换。
根据上面link中的答案,我切换到SyntaxVisitor
,完全没有任何作用。我重写的方法从未被调用,Visit()
方法 returns 一个空节点:
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()
是您需要的,但是您应该从深度替换节点,因此在当前深度级别中您将只有一个更改用于比较。
您可以重写您的第一个示例,保存交换顺序并保存一个中间值 SyntaxTree
,它会起作用。但是 Roslyn 内置了深度优先顺序重写的实现——CSharpSyntaxRewriter
并且在你 post @JoshVarty 指向 CSharpSyntaxRewriter
的 link 中。
你的第二个例子不起作用,因为你使用的自定义 CSharpSyntaxVisitor<SyntaxNode>
不会通过设计深入,当你调用 replacer.Visit(root);
时你只调用 VisitCompilationUnit(...)
和没有其他的。相反,CSharpSyntaxRewriter
转到子节点并将为所有子节点调用 Visit*()
方法。
作为自定义编译过程的一部分,我将替换 SyntaxTree 中的各种节点以生成有效的 C#。当替换节点嵌套时会出现问题,因为所有类型的不变性意味着一旦一个节点被换出,其层次结构中就不再存在相等性。
已经有 a similar question on SO,但它似乎针对旧版本的 Roslyn 并依赖于一些现在私有的方法。我已经有一个 SyntaxTree
和一个 SemanticModel
,但到目前为止我还不需要 Document
s、Project
s 或 Solution
s,所以我已经犹豫着走那条路。
假设我有以下字符串 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
根本无法处理嵌套替换。
根据上面link中的答案,我切换到SyntaxVisitor
,完全没有任何作用。我重写的方法从未被调用,Visit()
方法 returns 一个空节点:
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()
是您需要的,但是您应该从深度替换节点,因此在当前深度级别中您将只有一个更改用于比较。
您可以重写您的第一个示例,保存交换顺序并保存一个中间值 SyntaxTree
,它会起作用。但是 Roslyn 内置了深度优先顺序重写的实现——CSharpSyntaxRewriter
并且在你 post @JoshVarty 指向 CSharpSyntaxRewriter
的 link 中。
你的第二个例子不起作用,因为你使用的自定义 CSharpSyntaxVisitor<SyntaxNode>
不会通过设计深入,当你调用 replacer.Visit(root);
时你只调用 VisitCompilationUnit(...)
和没有其他的。相反,CSharpSyntaxRewriter
转到子节点并将为所有子节点调用 Visit*()
方法。