-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
David Wengier
committed
Aug 16, 2020
1 parent
552ce82
commit c92c1a2
Showing
6 changed files
with
294 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
| ||
namespace MyApp | ||
{ | ||
class Program | ||
{ | ||
static void Main() | ||
{ | ||
HelloWorldGenerated.HelloWorld.SayHello(); | ||
} | ||
} | ||
} |