docs/wiki/How-To-Write-a-Visual-Basic-Analyzer-and-Code-Fix.md
In previous releases of Visual Studio, it has been difficult to create custom warnings that target C# or Visual Basic. With the Diagnostics API in the .NET Compiler Platform ("Roslyn"), this once difficult task has become easy! All that is needed is to perform a bit of analysis to identify an issue, and optionally provide a tree transformation as a code fix. The heavy lifting of running your analysis on a background thread, showing squiggly underlines in the editor, populating the Visual Studio Error List, creating "light bulb" suggestions and showing rich previews is all done for you automatically.
In this walkthrough, we'll explore the creation of an Analyzer and an accompanying Code Fix using the Roslyn APIs. An Analyzer is a way to perform source code analysis and report a problem to the user. Optionally, an Analyzer can also provide a Code Fix which represents a modification to the user's source code. For example, an Analyzer could be created to detect and report any local variable names that begin with an uppercase letter, and provide a Code Fix that corrects them.
Suppose that you wanted to report to the user any local variable declarations that can be converted to local constants. For example, consider the following code:
Dim x As Integer = 0
Console.WriteLine(x)
In the code above, x is assigned a constant value and is never written to. Thus, it can be declared using the Const modifier:
Const x As Integer = 0
Console.WriteLine(x)
The analysis to determine whether a variable can be made constant is actually fairly involved, requiring syntactic analysis, constant analysis of the initializer expression and dataflow analysis to ensure that the variable is never written to. However, performing this analysis with the .NET Compiler Platform and exposing it as an Analyzer is pretty easy.
This Analyzer is provided by the AnalyzeSymbol method in the debugger project. So initially, the debugger project contains enough code to create an Analyzer for every type declaration in a Visual Basic file whose identifier contains lowercase letters.
context.RegisterSyntaxNodeAction(AddressOf AnalyzeNode, SyntaxKind.LocalDeclarationStatement)
Public Const DiagnosticId = "MakeConstVB"
Private Const Title = "Variable can be made constant"
Private Const MessageFormat = "Can be made constant"
Private Const Description = "Make Constant"
Private Const Category = "Usage"
<DiagnosticAnalyzer(LanguageNames.VisualBasic)>
Public Class FirstAnalyzerVBAnalyzer
Inherits DiagnosticAnalyzer
Public Const DiagnosticId = "MakeConstVB"
Private Const Title = "Variable can be made constant"
Private Const MessageFormat = "Can be made constant"
Private Const Description = "Make Constant"
Private Const Category = "Usage"
Private Shared Rule As New DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault:=True, description:=Description)
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)
context.RegisterSyntaxNodeAction(AddressOf AnalyzeNode, SyntaxKind.LocalDeclarationStatement)
End Sub
Private Sub AnalyzeNode(context As SyntaxNodeAnalysisContext)
Throw New NotImplementedException()
End Sub
End Class
Dim localDeclaration = CType(context.Node, LocalDeclarationStatementSyntax)
' Only consider local variable declarations that are Dim (no Static or Const).
If Not localDeclaration.Modifiers.All(Function(m) m.Kind() = SyntaxKind.DimKeyword) Then
Return
End If
' Ensure that all variable declarators in the local declaration have
' initializers and a single variable name. Additionally, ensure that
' each variable is assigned with a constant value.
For Each declarator In localDeclaration.Declarators
If declarator.Initializer Is Nothing OrElse declarator.Names.Count <> 1 Then
Return
End If
If Not context.SemanticModel.GetConstantValue(declarator.Initializer.Value).HasValue Then
Return
End If
Next
' Perform data flow analysis on the local declaration.
Dim dataFlowAnalysis = context.SemanticModel.AnalyzeDataFlow(localDeclaration)
' Retrieve the local symbol for each variable in the local declaration
' and ensure that it is not written outside of the data flow analysis region.
For Each declarator In localDeclaration.Declarators
Dim variable = declarator.Names.Single()
Dim variableSymbol = context.SemanticModel.GetDeclaredSymbol(variable)
If dataFlowAnalysis.WrittenOutside.Contains(variableSymbol) Then
Return
End If
Next
context.ReportDiagnostic(Diagnostic.Create(Rule, context.Node.GetLocation()))
Private Sub AnalyzeNode(context As SyntaxNodeAnalysisContext)
Dim localDeclaration = CType(context.Node, LocalDeclarationStatementSyntax)
' Only consider local variable declarations that are Dim (no Static or Const).
If Not localDeclaration.Modifiers.All(Function(m) m.Kind() = SyntaxKind.DimKeyword) Then
Return
End If
' Ensure that all variable declarators in the local declaration have
' initializers and a single variable name. Additionally, ensure that
' each variable is assigned with a constant value.
For Each declarator In localDeclaration.Declarators
If declarator.Initializer Is Nothing OrElse declarator.Names.Count <> 1 Then
Return
End If
If Not context.SemanticModel.GetConstantValue(declarator.Initializer.Value).HasValue Then
Return
End If
Next
' Perform data flow analysis on the local declaration.
Dim dataFlowAnalysis = context.SemanticModel.AnalyzeDataFlow(localDeclaration)
' Retrieve the local symbol for each variable in the local declaration
' and ensure that it is not written outside the data flow analysis region.
For Each declarator In localDeclaration.Declarators
Dim variable = declarator.Names.Single()
Dim variableSymbol = context.SemanticModel.GetDeclaredSymbol(variable)
If dataFlowAnalysis.WrittenOutside.Contains(variableSymbol) Then
Return
End If
Next
context.ReportDiagnostic(Diagnostic.Create(Rule, context.Node.GetLocation()))
End Sub
Sub Main()
Dim i As Integer = 1
Dim j As Integer = 2
Dim k As Integer = i + j
End Sub
Any Analyzer can provide one or more Code Fixes which define an edit that can be performed to the source code to address the reported issue. For the Analyzer that you just created, you can provide a Code Fix that replaces Dim with the Const keyword when the user chooses it from the light bulb UI in the editor. To do so, follow the steps below.
First, open the CodeFixProvider.vb file that was already added by the Analyzer with Code Fix template. This Code Fix is already wired up to the Diagnostic ID produced by your Diagnostic Analyzer, but it doesn't yet implement the right code transform.
Change the title string to "Make constant".
Delete the MakeUppercaseAsync method, which no longer applies.
In RegisterCodeFixesAsync, change the ancestor node type you're searching for to LocalDeclarationStatementSyntax to match the Diagnostic.
' Find the type statement identified by the diagnostic.
Dim declaration = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType(Of LocalDeclarationStatementSyntax)().First()
' Register a code action that will invoke the fix.
context.RegisterCodeFix(
CodeAction.Create(
title:=title,
createChangedDocument:=Function(c) MakeConstAsync(context.Document, declaration, c),
equivalenceKey:=title),
diagnostic)
Imports System.Collections.Immutable
Imports Microsoft.CodeAnalysis.Rename
<ExportCodeFixProvider(LanguageNames.VisualBasic, Name:=NameOf(Analyzer3CodeFixProvider)), [Shared]>
Public Class Analyzer3CodeFixProvider
Inherits CodeFixProvider
Private Const title As String = "Make constant"
Public NotOverridable Overrides ReadOnly Property FixableDiagnosticIds As ImmutableArray(Of String)
Get
Return ImmutableArray.Create(Analyzer3Analyzer.DiagnosticId)
End Get
End Property
Public NotOverridable Overrides Function GetFixAllProvider() As FixAllProvider
Return WellKnownFixAllProviders.BatchFixer
End Function
Public NotOverridable Overrides Async Function RegisterCodeFixesAsync(context As CodeFixContext) As Task
Dim root = Await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(False)
' TODO: Replace the following code with your own analysis, generating a CodeAction for each fix to suggest
Dim diagnostic = context.Diagnostics.First()
Dim diagnosticSpan = diagnostic.Location.SourceSpan
' Find the type statement identified by the diagnostic.
Dim declaration = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType(Of LocalDeclarationStatementSyntax)().First()
' Register a code action that will invoke the fix.
context.RegisterCodeFix(
CodeAction.Create(
title:=title,
createChangedDocument:=Function(c) MakeConstAsync(context.Document, declaration, c),
equivalenceKey:=title),
diagnostic)
End Function
End Class
Private Async Function MakeConstAsync(document As Document, localDeclaration As LocalDeclarationStatementSyntax, cancellationToken As CancellationToken) As Task(Of Document)
' Create a const token with the leading trivia from the local declaration.
Dim firstToken = localDeclaration.GetFirstToken()
Dim constToken = SyntaxFactory.Token(firstToken.LeadingTrivia, SyntaxKind.ConstKeyword, firstToken.TrailingTrivia)
' Create a new modifier list with the const token.
Dim newModifiers = SyntaxFactory.TokenList(constToken)
' Produce new local declaration.
Dim newLocalDeclaration = localDeclaration.WithModifiers(newModifiers)
' Add an annotation to format the new local declaration.
Dim formattedLocalDeclaration = newLocalDeclaration.WithAdditionalAnnotations(Formatter.Annotation)
' Replace the old local declaration with the new local declaration.
Dim oldRoot = Await document.GetSyntaxRootAsync(cancellationToken)
Dim newRoot = oldRoot.ReplaceNode(localDeclaration, formattedLocalDeclaration)
' Return document with transformed tree.
Return document.WithSyntaxRoot(newRoot)
Private Async Function MakeConstAsync(document As Document, localDeclaration As LocalDeclarationStatementSyntax, cancellationToken As CancellationToken) As Task(Of Document)
' Create a const token with the leading trivia from the local declaration.
Dim firstToken = localDeclaration.GetFirstToken()
Dim constToken = SyntaxFactory.Token(
firstToken.LeadingTrivia, SyntaxKind.ConstKeyword, firstToken.TrailingTrivia)
' Create a new modifier list with the const token.
Dim newModifiers = SyntaxFactory.TokenList(constToken)
' Produce new local declaration.
Dim newLocalDeclaration = localDeclaration.WithModifiers(newModifiers)
' Add an annotation to format the new local declaration.
Dim formattedLocalDeclaration = newLocalDeclaration.WithAdditionalAnnotations(Formatter.Annotation)
' Replace the old local declaration with the new local declaration.
Dim oldRoot = Await document.GetSyntaxRootAsync(cancellationToken)
Dim newRoot = oldRoot.ReplaceNode(localDeclaration, formattedLocalDeclaration)
' Return document with transformed tree.
Return document.WithSyntaxRoot(newRoot)
End Function
Sub Main()
Dim i As Integer = 1
Dim j As Integer = 2
Dim k As Integer = i + j
End Sub