diff --git a/samples/CSharp/SourceGenerators/GeneratedDemo/CSharpGeneratedDemo.csproj b/samples/CSharp/SourceGenerators/GeneratedDemo/CSharpGeneratedDemo.csproj index 4b3054e9f..650c9bad6 100644 --- a/samples/CSharp/SourceGenerators/GeneratedDemo/CSharpGeneratedDemo.csproj +++ b/samples/CSharp/SourceGenerators/GeneratedDemo/CSharpGeneratedDemo.csproj @@ -3,6 +3,7 @@ Exe netcoreapp3.1 + 10.0 diff --git a/samples/CSharp/SourceGenerators/GeneratedDemo/Program.cs b/samples/CSharp/SourceGenerators/GeneratedDemo/Program.cs index 297582c51..49f29005e 100644 --- a/samples/CSharp/SourceGenerators/GeneratedDemo/Program.cs +++ b/samples/CSharp/SourceGenerators/GeneratedDemo/Program.cs @@ -1,29 +1,28 @@ using System; -namespace GeneratedDemo +namespace GeneratedDemo; + +class Program { - class Program + static void Main() { - static void Main(string[] args) - { - // Run the various scenarios - Console.WriteLine("Running HelloWorld:\n"); - UseHelloWorldGenerator.Run(); + // Run the various scenarios + Console.WriteLine("Running HelloWorld:\n"); + UseHelloWorldGenerator.Run(); - Console.WriteLine("\n\nRunning AutoNotify:\n"); - UseAutoNotifyGenerator.Run(); + Console.WriteLine("\n\nRunning AutoNotify:\n"); + UseAutoNotifyGenerator.Run(); - Console.WriteLine("\n\nRunning XmlSettings:\n"); - UseXmlSettingsGenerator.Run(); + Console.WriteLine("\n\nRunning XmlSettings:\n"); + UseXmlSettingsGenerator.Run(); - Console.WriteLine("\n\nRunning CsvGenerator:\n"); - UseCsvGenerator.Run(); + Console.WriteLine("\n\nRunning CsvGenerator:\n"); + UseCsvGenerator.Run(); - Console.WriteLine("\n\nRunning MustacheGenerator:\n"); - UseMustacheGenerator.Run(); + Console.WriteLine("\n\nRunning MustacheGenerator:\n"); + UseMustacheGenerator.Run(); - Console.WriteLine("\n\nRunning MathsGenerator:\n"); - UseMathsGenerator.Run(); - } + Console.WriteLine("\n\nRunning MathsGenerator:\n"); + UseMathsGenerator.Run(); } } diff --git a/samples/CSharp/SourceGenerators/GeneratedDemo/UseAutoNotifyGenerator.cs b/samples/CSharp/SourceGenerators/GeneratedDemo/UseAutoNotifyGenerator.cs index 77e346c9a..0325b80b8 100644 --- a/samples/CSharp/SourceGenerators/GeneratedDemo/UseAutoNotifyGenerator.cs +++ b/samples/CSharp/SourceGenerators/GeneratedDemo/UseAutoNotifyGenerator.cs @@ -1,39 +1,38 @@ using System; using AutoNotify; -namespace GeneratedDemo +namespace GeneratedDemo; + +// The view model we'd like to augment +public partial class ExampleViewModel { - // The view model we'd like to augment - public partial class ExampleViewModel - { - [AutoNotify] - private string _text = "private field text"; + [AutoNotify] + private string _text = "private field text"; - [AutoNotify(PropertyName = "Count")] - private int _amount = 5; - } + [AutoNotify(PropertyName = "Count")] + private int _amount = 5; +} - public static class UseAutoNotifyGenerator +public static class UseAutoNotifyGenerator +{ + public static void Run() { - public static void Run() - { - ExampleViewModel vm = new ExampleViewModel(); - - // we didn't explicitly create the 'Text' property, it was generated for us - string text = vm.Text; - Console.WriteLine($"Text = {text}"); - - // Properties can have differnt names generated based on the PropertyName argument of the attribute - int count = vm.Count; - Console.WriteLine($"Count = {count}"); - - // the viewmodel will automatically implement INotifyPropertyChanged - vm.PropertyChanged += (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 [AutoNotify] attribute - // You'll see the matching generated properties visibile in IntelliSense in realtime - } + ExampleViewModel vm = new ExampleViewModel(); + + // we didn't explicitly create the 'Text' property, it was generated for us + string text = vm.Text; + Console.WriteLine($"Text = {text}"); + + // Properties can have differnt names generated based on the PropertyName argument of the attribute + int count = vm.Count; + Console.WriteLine($"Count = {count}"); + + // the viewmodel will automatically implement INotifyPropertyChanged + vm.PropertyChanged += (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 [AutoNotify] attribute + // You'll see the matching generated properties visibile in IntelliSense in realtime } } diff --git a/samples/CSharp/SourceGenerators/GeneratedDemo/UseCsvGenerator.cs b/samples/CSharp/SourceGenerators/GeneratedDemo/UseCsvGenerator.cs index 7f7291d0e..b619a90c7 100644 --- a/samples/CSharp/SourceGenerators/GeneratedDemo/UseCsvGenerator.cs +++ b/samples/CSharp/SourceGenerators/GeneratedDemo/UseCsvGenerator.cs @@ -5,16 +5,15 @@ using static System.Console; using CSV; -namespace GeneratedDemo +namespace GeneratedDemo; + +class UseCsvGenerator { - class UseCsvGenerator + public static void Run() { - public static void Run() - { - WriteLine("## CARS"); - Cars.All.ToList().ForEach(c => WriteLine($"{c.Brand}\t{c.Model}\t{c.Year}\t{c.Cc}")); - WriteLine("\n## PEOPLE"); - People.All.ToList().ForEach(p => WriteLine($"{p.Name}\t{p.Address}\t{p._11Age}")); - } + WriteLine("## CARS"); + Cars.All.ToList().ForEach(c => WriteLine($"{c.Brand}\t{c.Model}\t{c.Year}\t{c.Cc}")); + WriteLine("\n## PEOPLE"); + People.All.ToList().ForEach(p => WriteLine($"{p.Name}\t{p.Address}\t{p._11Age}")); } } diff --git a/samples/CSharp/SourceGenerators/GeneratedDemo/UseHelloWorldGenerator.cs b/samples/CSharp/SourceGenerators/GeneratedDemo/UseHelloWorldGenerator.cs index 000328660..f40871fab 100644 --- a/samples/CSharp/SourceGenerators/GeneratedDemo/UseHelloWorldGenerator.cs +++ b/samples/CSharp/SourceGenerators/GeneratedDemo/UseHelloWorldGenerator.cs @@ -1,11 +1,10 @@ -namespace GeneratedDemo +namespace GeneratedDemo; + +public static class UseHelloWorldGenerator { - public static class UseHelloWorldGenerator + public static void Run() { - public static void Run() - { - // The static call below is generated at build time, and will list the syntax trees used in the compilation - HelloWorldGenerated.HelloWorld.SayHello(); - } + // The static call below is generated at build time, and will list the syntax trees used in the compilation + HelloWorldGenerated.HelloWorld.SayHello(); } } diff --git a/samples/CSharp/SourceGenerators/GeneratedDemo/UseMathsGenerator.cs b/samples/CSharp/SourceGenerators/GeneratedDemo/UseMathsGenerator.cs index 2c8b7ecf0..7523e3d1e 100644 --- a/samples/CSharp/SourceGenerators/GeneratedDemo/UseMathsGenerator.cs +++ b/samples/CSharp/SourceGenerators/GeneratedDemo/UseMathsGenerator.cs @@ -1,15 +1,14 @@ using static System.Console; using Maths; -namespace GeneratedDemo +namespace GeneratedDemo; + +public static class UseMathsGenerator { - public static class UseMathsGenerator + public static void Run() { - public static void Run() - { - WriteLine($"The area of a (10, 5) rectangle is: {Formulas.AreaRectangle(10, 5)}"); - WriteLine($"The area of a (10) square is: {Formulas.AreaSquare(10)}"); - WriteLine($"The GoldHarmon of 3 is: {Formulas.GoldHarm(3)}"); - } + WriteLine($"The area of a (10, 5) rectangle is: {Formulas.AreaRectangle(10, 5)}"); + WriteLine($"The area of a (10) square is: {Formulas.AreaSquare(10)}"); + WriteLine($"The GoldHarmon of 3 is: {Formulas.GoldHarm(3)}"); } } diff --git a/samples/CSharp/SourceGenerators/GeneratedDemo/UseMustacheGenerator.cs b/samples/CSharp/SourceGenerators/GeneratedDemo/UseMustacheGenerator.cs index 992d56800..e749b1270 100644 --- a/samples/CSharp/SourceGenerators/GeneratedDemo/UseMustacheGenerator.cs +++ b/samples/CSharp/SourceGenerators/GeneratedDemo/UseMustacheGenerator.cs @@ -12,28 +12,28 @@ [assembly: Mustache("Section", t4, h4)] [assembly: Mustache("NestedSection", t5, h5)] -namespace GeneratedDemo +namespace GeneratedDemo; + +class UseMustacheGenerator { - class UseMustacheGenerator + public static void Run() { - public static void Run() - { - WriteLine(Mustache.Constants.Lottery); - WriteLine(Mustache.Constants.HR); - WriteLine(Mustache.Constants.HTML); - WriteLine(Mustache.Constants.Section); - WriteLine(Mustache.Constants.NestedSection); - } + WriteLine(Mustache.Constants.Lottery); + WriteLine(Mustache.Constants.HR); + WriteLine(Mustache.Constants.HTML); + WriteLine(Mustache.Constants.Section); + WriteLine(Mustache.Constants.NestedSection); + } - // Mustache templates and hashes from the manual at https://mustache.github.io/mustache.1.html... - public const string t1 = @" + // Mustache templates and hashes from the manual at https://mustache.github.io/mustache.1.html... + public const string t1 = @" Hello {{name}} You have just won {{value}} dollars! {{#in_ca}} Well, {{taxed_value}} dollars, after taxes. {{/in_ca}} "; - public const string h1 = @" + public const string h1 = @" { ""name"": ""Chris"", ""value"": 10000, @@ -41,35 +41,35 @@ You have just won {{value}} dollars! ""in_ca"": true } "; - public const string t2 = @" + public const string t2 = @" * {{name}} * {{age}} * {{company}} * {{{company}}} "; - public const string h2 = @" + public const string h2 = @" { ""name"": ""Chris"", ""company"": ""GitHub"" } "; - public const string t3 = @" + public const string t3 = @" Shown {{#person}} Never shown! {{/person}} "; - public const string h3 = @" + public const string h3 = @" { ""person"": false } "; - public const string t4 = @" + public const string t4 = @" {{#repo}} {{name}} {{/repo}} "; - public const string h4 = @" + public const string h4 = @" { ""repo"": [ { ""name"": ""resque"" }, @@ -78,7 +78,7 @@ Never shown! ] } "; - public const string t5 = @" + public const string t5 = @" {{#repo}} {{name}} {{#nested}} @@ -86,7 +86,7 @@ Never shown! {{/nested}} {{/repo}} "; - public const string h5 = @" + public const string h5 = @" { ""repo"": [ { ""name"": ""resque"", ""nested"":[{""name"":""nestedResque""}] }, @@ -96,5 +96,4 @@ Never shown! } "; - } } diff --git a/samples/CSharp/SourceGenerators/GeneratedDemo/UseXmlSettingsGenerator.cs b/samples/CSharp/SourceGenerators/GeneratedDemo/UseXmlSettingsGenerator.cs index 213181e51..d5631238e 100644 --- a/samples/CSharp/SourceGenerators/GeneratedDemo/UseXmlSettingsGenerator.cs +++ b/samples/CSharp/SourceGenerators/GeneratedDemo/UseXmlSettingsGenerator.cs @@ -1,27 +1,26 @@ using System; using AutoSettings; -namespace GeneratedDemo +namespace GeneratedDemo; + +public static class UseXmlSettingsGenerator { - public static class UseXmlSettingsGenerator + public static void Run() { - public static void Run() - { - // This XmlSettings generator makes a static property in the XmlSettings class for each .xmlsettings file + // 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 - XmlSettings.MainSettings main = XmlSettings.Main; - Console.WriteLine($"Reading settings from {main.GetLocation()}"); + // here we have the 'Main' settings file from MainSettings.xmlsettings + // the name is determined by the 'name' attribute of the root settings element + XmlSettings.MainSettings main = XmlSettings.Main; + Console.WriteLine($"Reading settings from {main.GetLocation()}"); - // settings are strongly typed and can be read directly from the static instance - bool firstRun = XmlSettings.Main.FirstRun; - Console.WriteLine($"Setting firstRun = {firstRun}"); + // settings are strongly typed and can be read directly from the static instance + bool firstRun = XmlSettings.Main.FirstRun; + Console.WriteLine($"Setting firstRun = {firstRun}"); - int cacheSize = XmlSettings.Main.CacheSize; - Console.WriteLine($"Setting cacheSize = {cacheSize}"); + int cacheSize = 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 - } + // Try adding some keys to the settings file and see the settings become available to read from } } diff --git a/samples/CSharp/SourceGenerators/README.md b/samples/CSharp/SourceGenerators/README.md index 958cf03f6..83fdc60ed 100644 --- a/samples/CSharp/SourceGenerators/README.md +++ b/samples/CSharp/SourceGenerators/README.md @@ -3,7 +3,7 @@ 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 infomation on the Source Generators feature, see the [design document](https://github.com/dotnet/roslyn/blob/main/docs/features/source-generators.md). +For more information on the Source Generators feature, see the [design document](https://github.com/dotnet/roslyn/blob/main/docs/features/source-generators.md). Prerequisites ----- diff --git a/samples/CSharp/SourceGenerators/SourceGeneratorSamples/.editorconfig b/samples/CSharp/SourceGenerators/SourceGeneratorSamples/.editorconfig new file mode 100644 index 000000000..bb06d5e60 --- /dev/null +++ b/samples/CSharp/SourceGenerators/SourceGeneratorSamples/.editorconfig @@ -0,0 +1,103 @@ + +# Avoid "this." and "Me." if not necessary +dotnet_style_qualification_for_field = true:error +dotnet_style_qualification_for_property = true:error +dotnet_style_qualification_for_method = true:error +dotnet_style_qualification_for_event = true:error + +# Use language keywords instead of framework type names for type references +dotnet_style_predefined_type_for_locals_parameters_members = true:error +dotnet_style_predefined_type_for_member_access = true:error + +# Suggest more modern language features when available +dotnet_style_object_initializer = true:error +dotnet_style_collection_initializer = true:error +dotnet_style_coalesce_expression = true:error +dotnet_style_null_propagation = true:error +dotnet_style_explicit_tuple_names = true:error + +# CSharp code style settings: +[*.cs] +# Prefer "var" everywhere +csharp_style_var_for_built_in_types = true:silent +csharp_style_var_when_type_is_apparent = true:silent +csharp_style_var_elsewhere = true:silent + +# Suggest more modern language features when available +csharp_style_pattern_matching_over_is_with_cast_check = true:error +csharp_style_pattern_matching_over_as_with_null_check = true:error +csharp_style_inlined_variable_declaration = true:error +csharp_style_throw_expression = true:error +csharp_style_conditional_delegate_call = true:error +csharp_using_directive_placement = outside_namespace:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_prefer_braces = true:silent +csharp_style_namespace_declarations = block_scoped:silent +csharp_style_expression_bodied_methods = false:none +csharp_style_expression_bodied_constructors = false:none +csharp_style_expression_bodied_operators = false:none +csharp_style_expression_bodied_properties = true:none +csharp_style_expression_bodied_indexers = true:none +csharp_style_expression_bodied_accessors = true:none +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_indent_labels = one_less_than_current +csharp_style_deconstructed_variable_declaration = true:suggestion + +[*.{cs,vb}] +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf +dotnet_style_collection_initializer = true:suggestion diff --git a/samples/CSharp/SourceGenerators/SourceGeneratorSamples/AutoNotifyGenerator.cs b/samples/CSharp/SourceGenerators/SourceGeneratorSamples/AutoNotifyGenerator.cs index 3c8008f3e..7904ea4d2 100644 --- a/samples/CSharp/SourceGenerators/SourceGeneratorSamples/AutoNotifyGenerator.cs +++ b/samples/CSharp/SourceGenerators/SourceGeneratorSamples/AutoNotifyGenerator.cs @@ -1,18 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Text; - -namespace SourceGeneratorSamples +namespace SourceGeneratorSamples; + +[Generator] +public class AutoNotifyGenerator : IIncrementalGenerator { - [Generator] - public class AutoNotifyGenerator : ISourceGenerator - { - private const string attributeText = @" + private const string attributeText = @" using System; namespace AutoNotify { @@ -29,84 +20,55 @@ public AutoNotifyAttribute() "; - public void Initialize(GeneratorInitializationContext context) - { - // Register the attribute source - context.RegisterForPostInitialization((i) => i.AddSource("AutoNotifyAttribute.g.cs", attributeText)); - - // Register a syntax receiver that will be created for each generation pass - context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); - } - - public void Execute(GeneratorExecutionContext context) - { - // retrieve the populated receiver - if (!(context.SyntaxContextReceiver is SyntaxReceiver receiver)) - return; - - // get the added attribute, and INotifyPropertyChanged - INamedTypeSymbol attributeSymbol = context.Compilation.GetTypeByMetadataName("AutoNotify.AutoNotifyAttribute"); - INamedTypeSymbol notifySymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanged"); - - // group the fields by class, and generate the source - foreach (IGrouping group in receiver.Fields.GroupBy(f => f.ContainingType, SymbolEqualityComparer.Default)) - { - string classSource = ProcessClass(group.Key, group.ToList(), attributeSymbol, notifySymbol, context); - context.AddSource($"{group.Key.Name}_autoNotify.g.cs", SourceText.From(classSource, Encoding.UTF8)); - } - } - - private string ProcessClass(INamedTypeSymbol classSymbol, List fields, ISymbol attributeSymbol, ISymbol notifySymbol, GeneratorExecutionContext context) - { - if (!classSymbol.ContainingSymbol.Equals(classSymbol.ContainingNamespace, SymbolEqualityComparer.Default)) - { - return null; //TODO: issue a diagnostic that it must be top level - } + private string? ProcessClass(INamedTypeSymbol classSymbol, List fields, ISymbol attributeSymbol, ISymbol notifySymbol) + { + if (!classSymbol.ContainingSymbol.Equals(classSymbol.ContainingNamespace, SymbolEqualityComparer.Default)) + return null; //TODO: issue a diagnostic that it must be top level - string namespaceName = classSymbol.ContainingNamespace.ToDisplayString(); + string namespaceName = classSymbol.ContainingNamespace.ToDisplayString(); - // begin building the generated source - StringBuilder source = new StringBuilder($@" + // begin building the generated source + StringBuilder source = new ($@" namespace {namespaceName} {{ public partial class {classSymbol.Name} : {notifySymbol.ToDisplayString()} {{ "); - // if the class doesn't implement INotifyPropertyChanged already, add it - if (!classSymbol.Interfaces.Contains(notifySymbol, SymbolEqualityComparer.Default)) - { - source.Append("public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;"); - } - - // create properties for each field - foreach (IFieldSymbol fieldSymbol in fields) - { - ProcessField(source, fieldSymbol, attributeSymbol); - } - - source.Append("} }"); - return source.ToString(); + // if the class doesn't implement INotifyPropertyChanged already, add it + if (!classSymbol.Interfaces.Contains(notifySymbol, SymbolEqualityComparer.Default)) + { + source.Append("public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;"); } - private void ProcessField(StringBuilder source, IFieldSymbol fieldSymbol, ISymbol attributeSymbol) + // create properties for each field + foreach (IFieldSymbol fieldSymbol in fields) { - // get the name and type of the field - string fieldName = fieldSymbol.Name; - ITypeSymbol fieldType = fieldSymbol.Type; + ProcessField(source, fieldSymbol, attributeSymbol); + } - // get the AutoNotify attribute from the field, and any associated data - AttributeData attributeData = fieldSymbol.GetAttributes().Single(ad => ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default)); - TypedConstant overridenNameOpt = attributeData.NamedArguments.SingleOrDefault(kvp => kvp.Key == "PropertyName").Value; + source.Append("} }"); + return source.ToString(); + } - string propertyName = chooseName(fieldName, overridenNameOpt); - if (propertyName.Length == 0 || propertyName == fieldName) - { - //TODO: issue a diagnostic that we can't process this field - return; - } + private void ProcessField(StringBuilder source, IFieldSymbol fieldSymbol, ISymbol attributeSymbol) + { + // get the name and type of the field + string fieldName = fieldSymbol.Name; + ITypeSymbol fieldType = fieldSymbol.Type; + + // get the AutoNotify attribute from the field, and any associated data + AttributeData attributeData = fieldSymbol.GetAttributes().Single(ad => ad.AttributeClass!.Equals(attributeSymbol, SymbolEqualityComparer.Default)); + TypedConstant overridenNameOpt = attributeData.NamedArguments.SingleOrDefault(kvp => kvp.Key == "PropertyName").Value; - source.Append($@" + string propertyName = chooseName(fieldName, overridenNameOpt); + if (propertyName.Length == 0 || propertyName == fieldName) + { + //TODO: issue a diagnostic that we can't process this field + return; + } + + source.Append($@" public {fieldType} {propertyName} {{ get @@ -123,52 +85,66 @@ private void ProcessField(StringBuilder source, IFieldSymbol fieldSymbol, ISymbo "); - string chooseName(string fieldName, TypedConstant overridenNameOpt) + static string chooseName(string fieldName, TypedConstant overridenNameOpt) + { + if (!overridenNameOpt.IsNull) { - if (!overridenNameOpt.IsNull) - { - return overridenNameOpt.Value.ToString(); - } - - fieldName = fieldName.TrimStart('_'); - if (fieldName.Length == 0) - return string.Empty; + return overridenNameOpt.Value!.ToString(); + } - if (fieldName.Length == 1) - return fieldName.ToUpper(); + fieldName = fieldName.TrimStart('_'); + if (fieldName.Length == 0) + return string.Empty; - return fieldName.Substring(0, 1).ToUpper() + fieldName.Substring(1); - } + if (fieldName.Length == 1) + return fieldName.ToUpper(); + return fieldName.Substring(0, 1).ToUpper() + fieldName.Substring(1); } - /// - /// Created on demand before each generation pass - /// - class SyntaxReceiver : ISyntaxContextReceiver + } + + public void GenerateCode(SourceProductionContext context, (ImmutableArray symbols, Compilation compilation) source) + { + // get the added attribute, and INotifyPropertyChanged + INamedTypeSymbol attributeSymbol = source.compilation.GetTypeByMetadataName("AutoNotify.AutoNotifyAttribute")!; + INamedTypeSymbol notifySymbol = source.compilation.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanged")!; + + // group the fields by class, and generate the source + foreach (IGrouping group in source.symbols.GroupBy(f => f.ContainingType, SymbolEqualityComparer.Default)) { - public List Fields { get; } = new List(); + string? classSource = ProcessClass(group.Key, group.ToList(), attributeSymbol, notifySymbol); + if(classSource is not null) + context.AddSource($"{group.Key.Name}_autoNotify.g.cs", SourceText.From(classSource, Encoding.UTF8)); + } + } + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput((i) => i.AddSource("AutoNotifyAttribute", attributeText)); - /// - /// Called for every syntax node in the compilation, we can inspect the nodes and save any information useful for generation - /// - public void OnVisitSyntaxNode(GeneratorSyntaxContext context) - { - // any field with at least one attribute is a candidate for property generation - if (context.Node is FieldDeclarationSyntax fieldDeclarationSyntax - && fieldDeclarationSyntax.AttributeLists.Count > 0) - { - foreach (VariableDeclaratorSyntax variable in fieldDeclarationSyntax.Declaration.Variables) - { - // Get the symbol being declared by the field, and keep it if its annotated - IFieldSymbol fieldSymbol = context.SemanticModel.GetDeclaredSymbol(variable) as IFieldSymbol; - if (fieldSymbol.GetAttributes().Any(ad => ad.AttributeClass.ToDisplayString() == "AutoNotify.AutoNotifyAttribute")) - { - Fields.Add(fieldSymbol); - } - } - } - } + var fieldsEnum = context.SyntaxProvider.CreateSyntaxProvider( + predicate: (node, ct) => node is FieldDeclarationSyntax fieldDeclarationSyntax + && fieldDeclarationSyntax.AttributeLists.Count > 0, + transform: FieldsTransform); + + var fields = fieldsEnum.SelectMany((en, _) => en); + + var collected = fields.Collect() + .Combine(context.CompilationProvider); + + context.RegisterSourceOutput(collected, GenerateCode); + } + + + IEnumerable FieldsTransform(GeneratorSyntaxContext context, CancellationToken ct) + { + var fieldDeclarationSyntax = (FieldDeclarationSyntax) context.Node; + + foreach (VariableDeclaratorSyntax variable in fieldDeclarationSyntax.Declaration.Variables) + { + IFieldSymbol? fieldSymbol = context.SemanticModel.GetDeclaredSymbol(variable) as IFieldSymbol; + if (fieldSymbol!.GetAttributes().Any(ad => ad.AttributeClass!.ToDisplayString() == "AutoNotify.AutoNotifyAttribute")) + yield return fieldSymbol; } } } diff --git a/samples/CSharp/SourceGenerators/SourceGeneratorSamples/CSharpSourceGeneratorSamples.csproj b/samples/CSharp/SourceGenerators/SourceGeneratorSamples/CSharpSourceGeneratorSamples.csproj index 47c428cf5..3c1472887 100644 --- a/samples/CSharp/SourceGenerators/SourceGeneratorSamples/CSharpSourceGeneratorSamples.csproj +++ b/samples/CSharp/SourceGenerators/SourceGeneratorSamples/CSharpSourceGeneratorSamples.csproj @@ -2,7 +2,8 @@ netstandard2.0 - 8.0 + 10.0 + enable diff --git a/samples/CSharp/SourceGenerators/SourceGeneratorSamples/CsvGenerator.cs b/samples/CSharp/SourceGenerators/SourceGeneratorSamples/CsvGenerator.cs index 710d491da..ab1c834b8 100644 --- a/samples/CSharp/SourceGenerators/SourceGeneratorSamples/CsvGenerator.cs +++ b/samples/CSharp/SourceGenerators/SourceGeneratorSamples/CsvGenerator.cs @@ -1,185 +1,174 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; -using NotVisualBasic.FileIO; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; -#nullable enable - -// CsvTextFileParser from https://github.com/22222/CsvTextFieldParser adding suppression rules for default VS config +namespace CsvGenerator; -namespace CsvGenerator +[Generator] +public class CSVGenerator : IIncrementalGenerator { - [Generator] - public class CSVGenerator : ISourceGenerator + public enum CsvLoadType { - public enum CsvLoadType - { - Startup, - OnDemand - } + Startup, + OnDemand + } + + // Guesses type of property for the object from the value of a csv field + public static string GetCsvFieldType(string exemplar) => exemplar switch + { + _ when bool.TryParse(exemplar, out _) => "bool", + _ when int.TryParse(exemplar, out _) => "int", + _ when double.TryParse(exemplar, out _) => "double", + _ => "string" + }; + + // 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 cas. If the file is completely empty, an error is generated. + public static (string[], string[], string[]?) ExtractProperties(CsvTextFieldParser parser) + { + string[]? headerFields = parser.ReadFields(); + if (headerFields == null) throw new Exception("Empty csv file!"); - // Guesses type of property for the object from the value of a csv field - public static string GetCsvFieldType(string exemplar) => exemplar switch + string[]? firstLineFields = parser.ReadFields(); + if (firstLineFields == null) { - _ when bool.TryParse(exemplar, out _) => "bool", - _ when int.TryParse(exemplar, out _) => "int", - _ when double.TryParse(exemplar, out _) => "double", - _ => "string" - }; - - // 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 cas. If the file is completely empty, an error is generated. - public static (string[], string[], string[]?) ExtractProperties(CsvTextFieldParser parser) + return (Enumerable.Repeat("string", headerFields.Length).ToArray(), headerFields, firstLineFields); + } + else { - string[]? headerFields = parser.ReadFields(); - if (headerFields == null) throw new Exception("Empty csv file!"); - - string[]? firstLineFields = parser.ReadFields(); - if (firstLineFields == null) - { - return (Enumerable.Repeat("string", headerFields.Length).ToArray(), headerFields, firstLineFields); - } - else - { - return (firstLineFields.Select(GetCsvFieldType).ToArray(), headerFields.Select(StringToValidPropertyName).ToArray(), firstLineFields); - } + return (firstLineFields.Select(GetCsvFieldType).ToArray(), headerFields.Select(StringToValidPropertyName).ToArray(), firstLineFields); } + } - // 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 static string GenerateClassFile(string className, string csvText, CsvLoadType loadTime, bool cacheObjects) - { - StringBuilder sb = new StringBuilder(); - using CsvTextFieldParser parser = new CsvTextFieldParser(new StringReader(csvText)); + // 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 static string GenerateClassFile(string className, string csvText, CsvLoadType loadTime, bool cacheObjects) + { + StringBuilder sb = new (); + using CsvTextFieldParser parser = new (new StringReader(csvText)); - //// Usings - sb.Append(@" + //// Usings + sb.Append(@" #nullable enable namespace CSV { using System.Collections.Generic; "); - //// Class Definition - sb.Append($" public class {className} {{\n"); + //// Class Definition + sb.Append($" public class {className} {{\n"); - if (loadTime == CsvLoadType.Startup) - { - sb.Append(@$" + if (loadTime == CsvLoadType.Startup) + { + sb.Append(@$" static {className}() {{ var x = All; }} "); - } - (string[] types, string[] names, string[]? fields) = ExtractProperties(parser); - int minLen = Math.Min(types.Length, names.Length); + } + (string[] types, string[] names, string[]? fields) = ExtractProperties(parser); + int minLen = Math.Min(types.Length, names.Length); - for (int i = 0; i < minLen; i++) - { - sb.AppendLine($" public {types[i]} {StringToValidPropertyName(names[i])} {{ get; set;}} = default!;"); - } - sb.Append("\n"); + for (int i = 0; i < minLen; i++) + { + sb.AppendLine($" public {types[i]} {StringToValidPropertyName(names[i])} {{ get; set;}} = default!;"); + } + sb.Append("\n"); - //// Loading data - sb.AppendLine($" static IEnumerable<{className}>? _all = null;"); - sb.Append($@" + //// Loading data + sb.AppendLine($" static IEnumerable<{className}>? _all = null;"); + sb.Append($@" public static IEnumerable<{className}> All {{ get {{"); - if (cacheObjects) sb.Append(@" + if (cacheObjects) sb.Append(@" if(_all != null) return _all; "); - sb.Append(@$" + sb.Append(@$" List<{className}> l = new List<{className}>(); {className} c; "); - // This awkwardness comes from having to pre-read one row to figure out the types of props. - do + // This awkwardness comes from having to pre-read one row to figure out the types of props. + do + { + if (fields == null) continue; + if (fields.Length < minLen) throw new Exception("Not enough fields in CSV file."); + + sb.AppendLine($" c = new {className}();"); + string value = ""; + for (int i = 0; i < minLen; i++) { - if (fields == null) continue; - if (fields.Length < minLen) throw new Exception("Not enough fields in CSV file."); + // Wrap strings in quotes. + value = GetCsvFieldType(fields[i]) == "string" ? $"\"{fields[i].Trim().Trim(new char[] { '"' })}\"" : fields[i]; + sb.AppendLine($" c.{names[i]} = {value};"); + } + sb.AppendLine(" l.Add(c);"); - sb.AppendLine($" c = new {className}();"); - string value = ""; - for (int i = 0; i < minLen; i++) - { - // Wrap strings in quotes. - value = GetCsvFieldType(fields[i]) == "string" ? $"\"{fields[i].Trim().Trim(new char[] { '"' })}\"" : fields[i]; - sb.AppendLine($" c.{names[i]} = {value};"); - } - sb.AppendLine(" l.Add(c);"); + fields = parser.ReadFields(); + } while (!(fields == null)); - fields = parser.ReadFields(); - } while (!(fields == null)); + sb.AppendLine(" _all = l;"); + sb.AppendLine(" return l;"); - sb.AppendLine(" _all = l;"); - sb.AppendLine(" return l;"); + // Close things (property, class, namespace) + sb.Append(" }\n }\n }\n}\n"); + return sb.ToString(); - // Close things (property, class, namespace) - sb.Append(" }\n }\n }\n}\n"); - return sb.ToString(); + } - } + static string StringToValidPropertyName(string s) + { + s = s.Trim(); + s = char.IsLetter(s[0]) ? char.ToUpper(s[0]) + s.Substring(1) : s; + s = char.IsDigit(s.Trim()[0]) ? "_" + s : s; + s = new string(s.Select(ch => char.IsDigit(ch) || char.IsLetter(ch) ? ch : '_').ToArray()); + return s; + } - static string StringToValidPropertyName(string s) - { - s = s.Trim(); - s = char.IsLetter(s[0]) ? char.ToUpper(s[0]) + s.Substring(1) : s; - s = char.IsDigit(s.Trim()[0]) ? "_" + s : s; - s = new string(s.Select(ch => char.IsDigit(ch) || char.IsLetter(ch) ? ch : '_').ToArray()); - return s; - } + static IEnumerable<(string, string)> SourceFilesFromAdditionalFile(CsvLoadType loadTime, bool cacheObjects, AdditionalText file) + { + string className = Path.GetFileNameWithoutExtension(file.Path); + string csvText = file.GetText()!.ToString(); + return new (string, string)[] { (className, GenerateClassFile(className, csvText, loadTime, cacheObjects)) }; + } - static IEnumerable<(string, string)> SourceFilesFromAdditionalFile(CsvLoadType loadTime, bool cacheObjects, AdditionalText file) - { - string className = Path.GetFileNameWithoutExtension(file.Path); - string csvText = file.GetText()!.ToString(); - return new (string, string)[] { (className, GenerateClassFile(className, csvText, loadTime, cacheObjects)) }; - } + public (CsvLoadType, bool, AdditionalText) GetLoadOptionProvider( + (AdditionalText file, AnalyzerConfigOptionsProvider config) source, + CancellationToken ct) + { + var options = source.config.GetOptions(source.file); + + options.TryGetValue("build_metadata.additionalfiles.CsvLoadType", out string? loadTimeString); + Enum.TryParse(loadTimeString, ignoreCase: true, out CsvLoadType loadType); - static IEnumerable<(string, string)> SourceFilesFromAdditionalFiles(IEnumerable<(CsvLoadType loadTime, bool cacheObjects, AdditionalText file)> pathsData) - => pathsData.SelectMany(d => SourceFilesFromAdditionalFile(d.loadTime, d.cacheObjects, d.file)); + options.TryGetValue("build_metadata.additionalfiles.CacheObjects", out string? cacheObjectsString); + bool.TryParse(cacheObjectsString, out bool cacheObjects); - static IEnumerable<(CsvLoadType, bool, AdditionalText)> GetLoadOptions(GeneratorExecutionContext context) - { - foreach (AdditionalText file in context.AdditionalFiles) - { - if (Path.GetExtension(file.Path).Equals(".csv", StringComparison.OrdinalIgnoreCase)) - { - // are there any options for it? - context.AnalyzerConfigOptions.GetOptions(file).TryGetValue("build_metadata.additionalfiles.CsvLoadType", out string? loadTimeString); - Enum.TryParse(loadTimeString, ignoreCase: true, out CsvLoadType loadType); + return (loadType, cacheObjects, source.file); + } - context.AnalyzerConfigOptions.GetOptions(file).TryGetValue("build_metadata.additionalfiles.CacheObjects", out string? cacheObjectsString); - bool.TryParse(cacheObjectsString, out bool cacheObjects); + public void Initialize(IncrementalGeneratorInitializationContext context) + { + IncrementalValuesProvider files + = context.AdditionalTextsProvider.Where( static file => file.Path.EndsWith(".csv")); - yield return (loadType, cacheObjects, file); - } - } - } + IncrementalValuesProvider<(AdditionalText, AnalyzerConfigOptionsProvider)> + combined = files.Combine(context.AnalyzerConfigOptionsProvider); - public void Execute(GeneratorExecutionContext context) - { - IEnumerable<(CsvLoadType, bool, AdditionalText)> options = GetLoadOptions(context); - IEnumerable<(string, string)> nameCodeSequence = SourceFilesFromAdditionalFiles(options); - foreach ((string name, string code) in nameCodeSequence) - context.AddSource($"Csv_{name}.g.cs", SourceText.From(code, Encoding.UTF8)); - } + IncrementalValuesProvider<(CsvLoadType, bool, AdditionalText)> transformed + = combined.Select(GetLoadOptionProvider); - public void Initialize(GeneratorInitializationContext context) - { - } + var selector = static ((CsvLoadType ltype, bool cache, AdditionalText text)source, CancellationToken ctoken) + => SourceFilesFromAdditionalFile(source.ltype, source.cache, source.text); + IncrementalValuesProvider<(string name, string code)> nameAndContent = transformed.SelectMany(selector); + + context.RegisterSourceOutput(nameAndContent, (spc, nameAndContent) => + spc.AddSource($"{nameAndContent.name}.g.cs", nameAndContent.code)); } } -#pragma warning restore IDE0008 // Use explicit type diff --git a/samples/CSharp/SourceGenerators/SourceGeneratorSamples/Globals.cs b/samples/CSharp/SourceGenerators/SourceGeneratorSamples/Globals.cs new file mode 100644 index 000000000..61591c2c3 --- /dev/null +++ b/samples/CSharp/SourceGenerators/SourceGeneratorSamples/Globals.cs @@ -0,0 +1,21 @@ +global using System; +global using System.Collections.Generic; +global using System.Collections.Immutable; +global using System.Text; +global using System.Threading; +global using Microsoft.CodeAnalysis; +global using Microsoft.CodeAnalysis.CSharp.Syntax; +global using Microsoft.CodeAnalysis.Text; +global using System.Linq; +global using Microsoft.CodeAnalysis.CSharp; +global using System.IO; +global using NotVisualBasic.FileIO; +global using System.Text.RegularExpressions; +global using Tokens = System.Collections.Generic.IEnumerable; +global using SymTable = System.Collections.Generic.HashSet; +global using System.Xml; + +global using static System.Console; + + + diff --git a/samples/CSharp/SourceGenerators/SourceGeneratorSamples/HelloWorldGenerator.cs b/samples/CSharp/SourceGenerators/SourceGeneratorSamples/HelloWorldGenerator.cs index 4a1b2b972..35a5c7476 100644 --- a/samples/CSharp/SourceGenerators/SourceGeneratorSamples/HelloWorldGenerator.cs +++ b/samples/CSharp/SourceGenerators/SourceGeneratorSamples/HelloWorldGenerator.cs @@ -1,51 +1,55 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; +namespace SourceGeneratorSamples; -namespace SourceGeneratorSamples +[Generator] +public class HelloWorldGenerator : IIncrementalGenerator { - [Generator] - public class HelloWorldGenerator : ISourceGenerator + public void Initialize(IncrementalGeneratorInitializationContext context) { - public void Execute(GeneratorExecutionContext context) - { - // begin creating the source we'll inject into the users compilation - StringBuilder sourceBuilder = new StringBuilder(@" + // Use this method to generate constant code (aka that doesn't depend on the syntax tree). + context.RegisterPostInitializationOutput(context => + context.AddSource("AStaticFunc.g.cs", @" + namespace HelloWorldGenerated; + static class Printer { + internal static void PrintUpper(string s) => System.Console.WriteLine(s.ToUpper()); + }") + ); + + // Dependency pipeline. Select class syntax nodes, transform to type symbols and collect their names. + var classNames = context.SyntaxProvider.CreateSyntaxProvider( + predicate: static (sn, c) => sn is ClassDeclarationSyntax, + transform: static (ct, c) => ct.SemanticModel.GetDeclaredSymbol(ct.Node, c)) + .Where (static t => t is not null) + .Select (static (t, c) => t!.Name) + .Collect(); + + // Register a function to generate the code using the collected type symbols. + context.RegisterSourceOutput(classNames, GenerateSource); + } + + // Main function to generate the source code. + private void GenerateSource(SourceProductionContext context, ImmutableArray typeNames) + { + // Begin creating the source we'll inject into the users compilation. + StringBuilder sourceBuilder = new(@" using System; -namespace HelloWorldGenerated -{ +namespace HelloWorldGenerated; public static class HelloWorld { public static void SayHello() { - Console.WriteLine(""Hello from generated code!""); - Console.WriteLine(""The following syntax trees existed in the compilation that created this program:""); + Printer.PrintUpper(""Hello from generated code!""); // Uses the Printer static class + Console.WriteLine(""The following classes existed in the compilation that created this program:""); "); - // using the context, get a list of syntax trees in the users compilation - IEnumerable syntaxTrees = context.Compilation.SyntaxTrees; + // Print out each symbol name we find. + foreach (var name in typeNames) + sourceBuilder.AppendLine($"Console.WriteLine(\"{name}\");"); - // add the filepath of each tree to the class we're building - foreach (SyntaxTree tree in syntaxTrees) - { - sourceBuilder.AppendLine($@"Console.WriteLine(@"" - {tree.FilePath}"");"); - } - - // finish creating the source to inject - sourceBuilder.Append(@" + // Finish creating the source to inject. + sourceBuilder.Append(@" } + }"); + context.AddSource($"hello_world.g.cs", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8)); } -}"); - - // inject the created source into the users compilation - context.AddSource("helloWorldGenerated.g.cs", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8)); - } - public void Initialize(GeneratorInitializationContext context) - { - // No initialization required - } - } } diff --git a/samples/CSharp/SourceGenerators/SourceGeneratorSamples/MathsGenerator.cs b/samples/CSharp/SourceGenerators/SourceGeneratorSamples/MathsGenerator.cs index 0b7645a06..b000a70bd 100644 --- a/samples/CSharp/SourceGenerators/SourceGeneratorSamples/MathsGenerator.cs +++ b/samples/CSharp/SourceGenerators/SourceGeneratorSamples/MathsGenerator.cs @@ -1,171 +1,138 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Text.RegularExpressions; -using static System.Console; -using Tokens = System.Collections.Generic.IEnumerable; -using SymTable = System.Collections.Generic.HashSet; -using System.Linq; -using Microsoft.CodeAnalysis; -using System.IO; -using Microsoft.CodeAnalysis.Text; -using System.Diagnostics; +namespace MathsGenerator; + +public enum TokenType { + Number, + Identifier, + Operation, + OpenParens, + CloseParens, + Equal, + EOL, + EOF, + Spaces, + Comma, + Sum, + None +} -#pragma warning disable IDE0008 // Use explicit type +public struct Token { + public TokenType Type; + public string Value; + public int Line; + public int Column; +} -namespace MathsGenerator -{ - public enum TokenType - { - Number, - Identifier, - Operation, - OpenParens, - CloseParens, - Equal, - EOL, - EOF, - Spaces, - Comma, - Sum, - None - } +public static class Lexer { - public struct Token - { - public TokenType Type; - public string Value; - public int Line; - public int Column; + public static void PrintTokens(IEnumerable tokens) { + foreach (var token in tokens) { + WriteLine($"{token.Line}, {token.Column}, {token.Type}, {token.Value}"); + } } - public static class Lexer - { - - public static void PrintTokens(IEnumerable tokens) - { - foreach (var token in tokens) - { - WriteLine($"{token.Line}, {token.Column}, {token.Type}, {token.Value}"); + static readonly (TokenType, string)[] tokenStrings = { + (TokenType.EOL, @"(\r\n|\r|\n)"), + (TokenType.Spaces, @"\s+"), + (TokenType.Number, @"[+-]?((\d+\.?\d*)|(\.\d+))"), + (TokenType.Identifier, @"[_a-zA-Z][`'""_a-zA-Z0-9]*"), + (TokenType.Operation, @"[\+\-/\*]"), + (TokenType.OpenParens, @"[([{]"), + (TokenType.CloseParens, @"[)\]}]"), + (TokenType.Equal, @"="), + (TokenType.Comma, @","), + (TokenType.Sum, @"∑") + }; + + static readonly IEnumerable<(TokenType, Regex)> tokenExpressions = + tokenStrings.Select( + t => (t.Item1, new Regex($"^{t.Item2}", RegexOptions.Compiled | RegexOptions.Singleline))); + + // Can be optimized with spans to avoid so many allocations ... + static public Tokens Tokenize(string source) { + var currentLine = 1; + var currentColumn = 1; + + while (source.Length > 0) { + + var matchLength = 0; + var tokenType = TokenType.None; + var value = ""; + + foreach (var (type, rule) in tokenExpressions) { + var match = rule.Match(source); + if(match.Success) { + matchLength = match.Length; + tokenType = type; + value = match.Value; + break; + } } - } - static (TokenType, string)[] tokenStrings = { - (TokenType.EOL, @"(\r\n|\r|\n)"), - (TokenType.Spaces, @"\s+"), - (TokenType.Number, @"[+-]?((\d+\.?\d*)|(\.\d+))"), - (TokenType.Identifier, @"[_a-zA-Z][`'""_a-zA-Z0-9]*"), - (TokenType.Operation, @"[\+\-/\*]"), - (TokenType.OpenParens, @"[([{]"), - (TokenType.CloseParens, @"[)\]}]"), - (TokenType.Equal, @"="), - (TokenType.Comma, @","), - (TokenType.Sum, @"∑") - }; - - static IEnumerable<(TokenType, Regex)> tokenExpressions = - tokenStrings.Select( - t => (t.Item1, new Regex($"^{t.Item2}", RegexOptions.Compiled | RegexOptions.Singleline))); - - // Can be optimized with spans to avoid so many allocations ... - static public Tokens Tokenize(string source) - { - var currentLine = 1; - var currentColumn = 1; - - while (source.Length > 0) - { - - var matchLength = 0; - var tokenType = TokenType.None; - var value = ""; - - foreach (var (type, rule) in tokenExpressions) - { - var match = rule.Match(source); - if (match.Success) - { - matchLength = match.Length; - tokenType = type; - value = match.Value; - break; - } - } + if (matchLength == 0) { - if (matchLength == 0) - { + throw new Exception($"Unrecognized symbol '{source[currentLine - 1]}' at index {currentLine - 1} (line {currentLine}, column {currentColumn})."); - throw new Exception($"Unrecognized symbol '{source[currentLine - 1]}' at index {currentLine - 1} (line {currentLine}, column {currentColumn})."); + } else { + if(tokenType != TokenType.Spaces) + yield return new Token { + Type = tokenType, + Value = value, + Line = currentLine, + Column = currentColumn + }; + + currentColumn += matchLength; + if(tokenType == TokenType.EOL) { + currentLine += 1; + currentColumn = 0; } - else - { - - if (tokenType != TokenType.Spaces) - yield return new Token - { - Type = tokenType, - Value = value, - Line = currentLine, - Column = currentColumn - }; - - currentColumn += matchLength; - if (tokenType == TokenType.EOL) - { - currentLine += 1; - currentColumn = 0; - } - - source = source.Substring(matchLength); - } - } - yield return new Token - { - Type = TokenType.EOF, - Line = currentLine, - Column = currentColumn - }; + source = source.Substring(matchLength); + } } - } - - /* EBNF for the language - lines = {line} EOF - line = {EOL} identifier [lround args rround] equal expr EOL {EOL} - args = identifier {comma identifier} - expr = [plus|minus] term { (plus|minus) term } - term = factor { (times|divide) factor }; - factor = number | var | func | sum | matrix | lround expr rround; - var = identifier; - func = identifier lround expr {comma expr} rround; - sum = ∑ lround identifier comma expr comma expr comma expr rround; - */ - public static class Parser - { + yield return new Token { + Type = TokenType.EOF, + Line = currentLine, + Column = currentColumn + }; + } +} - public static string Parse(Tokens tokens) - { - var globalSymbolTable = new SymTable(); - var symbolTable = new SymTable(); - var buffer = new StringBuilder(); - - var en = tokens.GetEnumerator(); - en.MoveNext(); - - buffer = Lines(new Context - { - tokens = en, - globalSymbolTable = globalSymbolTable, - symbolTable = symbolTable, - buffer = buffer +/* EBNF for the language + lines = {line} EOF + line = {EOL} identifier [lround args rround] equal expr EOL {EOL} + args = identifier {comma identifier} + expr = [plus|minus] term { (plus|minus) term } + term = factor { (times|divide) factor }; + factor = number | var | func | sum | matrix | lround expr rround; + var = identifier; + func = identifier lround expr {comma expr} rround; + sum = ∑ lround identifier comma expr comma expr comma expr rround; +*/ +public static class Parser { + + + public static string Parse(Tokens tokens) { + var globalSymbolTable = new SymTable(); + var symbolTable = new SymTable(); + var buffer = new StringBuilder(); + + var en = tokens.GetEnumerator(); + en.MoveNext(); + + buffer = Lines(new Context { + tokens = en, + globalSymbolTable = globalSymbolTable, + symbolTable = symbolTable, + buffer = buffer }); - return buffer.ToString(); + return buffer.ToString(); - } + } - private readonly static string Preamble = @" + private readonly static string Preamble = @" using static System.Math; using static Maths.FormulaHelpers; @@ -173,300 +140,262 @@ namespace Maths { public static partial class Formulas { "; - private readonly static string Ending = @" + private readonly static string Ending = @" } }"; - private struct Context - { - public IEnumerator tokens; - public SymTable globalSymbolTable; - public SymTable symbolTable; - public StringBuilder buffer; - } + private struct Context { + public IEnumerator tokens; + public SymTable globalSymbolTable; + public SymTable symbolTable; + public StringBuilder buffer; + } - private static StringBuilder Error(Token token, TokenType type, string value = "") => - throw new Exception($"Expected {type} {(value == "" ? "" : $" with {token.Value}")} at {token.Line},{token.Column} Instead found {token.Type} with value {token.Value}"); + private static StringBuilder Error(Token token, TokenType type, string value = "") => + throw new Exception($"Expected {type} {(value == "" ? "" : $" with {token.Value}")} at {token.Line},{token.Column} Instead found {token.Type} with value {token.Value}"); - static HashSet validFunctions = - new HashSet(typeof(System.Math).GetMethods().Select(m => m.Name.ToLower())); + static readonly SymTable validFunctions = + new(typeof(System.Math).GetMethods().Select(m => m.Name.ToLower())); - static Dictionary replacementStrings = new Dictionary { - {"'''", "Third" }, {"''", "Second" }, {"'", "Prime"} - }; + static readonly Dictionary replacementStrings = new() + { + {"'''", "Third" }, {"''", "Second" }, {"'", "Prime"} + }; - private static StringBuilder EmitIdentifier(Context ctx, Token token) - { - var val = token.Value; + private static StringBuilder EmitIdentifier(Context ctx, Token token) { + var val = token.Value; - if (val == "pi") - { - ctx.buffer.Append("PI"); // Doesn't follow pattern - return ctx.buffer; - } + if(val == "pi") { + ctx.buffer.Append("PI"); // Doesn't follow pattern + return ctx.buffer; + } - if (validFunctions.Contains(val)) - { - ctx.buffer.Append(char.ToUpper(val[0]) + val.Substring(1)); - return ctx.buffer; - } + if(validFunctions.Contains(val)) { + ctx.buffer.Append(char.ToUpper(val[0]) + val.Substring(1)); + return ctx.buffer; + } - string id = token.Value; - if (ctx.globalSymbolTable.Contains(token.Value) || - ctx.symbolTable.Contains(token.Value)) - { - foreach (var r in replacementStrings) - { - id = id.Replace(r.Key, r.Value); - } - return ctx.buffer.Append(id); - } - else - { - throw new Exception($"{token.Value} not a known identifier or function."); + string id = token.Value; + if(ctx.globalSymbolTable.Contains(token.Value) || + ctx.symbolTable.Contains(token.Value)) { + foreach (var r in replacementStrings) { + id = id.Replace(r.Key, r.Value); } + return ctx.buffer.Append(id); + } else { + throw new Exception($"{token.Value} not a known identifier or function."); } + } - private static StringBuilder Emit(Context ctx, Token token) => token.Type switch - { - TokenType.EOL => ctx.buffer.Append("\n"), - TokenType.CloseParens => ctx.buffer.Append(')'), // All parens become rounded - TokenType.OpenParens => ctx.buffer.Append('('), - TokenType.Equal => ctx.buffer.Append("=>"), - TokenType.Comma => ctx.buffer.Append(token.Value), - - // Identifiers are normalized and checked for injection attacks - TokenType.Identifier => EmitIdentifier(ctx, token), - TokenType.Number => ctx.buffer.Append(token.Value), - TokenType.Operation => ctx.buffer.Append(token.Value), - TokenType.Sum => ctx.buffer.Append("MySum"), - _ => Error(token, TokenType.None) - }; - - private static bool Peek(Context ctx, TokenType type, string value = "") - { - var token = ctx.tokens.Current; - - return (token.Type == type && value == "") || - (token.Type == type && value == token.Value); - } - private static Token NextToken(Context ctx) - { + private static StringBuilder Emit(Context ctx, Token token) => token.Type switch + { + TokenType.EOL => ctx.buffer.Append("\n"), + TokenType.CloseParens => ctx.buffer.Append(')'), // All parens become rounded + TokenType.OpenParens => ctx.buffer.Append('('), + TokenType.Equal => ctx.buffer.Append("=>"), + TokenType.Comma => ctx.buffer.Append(token.Value), + + // Identifiers are normalized and checked for injection attacks + TokenType.Identifier => EmitIdentifier(ctx, token), + TokenType.Number => ctx.buffer.Append(token.Value), + TokenType.Operation => ctx.buffer.Append(token.Value), + TokenType.Sum => ctx.buffer.Append("MySum"), + _ => Error(token, TokenType.None) + }; + + private static bool Peek(Context ctx, TokenType type, string value = "") { + var token = ctx.tokens.Current; + + return (token.Type == type && value == "") || + (token.Type == type && value == token.Value); + } + private static Token NextToken(Context ctx) { - var token = ctx.tokens.Current; - ctx.tokens.MoveNext(); - return token; - } - private static void Consume(Context ctx, TokenType type, string value = "") - { + var token = ctx.tokens.Current; + ctx.tokens.MoveNext(); + return token; + } + private static void Consume(Context ctx, TokenType type, string value = "") { - var token = NextToken(ctx); + var token = NextToken(ctx); - if ((token.Type == type && value == "") || - (token.Type == type && value == token.Value)) - { + if((token.Type == type && value == "") || + (token.Type == type && value == token.Value)) { - ctx.buffer.Append(" "); - Emit(ctx, token); - } - else - { - Error(token, type, value); - } + ctx.buffer.Append(" "); + Emit(ctx, token); + } else { + Error(token, type, value); } + } - private static StringBuilder Lines(Context ctx) - { - // lines = {line} EOF + private static StringBuilder Lines(Context ctx) { + // lines = {line} EOF - ctx.buffer.Append(Preamble); + ctx.buffer.Append(Preamble); - while (!Peek(ctx, TokenType.EOF)) - Line(ctx); + while(!Peek(ctx, TokenType.EOF)) + Line(ctx); - ctx.buffer.Append(Ending); + ctx.buffer.Append(Ending); - return ctx.buffer; - } + return ctx.buffer; + } - private static void AddGlobalSymbol(Context ctx) - { - var token = ctx.tokens.Current; - if (Peek(ctx, TokenType.Identifier)) - { - ctx.globalSymbolTable.Add(token.Value); - } - else - { - Error(token, TokenType.Identifier); - } + private static void AddGlobalSymbol(Context ctx) { + var token = ctx.tokens.Current; + if(Peek(ctx, TokenType.Identifier)) { + ctx.globalSymbolTable.Add(token.Value); + } else { + Error(token, TokenType.Identifier); } - private static void AddSymbol(Context ctx) - { - var token = ctx.tokens.Current; - if (Peek(ctx, TokenType.Identifier)) - { - ctx.symbolTable.Add(token.Value); - } - else - { - Error(token, TokenType.Identifier); - } + } + private static void AddSymbol(Context ctx) { + var token = ctx.tokens.Current; + if(Peek(ctx, TokenType.Identifier)) { + ctx.symbolTable.Add(token.Value); + } else { + Error(token, TokenType.Identifier); } - private static void Line(Context ctx) - { - // line = {EOL} identifier [lround args rround] equal expr EOL {EOL} + } + private static void Line(Context ctx) { + // line = {EOL} identifier [lround args rround] equal expr EOL {EOL} - ctx.symbolTable.Clear(); + ctx.symbolTable.Clear(); - while (Peek(ctx, TokenType.EOL)) - Consume(ctx, TokenType.EOL); + while(Peek(ctx, TokenType.EOL)) + Consume(ctx, TokenType.EOL); - ctx.buffer.Append("\tpublic static double "); + ctx.buffer.Append("\tpublic static double "); - AddGlobalSymbol(ctx); - Consume(ctx, TokenType.Identifier); + AddGlobalSymbol(ctx); + Consume(ctx, TokenType.Identifier); - if (Peek(ctx, TokenType.OpenParens, "(")) - { - Consume(ctx, TokenType.OpenParens, "("); // Just round parens - Args(ctx); - Consume(ctx, TokenType.CloseParens, ")"); - } + if(Peek(ctx, TokenType.OpenParens, "(")) { + Consume(ctx, TokenType.OpenParens, "("); // Just round parens + Args(ctx); + Consume(ctx, TokenType.CloseParens, ")"); + } - Consume(ctx, TokenType.Equal); - Expr(ctx); - ctx.buffer.Append(" ;"); + Consume(ctx, TokenType.Equal); + Expr(ctx); + ctx.buffer.Append(" ;"); + Consume(ctx, TokenType.EOL); + + while(Peek(ctx, TokenType.EOL)) Consume(ctx, TokenType.EOL); + } + private static void Args(Context ctx) { + // args = identifier {comma identifier} + // It doesn't make sense for a math function to have zero args (I think) - while (Peek(ctx, TokenType.EOL)) - Consume(ctx, TokenType.EOL); - } - private static void Args(Context ctx) - { - // args = identifier {comma identifier} - // It doesn't make sense for a math function to have zero args (I think) + ctx.buffer.Append("double "); + AddSymbol(ctx); + Consume(ctx, TokenType.Identifier); + while(Peek(ctx, TokenType.Comma)) { + Consume(ctx, TokenType.Comma); ctx.buffer.Append("double "); AddSymbol(ctx); Consume(ctx, TokenType.Identifier); - - while (Peek(ctx, TokenType.Comma)) - { - Consume(ctx, TokenType.Comma); - ctx.buffer.Append("double "); - AddSymbol(ctx); - Consume(ctx, TokenType.Identifier); - } } - private static Func IsOp = (ctx, op) - => Peek(ctx, TokenType.Operation, op); - private static Action ConsOp = (ctx, op) - => Consume(ctx, TokenType.Operation, op); + } + private readonly static Func IsOp = (ctx, op) + => Peek(ctx, TokenType.Operation, op); + private static readonly Action ConsOp = (ctx, op) + => Consume(ctx, TokenType.Operation, op); - private static void Expr(Context ctx) - { - // expr = [plus|minus] term { (plus|minus) term } + private static void Expr(Context ctx) { + // expr = [plus|minus] term { (plus|minus) term } - if (IsOp(ctx, "+")) ConsOp(ctx, "+"); - if (IsOp(ctx, "-")) ConsOp(ctx, "-"); + if(IsOp(ctx, "+")) ConsOp(ctx, "+"); + if(IsOp(ctx, "-")) ConsOp(ctx, "-"); - Term(ctx); + Term(ctx); - while (IsOp(ctx, "+") || IsOp(ctx, "-")) - { + while(IsOp(ctx, "+") || IsOp(ctx, "-")) { - if (IsOp(ctx, "+")) ConsOp(ctx, "+"); - if (IsOp(ctx, "-")) ConsOp(ctx, "-"); + if(IsOp(ctx, "+")) ConsOp(ctx, "+"); + if(IsOp(ctx, "-")) ConsOp(ctx, "-"); - Term(ctx); - } + Term(ctx); } - private static void Term(Context ctx) - { - // term = factor { (times|divide) factor }; - Factor(ctx); + } + private static void Term(Context ctx) { + // term = factor { (times|divide) factor }; + Factor(ctx); - while (IsOp(ctx, "*") || IsOp(ctx, "/")) - { - if (IsOp(ctx, "*")) ConsOp(ctx, "*"); - if (IsOp(ctx, "/")) ConsOp(ctx, "/"); + while(IsOp(ctx, "*") || IsOp(ctx, "/")) { + if(IsOp(ctx, "*")) ConsOp(ctx, "*"); + if(IsOp(ctx, "/")) ConsOp(ctx, "/"); - Term(ctx); - } + Term(ctx); + } + } + private static void Factor(Context ctx) { + // factor = number | var | func | lround expr rround; + if(Peek(ctx, TokenType.Number)) { + Consume(ctx, TokenType.Number); + return; } - private static void Factor(Context ctx) - { - // factor = number | var | func | lround expr rround; - if (Peek(ctx, TokenType.Number)) - { - Consume(ctx, TokenType.Number); - return; - } - if (Peek(ctx, TokenType.Identifier)) - { - Consume(ctx, TokenType.Identifier); // Is either var or func - if (Peek(ctx, TokenType.OpenParens, "(")) - { // Is Func, but we already consumed its name - Funct(ctx); - } - return; - } - if (Peek(ctx, TokenType.Sum)) - { - Sum(ctx); - return; + if(Peek(ctx, TokenType.Identifier)) { + Consume(ctx, TokenType.Identifier); // Is either var or func + if(Peek(ctx, TokenType.OpenParens, "(")) { // Is Func, but we already consumed its name + Funct(ctx); } - // Must be a parenthesized expression - Consume(ctx, TokenType.OpenParens); - Expr(ctx); - Consume(ctx, TokenType.CloseParens); + return; } - private static void Sum(Context ctx) - { - // sum = ∑ lround identifier comma expr1 comma expr2 comma expr3 rround; - // TODO: differentiate in the language between integer and double, but complicated for a sample. - Consume(ctx, TokenType.Sum); - Consume(ctx, TokenType.OpenParens, "("); - - AddSymbol(ctx); - var varName = NextToken(ctx).Value; - NextToken(ctx); // consume the first comma without emitting it + if(Peek(ctx, TokenType.Sum)) { + Sum(ctx); + return; + } + // Must be a parenthesized expression + Consume(ctx, TokenType.OpenParens); + Expr(ctx); + Consume(ctx, TokenType.CloseParens); + } + private static void Sum(Context ctx) { + // sum = ∑ lround identifier comma expr1 comma expr2 comma expr3 rround; + // TODO: differentiate in the language between integer and double, but complicated for a sample. + Consume(ctx, TokenType.Sum); + Consume(ctx, TokenType.OpenParens, "("); - ctx.buffer.Append("(int)"); - Expr(ctx); // Start index - Consume(ctx, TokenType.Comma); + AddSymbol(ctx); + var varName = NextToken(ctx).Value; + NextToken(ctx); // consume the first comma without emitting it - ctx.buffer.Append("(int)"); - Expr(ctx); // End index - Consume(ctx, TokenType.Comma); + ctx.buffer.Append("(int)"); + Expr(ctx); // Start index + Consume(ctx, TokenType.Comma); - ctx.buffer.Append($"{varName} => "); // It needs to be a lambda + ctx.buffer.Append("(int)"); + Expr(ctx); // End index + Consume(ctx, TokenType.Comma); - Expr(ctx); // expr to evaluate at each iteration + ctx.buffer.Append($"{varName} => "); // It needs to be a lambda + + Expr(ctx); // expr to evaluate at each iteration - Consume(ctx, TokenType.CloseParens, ")"); - } - private static void Funct(Context ctx) - { - // func = identifier lround expr {comma expr} rround; - Consume(ctx, TokenType.OpenParens, "("); + Consume(ctx, TokenType.CloseParens, ")"); + } + private static void Funct(Context ctx) { + // func = identifier lround expr {comma expr} rround; + Consume(ctx, TokenType.OpenParens, "("); + Expr(ctx); + while(Peek(ctx, TokenType.Comma)) { + Consume(ctx, TokenType.Comma); Expr(ctx); - while (Peek(ctx, TokenType.Comma)) - { - Consume(ctx, TokenType.Comma); - Expr(ctx); - } - Consume(ctx, TokenType.CloseParens, ")"); } + Consume(ctx, TokenType.CloseParens, ")"); } +} - [Generator] - public class MathsGenerator : ISourceGenerator - { - private const string libraryCode = @" +[Generator] +public class MathsGenerator : IIncrementalGenerator +{ + private const string libraryCode = @" using System.Linq; using System; using System.Collections.Generic; @@ -486,43 +415,41 @@ public static double MySum(int start, int end, Func f) => } "; - public void Execute(GeneratorExecutionContext context) - { - foreach (AdditionalText file in context.AdditionalFiles) - { - if (Path.GetExtension(file.Path).Equals(".math", StringComparison.OrdinalIgnoreCase)) - { - // Load formulas from .math files - var mathText = file.GetText(); - var mathString = ""; - - if (mathText != null) - { - mathString = mathText.ToString(); - } - else - { - throw new Exception($"Cannot load file {file.Path}"); - } - - // Get name of generated namespace from file name - string fileName = Path.GetFileNameWithoutExtension(file.Path); - - // Parse and gen the formulas functions - var tokens = Lexer.Tokenize(mathString); - var code = Parser.Parse(tokens); - - var codeFileName = $@"{fileName}.g.cs"; - - context.AddSource(codeFileName, SourceText.From(code, Encoding.UTF8)); - } - } - } + public (string name, SourceText code) GetNameAndCode(AdditionalText file, CancellationToken _) + { + // Load formulas from .math files + var mathText = file.GetText(); + string mathString; - public void Initialize(GeneratorInitializationContext context) + if (mathText != null) + { + mathString = mathText.ToString(); + } + else { - context.RegisterForPostInitialization((pi) => pi.AddSource("__MathLibrary__.g.cs", libraryCode)); + throw new Exception($"Cannot load file {file.Path}"); } + + // Get name of generated namespace from file name + string fileName = Path.GetFileNameWithoutExtension(file.Path); + + // Parse and gen the formulas functions + var tokens = Lexer.Tokenize(mathString); + var code = Parser.Parse(tokens); + + var codeFileName = $@"{fileName}.g.cs"; + + return (codeFileName, SourceText.From(code, Encoding.UTF8)); + } + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput((pi) => pi.AddSource("__MathLibrary__.g.cs", libraryCode)); + + var files = context.AdditionalTextsProvider.Where(file => Path.GetExtension(file.Path).Equals(".math", StringComparison.OrdinalIgnoreCase)); + var nameAndCode = files.Select(GetNameAndCode); + + context.RegisterSourceOutput(nameAndCode, (ctx, source) => ctx.AddSource(source.name, source.code)); } } diff --git a/samples/CSharp/SourceGenerators/SourceGeneratorSamples/MustacheGenerator.cs b/samples/CSharp/SourceGenerators/SourceGeneratorSamples/MustacheGenerator.cs index ffc4307ef..0363a5dc7 100644 --- a/samples/CSharp/SourceGenerators/SourceGeneratorSamples/MustacheGenerator.cs +++ b/samples/CSharp/SourceGenerators/SourceGeneratorSamples/MustacheGenerator.cs @@ -1,18 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; +namespace Mustache; -#nullable enable - -namespace Mustache +[Generator] +public class MustacheGenerator : IIncrementalGenerator { - [Generator] - public class MustacheGenerator : ISourceGenerator - { - private const string attributeSource = @" + private const string attributeSource = @" [System.AttributeUsage(System.AttributeTargets.Assembly, AllowMultiple=true)] internal sealed class MustacheAttribute: System.Attribute { @@ -24,24 +15,14 @@ public MustacheAttribute(string name, string template, string hash) } "; - public void Execute(GeneratorExecutionContext context) - { - SyntaxReceiver rx = (SyntaxReceiver)context.SyntaxContextReceiver!; - foreach ((string name, string template, string hash) in rx.TemplateInfo) - { - string source = SourceFileFromMustachePath(name, template, hash); - context.AddSource($"Mustache{name}.g.cs", source); - } - } - - static string SourceFileFromMustachePath(string name, string template, string hash) - { - Func tree = HandlebarsDotNet.Handlebars.Compile(template); - object @object = Newtonsoft.Json.JsonConvert.DeserializeObject(hash); - string mustacheText = tree(@object); + static string SourceFileFromMustachePath(string name, string template, string hash) + { + Func tree = HandlebarsDotNet.Handlebars.Compile(template); + object @object = Newtonsoft.Json.JsonConvert.DeserializeObject(hash); + string mustacheText = tree(@object); - StringBuilder sb = new StringBuilder(); - sb.Append($@" + StringBuilder sb = new (); + sb.Append($@" namespace Mustache {{ public static partial class Constants {{ @@ -50,33 +31,42 @@ public static partial class Constants {{ }} }} "); - return sb.ToString(); - } + return sb.ToString(); + } - public void Initialize(GeneratorInitializationContext context) - { - context.RegisterForPostInitialization((pi) => pi.AddSource("Mustache_MainAttributes__.g.cs", attributeSource)); - context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); - } + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput( + ctx => ctx.AddSource("Mustache_MainAttributes__.g.cs", attributeSource)); - class SyntaxReceiver : ISyntaxContextReceiver - { - public List<(string name, string template, string hash)> TemplateInfo = new List<(string name, string template, string hash)>(); + var maybeTemplateData = context.SyntaxProvider.CreateSyntaxProvider( + predicate: (s, t) => s is AttributeSyntax attrib + && attrib.ArgumentList?.Arguments.Count == 3, + transform: GetTemplateData); - public void OnVisitSyntaxNode(GeneratorSyntaxContext context) - { - // find all valid mustache attributes - if (context.Node is AttributeSyntax attrib - && attrib.ArgumentList?.Arguments.Count == 3 - && context.SemanticModel.GetTypeInfo(attrib).Type?.ToDisplayString() == "MustacheAttribute") - { - string name = context.SemanticModel.GetConstantValue(attrib.ArgumentList.Arguments[0].Expression).ToString(); - string template = context.SemanticModel.GetConstantValue(attrib.ArgumentList.Arguments[1].Expression).ToString(); - string hash = context.SemanticModel.GetConstantValue(attrib.ArgumentList.Arguments[2].Expression).ToString(); + var templateData = maybeTemplateData + .Where(t => t is not null) + .Select((t, _) => t!.Value); - TemplateInfo.Add((name, template, hash)); - } - } + context.RegisterSourceOutput(templateData, + (ctx, data) => ctx.AddSource( + $"{data.name}.g.cs", + SourceFileFromMustachePath(data.name, data.template, data.hash))); + } + + internal static (string name, string template, string hash)? + GetTemplateData(GeneratorSyntaxContext context, CancellationToken cancellationToken) + { + var attrib = (AttributeSyntax)context.Node; + + if(attrib.ArgumentList is not null + && context.SemanticModel.GetTypeInfo(attrib).Type?.ToDisplayString() == "MustacheAttribute") + { + string name = context.SemanticModel.GetConstantValue(attrib.ArgumentList.Arguments[0].Expression).ToString(); + string template = context.SemanticModel.GetConstantValue(attrib.ArgumentList.Arguments[1].Expression).ToString(); + string hash = context.SemanticModel.GetConstantValue(attrib.ArgumentList.Arguments[2].Expression).ToString(); + return (name, template, hash); } + return null; } } diff --git a/samples/CSharp/SourceGenerators/SourceGeneratorSamples/SettingsXmlGenerator.cs b/samples/CSharp/SourceGenerators/SourceGeneratorSamples/SettingsXmlGenerator.cs index f3264e69a..d1afee0b6 100644 --- a/samples/CSharp/SourceGenerators/SourceGeneratorSamples/SettingsXmlGenerator.cs +++ b/samples/CSharp/SourceGenerators/SourceGeneratorSamples/SettingsXmlGenerator.cs @@ -1,48 +1,31 @@ -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Xml; +namespace Analyzer1; -namespace Analyzer1 +[Generator] +public class SettingsXmlGenerator : IIncrementalGenerator { - [Generator] - public class SettingsXmlGenerator : ISourceGenerator + private void ProcessSettingsFile(SourceProductionContext context, AdditionalText xmlFile) { - public void Execute(GeneratorExecutionContext context) + // try and load the settings file + XmlDocument xmlDoc = new (); + var text = xmlFile.GetText(context.CancellationToken)?.ToString(); + if(text is null) throw new Exception("Error reading the settings file"); + + try { - // Using the context, get any additional files that end in .xmlsettings - IEnumerable settingsFiles = context.AdditionalFiles.Where(at => at.Path.EndsWith(".xmlsettings")); - foreach (AdditionalText settingsFile in settingsFiles) - { - ProcessSettingsFile(settingsFile, context); - } + xmlDoc.LoadXml(text); } - - private void ProcessSettingsFile(AdditionalText xmlFile, GeneratorExecutionContext context) + catch { - // try and load the settings file - XmlDocument xmlDoc = new XmlDocument(); - string text = xmlFile.GetText(context.CancellationToken).ToString(); - try - { - xmlDoc.LoadXml(text); - } - catch - { - //TODO: issue a diagnostic that says we couldn't parse it - return; - } - + //TODO: issue a diagnostic that says we couldn't parse it + return; + } - // create a class in the XmlSetting class that represnts this entry, and a static field that contains a singleton instance. - string fileName = Path.GetFileName(xmlFile.Path); - string name = xmlDoc.DocumentElement.GetAttribute("name"); + + // create a class in the XmlSetting class that represnts this entry, and a static field that contains a singleton instance. + string fileName = Path.GetFileName(xmlFile.Path); + string name = xmlDoc.DocumentElement.GetAttribute("name"); - StringBuilder sb = new StringBuilder($@" + StringBuilder sb = new($@" namespace AutoSettings {{ using System; @@ -69,13 +52,13 @@ public class {name}Settings }} "); - for (int i = 0; i < xmlDoc.DocumentElement.ChildNodes.Count; i++) - { - XmlElement setting = (XmlElement)xmlDoc.DocumentElement.ChildNodes[i]; - string settingName = setting.GetAttribute("name"); - string settingType = setting.GetAttribute("type"); + for(int i = 0; i < xmlDoc.DocumentElement.ChildNodes.Count; i++) + { + XmlElement setting = (XmlElement)xmlDoc.DocumentElement.ChildNodes[i]; + string settingName = setting.GetAttribute("name"); + string settingType = setting.GetAttribute("type"); - sb.Append($@" + sb.Append($@" public {settingType} {settingName} {{ @@ -85,15 +68,16 @@ public class {name}Settings }} }} "); - } - - sb.Append("} } }"); - - context.AddSource($"Settings_{name}.g.cs", SourceText.From(sb.ToString(), Encoding.UTF8)); } - public void Initialize(GeneratorInitializationContext context) - { - } + sb.Append("} } }"); + + context.AddSource($"Settings_{name}.g.cs", SourceText.From(sb.ToString(), Encoding.UTF8)); + } + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var xmlFiles = context.AdditionalTextsProvider.Where(at => at.Path.EndsWith(".xmlsettings")); + context.RegisterSourceOutput(xmlFiles, ProcessSettingsFile); } }