Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Factory pattern #25

Merged
merged 10 commits into from
Oct 8, 2024
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
Loading