Skip to content

Instantly share code, notes, and snippets.

@in-async
Last active January 20, 2022 10:14
Show Gist options
  • Save in-async/6526b25a66deadec89bbc6bd3fe1c468 to your computer and use it in GitHub Desktop.
Save in-async/6526b25a66deadec89bbc6bd3fe1c468 to your computer and use it in GitHub Desktop.
インターフェース(のプロパティ)を自動実装する Source Generator (仮)
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>9.0</LangVersion>
<Nullable>enable</Nullable>
<IsRoslynComponent>true</IsRoslynComponent>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.11.0" PrivateAssets="all" />
</ItemGroup>
</Project>
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 AutoImpl {
[Generator]
public class AutoImplGenerator : ISourceGenerator {
private const string _attrSource = @"
using System;
using System.Collections.Generic;
namespace AutoImpl {
[AttributeUsage(AttributeTargets.Interface, Inherited = false, AllowMultiple = false)]
sealed class AutoImplAttribute : Attribute { }
}
";
public void Initialize(GeneratorInitializationContext context) {
context.RegisterForPostInitialization(t => t.AddSource("AutoImpl.g.cs", _attrSource));
context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
}
public void Execute(GeneratorExecutionContext context) {
if (!(context.SyntaxContextReceiver is SyntaxReceiver receiver)) { return; }
foreach ((TypeDeclarationSyntax interfaceSyntax, INamedTypeSymbol interfaceSymbol, List<AutoPropertySource> props, string typeName) in receiver.Targets) {
StringBuilder source = new();
source.AppendLine("using System;");
source.AppendLine("using System.Collections.Generic;");
source.AppendLine("using System.Runtime.CompilerServices;");
source.AppendLine($"namespace {interfaceSymbol.ContainingNamespace.ToDisplayString()} {{");
source.AppendLine($" {interfaceSyntax.Modifiers} partial record {typeName} : {interfaceSymbol.Name} {{");
source.AppendLine($" public {typeName}({string.Join(", ", props.Select(t => t.TypeName + ' ' + t.PropertyName))}) {{");
foreach (AutoPropertySource prop in props) {
source.Append($" {prop.FieldName} = {prop.PropertyName}");
if (prop.IsNullable) {
source.Append($"?? throw new ArgumentNullException(nameof({prop.PropertyName}));");
}
source.AppendLine(";");
}
source.AppendLine($" }}");
foreach (AutoPropertySource prop in props) {
prop.Write(source);
}
source.AppendLine($" partial void Getter<T>(ref T value, [CallerMemberName] string? name = null);");
source.AppendLine($" partial void Setter<T>(ref T value, [CallerMemberName] string? name = null);");
source.AppendLine($" }}");
source.AppendLine($"}}");
context.AddSource($"{interfaceSymbol.Name}.g.cs", SourceText.From(source.ToString(), Encoding.UTF8));
}
}
private sealed class SyntaxReceiver : ISyntaxContextReceiver {
public List<(TypeDeclarationSyntax InterfaceSyntax, INamedTypeSymbol TypeSymbol, List<AutoPropertySource> Properties, string TypeName)> Targets { get; } = new();
public void OnVisitSyntaxNode(GeneratorSyntaxContext context) {
if (!(context.Node is InterfaceDeclarationSyntax interfaceSyntax)) { return; }
if (interfaceSyntax.AttributeLists.Count is 0) { return; }
if (!interfaceSyntax.Identifier.Text.StartsWith("I")) { return; }
IEnumerable<AttributeSyntax> attrs = interfaceSyntax.AttributeLists.SelectMany(t => t.Attributes);
AttributeSyntax attr = attrs.FirstOrDefault(t => t.Name.ToString() switch {
"AutoImpl" => true,
"AutoImplAttribute" => true,
"AutoImpl.AutoImpl" => true,
"AutoImpl.AutoImplAttribute" => true,
_ => false,
});
if (attr is null) { return; }
if (!(context.SemanticModel.GetDeclaredSymbol(interfaceSyntax) is INamedTypeSymbol interfaceSymbol)) { return; }
List<AutoPropertySource> autoProps = new();
foreach (ISymbol member in interfaceSymbol.GetMembers()) {
if (!(member is IPropertySymbol prop)) { continue; }
if (!AutoPropertySource.TryCreate(prop, out AutoPropertySource autoProp)) { continue; }
autoProps.Add(autoProp);
}
string typeName = interfaceSyntax.Identifier.Text.Substring(1);
Targets.Add((interfaceSyntax, interfaceSymbol, autoProps, typeName));
}
}
private readonly struct AutoPropertySource {
private static readonly char[] s_trimStartChars = new[] { '_' };
public static bool TryCreate(IPropertySymbol prop, out AutoPropertySource result) {
string propName = prop.Name;
string fieldName = '_' + propName;
string typeName = prop.Type.ToDisplayString();
result = new(typeName, fieldName, propName, prop.IsReadOnly, prop.Type.IsReferenceType);
return true;
Ignore:
result = default;
return false;
}
private AutoPropertySource(
string typeName
, string fieldName
, string propertyName
, bool isReadOnly
, bool isNullable
) {
TypeName = typeName;
FieldName = fieldName;
PropertyName = propertyName;
IsReadOnly = isReadOnly;
IsNullable = isNullable;
}
public readonly string TypeName;
public readonly string FieldName;
public readonly string PropertyName;
public readonly bool IsReadOnly;
public readonly bool IsNullable;
public void Write(StringBuilder source) {
source.AppendLine($"private {TypeName} {FieldName};");
source.AppendLine($"public {TypeName} {PropertyName} {{");
source.AppendLine($" get {{");
source.AppendLine($" var value = {FieldName};");
source.AppendLine($" Getter(ref value);");
source.AppendLine($" return value;");
source.AppendLine($" }}");
source.AppendLine($" set {{");
source.AppendLine($" Setter(ref value);");
source.AppendLine($" {FieldName} = value;");
source.AppendLine($" }}");
source.AppendLine($"}}");
}
}
}
}
using System;
using System.Collections.Generic;
using AutoImpl;
using FastMember;
using Newtonsoft.Json;
namespace Samples {
internal class Program {
private static void Main(string[] args) {
Account entity = new(1, "foo", DateTime.Now) {
Name = "bar",
};
Console.WriteLine(JsonConvert.SerializeObject(entity.GetChanges()));
// [{"Key":"Name","Value":"bar"}]
}
}
[AutoImpl]
internal interface IAccount {
int AccountId { get; }
string Name { get; set; }
DateTime CreatedAt { get; }
}
internal partial record Account {
private static readonly TypeAccessor s_accessor = TypeAccessor.Create(typeof(Account));
private HashSet<string> _dirties = new();
public IEnumerable<KeyValuePair<string, object>> GetChanges() {
foreach (string name in _dirties) {
yield return new(name, s_accessor[this, name]);
}
}
//partial void Getter<T>(ref T value, string? name) {
//}
partial void Setter<T>(ref T value, string? name) {
_dirties.Add(name!);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment