Roslyn Services API makes it easy to implement extensions that find and fix problems in the code right in Visual Studio. Roslyn Services API is available as part of
Roslyn CTP .
In this post, we will implement an extension for Visual Studio that detects calls to the Enumerable method of Count (), after which the result is checked for equality greater than zero, for example, someSequence.Count ()> 0. The problem with the code is that Count () must go through the entire sequence before returning the result. The more correct approach in this case is to call the Enumerable.Any () method.
To fix this, we will implement CodeIssueProvider, which detects the problem, and CodeAction, which replaces the condition with the call to Enumerable.Any (), as required. Those. our CodeAction will change something like someSequence.Count ()> 0 to someSequence.Any ().
There are a couple more additional conditions that I would also like to perform: first of all, the expression can be reversed and written as 0 <someSequence.Count (). The following case is an entry of type> = 1 instead of> 0, which is logically the same as before. We need the extension to work in both cases.
')
Obviously, we would not want to change not all calls with the Count () signature, but only if they refer to the extension method from IEnumerable defined in Enumerable.
Beginning of work
Roslyn CTP comes with a set of templates to make it easy to get started with your API. To begin with, we will create a new project of the Code Issue type from Roslyn templates. Let's name the project as ReplaceCountWithAny.

The template generates a simple working provider that highlights words containing the letter “a”. To see an example in action, build and run the project created by the template. This launches a new instance of Visual Studio with the extension included. From the Visual Studio just launched, create a console application and see it as the keywords namespace, class, etc. underlined by our extension.

Although the example is not as useful as an extension for Visual Studio, it prepares everything you need to start implementing your own extension. We only have to replace the contents of the generated method GetIssue. Note that there are three overloads for getIssues. We will work with overload, where one of the parameters is of type CommonSyntaxNode. The remaining two overloads can be left in our case.
The generated CodeIssueProvider class implements the ICodeIssueProvider interface and is decorated with the ExportSyntaxNodeCodeIssueProvide attribute. This allows Visual Studio to import this type as an extension containing the contract provided by the ICodeIssueProvide interface.
Implement GetIssues
Our GetIssues method will be called for each syntax, so the first thing we need to do is sift out nodes that are not interesting to us. Since we need constructions of type someSequence.Count ()> 0, we need only nodes of type BinaryExpressionSyntax. We can tell Visual Studio to use our provider only for specific nodes, providing a list of types through the ExportSyntaxNodeCodeIssueProvide attribute. So, update the attribute as shown below:
[ExportSyntaxNodeCodeIssueProvider("ReplaceCountWithAny", LanguageNames.CSharp, typeof(BinaryExpressionSyntax))] class CodeIssueProvider : ICodeIssueProvider ...
This allows you to safely cast the CommonSyntaxNode node to the BinaryExpressionSyntax type in the GetIssues method.
To select the cases that we want to handle, it is necessary to check a part of the expression for the presence of a call to Enumerable.Count (), and another for the comparison itself. We will allocate these checks to helper methods, so our implementation of GetIssues will look like this:
public IEnumerable<CodeIssue> GetIssues(IDocument document, CommonSyntaxNode node, CancellationToken cancellationToken) { var binaryExpression = (BinaryExpressionSyntax)node; var left = binaryExpression.Left; var right = binaryExpression.Right; var kind = binaryExpression.Kind; if (IsCallToEnumerableCount(document, left, cancellationToken) && IsRelevantRightSideComparison(document, right, kind, cancellationToken) || IsCallToEnumerableCount(document, right, cancellationToken) && IsRelevantLeftSideComparison(document, left, kind, cancellationToken)) { yield return new CodeIssue(CodeIssue.Severity.Info, binaryExpression.Span, string.Format("Change {0} to use Any() instead of Count() to avoid " + "possible enumeration of entire sequence.", binaryExpression)); } }
The instance of the CodeIssue class that we return indicates the level of the problem, which may be Error, Warning or Info, the description used to highlight a piece of code, and the text that describes the problem to the user.
Auxiliary methods
Now let's move our attention to the helper methods used in GetIssues. The IsCallToEnumerableCount method returns true if the part of the expression we are considering is a call to the Count () method on some sequence. Let me remind you once again: we start first with filtering unnecessary expressions.
First of all, the expression must be a method call. In this case, we get the necessary call from the property of the expression. So, if the construction looks like someSequence.Count ()> 0, then we will have a part with Count (); but how to check it belongs to the type Enumerable?
To answer such questions, you need to request a semantic model. Fortunately, one of the parameters of the GetIssues method is an IDocument, which is a document in the project and solution. We can get a semantic model through it, and already from it the SymbolInfo itself that we need.
Using SymbolInfo, you can check if our method call belongs to the desired [Enumerable.Count ()]. Since Count () is an extension method, working with it will be slightly different. Recall that C # allows extension methods to be invoked as part of a type. The semantic model provides this information through the ConstructedFrom property of the MethodSymbol class with reference to the original type. The ability to make it a little easier is there, so watch out for the API names.
All we have to do is specify the type of extension method. If it matches Enumerable, then we have found the call to Enumerable.Count ().
The implementation looks like this:
private bool IsCallToEnumerableCount(IDocument document, ExpressionSyntax expression, CancellationToken cancellationToken) { var invocation = expression as InvocationExpressionSyntax; if (invocation == null) { return false; } var call = invocation.Expression as MemberAccessExpressionSyntax; if (call == null) { return false; } var semanticModel = document.GetSemanticModel(cancellationToken); var methodSymbol = semanticModel.GetSemanticInfo(call, cancellationToken).Symbol as MethodSymbol; if (methodSymbol == null || methodSymbol.Name != "Count" || methodSymbol.ConstructedFrom == null) { return false; } var enumerable = semanticModel.Compilation.GetTypeByMetadataName( typeof(Enumerable).FullName); if (enumerable == null || !methodSymbol.ConstructedFrom.ContainingType.Equals(enumerable)) { return false; } return true; }
Before moving forward, it is also necessary to check the expression for correctness of the comparison in another part of the binary expression; and this is the work for the helper methods IsRelevantRightSideComparison and IsRelevantLeftSideComparison.
Below are their implementations:
private bool IsRelevantRightSideComparison(IDocument document, ExpressionSyntax expression, SyntaxKind kind, CancellationToken cancellationToken) { var semanticInfo = document.GetSemanticModel(cancellationToken). GetSemanticInfo(expression); int? value; if (!semanticInfo.IsCompileTimeConstant || (value = semanticInfo.ConstantValue as int?) == null) { return false; } if (kind == SyntaxKind.GreaterThanExpression && value == 0 || kind == SyntaxKind.GreaterThanOrEqualExpression && value == 1) { return true; } return false; } private bool IsRelevantLeftSideComparison(IDocument document, ExpressionSyntax expression, SyntaxKind kind, CancellationToken cancellationToken) { var semanticInfo = document.GetSemanticModel(cancellationToken). GetSemanticInfo(expression); int? value; if (!semanticInfo.IsCompileTimeConstant || (value = semanticInfo.ConstantValue as int?) == null) { return false; } if (kind == SyntaxKind.LessThanExpression && value == 0 || kind == SyntaxKind.LessThanOrEqualExpression && value == 1) { return true; } return false; }
Yes, they are almost identical with the only difference that both comparison options are checked, as well as the correctness of the value itself, so we don’t have to highlight something like Count ()> = 0.
Testing CodeIssueProvider
At the moment, our provider is able to detect problems of interest to us. Compile and run the project along with a new instance of Visual Studio along with the extension included. Add code and note that calls to Enumerable.Count () are underlined correctly, while calls to other methods with a signature of Count () are not.

The next step is to provide an action to solve the problem.
CodeAction
To implement the action, we need a class that implements the ICodeAction interface. ICodeAction is a simple interface that defines the description and icon for the action, as well as the only GetEdit method that returns an edit that transforms the current syntax tree. So let's start with the constructor of our CodeAction class.
public CodeAction(ICodeActionEditFactory editFactory, IDocument document, BinaryExpressionSyntax binaryExpression) { this.editFactory = editFactory; this.document = document; this.binaryExpression = binaryExpression; }
For each problem found, a new copy of the CodeAction class will be created, so for convenience we will omit some parameters and change the constructor itself. For this, an ICodeActionEditFactory implementation is needed to create the transformation of the newly created syntax tree. Since the syntactic trees in the Roslyn project are immutable, returning a new tree is the only way to make any changes. Fortunately, Roslyn tries to reuse the tree as much as possible, thus preventing the creation of unnecessary syntax nodes.
In addition, a document is required that gives our code access to the syntax tree, project and solution, as well as a link to the syntax node that we want to replace.
So, we are closer to the GetEdit method. It is here that we create a transformation that will replace the detected binary expression with a new one with a call to the Any () method. Creating a new node is assigned to a simple helper method GetNewNode. The implementation of both methods is given below:
public ICodeActionEdit GetEdit(CancellationToken cancellationToken) { var syntaxTree = (SyntaxTree)document.GetSyntaxTree(cancellationToken); var newExpression = GetNewNode(binaryExpression). WithLeadingTrivia(binaryExpression.GetLeadingTrivia()). WithTrailingTrivia(binaryExpression.GetTrailingTrivia()); var newRoot = syntaxTree.Root.ReplaceNode(binaryExpression, newExpression); return editFactory.CreateTreeTransformEdit( document.Project.Solution, syntaxTree, newRoot, cancellationToken: cancellationToken); } private ExpressionSyntax GetNewNode(BinaryExpressionSyntax node) { var invocation = node.DescendentNodes(). OfType<InvocationExpressionSyntax>().Single(); var caller = invocation.DescendentNodes(). OfType<MemberAccessExpressionSyntax>().Single(); return invocation.Update( caller.Update(caller.Expression, caller.OperatorToken, Syntax.IdentifierName("Any")), invocation.ArgumentList); }
Roslyn syntax tree completely coincides with the original code, so each node in the tree can contain extra spaces and comments. Those. we keep the original node along with comments and the structure of the code when the nodes themselves change. To do this, we call the WithLeadingTrivia and WithTrailingTrivia extension methods.
Also note that the GetNewNode method saves the parameter list of the Count () method, so if the extension method was called via a lambda expression to count specific elements in the sequence, it will still be replaced by Any ().
Let's sum up
To enable our action, update the GetIssues method in the CodeIssueProvider class to return a CodeAction instance for each CodeIssue. Each problem area of the code can have several actions, allowing the user to choose between them. In this case, we return one single action as shown below.
The updated part of the GetIssues method looks like this:
yield return new CodeIssue(CodeIssue.Severity.Info, binaryExpression.Span, string.Format("Change {0} to use Any() instead of Count() to avoid " + "possible enumeration of entire sequence.", binaryExpression), new CodeAction(editFactory, document, binaryExpression));
Rebuild and run the project to launch a new instance of Visual Studio with the extension loaded. Now we see that the problem section provides a drop-down list with options for correcting the code.

Thus, we have implemented an extension for Visual Studio that will help improve our code.