Live Code Analysis with Visual Basic 2015 and the .NET Compiler Platform
Introduction
In Part 1 of this series, you saw how Visual Studio 2015 introduces a new way of highlighting and fixing code issues based on the .NET Compiler Platform. The code editor offers a new experience for live code analysis as you type, and you can easily fix code issues or simply refactor your code by taking advantage of the Light Bulb and Quick Actions. You might recall that the code editor leverages a built-in set of code analyzers, each responsible for a given set of coding rules. The very good news is that you can write your own analyzers with the .NET Compiler Platform APIs, which can integrate into the code editor, also offering Quick Actions from the Light Bulb. This article provides a quick introduction to the .NET Compiler Platform and shows how to create a custom code analyzer that will work inside the code editor.
A Look at the .NET Compiler Platform
In .NET 2015, the open source Visual Basic and C# compilers expose the so-called analysis APIs, which can be consumed by developer tools, including Visual Studio 2015 itself. With the .NET Compiler Platform, developer tools can analyze the syntax tree and semantics of a single code file, a project, or an entire solution. This includes custom analyzers and code refactorings that integrate with the Light Bulb and that offer custom Quick Actions. This capability enables you to write an infinite number of domain-specific live analysis tools that you can easily share with other developers as either NuGet packages or Visual Studio extensions, using just a few lines of code and limited effort.
Of course, the .NET Compiler Platform is not limited to analyzers and code refactorings. It also provides the basis for many features in the Visual Studio 2015 IDE, including code editor features, expression evaluators, language services, visualizers, and much more.
The .NET Compiler Platform consists of a number of layers. This article focuses on the Compiler APIs and the Diagnostic APIs layers of the platform:
- The Compiler APIs layer represents the object model related to syntactic and semantic information exposed at each phase of the compiler pipeline. It also includes an immutable snapshot of a single invocation of a compiler; this immutable snapshot includes assembly references, source code files, and compiler options. This layer includes the Diagnostic APIs, which you actually work with in this article.
- The Diagnostic APIs are extensible APIs that allow compilers to analyze everything in your code, from syntax to semantics, to produce errors, warnings, squiggles, and related diagnostics for each code issue. This extensible set of APIs integrates naturally with tools such as MSBuild and Visual Studio 2015.
In order to build tools on the .NET Compiler Platform, you need the following additional software:
- Visual Studio 2015 SDK
- .NET Compiler Platform Syntax Visualizer, a tool that makes it easy to investigate syntax elements.
.NET Compiler Platform SDK Templates, which enable specific project templates in Visual Studio 2015 to build analyzers and code refactorings.
After you have installed these tools, the New Project dialog in Visual Studio 2015 contains three new project templates in the Extensibility node, as shown in Figure 1. The Stand-Alone Code Analysis Tool template allows for creating a code-analysis command-line application, and is not covered in this article series. The Analyzer with Code Fix template and the Code Refactoring template allow for creating a code analyzer and code refactoring, respectively. This article covers the Analyzer with Code Fix template, and Part 3 of this series will cover the Code Refactoring template.
Figure 1 New project templates for the .NET Compiler Platform.
Understanding Syntax
Before practicing with the APIs, let’s get an overview of how the platform is organized. The Compiler APIs layer exposes the lexical and syntactic structure of source code through syntax trees. These syntax trees are very important because they allow tools such as Visual Studio to process the syntactic structure of the source code in a project, and because they allow for rearranging the code in a managed way without working against pure text. The syntax trees serve as the base for compilation, code analysis, refactoring, and code generation, and they constitute the main entry point to identify and categorize the many structural elements of a language. Syntax trees represent everything the compiler finds in the source code, including lexical constructs, white spaces, comments, syntax tokens, and so on, exactly as they were typed. As an implication, the syntax tree can be round-tripped back to the text from which it was parsed, which means that you can use syntax trees to create the equivalent text.
Syntax trees are important for another reason: You can edit and replace a syntax tree with a new tree, without editing the actual text. Syntax trees have an important characteristic: They are immutable. After you get an instance of a syntax tree, it never changes; if you want to edit a syntax tree, you actually create a new one based on the existing tree. This design provides thread-safety and allows multiple developers to interact with the same syntax tree without locking or duplication problems. Immutability is a key concept in the .NET Compiler Platform; you will get more details on this topic when we create custom code analyzers later in this article.
Syntax trees are made of the following elements:
- Syntax nodes are primary elements in the syntax trees. They represent constructs such as statements, declarations, clauses, and expressions. Each syntax node has a corresponding class that derives from Microsoft.CodeAnalysis.SyntaxNode.
- Syntax tokens include keywords, identifiers, literals, and punctuation; they are never parents of other syntax nodes or syntax tokens. A syntax token is represented by an instance of the SyntaxToken structure, which exposes the Value property of type Object, which returns the raw value of the object it represents. By contrast, the ValueText property of type String returns the result of invoking ToString() on Value.
- Syntax trivia represent those portions in the source text that are typically insignificant to understanding the code, such as white spaces, end-of-line terminators, preprocessing directives, and comments (including XML comments). They are not included in the syntax tree as a child node, but they actually exist in the syntax tree to maintain full fidelity with the source text. In fact, when you generate a new syntax tree from an existing one, you must include trivia, represented by the SyntaxTrivia structure.
- Spans represent the position in the source text for syntax nodes, syntax tokens, and syntax trivia, and the number of characters they contain. Spans are represented by the TextSpan object. Detecting the code position with spans is very important to understand where error/warning squiggles must be placed in the code when writing analyzers.
- Kinds identify the exact syntax element you are using. This is expressed via the Kind extension method, of type Integer, which each node, token, or trivia exposes. Visual Basic has the SyntaxKind enumeration with integer values that represents all possible nodes, tokens, and trivia. With Kind, it’s easier to disambiguate syntax node types that share the same node class.
Syntax trees represent the syntactic structure of the source code, but a program is also made of semantics, which is the way in which the language rules are applied to produce meaningful results. To support semantics, the .NET Compiler Platform includes the Compilation, Symbol, and SemanticModel types, all from the Microsoft.CodeAnalysis namespace:
- The Compilation type represents everything needed to compile a Visual Basic program, such as source code files, assembly references, and compiler options. Compilation also represents each declared type, member, or variable as a symbol. A compilation is also immutable, which means that you can create a new compilation based on an existing one, supplying the required changes.
- The SemanticModel type represents the semantic information that the compiler has about the source code, including symbols, types, and so on.
- The Symbol type represents every namespace, type, type member, parameter, or local variable, both from the source text and from an external assembly. The Compilation type offers a number of methods and properties to find symbols. In turn, the symbols offer a common representation of namespaces, types, and members via objects that derive from the ISymbol interface, each with methods and properties tailored for a given symbol type. For instance, method symbols are of type IMethodSymbol.
Investigating Syntax with the Syntax Visualizer
The best way to get started with syntax trees and their syntax elements is by using a visual tool, the Syntax Visualizer, which makes it easier to familiarize yourself with the .NET Compiler Platform. To get started, create a new Console project with Visual Basic; then select View > Other Windows > Roslyn Syntax Visualizer. This tool window is made of two areas:
The upper area, called Syntax Tree (see Figure 2), provides a hierarchical view of the syntax tree for the current code file. The view is updated as you type or click anywhere in the code.
Figure 2 Using the Syntax Visualizer.
Every element has a different color according to its meaning. You can click the Legend button to get information. Basically, the blue color represents a syntax node; green represents a syntax token; maroon represents trivia (either leading or trailing); pink highlights represent code that has diagnostics (such as errors or warnings.)
The lower area of the Syntax Visualizer is called Properties; it shows properties for the selected item in the syntax tree. Figure 3 shows an example.
Figure 3 Properties in the Syntax Visualizer.
In the Properties area, you can learn the type and kind of each element in the tree, such as syntax nodes, syntax tokens, and syntax trivia. In addition, the Syntax Visualizer provides a way to get a visual representation of symbols (if any) in your code; just right-click an element in the syntax tree and select View Symbol. Symbol information is then presented in the Properties area of the tool window. We’ll work with the Syntax Visualizer shortly. As you will use the Syntax Visualizer often, take some time to browse this feature to see how your code is represented, including when you write new code. This is something you will do frequently when working with the .NET Compiler Platform, especially when you first begin using it.
Now that you have a basic knowledge of the core elements in the .NET Compiler Platform, it’s time to start writing code and learning how to build a code analyzer that integrates into the Light Bulb, offering Quick Actions.
Building Analyzers
Now that you’ve had a short overview of the .NET Compiler Platform, it’s time to get your hands dirty on some code. The goal of this article is explaining how to create an analyzer that checks whether developers are using a generic List(Of T) collection for data-binding over platforms, where instead they should use an ObservableCollection(Of T). For instance, in XAML-based development platforms such as WPF and Windows Store apps, using ObservableCollection(Of T) is the recommended choice because it supports change notification on data-binding. So the goal of your new analyzer will be detecting whether the code instantiates a List(Of T), squiggling the declaration with a green warning, and suggesting a fix to replace that object with an ObservableCollection(Of T).
Create a new project of type Analyzer with Code Fix (refer to Figure 1, if necessary), and name it ObservableCollectionAnalyzer. The selected template provides an analyzer skeleton whose instructional purpose is to detect and fix lowercase letters in type names.
It’s left to you as an exercise to explore the auto-generated code, because in this section you start the analysis from scratch. The first step is exploring the solution with the help of Solution Explorer. The solution contains three projects:
The first project is a portable class library that implements the analyzer, in this case called ObservableCollectionAnalyzer (Portable). This project contains two code files: DiagnosticAnalyzer.vb, which implements the analysis logic and raises the desired errors or warnings, and CodeFixProvider.vb, which is where you will implement code fixes that will integrate with the Light Bulb.
- The second project is a test project (ObservableCollectionAnalyzer.test) that contains unit tests to test your analyzer in a separate environment. This project is not covered in this article, as it’s not relevant to this discussion.
- The third project is a VSIX package that allows you to deploy your analyzer to Visual Studio with a .Vsix installer. This must always be the startup project, because it will be used to test the analyzer in the experimental instance of Visual Studio 2015.
Before implementing the analysis logic, you must learn which syntax nodes you will analyze. To accomplish this goal, you need the Syntax Visualizer tool discussed earlier. Open another instance of Visual Studio 2015, create a new Console project, and write the Main method as follows:
Sub Main() Dim newList As New List(Of String) End Sub
Select New List(Of String), which the analyzer wants to recognize as a code issue. As Figure 4 indicates, the Syntax Visualizer tool shows that New List(Of String) corresponds to a syntax node of type ObjectCreationExpression.
Figure 4 Detecting object types with the Syntax Visualizer.
The Properties area of the Syntax Visualizer shows that New List(Of String) is represented by an object of type ObjectCreationExpressionSyntax. At this point, you don’t need to investigate the whole syntax tree to discover all the ObjectCreationExpression elements and then iterate each of them to discover whether it’s a declaration of a new List(Of T) instance. The syntax tree provides very fast and easy ways to reach directly to only the elements you actually need.
Implementing the Analysis Logic
Open the DiagnosticAnalyzer.vb code file and consider the following code:
<DiagnosticAnalyzer(LanguageNames.VisualBasic)> Public Class ObservableCollectionAnalyzerAnalyzer Inherits DiagnosticAnalyzer Public Const DiagnosticId = "ObservableCollectionAnalyzer" ' You can change these strings in the Resources.resx file. ' If you do not want your analyzer to be localize-able, ' you can use regular strings for Title and MessageFormat. Friend Shared ReadOnly Title As LocalizableString = New LocalizableResourceString(NameOf(My.Resources.AnalyzerTitle), My.Resources.ResourceManager, GetType(My.Resources.Resources)) Friend Shared ReadOnly MessageFormat As LocalizableString = New LocalizableResourceString(NameOf(My.Resources.AnalyzerMessageFormat), My.Resources.ResourceManager, GetType(My.Resources.Resources)) Friend Shared ReadOnly Description As LocalizableString = New LocalizableResourceString(NameOf(My.Resources.AnalyzerDescription), My.Resources.ResourceManager, GetType(My.Resources.Resources)) Friend Const Category = "Naming" Friend Shared Rule As New DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault:=True) Public Overrides ReadOnly Property SupportedDiagnostics As _ ImmutableArray(Of DiagnosticDescriptor) Get Return ImmutableArray.Create(Rule) End Get End Property Public Overrides Sub Initialize(context As AnalysisContext) ' TODO: Consider registering other actions that act on syntax instead of or in addition to symbols context.RegisterSymbolAction(AddressOf AnalyzeSymbol, SymbolKind.NamedType) End Sub
Every analyzer class must be decorated with the DiagnosticAnalyzer attribute, which specifies the target language. Every analyzer class also inherits from Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer. This abstract class provides the common infrastructure for analyzers, based on the SupportedDiagnostics property and Initialize method. The DiagnosticId, Title, MessageFormat, Description, and Category fields determine the information that the analyzer sends to Visual Studio and that will be shown in the Error List and in the Light Bulb. The default implementation provides support for localization, storing string values in the Resources.resx file for the project, which you access with My Project > Resources. For the sake of simplicity, instead of using localizable strings and resources, the sample code will provide an English-only analyzer with plain strings, so replace the default definitions with the following:
Public Const DiagnosticId = "STA001" Friend Shared Readonly Title As String = "List(Of T) in Windows Store apps" Friend Shared Readonly MessageFormat As String = "Windows apps warning: {0}" Friend Shared ReadOnly Description As String = _ "Detects invalid usages of List(Of T) in a Windows Store app" Friend Const Category = "Syntax"
Thus, it’s possible to replace field values with regular strings. The values of DiagnosticId and MessageFormat are displayed in the Error List window when the code issue is detected. The shared Rule field returns a DiagnosticDescriptor. This type describes an analyzer to Visual Studio via the preceding string values, plus it allows you to specify whether the diagnostics must be enabled by default, as well as the severity of the diagnostics, with a value from the DiagnosticSeverity enumeration. The available values are Warning (the default), Error, Info, and Hidden.
The diagnostic descriptor is exposed to Visual Studio via the SupportedDiagnostics property, which tells Visual Studio how many diagnostics are in the analyzer, and Visual Studio returns an immutable array with one element (ImmutableArray.Create). In this case, the analyzer offers only one diagnostic, so there is nothing to change. But if your analyzer offers multiple diagnostics, you should generate multiple descriptors and return them from SupportedDiagnostics.
The Initialize method is the main entry point of the analyzer. Here you register a set of actions to respond to compiler events, such as finding syntax nodes or declaring a new symbol (as in our case). To register an action, you invoke one of the available methods from the context object, of type Microsoft.CodeAnalysis.Diagnostics.AnalysisContext, which represents the context for initializing an analyzer. Methods you can use have names starting with Register; IntelliSense will help you to understand the purpose of each method. In this case, you need to use RegisterSyntaxNodeAction because you’re working against the ObjectCreationExpressionSyntax type, which inherits from SyntaxNode. The first argument for this method is a delegate that performs the actual analysis and the exact kind of syntax node via the SyntaxKind enumeration. Therefore, rewrite the Initialize method as follows:
Public Overrides Sub Initialize(context As AnalysisContext) context.RegisterSyntaxNodeAction(AddressOf AnalyzeCollection, SyntaxKind.ObjectCreationExpression) End Sub
Determining the correct value for SyntaxKind is very easy: The Properties area of the Syntax Visualizer shows the Kind value that corresponds to the value you have to pick up from the enumeration. Now delete the AnalyzeSymbol method, which you don’t need, and create a new AnalyzeCollection method with the following definition:
Private Sub AnalyzeCollection(context as SyntaxNodeAnalysisContext) End Sub
This is the place where you actually implement the analysis logic. The first thing to do is casting the syntax node into the appropriate node type, as follows:
Dim creationExpression = TryCast(context.Node, ObjectCreationExpressionSyntax) If creationExpression Is Nothing Then Return
The SyntaxNodeAnalysisContext clearly represents the analysis context for a syntax node, and its Node property represents the actual syntax node to be analyzed. The code converts the node type into ObjectCreationExpressionSyntax, which is the type on which the analysis logic is based. If the conversion fails, it’s not a node of the type you want to analyze, so the process ends here.
Now that you have an instance of the syntax node, you need a way to determine that it declares a new List(Of T). With the help of the Syntax Visualizer, you can see the list of properties for the ObjectCreationExpressionSyntax object. A good way to understand what you need is by considering the Type property, which contains the full generic type declaration. So the second test is the following:
If creationExpression.Type.ToString.StartsWith("List(Of") = False Then Return Else End If
The code simply checks whether the string representation for the Type property starts with the desired type name. If not, it’s not a syntax node you want to analyze, so it returns. If the result is True, however, you have to create a new diagnostic with the specified rules, as follows:
If creationExpression.Type.ToString.StartsWith("List(Of") = False Then Return Else Dim diagn = Diagnostic.Create(Rule, creationExpression.GetLocation, "Consider using ObservableCollection instead of List") context.ReportDiagnostic(diagn) End If
The Diagnostic.Create method creates a new diagnostic for the current syntax node, using the rules defined in the Rule property. It also specifies the location for placing the squiggle, which you can retrieve via the GetLocation method of the syntax node instance. Finally, you send the diagnostic information to Visual Studio with the ReportDiagnostic method from the SyntaxNodeAnalysisContext class instance, which is context in this case.
This is enough to detect the code issue. Now press F5 to start the experimental instance of Visual Studio 2015; the analyzer package will be deployed to the experimental instance and will be running inside an isolated environment that does not affect the development environment. When the experimental instance is running, create a new project for either a Windows Store 8.1 app or a Windows Phone 8.1 app with Visual Basic and declare a new List(Of String) object wherever you like. Figure 5 shows how both the code editor and the Error List show a warning based on the analyzer.
Figure 5 The analyzer detects a domain-specific code issue.
Implementing Quick Actions
The analyzer detects and highlights a code issue, but it also needs to suggest potential fixes that will be listed in the Light Bulb. Suggesting potential fixes is optional, but recommended in many cases. This can be implemented in the CodeFixProvider.vb code file. In this file, you can find the definition for the ObservableCollectionAnalyzerCodeFixProvider class, which inherits from Microsoft.CodeAnalysis.CodeFixes.CodeFixProvider. This abstract class provides a common infrastructure for code fixes, made of three methods: GetFixableDiagnosticIds, GetAllFixProvider, and ComputeFixesAsyncRegisterCodeFixesAsync. The first method, GetFixableDiagnosticIds, returns an immutable array with one element, representing the diagnostic ID that is associated to and that can be fixed with the current class. The second method, GetAllFixProvider, defines a way to provide fixes, which you can select from the FixAllProvider class. Following is the relevant code:
Imports Microsoft.CodeAnalysis.CodeGeneration Imports Microsoft.CodeAnalysis.Formatting Imports Microsoft.CodeAnalysis.Rename <ExportCodeFixProvider("ObservableCollectionAnalyzerCodeFixProvider", LanguageNames.VisualBasic), [Shared]> Public Class ObservableCollectionAnalyzerCodeFixProvider Inherits CodeFixProvider Public NotOverridable Overrides Function FixableDiagnosticIds() _ As ImmutableArray(Of String) Return ImmutableArray.Create(ObservableCollectionAnalyzerAnalyzer.DiagnosticId) End Function Public NotOverridable Overrides Function GetFixAllProvider() As FixAllProvider Return WellKnownFixAllProviders.BatchFixer End Function
Notice that the class is decorated with the ExportCodeFixProvider attribute, which basically makes the code fix visible to the IDE. It requires specifying the name and the supported languages. The CodeFixProvider base class also requires implementing a third method, ComputeFixesAsyncRegisterCodeFixesAsync, which is where you implement your actual logic. Delete the auto-generated MakeUppercaseAsync method, which is not required, and replace the ComputeFixesAsyncRegisterCodeFixesAsync definition with the following (see the comments for details):
Public NotOverridable Overrides Async Function _ RegisterCodeFixesAsync(context As CodeFixContext) As Task 'Get the root syntax node for the current document Dim root = Await context.Document. GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(False) 'Get a reference to the diagnostic (warning squiggle) to fix Dim diagnostic = context.Diagnostics.First() 'Get the location for the diagnostic Dim diagnosticSpan = diagnostic.Location.SourceSpan 'Find the syntax node on the squiggle span Dim node = root.FindNode(context.Span) ' Register a code action that will invoke the fix. context.RegisterCodeFix( CodeAction.Create("Replace List(Of T) with ObservableCollection(Of T)", Function(c) ReplaceListAsync(context. Document, node, c)), diagnostic) End Function
The method argument is a structure of type CodeFixContext, which represents the context for code fixes, made of syntax nodes that have diagnostics. The code finds the root syntax node for the document (see the root declaration) and then it gets a reference to the diagnostic to fix. The code then uses the diagnostic.Location.SourceSpan property that returns the location in the source code for the diagnostic. Next, it retrieves the syntax node that you must fix. The last step in the method is registering an action that will be displayed in the Light Bulb and that will invoke the fix. This is accomplished with the CodeFixContent.RegisterFixRegisterCodeFix method; this invokes the Microsoft.CodeAnalysis.CodeActions.CodeAction.Create method, passing the text message to show in the Light Bulb, a delegate that will perform the fix, and the diagnostic instance. For the current example, this delegate is called ReplaceListAsync, and its signature is the following:
Private Async Function ReplaceListAsync(document As Document, node As SyntaxNode, cancellationToken As CancellationToken) _ As Task(Of Document)
This function receives three arguments: the document instance, the syntax node that must be fixed, and a cancellation token. The document instance is required because you will create a new document from the existing one, replacing the old syntax node with the fixed node. In a code fix provider, you typically won’t need to repeat tests you made in the analyzer class, because the code fix provider receives the expected nodes. Let’s start writing the code fix with the following lines:
Dim root = Await document.GetSyntaxRootAsync Dim objectCreationNode = CType(node, ObjectCreationExpressionSyntax)
The Document.GetSyntaxRootAsync method returns an instance of the syntax node for the document. The second line converts the passed syntax node instance into an ObjectCreationExpressionSyntax node, which is where you want to apply the code fix. Understanding the next step requires you to use the Syntax Visualizer. If you expand the ObjectCreationExpression node for the instance of the List(Of String) you wrote previously in a separate project, you will see that the generic type is represented with a GenericName element, mapped with a GenericNameSyntax object, which represents the collection’s name (List); this also has a descendant node called TypeArgumentList, mapped with a TypeArgumentListSyntax object, which represents the generic type parameter for the collection ((Of String)). So your goal is generating a new GenericNameSyntax for ObservableCollection, and retrieving the existing TypeArgumentListSyntax. This is accomplished with the following code:
'Get the GenericNameSyntax Dim nodes = objectCreationNode.DescendantNodes(node.Span) Dim genericNameNode = CType(nodes.First, GenericNameSyntax) Dim newGenericNameNode = SyntaxFactory.GenericName("ObservableCollection", genericNameNode.TypeArgumentList)
Since you saw that the GenericName element is the first descendant node for the ObjectCreationExpression, you can cast the first descendant node into a GenericNameSyntax object. The result is assigned to the genericNameNode variable, which represents an immutable generic name. To create a new generic name, you use the SyntaxFactory.GenericName method. The SyntaxFactory class exposes many method to create new elements in a syntax node, and you will work with it very often. It’s not possible here to summarize all available methods from the class, but you’ll get additional examples in the next subsections, and you can use IntelliSense to discover what each offers.
The SyntaxFactory.GenericName method generates a new GenericName node with a new name, but with the existing type parameters taken from the immutable node’s TypeArgumentList object. Now that you have a new GenericName, you must create a new syntax node for it. Before you do that, consider that the ObservableCollection type requires an Imports System.Collections.ObjectModel directive, and you cannot assume that the user already added it; if you fix the collection issue, you must also ensure that the user doesn’t need additional code fixes. Adding an Imports directive is possible, but it is a little tricky at this point. As an easier alternative, you can edit the new GenericName with its fully qualified name. The SyntaxFactory class has a QualifiedName method that returns a qualified name, but it requires specifying additional tokens. The .NET Compiler Platform offers an additional opportunity with the Microsoft.CodeAnalysis.Editing.SyntaxGenerator class. This class allows for generating syntax nodes that are language-specific but yet semantically similar between languages. You get an instance for the current document as follows:
Dim generator = SyntaxGenerator.GetGenerator(document)
Now you have to generate a fully qualified name that must be bound to the generic name. This is accomplished with the SyntaxGenerator.QualifiedName method, whose first argument is the resulting expression from the invocation of the SyntaxGenerator.DottedName, which produces a qualified name from a string containing dots, and the second argument is the syntax node to be bound:
Dim boundNode = generator. QualifiedName(generator. DottedName("System.Collections.ObjectModel"), newGenericNameNode)
The boundNode variable represents the new syntax node that must replace the node with issues. The root variable, of type SyntaxNode, exposes a ReplaceNode method that you invoke as follows:
Dim newRoot = root.ReplaceNode(genericNameNode, boundNode)
At this point, you must generate a new document containing the replaced node, as demonstrated in the following code:
Dim newDocument = document.WithSyntaxRoot(newRoot) Return newDocument
Since the document is an immutable object, you create a new one based on the existing instance and invoke the WithSyntaxRoot method, which basically replaces the existing syntax tree. As you get more experienced with .NET Compiler Platform, you will discover how specialized classes that represent syntax nodes, such as ObjectCreationExpressionSyntax and GenericNameSyntax, have With* methods that allow for creating new syntax nodes from an immutable node, replacing the desired information.
Now start the experimental instance of Visual Studio 2015 by pressing F5, to discover how the code fix works. As Figure 6 shows, the Light Bulb suggests the potential code fix you implemented, adding the proper qualified name for the new collection.
Figure 6 Fixing code issues with a custom analyzer.
With the help of the Syntax Visualizer, you could extend and improve the sample code to suggest fixes for every declaration of List(Of T), not just new instance declarations, or find where instances of List(Of T) are assigned to data-bound properties. As it is, the analyzer would search for the specified code issue in every project type, but this isn’t a good idea; using a List is still an appropriate choice in non-XAML development platforms, so you might want to restrict the analyzer to work only in Windows 8.1 and Windows Phone 8.1 apps. The Compilation type exposes a method called GetTypeByMetadataName, which returns an INamedTypeSymbol object for the specified fully qualified object name. You can use this method against a type that you know exists only in the Windows Runtime platform, as in the following example:
If context.SemanticModel.Compilation. GetTypeByMetadataName("Windows.Storage") _ Is Nothing Then Return
You know that the Windows.Storage type exists only in the Windows Runtime, so if the method returns nothing, that type is not available in the current platform; therefore, the analyzer doesn’t apply to the current project, and it simply returns. The preceding line of code can be added as the very first small test in the AnalyzeCollection method of the analyzer. If you now run the sample again, you’ll see that instantiating a List(Of T) on development platforms other than Windows Runtime (for instance, ASP.NET or Windows Forms), it will simply be ignored, whereas it will work as expected against Windows 8.1 and Windows Phone 8.1 apps.
Conclusions
The .NET Compiler Platform and the open source compilers provide a great way to build domain-specific code analysis rules that can easily integrate into the Visual Studio 2015 code editor. This is a tremendous benefit, especially for developers whose business is building class libraries or user controls. In fact, with these APIs, you can distribute code analyzers that will help people to write code in the most appropriate way for your products.