Skip to content

Commit

Permalink
Add a couple more samples
Browse files Browse the repository at this point in the history
  • Loading branch information
David Wengier committed Aug 16, 2020
1 parent 552ce82 commit c92c1a2
Show file tree
Hide file tree
Showing 6 changed files with 294 additions and 2 deletions.
3 changes: 1 addition & 2 deletions MainForm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
186 changes: 186 additions & 0 deletions Samples/Auto Notify.Generator.cs
Original file line number Diff line number Diff line change
@@ -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<IFieldSymbol> fieldSymbols = new List<IFieldSymbol>();
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<INamedTypeSymbol, IFieldSymbol> 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<IFieldSymbol> 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);
}

}

/// <summary>
/// Created on demand before each generation pass
/// </summary>
class SyntaxReceiver : ISyntaxReceiver
{
public List<FieldDeclarationSyntax> CandidateFields { get; } = new List<FieldDeclarationSyntax>();

/// <summary>
/// Called for every syntax node in the compilation, we can inspect the nodes and save any information useful for generation
/// </summary>
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);
}
}
}
}
}
40 changes: 40 additions & 0 deletions Samples/Auto Notify.Program.cs
Original file line number Diff line number Diff line change
@@ -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
}
}
}
52 changes: 52 additions & 0 deletions Samples/Hello World.Generator.cs
Original file line number Diff line number Diff line change
@@ -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<SyntaxTree> 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
}
}
}
11 changes: 11 additions & 0 deletions Samples/Hello World.Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

namespace MyApp
{
class Program
{
static void Main()
{
HelloWorldGenerated.HelloWorld.SayHello();
}
}
}

0 comments on commit c92c1a2

Please sign in to comment.