Skip to content

Commit

Permalink
Doc Cookbook Auto Interface Implementation (#73906)
Browse files Browse the repository at this point in the history
Address one of the TODOs in the incremental generators cookbook by adding an example of automatically implementing interfaces on types. Contributes to #72149.
  • Loading branch information
bdbonazz committed Jul 1, 2024
1 parent 105491f commit 4e35469
Showing 1 changed file with 184 additions and 1 deletion.
185 changes: 184 additions & 1 deletion docs/features/incremental-generators.cookbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 = $$"""
// <auto-generated/>
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<ClassModel> 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($$"""
// <auto-generated/>
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<InterfaceModel> GetInterfaceModels(AttributeData attribute)
{
EquatableList<InterfaceModel> 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<string> properties = new();

foreach (IPropertySymbol interfaceProperty in interfaceSymbol
.GetMembers()
.OfType<IPropertySymbol>())
{
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<InterfaceModel> Interfaces);
private record InterfaceModel(string FullyQualifiedName, EquatableList<string> Properties);

private class EquatableList<T> : List<T>, IEquatable<EquatableList<T>>
{
public bool Equals(EquatableList<T>? 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<T>.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<T>);
}
public override int GetHashCode()
{
return this.Select(item => item?.GetHashCode() ?? 0).Aggregate((x, y) => x ^ y);
}
public static bool operator ==(EquatableList<T> list1, EquatableList<T> list2)
{
return ReferenceEquals(list1, list2)
|| list1 is not null && list2 is not null && list1.Equals(list2);
}
public static bool operator !=(EquatableList<T> list1, EquatableList<T> list2)
{
return !(list1 == list2);
}
}
}
```

## Breaking Changes:

Expand Down

0 comments on commit 4e35469

Please sign in to comment.