From 4e35469620bb19998b393793ac0897c66105c369 Mon Sep 17 00:00:00 2001 From: bdbonazz <92273914+bdbonazz@users.noreply.github.com> Date: Mon, 1 Jul 2024 20:16:07 +0200 Subject: [PATCH] Doc Cookbook Auto Interface Implementation (#73906) Address one of the TODOs in the incremental generators cookbook by adding an example of automatically implementing interfaces on types. Contributes to https://github.com/dotnet/roslyn/issues/72149. --- .../incremental-generators.cookbook.md | 185 +++++++++++++++++- 1 file changed, 184 insertions(+), 1 deletion(-) diff --git a/docs/features/incremental-generators.cookbook.md b/docs/features/incremental-generators.cookbook.md index 3cf0870f6bf57..89f92a4564f28 100644 --- a/docs/features/incremental-generators.cookbook.md +++ b/docs/features/incremental-generators.cookbook.md @@ -636,7 +636,190 @@ TODO: https://github.com/dotnet/roslyn/issues/72149 ### Auto interface implementation -TODO: https://github.com/dotnet/roslyn/issues/72149 +**User scenario:** As a generator author I want to be able to implement the properties of interfaces passed as arguments to a decorator of a class automatically for a user + +**Solution:** Require the user to decorate the class with the `[AutoImplement]` Attribute and pass as arguments the types of the interfaces they want to self-implement themselves; The classes that implement the attribute have to be `partial class`. +Provide that attribute in a `RegisterPostInitializationOutput` step. Register for callbacks on the classes with +`ForAttributeWithMetadataName` using the fullyQualifiedMetadataName `FullyQualifiedAttributeName`, and use tuples (or create an equatable model) to pass along that information. +The attribute could work for structs too, the example was kept simple on purpose for the workbook sample. + +**Example:** + +```csharp +public interface IUserInterface +{ + int InterfaceProperty { get; set; } +} + +public interface IUserInterface2 +{ + float InterfacePropertyOnlyGetter { get; } +} + +[AutoImplementProperties(typeof(IUserInterface), typeof(IUserInterface2))] +public partial class UserClass +{ + public string UserProp { get; set; } +} +``` + +```csharp +#nullable enable +[Generator] +public class AutoImplementGenerator : IIncrementalGenerator +{ + private const string AttributeNameSpace = "AttributeGenerator"; + private const string AttributeName = "AutoImplementProperties"; + private const string AttributeClassName = $"{AttributeName}Attribute"; + private const string FullyQualifiedAttributeName = $"{AttributeNameSpace}.{AttributeClassName}"; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput(ctx => + { + //Generate the AutoImplementProperties Attribute + const string autoImplementAttributeDeclarationCode = $$""" +// +using System; +namespace {{AttributeNameSpace}}; + +[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] +sealed class {{AttributeClassName}} : Attribute +{ + public Type[] InterfacesTypes { get; } + public {{AttributeClassName}}(params Type[] interfacesTypes) + { + InterfacesTypes = interfacesTypes; + } +} +"""; + ctx.AddSource($"{AttributeClassName}.g.cs", autoImplementAttributeDeclarationCode); + }); + + IncrementalValuesProvider provider = context.SyntaxProvider.ForAttributeWithMetadataName( + fullyQualifiedMetadataName: FullyQualifiedAttributeName, + predicate: static (node, cancellationToken_) => node is ClassDeclarationSyntax, + transform: static (ctx, cancellationToken) => + { + ISymbol classSymbol = ctx.TargetSymbol; + + return new ClassModel( + classSymbol.Name, + classSymbol.ContainingNamespace.ToDisplayString(), + GetInterfaceModels(ctx.Attributes[0]) + ); + }); + + context.RegisterSourceOutput(provider, static (context, classModel) => + { + foreach (InterfaceModel interfaceModel in classModel.Interfaces) + { + StringBuilder sourceBuilder = new($$""" + // + namespace {{classModel.NameSpace}}; + + public partial class {{classModel.Name}} : {{interfaceModel.FullyQualifiedName}} + { + + """); + + foreach (string property in interfaceModel.Properties) + { + sourceBuilder.AppendLine(property); + } + + sourceBuilder.AppendLine(""" + } + """); + + //Concat class name and interface name to have unique file name if a class implements two interfaces with AutoImplement Attribute + string generatedFileName = $"{classModel.Name}_{interfaceModel.FullyQualifiedName}.g.cs"; + context.AddSource(generatedFileName, sourceBuilder.ToString()); + } + }); + } + + private static EquatableList GetInterfaceModels(AttributeData attribute) + { + EquatableList ret = []; + + if (attribute.ConstructorArguments.Length == 0) + return ret; + + foreach(TypedConstant constructorArgumentValue in attribute.ConstructorArguments[0].Values) + { + if (constructorArgumentValue.Value is INamedTypeSymbol { TypeKind: TypeKind.Interface } interfaceSymbol) + { + EquatableList properties = new(); + + foreach (IPropertySymbol interfaceProperty in interfaceSymbol + .GetMembers() + .OfType()) + { + string type = interfaceProperty.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + + //Check if property has a setter + string setter = interfaceProperty.SetMethod is not null + ? "set; " + : string.Empty; + + properties.Add($$""" + public {{type}} {{interfaceProperty.Name}} { get; {{setter}}} + """); + } + + ret.Add(new InterfaceModel(interfaceSymbol.ToDisplayString(), properties)); + } + } + + return ret; + } + + private record ClassModel(string Name, string NameSpace, EquatableList Interfaces); + private record InterfaceModel(string FullyQualifiedName, EquatableList Properties); + + private class EquatableList : List, IEquatable> + { + public bool Equals(EquatableList? other) + { + // If the other list is null or a different size, they're not equal + if (other is null || Count != other.Count) + { + return false; + } + + // Compare each pair of elements for equality + for (int i = 0; i < Count; i++) + { + if (!EqualityComparer.Default.Equals(this[i], other[i])) + { + return false; + } + } + + // If we got this far, the lists are equal + return true; + } + public override bool Equals(object obj) + { + return Equals(obj as EquatableList); + } + public override int GetHashCode() + { + return this.Select(item => item?.GetHashCode() ?? 0).Aggregate((x, y) => x ^ y); + } + public static bool operator ==(EquatableList list1, EquatableList list2) + { + return ReferenceEquals(list1, list2) + || list1 is not null && list2 is not null && list1.Equals(list2); + } + public static bool operator !=(EquatableList list1, EquatableList list2) + { + return !(list1 == list2); + } + } +} +``` ## Breaking Changes: