Skip to content

Instantly share code, notes, and snippets.

@Siphonophora
Created October 11, 2020 15:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Siphonophora/54a38ad8d0034d01ee434f37580e9e6a to your computer and use it in GitHub Desktop.
Save Siphonophora/54a38ad8d0034d01ee434f37580e9e6a to your computer and use it in GitHub Desktop.
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);
}
}
}
}
}
@ignatandrei
Copy link

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

@Siphonophora
Copy link
Author

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.

@ignatandrei
Copy link

thanks!

@ignatandrei
Copy link

If you have any news ;-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment