Skip to content

Commit

Permalink
Factory pattern (#25)
Browse files Browse the repository at this point in the history
  • Loading branch information
niklasstich authored Oct 8, 2024
1 parent a253313 commit 474a99a
Show file tree
Hide file tree
Showing 29 changed files with 727 additions and 6 deletions.
4 changes: 4 additions & 0 deletions Moyou.Aspects/Moyou.Aspects.Factory/AspectOrder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
using Metalama.Framework.Aspects;
using Moyou.Aspects.Factory;

[assembly: AspectOrder(AspectOrderDirection.CompileTime, typeof(FactoryMemberAspect), typeof(FactoryAttribute))]
123 changes: 123 additions & 0 deletions Moyou.Aspects/Moyou.Aspects.Factory/FactoryAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Metalama.Framework.Advising;
using Metalama.Framework.Aspects;
using Metalama.Framework.Code;
using Metalama.Framework.Diagnostics;
using Moyou.Diagnostics;
using Moyou.Extensions;

namespace Moyou.Aspects.Factory;

[AttributeUsage(AttributeTargets.Class)]
public class FactoryAttribute : TypeAspect
{
private static readonly DiagnosticDefinition<INamedType> ErrorNoSuitableConstructor =
new(Errors.Factory.NoSuitableConstructorId, Severity.Error,
Errors.Factory.NoSuitableConstructorMessageFormat,
Errors.Factory.NoSuitableConstructorTitle,
Errors.Factory.NoSuitableConstructorCategory);

private static readonly DiagnosticDefinition<INamedType> ErrorMultipleMarkedConstructors =
new(Errors.Factory.MultipleMarkedConstructorsId, Severity.Error,
Errors.Factory.MultipleMarkedConstructorsMessageFormat,
Errors.Factory.MultipleMarkedConstructorTitle,
Errors.Factory.MultipleMarkedConstructorCategory);

[SuppressMessage("Usage", "CA2208:Instantiate argument exceptions correctly")] //property is argument
public override void BuildAspect(IAspectBuilder<INamedType> builder)
{
base.BuildAspect(builder);

//read the annotations from FactoryMemberAspect and process all tuples
var annotations = builder.Target.Enhancements().GetAnnotations<FactoryMemberAnnotation>();
var tuples = annotations.Select(annotation => annotation.AsTuple()).ToList();
foreach (var tuple in tuples)
{
AddMemberToFactory(builder, tuple);
}
}

private void AddMemberToFactory(IAspectBuilder<INamedType> builder, (INamedType, INamedType) tuple)
{
var memberType = tuple.Item1;
var primaryInterface = tuple.Item2;
var trimmedInterfaceName = primaryInterface.Name.StartsWith("I")
? primaryInterface.Name[1..]
: primaryInterface.Name;
if (memberType.HasPublicDefaultConstructor())
{
builder.IntroduceMethod(nameof(CreateTemplateDefaultConstructor), IntroductionScope.Instance,
buildMethod: methodBuilder =>
{
//drop the leading 'I' from the interface in the method name
methodBuilder.Name = $"Create{trimmedInterfaceName}";
methodBuilder.Accessibility = Accessibility.Public;
}, args: new { TInterface = primaryInterface, memberType });
}
else
{
HandleNonDefaultConstructor(builder, memberType, trimmedInterfaceName, primaryInterface);
}
}

private static void HandleNonDefaultConstructor(IAspectBuilder<INamedType> builder, INamedType memberType,
string trimmedInterfaceName, INamedType primaryInterface)
{
IConstructor? constructor;
if (memberType.Constructors.Count == 1)
{
constructor = memberType.Constructors.Single();
}
else
{
try
{
constructor =
memberType.Constructors.SingleOrDefault(ctor => ctor.HasAttribute<FactoryConstructorAttribute>());
}
catch (InvalidOperationException iox)

Check warning on line 79 in Moyou.Aspects/Moyou.Aspects.Factory/FactoryAttribute.cs

View workflow job for this annotation

GitHub Actions / Build and Test on ubuntu-latest with .NET Core 8.x

The variable 'iox' is declared but never used

Check warning on line 79 in Moyou.Aspects/Moyou.Aspects.Factory/FactoryAttribute.cs

View workflow job for this annotation

GitHub Actions / Build and Test on windows-latest with .NET Core 8.x

The variable 'iox' is declared but never used

Check warning on line 79 in Moyou.Aspects/Moyou.Aspects.Factory/FactoryAttribute.cs

View workflow job for this annotation

GitHub Actions / Build and Test on macos-latest with .NET Core 8.x

The variable 'iox' is declared but never used
{
//only one constructor with attribute allowed
foreach (var markedCtor in memberType.Constructors.Where(ctor => ctor.HasAttribute<FactoryConstructorAttribute>()))
{
builder.Diagnostics.Report(ErrorMultipleMarkedConstructors.WithArguments(memberType), markedCtor);
}

return;
}
}

if (constructor == null)
{
//no constructor is marked
builder.Diagnostics.Report(ErrorNoSuitableConstructor.WithArguments(memberType), memberType);
return;
}

builder.IntroduceMethod(nameof(CreateTemplate), IntroductionScope.Instance, buildMethod: builder =>
{
builder.Name = $"Create{trimmedInterfaceName}";
builder.Accessibility = Accessibility.Public;
//add all constructor parameters to factory method
foreach (var constructorParameter in constructor.Parameters)
{
builder.AddParameter(constructorParameter.Name, constructorParameter.Type);
}
}, args: new { TInterface = primaryInterface, constructor });
}

[Template]
public static TInterface CreateTemplateDefaultConstructor<[CompileTime] TInterface>(
[CompileTime] INamedType memberType)
{
var constructor = meta.CompileTime(memberType.Constructors.GetPublicDefaultConstructor());
return constructor.Invoke()!;
}

[Template]
public static TInterface CreateTemplate<[CompileTime] TInterface>([CompileTime] IConstructor constructor)
{
return constructor.Invoke(constructor.Parameters.Select(param => (IExpression)param.Value!));

Check warning on line 121 in Moyou.Aspects/Moyou.Aspects.Factory/FactoryAttribute.cs

View workflow job for this annotation

GitHub Actions / Build and Test on ubuntu-latest with .NET Core 8.x

Possible null reference return.

Check warning on line 121 in Moyou.Aspects/Moyou.Aspects.Factory/FactoryAttribute.cs

View workflow job for this annotation

GitHub Actions / Build and Test on windows-latest with .NET Core 8.x

Possible null reference return.

Check warning on line 121 in Moyou.Aspects/Moyou.Aspects.Factory/FactoryAttribute.cs

View workflow job for this annotation

GitHub Actions / Build and Test on macos-latest with .NET Core 8.x

Possible null reference return.
}
}
14 changes: 14 additions & 0 deletions Moyou.Aspects/Moyou.Aspects.Factory/FactoryAttributeOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace Moyou.Aspects.Factory;

// public class FactoryAttributeOptions : IHierarchicalOptions<INamedType>
// {
// public INamedType? AbstractFactoryType { get; set; }
// public object ApplyChanges(object changes, in ApplyChangesContext context)
// {
// var other = (FactoryAttributeOptions)changes;
// return new FactoryAttributeOptions
// {
// AbstractFactoryType = other.AbstractFactoryType ?? AbstractFactoryType
// };
// }
// }
10 changes: 10 additions & 0 deletions Moyou.Aspects/Moyou.Aspects.Factory/FactoryConstructorAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Metalama.Framework.Aspects;

namespace Moyou.Aspects.Factory;

[AttributeUsage(AttributeTargets.Constructor)]
[RunTimeOrCompileTime]
public class FactoryConstructorAttribute : Attribute
{

}
21 changes: 21 additions & 0 deletions Moyou.Aspects/Moyou.Aspects.Factory/FactoryMemberAnnotation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Metalama.Framework.Aspects;
using Metalama.Framework.Code;

namespace Moyou.Aspects.Factory;

[CompileTime]
public record FactoryMemberAnnotation : IAnnotation<INamedType>
{
public IRef<IDeclaration> FactoryMemberType { get; }
public IRef<IDeclaration> PrimaryInterface { get; }

public FactoryMemberAnnotation(IRef<IDeclaration> factoryMemberType, IRef<IDeclaration> primaryInterface)
{
FactoryMemberType = factoryMemberType;
PrimaryInterface = primaryInterface;
}

public (INamedType, INamedType) AsTuple() => (
(INamedType)FactoryMemberType.GetTarget(ReferenceResolutionOptions.Default),
(INamedType)PrimaryInterface.GetTarget(ReferenceResolutionOptions.Default));
}
29 changes: 29 additions & 0 deletions Moyou.Aspects/Moyou.Aspects.Factory/FactoryMemberAspect.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Diagnostics.CodeAnalysis;
using Metalama.Framework.Advising;
using Metalama.Framework.Aspects;
using Metalama.Framework.Code;

namespace Moyou.Aspects.Factory;

public class FactoryMemberAspect : IAspect<INamedType>
{
public List<(INamedType, INamedType)> TargetTuples { get; }

public FactoryMemberAspect(List<(INamedType, INamedType)> targetTuples)
{
TargetTuples = targetTuples;
}


[SuppressMessage("Usage", "CA2208:Instantiate argument exceptions correctly")] //property is argument
public void BuildAspect(IAspectBuilder<INamedType> builder)
{
//write an annotation on the target type containing the factory members and primary interface
var annotations = TargetTuples
.Select(tup => new FactoryMemberAnnotation(tup.Item1.ToRef(), tup.Item2.ToRef()));
foreach (var annotation in annotations)
{
builder.AddAnnotation(annotation, true);
}
}
}
11 changes: 11 additions & 0 deletions Moyou.Aspects/Moyou.Aspects.Factory/FactoryMemberAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Metalama.Framework.Aspects;

namespace Moyou.Aspects.Factory;

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = true)]
[RunTimeOrCompileTime]
public class FactoryMemberAttribute : Attribute
{
public Type TargetType { get; set; }

Check warning on line 9 in Moyou.Aspects/Moyou.Aspects.Factory/FactoryMemberAttribute.cs

View workflow job for this annotation

GitHub Actions / Build and Test on ubuntu-latest with .NET Core 8.x

Non-nullable property 'TargetType' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 9 in Moyou.Aspects/Moyou.Aspects.Factory/FactoryMemberAttribute.cs

View workflow job for this annotation

GitHub Actions / Build and Test on ubuntu-latest with .NET Core 8.x

Non-nullable property 'TargetType' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 9 in Moyou.Aspects/Moyou.Aspects.Factory/FactoryMemberAttribute.cs

View workflow job for this annotation

GitHub Actions / Build and Test on windows-latest with .NET Core 8.x

Non-nullable property 'TargetType' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 9 in Moyou.Aspects/Moyou.Aspects.Factory/FactoryMemberAttribute.cs

View workflow job for this annotation

GitHub Actions / Build and Test on windows-latest with .NET Core 8.x

Non-nullable property 'TargetType' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 9 in Moyou.Aspects/Moyou.Aspects.Factory/FactoryMemberAttribute.cs

View workflow job for this annotation

GitHub Actions / Build and Test on macos-latest with .NET Core 8.x

Non-nullable property 'TargetType' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 9 in Moyou.Aspects/Moyou.Aspects.Factory/FactoryMemberAttribute.cs

View workflow job for this annotation

GitHub Actions / Build and Test on macos-latest with .NET Core 8.x

Non-nullable property 'TargetType' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
public Type? PrimaryInterface { get; set; }
}
163 changes: 163 additions & 0 deletions Moyou.Aspects/Moyou.Aspects.Factory/FactoryMemberFabric.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
using JetBrains.Annotations;
using Metalama.Framework.Code;
using Metalama.Framework.Diagnostics;
using Metalama.Framework.Fabrics;
using Moyou.Diagnostics;
using Moyou.Extensions;

