Created
October 11, 2020 15:22
-
-
Save Siphonophora/54a38ad8d0034d01ee434f37580e9e6a to your computer and use it in GitHub Desktop.
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
using System; | |
using System.Collections.Generic; | |
using System.Diagnostics; | |
using System.IO; | |
using System.Linq; | |
using System.Runtime.CompilerServices; | |
using System.Text; | |
using System.Threading.Tasks; | |
using Microsoft.CodeAnalysis; | |
using Microsoft.CodeAnalysis.CSharp; | |
using Microsoft.CodeAnalysis.CSharp.Syntax; | |
using Microsoft.CodeAnalysis.Text; | |
namespace SourceGeneratorSamples | |
{ | |
[Generator] | |
public class EncapsulatedCollectionGenerator : ISourceGenerator | |
{ | |
private const string attributeText = @" | |
using System; | |
namespace EncapsulatedCollection | |
{ | |
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] | |
sealed class EncapsulatedCollectionAttribute : Attribute | |
{ | |
public EncapsulatedCollectionAttribute() | |
{ | |
} | |
} | |
} | |
"; | |
public void Initialize(GeneratorInitializationContext context) | |
{ | |
// Register a syntax receiver that will be created for each generation pass | |
context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); | |
} | |
public void Execute(GeneratorExecutionContext context) | |
{ | |
// add the attribute text | |
context.AddSource("EncapsulatedCollectionAttribute", 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("EncapsulatedCollection.EncapsulatedCollectionAttribute"); | |
// 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, context); | |
context.AddSource($"{group.Key.Name}_encapsulatedCollections.cs", SourceText.From(classSource, Encoding.UTF8)); | |
} | |
} | |
private string ProcessClass(INamedTypeSymbol classSymbol, List<IFieldSymbol> fields, ISymbol attributeSymbol, GeneratorExecutionContext 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(); | |
string members = string.Join(", ", classSymbol.GetMembers().Select(x => x.Name + " - " + BaseType(x))); | |
ITypeSymbol implementingClass = (ITypeSymbol)classSymbol.GetMembers().SingleOrDefault(x => BaseType(x) == $"{classSymbol.Name}.Validator"); | |
string validator = implementingClass is ITypeSymbol ? implementingClass.ToString() : classSymbol.ToString() + ".Validator"; | |
// begin building the generated source | |
StringBuilder source = new StringBuilder($@" | |
using System.Collections.Generic; | |
namespace {namespaceName} | |
{{ | |
// {members} | |
//implementing class {implementingClass} | |
//validator class {validator} | |
public partial class {classSymbol.Name} | |
{{ | |
private readonly Validator validator = new {validator}(); | |
private class Validator | |
{{ | |
"); | |
// create properties for each field | |
foreach (IFieldSymbol fieldSymbol in fields) | |
{ | |
ValidatorVirtualMethods(source, fieldSymbol, attributeSymbol, validator); | |
} | |
source.Append("} \r"); | |
// create properties for each field | |
foreach (IFieldSymbol fieldSymbol in fields) | |
{ | |
ProcessField(source, fieldSymbol, attributeSymbol, validator); | |
} | |
source.Append("} }"); | |
File.WriteAllText(@"c:\temp\source.cs", source.ToString()); | |
return source.ToString(); | |
} | |
private string BaseType(ISymbol x) | |
{ | |
try | |
{ | |
INamedTypeSymbol y = (INamedTypeSymbol)x; | |
return y.ContainingType.Name + "." + y.BaseType.Name; | |
} | |
catch (Exception ex) | |
{ | |
return $"Base type exception: {ex.Message}"; | |
} | |
} | |
private void ProcessField(StringBuilder source, IFieldSymbol fieldSymbol, ISymbol attributeSymbol, string validator) | |
{ | |
// get the name and type of the field | |
string fieldName = fieldSymbol.Name; | |
ITypeSymbol fieldType = fieldSymbol.Type; | |
INamedTypeSymbol namedTypeSymbol = (INamedTypeSymbol)fieldType; | |
string typeArgs = string.Join(", ", namedTypeSymbol.TypeArguments.Select(x => x.Name)); | |
string listType = namedTypeSymbol.TypeArguments.First().Name; | |
string listTypeNamespace = namedTypeSymbol.TypeArguments.First().ContainingNamespace.Name; | |
// 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 = ChooseFieldName(fieldName, overridenNameOpt, true); | |
if (propertyName.Length == 0 || propertyName == fieldName) | |
{ | |
//TODO: issue a diagnostic that we can't process this field | |
return; | |
} | |
source.AppendLine($"public IEnumerable<{listTypeNamespace}.{listType}> {ChooseFieldName(fieldName, overridenNameOpt, false)} => {fieldName}.AsReadOnly();"); | |
source.Append($@" | |
public void Add{propertyName}({listTypeNamespace}.{listType} item) | |
{{ | |
if(this.validator.Add{ChooseFieldName(fieldName, overridenNameOpt, true)}Permitted(item)) | |
{{ | |
{fieldName}.Add(item); | |
this.validator.Add{ChooseFieldName(fieldName, overridenNameOpt, true)}SideEffect(item); | |
}} | |
}} | |
"); | |
} | |
private string ChooseFieldName(string fieldName, TypedConstant overridenNameOpt, bool removePlural) | |
{ | |
if (!overridenNameOpt.IsNull) | |
{ | |
return overridenNameOpt.Value.ToString(); | |
} | |
fieldName = fieldName.TrimStart('_'); | |
if (removePlural) | |
fieldName = fieldName.TrimEnd('s'); | |
if (fieldName.Length == 0) | |
return string.Empty; | |
if (fieldName.Length == 1) | |
return fieldName.ToUpper(); | |
return fieldName.Substring(0, 1).ToUpper() + fieldName.Substring(1); | |
} | |
private void ValidatorVirtualMethods(StringBuilder source, IFieldSymbol fieldSymbol, ISymbol attributeSymbol, string validator) | |
{ | |
// get the name and type of the field | |
string fieldName = fieldSymbol.Name; | |
ITypeSymbol fieldType = fieldSymbol.Type; | |
INamedTypeSymbol namedTypeSymbol = (INamedTypeSymbol)fieldType; | |
string typeArgs = string.Join(", ", namedTypeSymbol.TypeArguments.Select(x => x.Name)); | |
string listType = namedTypeSymbol.TypeArguments.First().Name; | |
string listTypeNamespace = namedTypeSymbol.TypeArguments.First().ContainingNamespace.Name; | |
// 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 = ChooseFieldName(fieldName, overridenNameOpt, true); | |
if (propertyName.Length == 0 || propertyName == fieldName) | |
{ | |
//TODO: issue a diagnostic that we can't process this field | |
return; | |
} | |
source.Append($@" | |
public virtual bool Add{ChooseFieldName(fieldName, overridenNameOpt, true)}Permitted(string item) => false; | |
public virtual void Add{ChooseFieldName(fieldName, overridenNameOpt, true)}SideEffect(string item) {{ }} | |
"); | |
} | |
/// <summary> | |
/// Created on demand before each generation pass | |
/// </summary> | |
private 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); | |
} | |
} | |
} | |
} | |
} |
Hi @ignatandrei I have been wanting to do a version 2 of this. Now that source generators are out, there were a few changes (partial virtual methods in particular) which will allow this to be done much more cleanly. I think I don't need the subclass any longer. I will make a note to ping you when the new version is done.
thanks!
If you have any news ;-)
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Do you think to put into a nuget package ?
I think it is a good example to be mentioned at https://github.com/ignatandrei/RSCG_Examples