docs/wiki/Getting-Started-VB-Syntax-Transformation.md
This walkthrough builds on concepts and techniques explored in the Getting Started: Syntax Analysis and Getting Started: Semantic Analysis walkthroughs. If you haven't already, it's strongly advised that you complete those walkthroughs before beginning this one.
In this walkthrough, you'll explore techniques for creating and transforming syntax trees. In combination with the techniques you learned in previous Getting Started walkthroughs, you will create your first command-line refactoring!
A fundamental tenet of the .NET Compiler Platform is immutability. Because immutable data structures cannot be changed after they are created, they can be safely shared and analyzed by multiple consumers simultaneously without the dangers of one tool affecting another in unpredictable ways. No locks or other concurrency measures needed. This applies to syntax trees, compilations, symbols, semantic models, and every other data structure you'll encounter. Instead of modification, new objects are created based on specified differences to the old ones. You'll apply this concept to syntax trees to create tree transformations!
To create SyntaxNodes you must use the SyntaxFactory class factory methods. For each kind of node, token, or trivia there is a factory method which can be used to create an instance of that type. By composing nodes hierarchically in a bottom-up fashion you can create syntax trees.
This example uses the SyntaxFactory class factory methods to construct a NameSyntax representing the System.Collections.Generic namespace.
NameSyntax is the base class for four types of names that appear in VB:
<left-name>.<right-identifier-or-generic-name> such as System.IOOption Strict Off
Imports Microsoft.CodeAnalysis.VisualBasic.SyntaxFactory
Dim name As NameSyntax = IdentifierName("System")
name = QualifiedName(name, IdentifierName("Collections"))
Execute this statement to set the name variable to the new QualifiedNameSyntax node.
Using the Immediate Window evaluate the expression ? name.ToString(). It should evaluate to "System.Collections".
Continue this pattern by building another QualifiedNameSyntax node for the Generic namespace:
name = QualifiedName(name, IdentifierName("Generic"))
Because the syntax trees are immutable, the Syntax API provides no direct mechanism for modifying an existing syntax tree after construction. However, the Syntax API does provide methods for producing new trees based on specified changes to existing ones. Each concrete class that derives from SyntaxNode defines With* methods which you can use to specify changes to its child properties. Additionally, the ReplaceNode extension method can be used to replace a descendent node in a subtree. Without this method updating a node would also require manually updating its parent to point to the newly created child and repeating this process up the entire tree - a process known as re-spining the tree.
This example uses the WithName method to replace the name in an ImportsStatementSyntax node with the one constructed above.
Dim tree = VisualBasicSyntaxTree.ParseText(
"Imports System
Imports System.Collections
Imports System.Linq
Imports System.Text
Namespace HelloWorld
Module Module1
Sub Main(args As String())
Console.WriteLine(""Hello, World!"")
End Sub
End Module
End Namespace")
Dim root As CompilationUnitSyntax = tree.GetRoot()
Execute these statements.
Create a new SimpleImportsClauseNode node using the SimpleImportsClauseSyntax.WithName method to update the "System.Collections" import with the name we created above:
Dim oldImportClause As SimpleImportsClauseSyntax =
root.Imports(1).ImportsClauses(0)
Dim newImportClause = oldImportClause.WithName(name)
Execute these statements.
Using the Immediate Window evaluate the expression ? root.ToString() and observe that the original tree has not been changed to contain this new updated node.
Add the following line using the ReplaceNode extension method to create a new tree, replacing the existing import with the updated newImportClause node, and store the new tree in the existing root variable:
root = root.ReplaceNode(oldImportClause, newImportClause)
Execute this statement.
Using the Immediate Window evaluate the expression ? root.ToString() this time observing that the tree now correctly imports the System.Collections.Generic namespace.
Stop the program.
Option Strict Off
Imports Microsoft.CodeAnalysis.VisualBasic.SyntaxFactory
Module Module1
Sub Main()
Dim name As NameSyntax = IdentifierName("System")
name = QualifiedName(name, IdentifierName("Collections"))
name = QualifiedName(name, IdentifierName("Generic"))
Dim tree = VisualBasicSyntaxTree.ParseText(
"Imports System
Imports System.Collections
Imports System.Linq
Imports System.Text
Namespace HelloWorld
Module Module1
Sub Main(args As String())
Console.WriteLine(""Hello, World!"")
End Sub
End Module
End Namespace")
Dim root As CompilationUnitSyntax = tree.GetRoot()
Dim oldImportClause As SimpleImportsClauseSyntax =
root.Imports(1).ImportsClauses(0)
Dim newImportClause = oldImportClause.WithName(name)
root = root.ReplaceNode(oldImportClause, newImportClause)
End Sub
End Module
The With* and ReplaceNode methods provide convenient means to transform individual branches of a syntax tree. However, often it may be necessary to perform multiple transformations on a syntax tree in concert. The SyntaxRewriter class is a subclass of SyntaxVisitor which can be used to apply a transformation to a specific type of SyntaxNode. It is also possible to apply a set of transformations to multiple types of SyntaxNode wherever they appear in a syntax tree. The following example demonstrates this in a naive implementation of a command-line refactoring which removes explicit types in local variable declarations anywhere where type inference could be used. This example makes use of techniques discussed in this walkthrough as well as the Getting Started: Syntactic Analysis and Getting Started: Semantic Analysis walkthroughs.
Option Strict Off
Imports System.IO
Option Strict Off
Public Class TypeInferenceRewriter
Inherits VisualBasicSyntaxRewriter
Private ReadOnly SemanticModel As SemanticModel
Public Sub New(semanticModel As SemanticModel)
Me.SemanticModel = semanticModel
End Sub
Public Overrides Function VisitLocalDeclarationStatement (
node As LocalDeclarationStatementSyntax
) As SyntaxNode
End Function
Dim variable As Type = expression
The following forms of variable declarations in VB are either incompatible with type inference or left as an exercise to the reader.
' Multiple types in a single declaration.
Dim variable1 As Type1 = expression1,
variable2 As Type2 = expression2
' Multiple variables in a single declaration.
Dim variable1, variable2 As Type
' No initializer.
Dim variable1 As Type
Dim variable As New Type
' Already inferred.
Dim variable = expression
If node.Declarators.Count > 1 Then Return node
If node.Declarators(0).Names.Count > 1 Then Return node
If node.Declarators(0).AsClause Is Nothing Then Return node
If node.Declarators(0).AsClause.Kind = SyntaxKind.AsNewClause _
Then Return node
If node.Declarators(0).Initializer Is Nothing Then Return node
Dim declarator As VariableDeclaratorSyntax = node.Declarators(0)
Dim asClause As SimpleAsClauseSyntax = declarator.AsClause
Dim variableTypeName As TypeSyntax = asClause.Type
Dim variableType As ITypeSymbol =
SemanticModel.GetSymbolInfo(variableTypeName).Symbol
Dim initializerInfo As TypeInfo =
SemanticModel.GetTypeInfo(declarator.Initializer.Value)
If variableType Is initializerInfo.Type Then
Dim newDeclarator As VariableDeclaratorSyntax =
declarator.WithAsClause(Nothing)
Return node.ReplaceNode(declarator, newDeclarator)
Else
Return node
End If
Option Strict Off
Public Class TypeInferenceRewriter
Inherits VisualBasicSyntaxRewriter
Private ReadOnly SemanticModel As SemanticModel
Public Sub New(semanticModel As SemanticModel)
Me.SemanticModel = semanticModel
End Sub
Public Overrides Function VisitLocalDeclarationStatement(
node As LocalDeclarationStatementSyntax
) As SyntaxNode
If node.Declarators.Count > 1 Then Return node
If node.Declarators(0).Names.Count > 1 Then Return node
If node.Declarators(0).AsClause Is Nothing Then Return node
If node.Declarators(0).AsClause.Kind = SyntaxKind.AsNewClause _
Then Return node
If node.Declarators(0).Initializer Is Nothing Then Return node
Dim declarator As VariableDeclaratorSyntax = node.Declarators(0)
Dim asClause As SimpleAsClauseSyntax = declarator.AsClause
Dim variableTypeName As TypeSyntax = asClause.Type
Dim variableType As ITypeSymbol =
SemanticModel.GetSymbolInfo(variableTypeName).Symbol
Dim initializerInfo As TypeInfo =
SemanticModel.GetTypeInfo(declarator.Initializer.Value)
If variableType Is initializerInfo.Type Then
Dim newDeclarator As VariableDeclaratorSyntax =
declarator.WithAsClause(Nothing)
Return node.ReplaceNode(declarator, newDeclarator)
Else
Return node
End If
End Function
End Class
Return to your Module1.vb file.
To test your TypeInferenceRewriter you'll need to create a test Compilation to obtain the SemanticModel required for the type inference analysis. You'll do this step last. In the meantime declare a placeholder variable representing your test Compilation:
Dim test As Compilation = CreateTestCompilation()
After a short delay you should see an error squiggle appear reporting that no CreateTestCompilation method exists. Press Ctrl+Period to open the smart tag and then select the Generate method Module1.CreateTestCompilation' option. This will generate a method stub for the CreateTestCompilation method in Module1. You'll come back to fill this in later:
Next, write the following code to iterate over each SyntaxTree in the test Compilation. For each one initialize a new TypeInferenceRewriter with the SemanticModel for that tree:
For Each sourceTree As SyntaxTree In test.SyntaxTrees
Dim model As SemanticModel = test.GetSemanticModel(sourceTree)
Dim rewriter As New TypeInferenceRewriter(model)
Next
Dim newSource As SyntaxNode = rewriter.Visit(sourceTree.GetRoot())
If newSource IsNot sourceTree.GetRoot() Then
File.WriteAllText(sourceTree.FilePath, newSource.ToFullString())
End If
Dim globalImports As String() =
{"Microsoft.CodeAnalysis",
"Microsoft.CodeAnalysis.VisualBasic",
"Microsoft.CodeAnalysis.VisualBasic.Syntax"}
Dim options = New VisualBasicCompilationOptions(OutputKind.ConsoleApplication).
WithGlobalImports(From s In globalImports
Select GlobalImport.Parse(s))
Dim module1Tree As SyntaxTree =
VisualBasicSyntaxTree.ParseText(File.ReadAllText("..\..\Module1.vb"),, "..\..\Module1.vb",)
Dim rewriterTree As SyntaxTree =
VisualBasicSyntaxTree.ParseText(File.ReadAllText(
"..\..\TypeInferenceRewriter.vb"),, "..\..\TypeInferenceRewriter.vb",)
Dim sourceTrees As SyntaxTree() = {module1Tree, rewriterTree}
Dim mscorlib As MetadataReference =
MetadataReference.CreateFromFile(GetType(Object).Assembly.Location)
Dim codeAnalysis As MetadataReference =
MetadataReference.CreateFromFile(GetType(SyntaxTree).Assembly.Location)
Dim vbCodeAnalysis As MetadataReference =
MetadataReference.CreateFromFile(GetType(VisualBasicSyntaxTree).Assembly.Location)
Dim references As MetadataReference() = {mscorlib, codeAnalysis, vbCodeAnalysis}
Return VisualBasicCompilation.Create("TransformationVB",
sourceTrees,
references,
options)
Option Strict Off
Module Module1
Sub Main()
Dim test As Compilation = CreateTestCompilation()
For Each sourceTree As SyntaxTree In test.SyntaxTrees
Dim model As SemanticModel = test.GetSemanticModel(sourceTree)
Dim rewriter As New TypeInferenceRewriter(model)
Dim newSource As SyntaxNode = rewriter.Visit(sourceTree.GetRoot())
If newSource IsNot sourceTree.GetRoot() Then
File.WriteAllText(sourceTree.FilePath, newSource.ToFullString())
End If
Next
End Sub
Private Function CreateTestCompilation() As Compilation
Dim globalImports As String() =
{"Microsoft.CodeAnalysis",
"Microsoft.CodeAnalysis.VisualBasic",
"Microsoft.CodeAnalysis.VisualBasic.Syntax"}
Dim options = New VisualBasicCompilationOptions(OutputKind.ConsoleApplication).
WithGlobalImports(From s In globalImports
Select GlobalImport.Parse(s))
Dim module1Tree As SyntaxTree =
VisualBasicSyntaxTree.ParseText(File.ReadAllText("..\..\Module1.vb"),, "..\..\Module1.vb",)
Dim rewriterTree As SyntaxTree =
VisualBasicSyntaxTree.ParseText(File.ReadAllText(
"..\..\TypeInferenceRewriter.vb"),, "..\..\TypeInferenceRewriter.vb",)
Dim sourceTrees As SyntaxTree() = {module1Tree, rewriterTree}
Dim mscorlib As MetadataReference =
MetadataReference.CreateFromFile(GetType(Object).Assembly.Location)
Dim codeAnalysis As MetadataReference =
MetadataReference.CreateFromFile(GetType(SyntaxTree).Assembly.Location)
Dim vbCodeAnalysis As MetadataReference =
MetadataReference.CreateFromFile(GetType(VisualBasicSyntaxTree).Assembly.Location)
Dim references As MetadataReference() = {mscorlib, codeAnalysis, vbCodeAnalysis}
Return VisualBasicCompilation.Create("TransformationVB",
sourceTrees,
references,
options)
End Function
End Module