namespace Moyou.Aspects.Factory;

[UsedImplicitly]
public class FactoryMemberFabric : TransitiveProjectFabric
{
//MOYOU2201
private static readonly DiagnosticDefinition<INamedType> ErrorNoTargetTypeInMemberAttribute =
new(Errors.Factory.NoTargetTypeInMemberAttributeId, Severity.Error,
Errors.Factory.NoTargetTypeInMemberAttributeMessageFormat,
Errors.Factory.NoTargetTypeInMemberAttributeTitle,
Errors.Factory.NoTargetTypeInMemberAttributeCategory);

//MOYOU2202
private static readonly DiagnosticDefinition<INamedType> ErrorTypeDoesntImplementAnyInterfaces =
new(Errors.Factory.TypeDoesntImplementAnyInterfacesId, Severity.Error,
Errors.Factory.TypeDoesntImplementAnyInterfacesMessageFormat,
Errors.Factory.TypeDoesntImplementAnyInterfacesTitle,
Errors.Factory.TypeDoesntImplementAnyInterfacesCategory);

//MOYOU2203
private static readonly DiagnosticDefinition<INamedType> ErrorAmbiguousInterfacesOnTargetType =
new(Errors.Factory.AmbiguousInterfacesOnTargetTypeId, Severity.Error,
Errors.Factory.AmbiguousInterfacesOnTargetTypeMessageFormat,
Errors.Factory.AmbiguousInterfacesOnTargetTypeTitle,
Errors.Factory.AmbiguousInterfacesOnTargetTypeCategory);

//MOYOU2204
private static readonly DiagnosticDefinition<INamedType>
ErrorTargetTypeDoesntImplementPrimaryInterface =
new(Errors.Factory.TargetTypeDoesntImplementPrimaryInterfaceId, Severity.Error,
Errors.Factory.TargetTypeDoesntImplementPrimaryInterfaceMessageFormat,
Errors.Factory.TargetTypeDoesntImplementPrimaryInterfaceTitle,
Errors.Factory.TargetTypeDoesntImplementPrimaryInterfaceCategory);

public override void AmendProject(IProjectAmender amender)
{
var types = amender
.SelectTypes()
.Where(type => type.HasAttribute<FactoryMemberAttribute>());

//MOYOU2201 no target type
types
.Where(type => type
.Attributes
.Where(IsFactoryMemberAttribute)
.Any(NoTargetTypeInAttribute)
)
.ReportDiagnostic(type => ErrorNoTargetTypeInMemberAttribute.WithArguments(type));

//MOYOU2202 no implemented interfaces
types
.Where(type => type
.Attributes
.Where(IsFactoryMemberAttribute)
.Any(TargetTypeImplementsNoInterfaces)
)
.ReportDiagnostic(type => ErrorTypeDoesntImplementAnyInterfaces.WithArguments(type));

//MOYOU2203 ambiguous interfaces
types
.Where(type => type
.Attributes
.Where(IsFactoryMemberAttribute)
.Where(TargetTypeInAttribute)
.Where(TargetTypeImplementsMultipleInterfaces)
.Any(NoPrimaryInterfaceInAttribute)
)
.ReportDiagnostic(type => ErrorAmbiguousInterfacesOnTargetType.WithArguments(type));

//MOYOU2204 target type doesn't implement primary interface
types
.Where(type => type
.Attributes
.Where(IsFactoryMemberAttribute)
.Where(TargetTypeInAttribute)
.Any(TargetTypeDoesNotImplementPrimaryInterface)
)
.ReportDiagnostic(type => ErrorTargetTypeDoesntImplementPrimaryInterface.WithArguments(type));

types.AddAspect(type => BuildAspect(type, amender));
}

private static bool TargetTypeImplementsMultipleInterfaces(IAttribute attribute)
{
var targetType = (INamedType)attribute.NamedArguments[nameof(FactoryMemberAttribute.TargetType)].Value!;
return targetType.ImplementedInterfaces.Count > 1;
}

private static bool IsFactoryMemberAttribute(IAttribute attribute)
{
return attribute.Type.FullName == typeof(FactoryMemberAttribute).FullName;
}

private static bool NoTargetTypeInAttribute(IAttribute attribute)
{
return !attribute.TryGetNamedArgument(nameof(FactoryMemberAttribute.TargetType), out _);
}

private static bool NoPrimaryInterfaceInAttribute(IAttribute attribute)
{
return !attribute.TryGetNamedArgument(nameof(FactoryMemberAttribute.PrimaryInterface), out _);
}

private static bool TargetTypeInAttribute(IAttribute attribute)
{
return attribute.TryGetNamedArgument(nameof(FactoryMemberAttribute.TargetType), out _);
}

private static bool TargetTypeImplementsNoInterfaces(IAttribute attribute)
{
return attribute.TryGetNamedArgument(nameof(FactoryMemberAttribute.TargetType), out var targetType) &&
((INamedType)targetType.Value!).ImplementedInterfaces.Count == 0;
}

private static bool TargetTypeDoesNotImplementPrimaryInterface(IAttribute attribute)
{
var targetType = (INamedType)attribute.NamedArguments[nameof(FactoryMemberAttribute.TargetType)].Value!;
return attribute.TryGetNamedArgument(nameof(FactoryMemberAttribute.PrimaryInterface), out var primaryInterface) && !targetType.ImplementedInterfaces.Contains((INamedType)primaryInterface.Value!);
}

private static FactoryMemberAspect BuildAspect(INamedType type, IProjectAmender amender)
{
var memberAttributes = type
.Attributes
.Where(attr => attr.Type.FullName == typeof(FactoryMemberAttribute).FullName);
var targetTuples = GetTypeTuplesFromAttributes(type, memberAttributes);
var aspect = new FactoryMemberAspect(targetTuples);

Check warning on line 134 in Moyou.Aspects/Moyou.Aspects.Factory/FactoryMemberFabric.cs

View workflow job for this annotation

GitHub Actions / Build and Test on ubuntu-latest with .NET Core 8.x

Argument of type 'List<(INamedType, INamedType?)>' cannot be used for parameter 'targetTuples' of type 'List<(INamedType, INamedType)>' in 'FactoryMemberAspect.FactoryMemberAspect(List<(INamedType, INamedType)> targetTuples)' due to differences in the nullability of reference types.

Check warning on line 134 in Moyou.Aspects/Moyou.Aspects.Factory/FactoryMemberFabric.cs

View workflow job for this annotation

GitHub Actions / Build and Test on windows-latest with .NET Core 8.x

Argument of type 'List<(INamedType, INamedType?)>' cannot be used for parameter 'targetTuples' of type 'List<(INamedType, INamedType)>' in 'FactoryMemberAspect.FactoryMemberAspect(List<(INamedType, INamedType)> targetTuples)' due to differences in the nullability of reference types.

Check warning on line 134 in Moyou.Aspects/Moyou.Aspects.Factory/FactoryMemberFabric.cs

View workflow job for this annotation

GitHub Actions / Build and Test on macos-latest with .NET Core 8.x

Argument of type 'List<(INamedType, INamedType?)>' cannot be used for parameter 'targetTuples' of type 'List<(INamedType, INamedType)>' in 'FactoryMemberAspect.FactoryMemberAspect(List<(INamedType, INamedType)> targetTuples)' due to differences in the nullability of reference types.
return aspect;
}

private static List<(INamedType, INamedType?)> GetTypeTuplesFromAttributes(INamedType factoryType,
IEnumerable<IAttribute> memberAttributes)
{
return memberAttributes
.Select(GetTypeAndInterfaceTuple)
.Where(tuple => tuple.HasValue)
.Select(tuple => tuple.Value)

Check warning on line 144 in Moyou.Aspects/Moyou.Aspects.Factory/FactoryMemberFabric.cs

View workflow job for this annotation

GitHub Actions / Build and Test on ubuntu-latest with .NET Core 8.x

Nullable value type may be null.

Check warning on line 144 in Moyou.Aspects/Moyou.Aspects.Factory/FactoryMemberFabric.cs

View workflow job for this annotation

GitHub Actions / Build and Test on windows-latest with .NET Core 8.x

Nullable value type may be null.

Check warning on line 144 in Moyou.Aspects/Moyou.Aspects.Factory/FactoryMemberFabric.cs

View workflow job for this annotation

GitHub Actions / Build and Test on macos-latest with .NET Core 8.x

Nullable value type may be null.
.ToList();

(INamedType, INamedType?)? GetTypeAndInterfaceTuple(IAttribute attr)
{
if (!attr.NamedArguments.TryGetValue(nameof(FactoryMemberAttribute.TargetType), out var targetTypeConstant))
return null; //MOYOU2201
var targetType = (INamedType)targetTypeConstant.Value!;
var implementedInterfaces = targetType.ImplementedInterfaces;
if (!attr.NamedArguments.TryGetValue(nameof(FactoryMemberAttribute.PrimaryInterface),
out var primaryInterface))
return implementedInterfaces.Count == 1 ? (targetType, implementedInterfaces.First()) : null; //MOYOU2202 //MOYOU2203
var primaryInterfaceType = (INamedType)primaryInterface.Value!;
if (implementedInterfaces.Contains(primaryInterfaceType))
return (targetType, primaryInterface.Value as INamedType);
return null; //MOYOU2204

}
}
}
Loading

0 comments on commit 474a99a

Please sign in to comment.