With Roslyn, Find Calling Method from String Literal Parameter
I'm Trying to Use Roslyn to Replace Some Old, Slow Code in a Utility That Searches Our Source Code for String Literals Which Are Not Wrapped in an X() Function...
I'm trying to use Roslyn to replace some old, slow code in a utility that searches our source code for string literals which are not wrapped in an X() function call (things wrapped in X() will be be translated).
I was able to use the Syntax Tree to get the string literals quite easily, and identified most of the places where they were wrapped in X(). What I was doing: given an LiteralExpressionSyntax object, I found that this gave me the function call and I could match it with a regular expression.
s.Parent.Parent.Parent.ToFullString()
I quickly ran into a problem when the string literal was split between two lines. At that point I realized my means of checking if it was in an X() call was poor, because I'd have to keep adding .Parent to the chain. While I could write something to crawl backwards up the tree, that didn't seem like it was the right way (and probably wouldn't perform very well).
I've been trying to find a way, given a string literal syntax node, to determine if that is an argument in a method call. I haven't been able to find a decent way of going from the Syntax Tree to the Semantic Model to find what I'm looking for. I'm not even sure if that's the right approach, or if I'm missing something obvious.
I was able to use SymbolFinder.FindSymbolAtPositionAsync to get to the symbol, but that suffers from the same issue - I can't just pass the position of the string literal argument, I have to pass the position of the (correct) parent and I'm back where I started.
I'm hoping to avoid having to loop through the syntax tree multiple times, because that slows things way down. I can parse 553 files in about 1 second, but as soon as I try to loop to account for these multi-line situations, I'm up to about 12-13 seconds.
Just in case I've lost you in this novella (sorry), here's what I'm hoping to figure out: for a string literal being passed as an argument to a method, is there an easy way to determine what that method is?
Here is some example code - I've replaced calls to my X() function with Convert.ToString just to simulate the code I'm searching (I had to add references to one of our DLLs, so I switched calls to Convert.ToString() so I could just reference mscorlib for this example.
static void TestAttempt()
{
string source = @"
Imports System
Namespace Exceptions
Public NotInheritable Class ExampleException
Inherits Validation
Public Sub New()
Convert.ToString(""Ignore me 1"")
Console.WriteLine(""Report me"")
Console.WriteLine(Convert.ToString(""Ignore me 2""))
MyBase.New(Convert.ToString(""Ignore me 3, "" & _
""Because I'm already translated.""))
End Sub
End Class
End Namespace";
var tree = VisualBasicSyntaxTree.ParseText(source);
var syntaxRoot = tree.GetRoot();
int i = 0;
foreach (var s in syntaxRoot.DescendantNodes().OfType<LiteralExpressionSyntax>())
{
// things to skip:
if (s == null) { continue; }
if (s.Kind() != SyntaxKind.StringLiteralExpression) { continue; }
var Mscorlib = PortableExecutableReference.CreateFromFile(@"C:\Windows\Microsoft.NET\Framework\v4.0.30319\mscorlib.dll");
var compilation = VisualBasicCompilation.Create("MyCompilation", syntaxTrees: new[] { tree }, references: new[] { Mscorlib });
var model = compilation.GetSemanticModel(tree);
var symbol = SymbolFinder.FindSymbolAtPositionAsync(model, s.Parent.Parent.Parent.Span.Start, new AdhocWorkspace()).Result;
if (symbol.ToDisplayString().EndsWith("Convert")) { continue; }
Console.WriteLine(symbol);
Console.WriteLine($" Reported: {s.ToString()}");
i++;
}
Console.WriteLine();
Console.WriteLine($"Total: {i}");
}
1 Answer
With a warning that I have never used Roslyn before, and I normally code in VB.NET, I think I hacked together something that seems to do what you want (using LINQPad and lots of Dump() calls helped to find out what was going on).
void TestAttempt()
{
string source = @"
Imports System
Namespace Exceptions
Public NotInheritable Class ExampleException
Inherits Validation
Public Sub New()
X(""Ignore me 1"")
Console.WriteLine(""Report me"")
Console.WriteLine(X(""Ignore me 2""))
MyBase.New(X(""Ignore me 3, "" & _
""Because I'm already translated.""))
End Sub
End Class
End Namespace";
var tree = VisualBasicSyntaxTree.ParseText(source);
var syntaxRoot = tree.GetRoot();
int i = 0, notWrapped = 0;
foreach (var s in syntaxRoot.DescendantNodes().OfType<LiteralExpressionSyntax>())
{
// things to skip:
if (s == null) { continue; }
if (s.Kind() != SyntaxKind.StringLiteralExpression) { continue; }
if (!IsWrappedInCallToX(s))
{
Console.WriteLine($" Reported: {s.ToString()}");
notWrapped++;
}
i++;
}
Console.WriteLine();
Console.WriteLine($"Total: {i}, Not Wrapped In X: {notWrapped}");
}
bool IsWrappedInCallToX(SyntaxNode node)
{
var invocation = node as InvocationExpressionSyntax;
if (invocation != null)
{
var exp = invocation.Expression as IdentifierNameSyntax;
if (exp != null && exp.ToString() == "X")
{
return true;
}
}
if (node.Parent != null)
{
return IsWrappedInCallToX(node.Parent);
}
return false;
}
This results in:
Reported: "Report me" Total: 5, Not Wrapped In X: 1
The IsWrappedInCallToX function just recurses up the tree looking for an InvocationExpressionSyntax for the X function. I know you said "While I could write something to crawl backwards up the tree, that didn't seem like it was the right way (and probably wouldn't perform very well)", but to me it seems like this is the right way - if the performance is horrible on your code base, maybe not!
Again, I know nothing about Roslyn (this just sounded interesting), so this is very likely a terrible solution! :-)