From 43f2f3d8b0d6e713307b7e80cbf18355164f90f8 Mon Sep 17 00:00:00 2001 From: Cory Smith Date: Tue, 5 Jan 2021 18:45:06 -0600 Subject: [PATCH] Add SourceGenerators (VB) examples. --- .../SourceGenerators/GeneratedDemo/Cars.csv | 4 + .../GeneratedDemo/GeneratedDemo.vbproj | 23 ++ .../GeneratedDemo/MainSettings.xmlsettings | 6 + .../SourceGenerators/GeneratedDemo/People.csv | 3 + .../SourceGenerators/GeneratedDemo/Program.vb | 39 ++++ .../GeneratedDemo/UseAutoNotifyGenerator.vb | 42 ++++ .../GeneratedDemo/UseCsvGenerator.vb | 18 ++ .../GeneratedDemo/UseHelloWorldGenerator.vb | 8 + .../GeneratedDemo/UseMustacheGenerator.vb | 94 ++++++++ .../GeneratedDemo/UseXmlSettingsGenerator.vb | 25 +++ .../VisualBasic/SourceGenerators/README.md | 35 +++ .../AutoNotifyGenerator.vb | 201 +++++++++++++++++ .../SourceGeneratorSamples/CsvGenerator.props | 7 + .../SourceGeneratorSamples/CsvGenerator.vb | 208 ++++++++++++++++++ .../HelloWorldGenerator.vb | 66 ++++++ .../MustacheGenerator.vb | 146 ++++++++++++ .../SettingsXmlGenerator.vb | 102 +++++++++ .../SourceGeneratorSamples.vbproj | 31 +++ .../SourceGenerators/SourceGenerators.sln | 31 +++ 19 files changed, 1089 insertions(+) create mode 100644 samples/VisualBasic/SourceGenerators/GeneratedDemo/Cars.csv create mode 100644 samples/VisualBasic/SourceGenerators/GeneratedDemo/GeneratedDemo.vbproj create mode 100644 samples/VisualBasic/SourceGenerators/GeneratedDemo/MainSettings.xmlsettings create mode 100644 samples/VisualBasic/SourceGenerators/GeneratedDemo/People.csv create mode 100644 samples/VisualBasic/SourceGenerators/GeneratedDemo/Program.vb create mode 100644 samples/VisualBasic/SourceGenerators/GeneratedDemo/UseAutoNotifyGenerator.vb create mode 100644 samples/VisualBasic/SourceGenerators/GeneratedDemo/UseCsvGenerator.vb create mode 100644 samples/VisualBasic/SourceGenerators/GeneratedDemo/UseHelloWorldGenerator.vb create mode 100644 samples/VisualBasic/SourceGenerators/GeneratedDemo/UseMustacheGenerator.vb create mode 100644 samples/VisualBasic/SourceGenerators/GeneratedDemo/UseXmlSettingsGenerator.vb create mode 100644 samples/VisualBasic/SourceGenerators/README.md create mode 100644 samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/AutoNotifyGenerator.vb create mode 100644 samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/CsvGenerator.props create mode 100644 samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/CsvGenerator.vb create mode 100644 samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/HelloWorldGenerator.vb create mode 100644 samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/MustacheGenerator.vb create mode 100644 samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/SettingsXmlGenerator.vb create mode 100644 samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/SourceGeneratorSamples.vbproj create mode 100644 samples/VisualBasic/SourceGenerators/SourceGenerators.sln diff --git a/samples/VisualBasic/SourceGenerators/GeneratedDemo/Cars.csv b/samples/VisualBasic/SourceGenerators/GeneratedDemo/Cars.csv new file mode 100644 index 0000000000..26a5d2051e --- /dev/null +++ b/samples/VisualBasic/SourceGenerators/GeneratedDemo/Cars.csv @@ -0,0 +1,4 @@ +Brand, Model, Year, cc, Favorite +Fiat, Punto, 2008, 12.3, No +Ford, Wagon, 1956, 20.3, No +BMW, "335", 2014, 20.3, Yes \ No newline at end of file diff --git a/samples/VisualBasic/SourceGenerators/GeneratedDemo/GeneratedDemo.vbproj b/samples/VisualBasic/SourceGenerators/GeneratedDemo/GeneratedDemo.vbproj new file mode 100644 index 0000000000..08120ed6f7 --- /dev/null +++ b/samples/VisualBasic/SourceGenerators/GeneratedDemo/GeneratedDemo.vbproj @@ -0,0 +1,23 @@ + + + + Exe + netcoreapp3.1 + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/VisualBasic/SourceGenerators/GeneratedDemo/MainSettings.xmlsettings b/samples/VisualBasic/SourceGenerators/GeneratedDemo/MainSettings.xmlsettings new file mode 100644 index 0000000000..96b96f5e54 --- /dev/null +++ b/samples/VisualBasic/SourceGenerators/GeneratedDemo/MainSettings.xmlsettings @@ -0,0 +1,6 @@ + + + False + 1234 + Hello World! + \ No newline at end of file diff --git a/samples/VisualBasic/SourceGenerators/GeneratedDemo/People.csv b/samples/VisualBasic/SourceGenerators/GeneratedDemo/People.csv new file mode 100644 index 0000000000..5179c3539f --- /dev/null +++ b/samples/VisualBasic/SourceGenerators/GeneratedDemo/People.csv @@ -0,0 +1,3 @@ +Name, address, 11Age +"Luca Bol", "23 Bell Street", 90 +"john doe", "32 Carl street", 45 \ No newline at end of file diff --git a/samples/VisualBasic/SourceGenerators/GeneratedDemo/Program.vb b/samples/VisualBasic/SourceGenerators/GeneratedDemo/Program.vb new file mode 100644 index 0000000000..154f716e0e --- /dev/null +++ b/samples/VisualBasic/SourceGenerators/GeneratedDemo/Program.vb @@ -0,0 +1,39 @@ +Option Explicit On +Option Strict On +Option Infer On + +Module Program + + Public Sub Main() + + Console.WriteLine("Running HelloWorld: +") + UseHelloWorldGenerator.Run() + + Console.WriteLine(" + +Running AutoNotify: +") + UseAutoNotifyGenerator.Run() + + Console.WriteLine(" + +Running XmlSettings: +") + UseXmlSettingsGenerator.Run() + + Console.WriteLine(" + +Running CsvGenerator: +") + UseCsvGenerator.Run() + + Console.WriteLine(" + +Running MustacheGenerator: +") + UseMustacheGenerator.Run() + + End Sub + +End Module \ No newline at end of file diff --git a/samples/VisualBasic/SourceGenerators/GeneratedDemo/UseAutoNotifyGenerator.vb b/samples/VisualBasic/SourceGenerators/GeneratedDemo/UseAutoNotifyGenerator.vb new file mode 100644 index 0000000000..ea1a1a3d83 --- /dev/null +++ b/samples/VisualBasic/SourceGenerators/GeneratedDemo/UseAutoNotifyGenerator.vb @@ -0,0 +1,42 @@ +Option Explicit On +Option Strict On +Option Infer On + +Imports AutoNotify + +' The view model we'd like to augment +Partial Public Class ExampleViewModel + + + Private _text As String = "private field text" + + + Private _amount As Integer = 5 + +End Class + +Public Module UseAutoNotifyGenerator + + Public Sub Run() + + Dim vm As New ExampleViewModel() + + ' we didn't explicitly create the 'Text' property, it was generated for us + Dim text = vm.Text + Console.WriteLine($"Text = {text}") + + ' Properties can have differnt names generated based on the PropertyName argument of the attribute + Dim count = vm.Count + Console.WriteLine($"Count = {count}") + + ' the viewmodel will automatically implement INotifyPropertyChanged + AddHandler vm.PropertyChanged, Sub(o, e) Console.WriteLine($"Property {e.PropertyName} was changed") + vm.Text = "abc" + vm.Count = 123 + + ' Try adding fields to the ExampleViewModel class above and tagging them with the attribute + ' You'll see the matching generated properties visibile in IntelliSense in realtime + + End Sub + +End Module \ No newline at end of file diff --git a/samples/VisualBasic/SourceGenerators/GeneratedDemo/UseCsvGenerator.vb b/samples/VisualBasic/SourceGenerators/GeneratedDemo/UseCsvGenerator.vb new file mode 100644 index 0000000000..78c1993265 --- /dev/null +++ b/samples/VisualBasic/SourceGenerators/GeneratedDemo/UseCsvGenerator.vb @@ -0,0 +1,18 @@ +Option Explicit On +Option Strict On +Option Infer On + +Imports CSV + +Friend Class UseCsvGenerator + + Public Shared Sub Run() + + Console.WriteLine("## CARS") + Cars.All.ToList().ForEach(Sub(c) Console.WriteLine(c.Brand & vbTab & c.Model & vbTab & c.Year & vbTab & c.Cc & vbTab & c.Favorite)) + Console.WriteLine(vbCr & "## PEOPLE") + People.All.ToList().ForEach(Sub(p) Console.WriteLine(p.Name & vbTab & p.Address & vbTab & p._11Age)) + + End Sub + +End Class \ No newline at end of file diff --git a/samples/VisualBasic/SourceGenerators/GeneratedDemo/UseHelloWorldGenerator.vb b/samples/VisualBasic/SourceGenerators/GeneratedDemo/UseHelloWorldGenerator.vb new file mode 100644 index 0000000000..78a173ca73 --- /dev/null +++ b/samples/VisualBasic/SourceGenerators/GeneratedDemo/UseHelloWorldGenerator.vb @@ -0,0 +1,8 @@ +Public Module UseHelloWorldGenerator + + Public Sub Run() + ' The static call below is generated at build time, and will list the syntax trees used in the compilation + HelloWorldGenerated.HelloWorld.SayHello() + End Sub + +End Module diff --git a/samples/VisualBasic/SourceGenerators/GeneratedDemo/UseMustacheGenerator.vb b/samples/VisualBasic/SourceGenerators/GeneratedDemo/UseMustacheGenerator.vb new file mode 100644 index 0000000000..3c9a97c414 --- /dev/null +++ b/samples/VisualBasic/SourceGenerators/GeneratedDemo/UseMustacheGenerator.vb @@ -0,0 +1,94 @@ +Option Explicit On +Option Strict On +Option Infer On + +Imports GeneratedDemo.UseMustacheGenerator + + + + + + + +Friend Class UseMustacheGenerator + + Public Shared Sub Run() + Console.WriteLine(Mustache.Constants.Lottery) + Console.WriteLine(Mustache.Constants.HR) + Console.WriteLine(Mustache.Constants.HTML) + Console.WriteLine(Mustache.Constants.Section) + Console.WriteLine(Mustache.Constants.NestedSection) + End Sub + + ' Mustache templates and hashes from the manual at https://mustache.github.io/mustache.1.html... + Public Const t1 As String = " +Hello {{name}} +You have just won {{value}} dollars! +{{#in_ca}} +Well, {{taxed_value}} dollars, after taxes. +{{/in_ca}} +" + Public Const h1 As String = " +{ + ""name"": ""Chris"", + ""value"": 10000, + ""taxed_value"": 5000, + ""in_ca"": true +} +" + Public Const t2 As String = " +* {{name}} +* {{age}} +* {{company}} +* {{{company}}} +" + Public Const h2 As String = " +{ + ""name"": ""Chris"", + ""company"": ""GitHub"" +} +" + Public Const t3 As String = " + Shown + {{#person}} + Never shown! + {{/person}} + " + Public Const h3 As String = " +{ + ""person"": false +} +" + Public Const t4 As String = " +{{#repo}} + {{name}} +{{/repo}} +" + Public Const h4 As String = " +{ + ""repo"": [ + { ""name"": ""resque"" }, + { ""name"": ""hub"" }, + { ""name"": ""rip"" } + ] +} +" + Public Const t5 As String = " +{{#repo}} + {{name}} + {{#nested}} + NestedName: {{name}} + {{/nested}} +{{/repo}} +" + Public Const h5 As String = " +{ + ""repo"": [ + { ""name"": ""resque"", ""nested"":[{""name"":""nestedResque""}] }, + { ""name"": ""hub"" }, + { ""name"": ""rip"" } + ] +} +" + +End Class \ No newline at end of file diff --git a/samples/VisualBasic/SourceGenerators/GeneratedDemo/UseXmlSettingsGenerator.vb b/samples/VisualBasic/SourceGenerators/GeneratedDemo/UseXmlSettingsGenerator.vb new file mode 100644 index 0000000000..0ace59e747 --- /dev/null +++ b/samples/VisualBasic/SourceGenerators/GeneratedDemo/UseXmlSettingsGenerator.vb @@ -0,0 +1,25 @@ +Imports AutoSettings + +Public Module UseXmlSettingsGenerator + + Public Sub Run() + + ' This XmlSettings generator makes a static property in the XmlSettings class for each .xmlsettings file + + ' here we have the 'Main' settings file from MainSettings.xmlsettings + ' the name is determined by the 'name' attribute of the root settings element + Dim main As XmlSettings.MainSettings = XmlSettings.Main + Console.WriteLine($"Reading settings from {main.GetLocation()}") + + ' settings are strongly typed and can be read directly from the static instance + Dim firstRun As Boolean = XmlSettings.Main.FirstRun + Console.WriteLine($"Setting firstRun = {firstRun}") + + Dim cacheSize As Integer = XmlSettings.Main.CacheSize + Console.WriteLine($"Setting cacheSize = {cacheSize}") + + ' Try adding some keys to the settings file and see the settings become available to read from + + End Sub + +End Module \ No newline at end of file diff --git a/samples/VisualBasic/SourceGenerators/README.md b/samples/VisualBasic/SourceGenerators/README.md new file mode 100644 index 0000000000..63d0029eb5 --- /dev/null +++ b/samples/VisualBasic/SourceGenerators/README.md @@ -0,0 +1,35 @@ +🚧 Work In Progress +======== + +These samples are for an in-progress feature of Roslyn. As such they may change or break as the feature is developed, and no level of support is implied. + +For more information on the Source Generators feature, see the [design document](https://github.com/dotnet/roslyn/blob/master/docs/features/source-generators.md). + +Prerequisites +----- + +These samples require **Visual Studio 16.9.0 Preview 2.0** or higher. + +Building the samples +----- +Open `SourceGenerators.sln` in Visual Studio or run `dotnet build` from the `\SourceGenerators` directory. + +Running the samples +----- + +The generators must be run as part of another build, as they inject source into the project being built. This repo contains a sample project `GeneratorDemo` that relies of the sample generators to add code to it's compilation. + +Run `GeneratedDemo` in Visual studio or run `dotnet run` from the `GeneratorDemo` directory. + +Using the samples in your project +----- + +You can add the sample generators to your own project by adding an item group containing an analyzer reference: + +```xml + + + +``` + +You will most likely need to close and reopen the solution in Visual Studio for any changes made to the generators to take effect. diff --git a/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/AutoNotifyGenerator.vb b/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/AutoNotifyGenerator.vb new file mode 100644 index 0000000000..981ae965de --- /dev/null +++ b/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/AutoNotifyGenerator.vb @@ -0,0 +1,201 @@ +Option Explicit On +Option Infer On +Option Strict On + +Imports System.Text + +Imports Microsoft.CodeAnalysis +Imports Microsoft.CodeAnalysis.Text +Imports Microsoft.CodeAnalysis.VisualBasic +Imports Microsoft.CodeAnalysis.VisualBasic.Syntax + +Namespace SourceGeneratorSamples + + + Public Class AutoNotifyGenerator + Implements ISourceGenerator + + Private Const ATTRIBUTE_TEXT As String = " +Imports System + +Namespace Global.AutoNotify + + Friend NotInheritable Class AutoNotifyAttribute + Inherits Attribute + + Public Sub New() + End Sub + + Public Property PropertyName As String + End Class +End Namespace +" + + Public Sub Initialize(context As GeneratorInitializationContext) Implements ISourceGenerator.Initialize + ' Register a syntax receiver that will be created for each generation pass + context.RegisterForSyntaxNotifications(Function() As ISyntaxReceiver + Return New SyntaxReceiver + End Function) + End Sub + + Public Sub Execute(context As GeneratorExecutionContext) Implements ISourceGenerator.Execute + + ' add the attribute text + context.AddSource("AutoNotifyAttribute", SourceText.From(ATTRIBUTE_TEXT, Encoding.UTF8)) + + ' retreive the populated receiver + Dim tempVar = TypeOf context.SyntaxReceiver Is SyntaxReceiver + Dim receiver = TryCast(context.SyntaxReceiver, SyntaxReceiver) + If Not tempVar Then + Return + End If + + ' we're going to create a new compilation that contains the attribute. + ' TODO: we should allow source generators to provide source during initialize, so that this step isn't required. + Dim options1 = context.Compilation.SyntaxTrees.First().Options + Dim compilation1 = context.Compilation.AddSyntaxTrees(VisualBasicSyntaxTree.ParseText(SourceText.From(ATTRIBUTE_TEXT, Encoding.UTF8), CType(options1, VisualBasicParseOptions))) + + ' get the newly bound attribute, and INotifyPropertyChanged + Dim attributeSymbol = compilation1.GetTypeByMetadataName("AutoNotify.AutoNotifyAttribute") + Dim notifySymbol = compilation1.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanged") + + ' loop over the candidate fields, and keep the ones that are actually annotated + Dim fieldSymbols As New List(Of IFieldSymbol) + + For Each field In receiver.CandidateFields + Dim model = compilation1.GetSemanticModel(field.SyntaxTree) + For Each variable In field.Declarators + For Each name In variable.Names + ' Get the symbol being decleared by the field, and keep it if its annotated + Dim fieldSymbol = TryCast(model.GetDeclaredSymbol(name), IFieldSymbol) + If fieldSymbol.GetAttributes().Any(Function(ad) ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.[Default])) Then + fieldSymbols.Add(fieldSymbol) + End If + Next + Next + Next + + ' group the fields by class, and generate the source + For Each group In fieldSymbols.GroupBy(Function(f) f.ContainingType) + Dim classSource = ProcessClass(group.Key, group.ToList(), attributeSymbol, notifySymbol) + context.AddSource($"{group.Key.Name}_AutoNotify.vb", SourceText.From(classSource, Encoding.UTF8)) + Next + + End Sub + + Private Function ProcessClass(classSymbol As INamedTypeSymbol, fields As List(Of IFieldSymbol), attributeSymbol As ISymbol, notifySymbol As ISymbol) As String + + If Not classSymbol.ContainingSymbol.Equals(classSymbol.ContainingNamespace, SymbolEqualityComparer.[Default]) Then + Return Nothing 'TODO: issue a diagnostic that it must be top level + End If + + Dim namespaceName = classSymbol.ContainingNamespace.ToDisplayString() + + ' begin building the generated source + Dim source = New StringBuilder($"Option Explicit On +Option Strict On +Option Infer On + +Namespace Global.{namespaceName} + + Partial Public Class {classSymbol.Name} + Implements {notifySymbol.ToDisplayString()} + +") + + ' if the class doesn't implement INotifyPropertyChanged already, add it + If Not classSymbol.Interfaces.Contains(CType(notifySymbol, INamedTypeSymbol)) Then + source.Append(" Public Event PropertyChanged As System.ComponentModel.PropertyChangedEventHandler Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged +") + End If + + ' create properties for each field + For Each fieldSymbol In fields + ProcessField(source, fieldSymbol, attributeSymbol) + Next + + source.Append(" + End Class + +End Namespace") + + Return source.ToString() + + End Function + + Private Sub ProcessField(source As StringBuilder, fieldSymbol As IFieldSymbol, attributeSymbol As ISymbol) + + Dim chooseName As Func(Of String, TypedConstant, String) = + Function(fieldName1 As String, overridenNameOpt1 As TypedConstant) As String + + If Not overridenNameOpt1.IsNull Then + Return overridenNameOpt1.Value.ToString() + End If + + fieldName1 = fieldName1.TrimStart("_"c) + If fieldName1.Length = 0 Then + Return String.Empty + End If + + If fieldName1.Length = 1 Then + Return fieldName1.ToUpper() + End If + + Return fieldName1.Substring(0, 1).ToUpper() & fieldName1.Substring(1) + + End Function + + ' get the name and type of the field + Dim fieldName = fieldSymbol.Name + Dim fieldType = fieldSymbol.Type + + ' get the AutoNotify attribute from the field, and any associated data + Dim attributeData = fieldSymbol.GetAttributes().[Single](Function(ad) ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.[Default])) + Dim overridenNameOpt = attributeData.NamedArguments.SingleOrDefault(Function(kvp) kvp.Key = "PropertyName").Value + + Dim propertyName = chooseName(fieldName, overridenNameOpt) + If propertyName.Length = 0 OrElse propertyName = fieldName Then + 'TODO: issue a diagnostic that we can't process this field + Return + End If + + source.Append($" + Public Property {propertyName} As {fieldType} + Get + Return Me.{fieldName} + End Get + Set(value As {fieldType}) + Me.{fieldName} = value + RaiseEvent PropertyChanged(Me, New System.ComponentModel.PropertyChangedEventArgs(NameOf({propertyName}))) + End Set + End Property +") + + End Sub + + ''' + ''' Created on demand before each generation pass + ''' + Class SyntaxReceiver + Implements ISyntaxReceiver + + Public ReadOnly Property CandidateFields As List(Of FieldDeclarationSyntax) = New List(Of FieldDeclarationSyntax) + + ''' + ''' Called for every syntax node in the compilation, we can inspect the nodes and save any information useful for generation + ''' + Public Sub OnVisitSyntaxNode(syntaxNode As SyntaxNode) Implements ISyntaxReceiver.OnVisitSyntaxNode + ' any field with at least one attribute is a candidate for property generation + If TypeOf syntaxNode Is FieldDeclarationSyntax Then + Dim fieldDeclarationSyntax = TryCast(syntaxNode, FieldDeclarationSyntax) + If fieldDeclarationSyntax.AttributeLists.Count > 0 Then + CandidateFields.Add(fieldDeclarationSyntax) + End If + End If + End Sub + + End Class + + End Class + +End Namespace \ No newline at end of file diff --git a/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/CsvGenerator.props b/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/CsvGenerator.props new file mode 100644 index 0000000000..0741032bb3 --- /dev/null +++ b/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/CsvGenerator.props @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/CsvGenerator.vb b/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/CsvGenerator.vb new file mode 100644 index 0000000000..d474fdca29 --- /dev/null +++ b/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/CsvGenerator.vb @@ -0,0 +1,208 @@ +Option Explicit On +Option Infer On +Option Strict On + +Imports System.IO +Imports System.Text + +Imports Microsoft.CodeAnalysis +Imports Microsoft.CodeAnalysis.Text + +Imports NotVisualBasic.FileIO + +' CsvTextFileParser from https://github.com/22222/CsvTextFieldParser adding suppression rules for default VS config + +Namespace SourceGeneratorSamples + + + Public Class CsvGenerator + Implements ISourceGenerator + + Public Enum CsvLoadType + Startup + OnDemand + End Enum + + Public Sub Initialize(context As GeneratorInitializationContext) Implements Microsoft.CodeAnalysis.ISourceGenerator.Initialize + + End Sub + + Public Sub Execute(context As GeneratorExecutionContext) Implements ISourceGenerator.Execute + Dim options As IEnumerable(Of (CsvLoadType, Boolean, AdditionalText)) = GetLoadOptions(context) + Dim nameCodeSequence As IEnumerable(Of (Name As String, Code As String)) = SourceFilesFromAdditionalFiles(options) + For Each entry In nameCodeSequence + context.AddSource($"Csv_{entry.Name}", SourceText.From(entry.Code, Encoding.UTF8)) + Next + End Sub + + ' Guesses type of property for the object from the value of a csv field + Public Shared Function GetCsvFieldType(exemplar As String) As String + Dim garbageBoolean As Boolean + Dim garbageInteger As Integer + Dim garbageDouble As Double + Select Case True + Case Boolean.TryParse(exemplar, garbageBoolean) : Return "Boolean" + Case Integer.TryParse(exemplar, garbageInteger) : Return "Integer" + Case Double.TryParse(exemplar, garbageDouble) : Return "Double" + Case Else : Return "String" + End Select + End Function + + ' Examines the header row and the first row in the csv file to gather all header types and names + ' Also it returns the first row of data, because it must be read to figure out the types, + ' As the CsvTextFieldParser cannot 'Peek' ahead of one line. If there is no first line, + ' it consider all properties as strings. The generator returns an empty list of properly + ' typed objects in such case. If the file is completely empty, an error is generated. + Public Shared Function ExtractProperties(parser As CsvTextFieldParser) As (Types As String(), Names As String(), Fields As String()) + + Dim headerFields = parser.ReadFields() + If headerFields Is Nothing Then + Throw New Exception("Empty csv file!") + End If + + Dim firstLineFields = parser.ReadFields() + If firstLineFields Is Nothing Then + Return (Enumerable.Repeat("String", headerFields.Length).ToArray(), headerFields, firstLineFields) + Else + Return (firstLineFields.[Select](Function(field) GetCsvFieldType(field)).ToArray(), headerFields.[Select](New Func(Of String, String)(AddressOf StringToValidPropertyName)).ToArray(), firstLineFields) + End If + + End Function + + ' Adds a class to the `CSV` namespace for each `csv` file passed in. The class has a static property + ' named `All` that returns the list of strongly typed objects generated on demand at first access. + ' There is the slight chance of a race condition in a multi-thread program, but the result is relatively benign + ' , loading the collection multiple times instead of once. Measures could be taken to avoid that. + Public Shared Function GenerateClassFile(className As String, csvText As String, loadTime As CsvLoadType, cacheObjects As Boolean) As String + + Dim sb As New StringBuilder + Dim parser As New CsvTextFieldParser(New StringReader(csvText)) + + ''' Imports + sb.Append("Option Explicit On +Option Strict On +Option Infer On + +Imports System.Collections.Generic + +Namespace Global.CSV +") + + ''' Class Definition + sb.Append($" + Public Class {className} + +") + + If loadTime = CsvLoadType.Startup Then + sb.Append($" Shared Sub New() + Dim x = All + End Sub + +") + End If + + Dim tupleTemp = ExtractProperties(parser) : Dim types = tupleTemp.Types, names = tupleTemp.Names, fields = tupleTemp.Fields + Dim minLen = Math.Min(types.Length, names.Length) + + For i = 0 To minLen - 1 + sb.AppendLine($" Public Property {StringToValidPropertyName(names(i))} As {types(i)}") + Next + + ''' Loading data + sb.Append($" + Private Shared m_all As IEnumerable(Of {className}) + + Public Shared ReadOnly Property All As IEnumerable(Of {className}) + Get +") + + If cacheObjects Then + sb.Append(" If m_all IsNot Nothing Then + Return m_all + End If +") + End If + + sb.Append($" Dim l As New List(Of {className})() + Dim c As {className} +") + + Do + + If fields Is Nothing Then + Continue Do + End If + If fields.Length < minLen Then + Throw New Exception("Not enough fields in CSV file.") + End If + + sb.AppendLine($" c = New {className}()") + + Dim value As String '= "" + For i As Integer = 0 To minLen - 1 + ' Wrap strings in quotes. + value = If(GetCsvFieldType(fields(i)) = "String", $"""{fields(i).Trim().Trim(New Char() {""""c})}""", fields(i)) + sb.AppendLine($" c.{names(i)} = {value}") + Next + + sb.AppendLine(" l.Add(c)") + + fields = parser.ReadFields() + + Loop While fields IsNot Nothing + + sb.Append($" m_all = l + Return l +") + + ' Close things (property, class, namespace) + sb.Append(" End Get + End Property + + End Class + +End Namespace") + + Return sb.ToString() + + End Function + + Private Shared Function StringToValidPropertyName(s As String) As String + s = s.Trim() + s = If(Char.IsLetter(s(0)), Char.ToUpper(s(0)) & s.Substring(1), s) + s = If(Char.IsDigit(s.Trim()(0)), "_" & s, s) + s = New String(s.[Select](Function(ch) If(Char.IsDigit(ch) OrElse Char.IsLetter(ch), ch, "_"c)).ToArray()) + Return s + End Function + + Private Shared Function SourceFilesFromAdditionalFile(loadType As CsvLoadType, cacheObjects As Boolean, file As AdditionalText) As IEnumerable(Of (Name As String, Code As String)) + Dim className = Path.GetFileNameWithoutExtension(file.Path) + Dim csvText = file.GetText().ToString() + Return New(String, String)() {(className, GenerateClassFile(className, csvText, loadType, cacheObjects))} + End Function + + Private Shared Function SourceFilesFromAdditionalFiles(pathsData As IEnumerable(Of (LoadType As CsvLoadType, CacheObjects As Boolean, File As AdditionalText))) As IEnumerable(Of (Name As String, Code As String)) + Return pathsData.SelectMany(Function(d) SourceFilesFromAdditionalFile(d.LoadType, d.CacheObjects, d.File)) + End Function + + Private Shared Iterator Function GetLoadOptions(context As GeneratorExecutionContext) As IEnumerable(Of (LoadType As CsvLoadType, CacheObjects As Boolean, File As AdditionalText)) + For Each file In context.AdditionalFiles + If Path.GetExtension(file.Path).Equals(".csv", StringComparison.OrdinalIgnoreCase) Then + ' are there any options for it? + Dim loadTimeString As String = Nothing + context.AnalyzerConfigOptions.GetOptions(file).TryGetValue("build_metadata.additionalfiles.CsvLoadType", loadTimeString) + Dim loadType As CsvLoadType = Nothing + [Enum].TryParse(loadTimeString, ignoreCase:=True, loadType) + Dim cacheObjectsString As String = Nothing + context.AnalyzerConfigOptions.GetOptions(file).TryGetValue("build_metadata.additionalfiles.CacheObjects", cacheObjectsString) + Dim cacheObjects As Boolean = Nothing + Boolean.TryParse(cacheObjectsString, cacheObjects) + Yield (loadType, cacheObjects, file) + End If + Next + End Function + + End Class + +End Namespace \ No newline at end of file diff --git a/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/HelloWorldGenerator.vb b/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/HelloWorldGenerator.vb new file mode 100644 index 0000000000..792c3b62ae --- /dev/null +++ b/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/HelloWorldGenerator.vb @@ -0,0 +1,66 @@ +Option Explicit On +Option Infer On +Option Strict On + +Imports System.Text + +Imports Microsoft.CodeAnalysis +Imports Microsoft.CodeAnalysis.Text + +Namespace SourceGeneratorSamples + + + Public Class HelloWorldGenerator + Implements ISourceGenerator + + Public Sub Initialize(context As GeneratorInitializationContext) Implements ISourceGenerator.Initialize + ' No initialization required + End Sub + + Public Sub Execute(context As GeneratorExecutionContext) Implements ISourceGenerator.Execute + + ' begin creating the source we'll inject into the users compilation + + Dim sourceBuilder = New StringBuilder("Option Explicit On +Option Strict On +Option Infer On + +Namespace Global.HelloWorldGenerated + + Public Module HelloWorld + + Public Sub SayHello() + + Console.WriteLine(""Hello from generated code!"") + Console.WriteLine(""The following syntax trees existed in the compilation that created this program:"") +") + + ' for testing... let's include a comment with the current date/time. + sourceBuilder.AppendLine($" ' Generated at {DateTime.Now}") + + ' using the context, get a list of syntax trees in the users compilation + ' add the filepath of each tree to the class we're building + + For Each tree In context.Compilation.SyntaxTrees + sourceBuilder.AppendLine($" Console.WriteLine("" - {tree.FilePath}"")") + Next + + ' finish creating the source to inject + + sourceBuilder.Append(" + + End Sub + + End Module + +End Namespace") + + ' inject the created source into the users compilation + + context.AddSource("HelloWorldGenerated", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8)) + + End Sub + + End Class + +End Namespace \ No newline at end of file diff --git a/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/MustacheGenerator.vb b/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/MustacheGenerator.vb new file mode 100644 index 0000000000..38004dbe7a --- /dev/null +++ b/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/MustacheGenerator.vb @@ -0,0 +1,146 @@ +Option Explicit On +Option Infer On +Option Strict On + +Imports System.Collections.Immutable +Imports System.Text + +Imports Microsoft.CodeAnalysis +Imports Microsoft.CodeAnalysis.Text + +Imports Microsoft.CodeAnalysis.VisualBasic +Imports Microsoft.CodeAnalysis.VisualBasic.Syntax + +Namespace SourceGeneratorSamples + + + Public Class MustacheGenerator + Implements ISourceGenerator + + Public Sub Initialize(context As GeneratorInitializationContext) Implements ISourceGenerator.Initialize + ' No initialization required + End Sub + + Public Sub Execute(context As GeneratorExecutionContext) Implements ISourceGenerator.Execute + + Dim attributeSource = "Option Explicit On +Option Strict On +Option Infer On + +Namespace Global + + + Friend NotInheritable Class MustacheAttribute + Inherits System.Attribute + + Public ReadOnly Property Name As String + Public ReadOnly Property Template As String + Public ReadOnly Property Hash As String + + Public Sub New(name As String, template As String, hash As String) + Me.Name = name + Me.Template = template + Me.Hash = hash + End Sub + + End Class + +End Namespace +" + + context.AddSource("Mustache_MainAttributes__", SourceText.From(attributeSource, Encoding.UTF8)) + + Dim compilation = context.Compilation + + Dim options = GetMustacheOptions(compilation) + Dim namesSources = SourceFilesFromMustachePaths(options) + + For Each entry In namesSources + context.AddSource($"Mustache{entry.Item1}", SourceText.From(entry.Item2, Encoding.UTF8)) + Next + + End Sub + + Private Shared Iterator Function GetMustacheOptions(compilation As Compilation) As IEnumerable(Of (String, String, String)) + + ' Get all Mustache attributes + + Dim allNodes = compilation.SyntaxTrees.SelectMany(Function(s) s.GetRoot().DescendantNodes()) + + + Dim allAttributes = allNodes.Where(Function(d) d.IsKind(SyntaxKind.Attribute)).OfType(Of AttributeSyntax)() + Dim attributes = allAttributes.Where(Function(d) d.Name.ToString() = "Mustache").ToImmutableArray() + + Dim models = compilation.SyntaxTrees.[Select](Function(st) compilation.GetSemanticModel(st)) + For Each att In attributes + + Dim mustacheName = "" + Dim template = "" + Dim hash = "" + Dim index = 0 + + If att.ArgumentList Is Nothing Then + Throw New Exception("Can't be null here") + End If + + Dim m = compilation.GetSemanticModel(att.SyntaxTree) + + For Each arg In att.ArgumentList.Arguments + + Dim expr As ExpressionSyntax = Nothing + If TypeOf arg Is SimpleArgumentSyntax Then + expr = TryCast(arg, SimpleArgumentSyntax).Expression + End If + If expr Is Nothing Then + Continue For + End If + + Dim t = m.GetTypeInfo(expr) + Dim v = m.GetConstantValue(expr) + If index = 0 Then + mustacheName = v.ToString() + ElseIf index = 1 Then + template = v.ToString() + Else + hash = v.ToString() + End If + index += 1 + + Next + + Yield (mustacheName, template, hash) + + Next + + End Function + + Private Shared Function SourceFileFromMustachePath(name As String, template As String, hash As String) As String + Dim tree = HandlebarsDotNet.Handlebars.Compile(template) + Dim o = Newtonsoft.Json.JsonConvert.DeserializeObject(hash) + Dim mustacheText = tree(o) + Return GenerateMustacheClass(name, mustacheText) + End Function + + Private Shared Iterator Function SourceFilesFromMustachePaths(pathsData As IEnumerable(Of (Name As String, Template As String, Hash As String))) As IEnumerable(Of (Name As String, Code As String)) + For Each entry In pathsData + Yield (entry.Name, SourceFileFromMustachePath(entry.Name, entry.Template, entry.Hash)) + Next + End Function + + Private Shared Function GenerateMustacheClass(className As String, mustacheText As String) As String + Return $" + +Namespace Global.Mustache + + Partial Public Module Constants + + Public Const {className} As String = ""{mustacheText.Replace("""", """""")}"" + + End Module + +End Namespace" + End Function + + End Class + +End Namespace \ No newline at end of file diff --git a/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/SettingsXmlGenerator.vb b/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/SettingsXmlGenerator.vb new file mode 100644 index 0000000000..1754315d85 --- /dev/null +++ b/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/SettingsXmlGenerator.vb @@ -0,0 +1,102 @@ +Option Explicit On +Option Infer On +Option Strict On + +Imports System.IO +Imports System.Text +Imports System.Xml + +Imports Microsoft.CodeAnalysis +Imports Microsoft.CodeAnalysis.Text + +Namespace SourceGeneratorSamples + + + Public Class SettingsXmlGenerator + Implements ISourceGenerator + + Public Sub Initialize(context As GeneratorInitializationContext) Implements ISourceGenerator.Initialize + + End Sub + + Public Sub Execute(context As GeneratorExecutionContext) Implements ISourceGenerator.Execute + ' Using the context, get any additional files that end in .xmlsettings + For Each settingsFile In context.AdditionalFiles.Where(Function(at) at.Path.EndsWith(".xmlsettings")) + ProcessSettingsFile(settingsFile, context) + Next + End Sub + + Private Sub ProcessSettingsFile(xmlFile As AdditionalText, context As GeneratorExecutionContext) + + ' try and load the settings file + Dim xmlDoc As New XmlDocument + Dim text = xmlFile.GetText(context.CancellationToken).ToString() + Try + xmlDoc.LoadXml(text) + Catch + 'TODO: issue a diagnostic that says we couldn't parse it + Return + End Try + + ' create a class in the XmlSetting class that represnts this entry, and a static field that contains a singleton instance. + Dim fileName = Path.GetFileName(xmlFile.Path) + Dim name = xmlDoc.DocumentElement.GetAttribute("name") + + Dim sb = New StringBuilder($"Option Explicit On +Option Strict On +Option Infer On + +Imports System.Xml + +Namespace Global.AutoSettings + + Partial Public Class XmlSettings + + Public Shared ReadOnly Property {name} As {name}Settings = New {name}Settings(""{fileName}"") + + Public Class {name}Settings + + Private m_xmlDoc As New XmlDocument() + + Private m_fileName As String + + Friend Sub New(fileName As String) + m_fileName = fileName + m_xmlDoc.Load(m_fileName) + End Sub + + Public Function GetLocation() As String + Return m_fileName + End Function") + + For i = 0 To xmlDoc.DocumentElement.ChildNodes.Count - 1 + + Dim setting = CType(xmlDoc.DocumentElement.ChildNodes(i), XmlElement) + Dim settingName = setting.GetAttribute("name") + Dim settingType = setting.GetAttribute("type") + + sb.Append($" + + Public ReadOnly Property {settingName} As {settingType} + Get + Return DirectCast(Convert.ChangeType(DirectCast(m_xmlDoc.DocumentElement.ChildNodes({i}), XmlElement).InnerText, GetType({settingType})), {settingType}) + End Get + End Property") + + Next + + sb.Append(" + + End Class + + End Class + +End Namespace") + + context.AddSource($"Settings_{name}", SourceText.From(sb.ToString(), Encoding.UTF8)) + + End Sub + + End Class + +End Namespace \ No newline at end of file diff --git a/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/SourceGeneratorSamples.vbproj b/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/SourceGeneratorSamples.vbproj new file mode 100644 index 0000000000..f719a9496d --- /dev/null +++ b/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/SourceGeneratorSamples.vbproj @@ -0,0 +1,31 @@ + + + + netstandard2.0 + + + + + + + + + + + + + + + + $(GetTargetPathDependsOn);GetDependencyTargetPaths + + + + + + + + + + + \ No newline at end of file diff --git a/samples/VisualBasic/SourceGenerators/SourceGenerators.sln b/samples/VisualBasic/SourceGenerators/SourceGenerators.sln new file mode 100644 index 0000000000..6b79479857 --- /dev/null +++ b/samples/VisualBasic/SourceGenerators/SourceGenerators.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30022.13 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "GeneratedDemo", "GeneratedDemo\GeneratedDemo.vbproj", "{08612C19-D039-44D1-9030-D192CEAF05BB}" +EndProject +Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "SourceGeneratorSamples", "SourceGeneratorSamples\SourceGeneratorSamples.vbproj", "{90BDB1C3-E353-448C-8A29-E5B2EF10670B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {08612C19-D039-44D1-9030-D192CEAF05BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {08612C19-D039-44D1-9030-D192CEAF05BB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {08612C19-D039-44D1-9030-D192CEAF05BB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {08612C19-D039-44D1-9030-D192CEAF05BB}.Release|Any CPU.Build.0 = Release|Any CPU + {90BDB1C3-E353-448C-8A29-E5B2EF10670B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {90BDB1C3-E353-448C-8A29-E5B2EF10670B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {90BDB1C3-E353-448C-8A29-E5B2EF10670B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {90BDB1C3-E353-448C-8A29-E5B2EF10670B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {623C84B6-B8A4-4F29-8E68-4ED37D4529D5} + EndGlobalSection +EndGlobal