Find all method calls for a specific method using Roslyn - c#

I am working on a code analyser using Roslyn and my current task is to find all internal methods which are unused in the assembly.
I start with a MethodDeclarationSyntax and get the symbol from that. I then use the FindCallersAsync method in SymbolFinder, but it returns an empty collection even when I am making a call to the method in question somewhere in the assembly. See the code below.
protected override void Analyze(SyntaxNodeAnalysisContext context)
{
NodeToAnalyze = context.Node;
var methodDeclaration = NodeToAnalyze as MethodDeclarationSyntax;
if (methodDeclaration == null)
return;
var methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodDeclaration) as ISymbol;
if (methodSymbol.DeclaredAccessibility != Accessibility.Internal)
return;
var solutionPath = GetSolutionPath();
var msWorkspace = MSBuildWorkspace.Create();
var solution = msWorkspace.OpenSolutionAsync(solutionPath).Result;
var callers = SymbolFinder.FindCallersAsync(symbol, solution).Result; // Returns empty collection.
...
}
I have seen similar code here, but in that example the method symbol is obtained using GetSymbolInfo on an InvocationExpressionSyntax:
//Get the syntax node for the first invocation to M()
var methodInvocation = doc.GetSyntaxRootAsync().Result.DescendantNodes().OfType<InvocationExpressionSyntax>().First();
var methodSymbol = model.GetSymbolInfo(methodInvocation).Symbol;
//Finds all references to M()
var referencesToM = SymbolFinder.FindReferencesAsync(methodSymbol, doc.Project.Solution).Result;
However, in my case, I need to find the invocations (if any) from a declaration. If I do get the invocation first and pass in the symbol from GetSymbolInfo the calls to the method are returned correctly - so the issue seems to be with the symbol parameter and not solution.
Since I am trying to get the underlying symbol of a declaration, I cannot use GetSymbolInfo, but use GetDeclaredSymbol instead (as suggested here).
My understanding from this article is that the symbols returned from GetDeclaredSymbol and GetSymbolInfo should be the same. However, a simple comparison using Equals returns false.
Does anyone have any idea of what the difference is between the two symbols returned and how I can get the 'correct' one which works? Or perhaps there is a better approach entirely? All my research seems to point to FindCallersAsync, but I just can't get it to work.

My understanding from this article is that the symbols returned from GetDeclaredSymbol and GetSymbolInfo should be the same. However, a simple comparison using Equals returns false.
This is because they're not the same symbol; they are coming from entirely different compilations which might or might not be different. One is coming from the compiler that is actively compiling, one is coming from MSBuildWorkspace.
Fundamentally, using MSBuildWorkspace in an analyzer is unsupported. Completely. Don't do that. Not only would that be really slow, but it also has various correctness issues, especially if you're running your analyzer in Visual Studio. If your goal is to find unused methods anywhere in a solution, that's something we don't really support implementing as an analyzer either, since that involves cross-project analysis.

Related

How to include keywords and aliases in Roslyn recommended symbols?

I am using Roslyn to create a C# scripting control with IntelliSense.
I am generally very happy with the results I am getting, however, the recommended symbols don't include keywords such as for and if et cetera and also don't contain type aliases such as int, when it includes Int32.
More specifically, I am using Microsoft.CodeAnalysis.Recommendations, that is:
Recommender.GetRecommendedSymbolsAtPositionAsync(mySemanticModel, scriptPosition, myAdhocWorkspace);
My SemanticModel object is obtained from a C# compilation which always has a reference to mscorlib.dll at the very least.
At all positions in my script, the recommended completions are always correct. However, I would argue that they are incomplete if they are missing keywords such as if, else and for etc.
I can see that it would be easy for me to include common type aliases in my IntelliSense manually. That is, if Int32 is a possible completion, then I could manually add int.
However, it is less obvious when an if statement or a for statement or even is/as would be appropriate in the given scope.
Is there a way to include these keywords when getting the recommended symbols this way?
Is there also a way to automatically include type aliases?
It seems that Recommender.GetRecommendedSymbolsAtPositionAsync provides only symbols completion. That mean, Methods, Types etc (ISymbol implementations).
If you want keywords or snippets completion, you can use Microsoft.CodeAnalysis.Completion.CompletionService
void CompletionExample()
{
var code = #"using System;
namespace NewConsoleApp
{
class NewClass
{
void Method()
{
fo // I want to get 'for' completion for this
}
}
}";
var completionIndex = code.LastIndexOf("fo") + 2;
// Assume you have a method that create a workspace for you
var workspace = CreateWorkspace("newSln", "newProj", code);
var doc = workspace.CurrentSolution.Projects.First().Documents.First();
var service = CompletionService.GetService(doc);
var completionItems = service.GetCompletionsAsync(doc, completionIndex).Result.Items;
foreach (var result in completionItems)
{
Console.WriteLine(result.DisplayText);
Console.WriteLine(string.Join(",", result.Tags));
Console.WriteLine();
}
}
You can play around to figure it out how to customize it for your needs (rules, filters).
Notice that each result comes from a specific completion provider (item.Properties["Provider"]) and you can create a custom CompletionProvider (at least you should be able).
You can also take a look at C# for VS code (that powered with OmniSharp) to see how they did the work.

