diff --git a/MainForm.cs b/MainForm.cs index 1071be5..83f25cf 100644 --- a/MainForm.cs +++ b/MainForm.cs @@ -11,7 +11,6 @@ public partial class MainForm : Form public MainForm() { - System.Console.BackgroundColor = ConsoleColor.White; this.StartPosition = FormStartPosition.WindowsDefaultBounds; this.Text = "Source Generator Playground - v" + ThisAssembly.AssemblyInformationalVersion; @@ -59,7 +58,7 @@ public MainForm() loadMenu.DropDownItems.Add(name.Split(".")[2]).Click += LoadSample; } } - loadMenu.DropDownItems[0].PerformClick(); + loadMenu.DropDownItems[2].PerformClick(); this.MainMenuStrip.Visible = true; void LoadSample(object? s, EventArgs e) diff --git a/Program.cs b/Program.cs index 82d5858..c7b3141 100644 --- a/Program.cs +++ b/Program.cs @@ -16,6 +16,10 @@ private static void Main() Application.SetCompatibleTextRenderingDefault(false); using var mainForm = new MainForm(); Application.Run(mainForm); + + // TODO: Use https://github.com/jaredpar/roslyn-codedom + System.Console.BackgroundColor = ConsoleColor.White; + System.ComponentModel.INotifyPropertyChanged? c = null; } } } diff --git a/Samples/Auto Notify.Generator.cs b/Samples/Auto Notify.Generator.cs new file mode 100644 index 0000000..e107b7d --- /dev/null +++ b/Samples/Auto Notify.Generator.cs @@ -0,0 +1,186 @@ +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 +{ + [Generator] + public class AutoNotifyGenerator : ISourceGenerator + { + private const string attributeText = @" +using System; +namespace AutoNotify +{ + [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] + sealed class AutoNotifyAttribute : Attribute + { + public AutoNotifyAttribute() + { + } + public string PropertyName { get; set; } + } +} +"; + + public void Initialize(InitializationContext context) + { + // Register a syntax receiver that will be created for each generation pass + context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); + } + + public void Execute(SourceGeneratorContext context) + { + // add the attribute text + context.AddSource("AutoNotifyAttribute", SourceText.From(attributeText, Encoding.UTF8)); + + // retreive the populated receiver + if (!(context.SyntaxReceiver is SyntaxReceiver receiver)) + return; + + // 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. + CSharpParseOptions options = (context.Compilation as CSharpCompilation).SyntaxTrees[0].Options as CSharpParseOptions; + Compilation compilation = context.Compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(attributeText, Encoding.UTF8), options)); + + // get the newly bound attribute, and INotifyPropertyChanged + INamedTypeSymbol attributeSymbol = compilation.GetTypeByMetadataName("AutoNotify.AutoNotifyAttribute"); + INamedTypeSymbol notifySymbol = compilation.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanged"); + + // loop over the candidate fields, and keep the ones that are actually annotated + List fieldSymbols = new List(); + foreach (FieldDeclarationSyntax field in receiver.CandidateFields) + { + SemanticModel model = compilation.GetSemanticModel(field.SyntaxTree); + foreach (VariableDeclaratorSyntax variable in field.Declaration.Variables) + { + // Get the symbol being decleared by the field, and keep it if its annotated + IFieldSymbol fieldSymbol = model.GetDeclaredSymbol(variable) as IFieldSymbol; + if (fieldSymbol.GetAttributes().Any(ad => ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default))) + { + fieldSymbols.Add(fieldSymbol); + } + } + } + + // group the fields by class, and generate the source + foreach (IGrouping group in fieldSymbols.GroupBy(f => f.ContainingType)) + { + string classSource = ProcessClass(group.Key, group.ToList(), attributeSymbol, notifySymbol, context); + context.AddSource($"{group.Key.Name}_autoNotify.cs", SourceText.From(classSource, Encoding.UTF8)); + } + } + + private string ProcessClass(INamedTypeSymbol classSymbol, List fields, ISymbol attributeSymbol, ISymbol notifySymbol, SourceGeneratorContext context) + { + 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(); + + // begin building the generated source + StringBuilder source = new StringBuilder($@" +namespace {namespaceName} +{{ + public partial class {classSymbol.Name} : {notifySymbol.ToDisplayString()} + {{ +"); + + // if the class doesn't implement INotifyPropertyChanged already, add it + if (!classSymbol.Interfaces.Contains(notifySymbol)) + { + 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(); + } + + 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; + + 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 + {{ + return this.{fieldName}; + }} + + set + {{ + this.{fieldName} = value; + this.PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(nameof({propertyName}))); + }} +}} + +"); + + string chooseName(string fieldName, TypedConstant overridenNameOpt) + { + if (!overridenNameOpt.IsNull) + { + return overridenNameOpt.Value.ToString(); + } + + fieldName = fieldName.TrimStart('_'); + if (fieldName.Length == 0) + return string.Empty; + + 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 : ISyntaxReceiver + { + public List CandidateFields { get; } = new List(); + + /// + /// Called for every syntax node in the compilation, we can inspect the nodes and save any information useful for generation + /// + public void OnVisitSyntaxNode(SyntaxNode syntaxNode) + { + // any field with at least one attribute is a candidate for property generation + if (syntaxNode is FieldDeclarationSyntax fieldDeclarationSyntax + && fieldDeclarationSyntax.AttributeLists.Count > 0) + { + CandidateFields.Add(fieldDeclarationSyntax); + } + } + } + } +} diff --git a/Samples/Auto Notify.Program.cs b/Samples/Auto Notify.Program.cs new file mode 100644 index 0000000..7fde69c --- /dev/null +++ b/Samples/Auto Notify.Program.cs @@ -0,0 +1,40 @@ +using System; + +using AutoNotify; + +namespace GeneratedDemo +{ + // The view model we'd like to augment + public partial class ExampleViewModel + { + [AutoNotify] + private string _text = "private field text"; + + [AutoNotify(PropertyName = "Count")] + private int _amount = 5; + } + + public static class Program + { + public static void Main() + { + 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/Hello World.Generator.cs b/Samples/Hello World.Generator.cs new file mode 100644 index 0000000..180a854 --- /dev/null +++ b/Samples/Hello World.Generator.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Text; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace SourceGeneratorSamples +{ + [Generator] + public class HelloWorldGenerator : ISourceGenerator + { + public void Execute(SourceGeneratorContext context) + { + // begin creating the source we'll inject into the users compilation + StringBuilder sourceBuilder = new StringBuilder(@" +using System; +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:""); +"); + + // using the context, get a list of syntax trees in the users compilation + IEnumerable syntaxTrees = context.Compilation.SyntaxTrees; + + // 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(@" + } + } +}"); + + // inject the created source into the users compilation + context.AddSource("helloWorldGenerated", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8)); + } + + public void Initialize(InitializationContext context) + { + // No initialization required + } + } +} diff --git a/Samples/Hello World.Program.cs b/Samples/Hello World.Program.cs new file mode 100644 index 0000000..25f58d4 --- /dev/null +++ b/Samples/Hello World.Program.cs @@ -0,0 +1,11 @@ + +namespace MyApp +{ + class Program + { + static void Main() + { + HelloWorldGenerated.HelloWorld.SayHello(); + } + } +}