As part of a custom compilation process I'm replacing various nodes in a SyntaxTree in order to generate valid 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. 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.
Let's assume I have the following string public void Test() { cosh(x); }
which I want to convert into 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. So only the cosh
replacement is made, the x
is left the same:
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.
Based on the answer in the above link, I switched to SyntaxVisitor
, which utterly fails to do anything at all. My overridden methods are never called, and the Visit()
method returns a null node:
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? 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. This is the SyntaxRewriter which works for me:
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;
}
}