Last active
January 20, 2022 10:14
-
-
Save in-async/6526b25a66deadec89bbc6bd3fe1c468 to your computer and use it in GitHub Desktop.
インターフェース(のプロパティ)を自動実装する Source Generator (仮)
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
<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> |
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.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($"}}"); | |
} | |
} | |
} | |
} |
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 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