How do I create syntax nodes in Roslyn from scratch?

I would like to generate syntax nodes with the Roslyn API without having a pre-existing syntax node. That is, I cannot simply use the WithXYZ() methods on an existing object to modify it because there is no existing object.
For example, I would like to generate an InvocationExpressionSyntax object. Assuming a constructor was available, I could do something like
var invoke = new InvocationExpressionSyntax(expression, arguments);
But the constructor for InvocationExpressionSyntax seems to not be public.
http://www.philjhale.com/2012/10/getting-started-with-roslyn.html
this blog suggests that I can use an API such as
Syntax.InvocationExpression()
but I don't see what Syntax refers to, and I don't see anything that resembles it in the Roslyn API.
I did find Microsoft.CodeAnalysis.VisualBasic.SyntaxFactory that lets me do
var invoke = SyntaxFactory.InvocationExpression().WithExpression(expression);
And this works well enough for me. There is also Microsoft.CodeAnalysis.CSharp.SyntaxFactory for anyone wondering.
Is SyntaxFactory the proper way to create new syntax nodes?
The way I found SyntaxFactory.InvocationExpression was by looking at the PublicAPI.txt file in the roslyn source code (https://github.com/dotnet/roslyn) under the src/Compilers/VisualBasic/Portable directory. Otherwise, I don't see where SyntaxFactory is documented.
As the other answer stated, the SyntaxFactory is the correct class to use. As you have found there are two syntax factories available, Microsoft.CodeAnalysis.CSharp.SyntaxFactory and Microsoft.CodeAnalysis.VisualBasic.SyntaxFactory, depending on which language you are using.
Usually the calls into the SyntaxFactory are chained together, so you end up with many calls to the SytnaxFactory methods to generate even simple lines of code. For example, the code Console.WriteLine("A"); would be represented by the following calls to the Syntax Factory:
var console = SyntaxFactory.IdentifierName("Console");
var writeline = SyntaxFactory.IdentifierName("WriteLine");
var memberaccess = SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, console, writeline);
var argument = SyntaxFactory.Argument(SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal("A")));
var argumentList = SyntaxFactory.SeparatedList(new[] { argument });
var writeLineCall =
SyntaxFactory.ExpressionStatement(
SyntaxFactory.InvocationExpression(memberaccess,
SyntaxFactory.ArgumentList(argumentList)));
If you are unsure of how to generate nodes for some specific code, Kirill Osenkov created the Roslyn Quoter project on GitHub, which you can use to generate the SyntaxFactory code for you.
I recently did a blog post on this topic if you would like to read further.
Yes, the SyntaxFactory type is the way to create syntax nodes from scratch.

Obtain case label constant in Roslyn

I'm trying to gather the switch section label constants from a SwitchStatement with Roslyn. But while I can see in the Syntax Visualizer that the CaseSwitchLabelSyntax has a Value property with the corresponding constant and the declared symbol (SourceLabelSymbol) has a SwitchCaseLabelConstant property, I cannot seem to get that information from what I have in my code.
// SwitchStatementSyntax node;
// SemanticModel model;
foreach (var section in node.Sections) {
foreach (var label in section.Labels) {
var labelSymbol = model.GetDeclaredSymbol(label);
// Here I'm stuck
}
}
I could probably look whether the SwitchLabelSyntax is a CaseSwitchLabelSyntax or a DefaultSwitchLabelSyntax and cast accordingly. SourceLabelSymbol is actually internal, so I cannot access its properties. model.GetConstantValue(label) returns null.
But given that Roslyn always hands out interfaces I believe that there's a reason for that and wildly casting around feels a bit hacky to me. Is there a better option?
Note: I'm doing this to translate C# syntax into another language. Technically, first into a separate AST that is then converted to text again. Above code is from within a CSharpSyntaxWalker and I could probably just store my partially converted switch statement away, continue visiting its descendants and build it up piecewise.
But that means having more state, building statements in half a dozen distinct locations which leads to hard-to-read and -follow code. I'd rather avoid it here, if possible.
Closest from API is semanticModel.GetConstantValue method, but still you need to pass Value node to it like this:
section.Labels
.OfType<CaseSwitchLabelSyntax>()
.Select(l => semanticModel.GetConstantValue(l.Value))
.ToArray()
As you can see filtering out CaseSwitchLabelSyntax is required anyway.

Get Symbol for ReferenceLocation

I'm using the SymbolFinder to find all references to a certain type in my solution like this:
ISymbol typeOfInterest = compilation.GetTypeByMetadataName(
"System.Reflection.PropertyInfo");
var references = SymbolFinder.FindReferencesAsync(typeOfInterest, solution).Result;
foreach (var reference in references)
{
// reference.Locations => symbol?
}
This part is working fine, the SymbolFinder returns correct ReferenceLocations (upon manual inspection). I'm actually interested in the symbols at these locations to get more (semantic) information about the references, so I can filter upon / work with it (e.g. only work on properties).
There seems to be very little public information on Roslyn yet and I couldn't find anything working with the results of SymbolFinder in the samples of the SDK Preview. So here is my question: Is it possible to get the symbol corresponding to a ReferenceLocation? How?
So, there isn't strictly a "symbol" at any of these locations, at least no innate concept of that. What you can do is take that Location, and find the enclosing symbol. You can take the location's SyntaxTree and get a Document. From there, call GetSemanticModelAsync, and then call ISemanticModel.GetEnclosingSymbol.
As an example, here's some (internal) code that does this for FAR itself: https://github.com/dotnet/roslyn/blob/748d6ab1b504ceee0c29f132fdcbe2a777aa88ea/src/Workspaces/Core/Portable/FindSymbols/ReferenceLocationExtensions.cs#L67-L101

How can I find a variable in a method body by type using mono.cecil?

I've had a look around the cecil questions here and I haven't seen anything regarding this particular question.
What I am trying to achieve is find a variable in method.Body.Variables that is of a certain type (System.Exception in my case)
I wrote the following code thinking it would do the trick:
var exceptionTypeReference = module.Import(typeof(Exception));
var exceptionVariable = method.Body.Variables.First(x => x.VariableType == exceptionTypeReference);
What seems strange to me even though I am sure the reason is my noobness with cecil is that I get a "Sequence contains no matching elements" error at runtime.
I've stepped through the code and I know there is a variable there and that it's type is System.Exception, but it does not want to match up with exceptionTypeReference.
I'm certain that this is simple and that my brain is fried from learning cecil. Even so, any pointers, smacks across the face with a wet fish, etc., would be much appreciated.
Each time you import a type it is a different instance of TypeReference
So this
var typeReference1 = moduleDefinition.Import(typeof (Exception));
var typeReference2 = moduleDefinition.Import(typeof (Exception));
Debug.WriteLine(typeReference1 == typeReference2);
Will output false.
So when you are doing the query
VariableType may be an instance of TypeReference representing Exception
exceptionTypeReference will be an instance of TypeReference representing Exception
But they are not the same reference and there is no built in equality checking on TypeReference.
What you need to do is
var exceptionType = module.Import(typeof(Exception));
var exceptionVariable = method
.Body
.Variables
.First(x => x.VariableType.FullName == exceptionType.FullName);
Also remember you have to handle inherited exception types.
As a side not be careful using .Import(typeof (Exception)). The reason is that it give you the Exception type of the current code, not the Exception type of the target assembly. For example you could be processing a WinRT assembly using a .net4 assembly. Importing the .net4 Exception type will probably give you some strange behavior.
So you are just as safe doing this
var exceptionVariable = method
.Body
.Variables
.First(x => x.VariableType.FullName == "System.Exception");

Categories