-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
- Loading branch information
There are no files selected for viewing
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))] |
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
|
||
{ | ||
//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
|
||
} | ||
} |
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 | ||
// }; | ||
// } | ||
// } |
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 | ||
{ | ||
|
||
} |
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)); | ||
} |
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); | ||
} | ||
} | ||
} |
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
|
||
public Type? PrimaryInterface { get; set; } | ||
} |
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
|
||
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
|
||
.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 | ||
|
||
} | ||
} | ||
} |