From 7f4a437f3ddf6e2d2edcddb3b4f84047bd6aed36 Mon Sep 17 00:00:00 2001 From: Nikolay Pianikov Date: Wed, 5 Jun 2024 13:58:30 +0300 Subject: [PATCH] Support for generic attributes --- readme/custom-attributes.md | 39 ++++- .../Core/ApiInvocationProcessor.cs | 12 +- .../Core/CompilationExtensions.cs | 10 +- .../ImplementationDependencyNodeBuilder.cs | 16 +- src/Pure.DI.Core/Core/Models/IMdAttribute.cs | 2 +- .../Core/Models/MdOrdinalAttribute.cs | 2 +- .../Core/Models/MdTagAttribute.cs | 2 +- .../Core/Models/MdTypeAttribute.cs | 2 +- src/Pure.DI.Core/Pure.DI.Core.csproj | 1 + .../TypeAttributeTests.cs | 152 ++++++++++++++++++ .../Attributes/CustomAttributesScenario.cs | 23 ++- 11 files changed, 242 insertions(+), 19 deletions(-) create mode 100644 tests/Pure.DI.IntegrationTests/TypeAttributeTests.cs diff --git a/readme/custom-attributes.md b/readme/custom-attributes.md index 3a2271324..af7e1c872 100644 --- a/readme/custom-attributes.md +++ b/readme/custom-attributes.md @@ -29,31 +29,45 @@ class MyTagAttribute(object tag) : Attribute; | AttributeTargets.Field)] class MyTypeAttribute(Type type) : Attribute; +[AttributeUsage( + AttributeTargets.Parameter + | AttributeTargets.Property + | AttributeTargets.Field)] +class MyGenericTypeAttribute : Attribute; + interface IPerson; class Person([MyTag("NikName")] string name) : IPerson { + private object? _state; + [MyOrdinal(1)] [MyType(typeof(int))] internal object Id = ""; - public override string ToString() => $"{Id} {name}"; + [MyOrdinal(2)] + public void Initialize([MyGenericType] object state) => + _state = state; + + public override string ToString() => $"{Id} {name} {_state}"; } DI.Setup(nameof(PersonComposition)) .TagAttribute() .OrdinalAttribute() .TypeAttribute() + .TypeAttribute>() .Arg("personId") - .Bind("NikName").To(_ => "Nik") - .Bind().To() + .Bind().To(_ => new Uri("https://github.com/DevTeam/Pure.DI")) + .Bind("NikName").To(_ => "Nik") + .Bind().To() // Composition root .Root("Person"); var composition = new PersonComposition(personId: 123); var person = composition.Person; -person.ToString().ShouldBe("123 Nik"); +person.ToString().ShouldBe("123 Nik https://github.com/DevTeam/Pure.DI"); ``` The following partial class will be generated: @@ -82,9 +96,11 @@ partial class PersonComposition [MethodImpl(MethodImplOptions.AggressiveInlining)] get { + Uri transientUri2 = new Uri("https://github.com/DevTeam/Pure.DI"); string transientString1 = "Nik"; Person transientPerson0 = new Person(transientString1); transientPerson0.Id = _argPersonId; + transientPerson0.Initialize(transientUri2); return transientPerson0; } } @@ -100,11 +116,25 @@ classDiagram +IPerson Person } class Int32 + Uri --|> ISpanFormattable + Uri --|> IFormattable + Uri --|> ISerializable + class Uri class String Person --|> IPerson class Person { +Person(String name) ~Object Id + +Initialize(Object state) : Void + } + class ISpanFormattable { + <> + } + class IFormattable { + <> + } + class ISerializable { + <> } class IPerson { <> @@ -112,5 +142,6 @@ classDiagram PersonComposition ..> Person : IPerson Person Person *-- String : "NikName" String Person o-- Int32 : Argument "personId" + Person *-- Uri : Uri ``` diff --git a/src/Pure.DI.Core/Core/ApiInvocationProcessor.cs b/src/Pure.DI.Core/Core/ApiInvocationProcessor.cs index e929e5a18..8631ccfd9 100644 --- a/src/Pure.DI.Core/Core/ApiInvocationProcessor.cs +++ b/src/Pure.DI.Core/Core/ApiInvocationProcessor.cs @@ -222,7 +222,13 @@ public void ProcessInvocation( case nameof(IConfiguration.TypeAttribute): if (genericName.TypeArgumentList.Arguments is [{ } typeAttributeType]) { - metadataVisitor.VisitTypeAttribute(new MdTypeAttribute(semanticModel, invocation.ArgumentList, semanticModel.GetTypeSymbol(typeAttributeType, cancellationToken), BuildConstantArgs(semanticModel, invocation.ArgumentList.Arguments) is [int positionVal] ? positionVal : 0)); + var type = semanticModel.GetTypeSymbol(typeAttributeType, cancellationToken); + if (type.IsGenericType) + { + type = type.ConstructUnboundGenericType(); + } + + metadataVisitor.VisitTypeAttribute(new MdTypeAttribute(semanticModel, invocation.ArgumentList, type, BuildConstantArgs(semanticModel, invocation.ArgumentList.Arguments) is [int positionVal] ? positionVal : 0)); } break; @@ -230,7 +236,7 @@ public void ProcessInvocation( case nameof(IConfiguration.TagAttribute): if (genericName.TypeArgumentList.Arguments is [{ } tagAttributeType]) { - metadataVisitor.VisitTagAttribute(new MdTagAttribute(semanticModel, invocation.ArgumentList, semanticModel.GetTypeSymbol(tagAttributeType, cancellationToken), BuildConstantArgs(semanticModel, invocation.ArgumentList.Arguments) is [int positionVal] ? positionVal : 0)); + metadataVisitor.VisitTagAttribute(new MdTagAttribute(semanticModel, invocation.ArgumentList, semanticModel.GetTypeSymbol(tagAttributeType, cancellationToken), BuildConstantArgs(semanticModel, invocation.ArgumentList.Arguments) is [int positionVal] ? positionVal : 0)); } break; @@ -238,7 +244,7 @@ public void ProcessInvocation( case nameof(IConfiguration.OrdinalAttribute): if (genericName.TypeArgumentList.Arguments is [{ } ordinalAttributeType]) { - metadataVisitor.VisitOrdinalAttribute(new MdOrdinalAttribute(semanticModel, invocation.ArgumentList, semanticModel.GetTypeSymbol(ordinalAttributeType, cancellationToken), BuildConstantArgs(semanticModel, invocation.ArgumentList.Arguments) is [int positionVal] ? positionVal : 0)); + metadataVisitor.VisitOrdinalAttribute(new MdOrdinalAttribute(semanticModel, invocation.ArgumentList, semanticModel.GetTypeSymbol(ordinalAttributeType, cancellationToken), BuildConstantArgs(semanticModel, invocation.ArgumentList.Arguments) is [int positionVal] ? positionVal : 0)); } break; diff --git a/src/Pure.DI.Core/Core/CompilationExtensions.cs b/src/Pure.DI.Core/Core/CompilationExtensions.cs index 8a8dc7b7d..960f31c87 100644 --- a/src/Pure.DI.Core/Core/CompilationExtensions.cs +++ b/src/Pure.DI.Core/Core/CompilationExtensions.cs @@ -4,14 +4,20 @@ namespace Pure.DI.Core; internal static class CompilationExtensions { [SuppressMessage("ReSharper", "HeapView.ClosureAllocation")] - public static IReadOnlyList GetAttributes(this ISymbol member, ITypeSymbol attributeType) => + public static IReadOnlyList GetAttributes(this ISymbol member, INamedTypeSymbol attributeType) => member .GetAttributes() - .Where(attr => attr.AttributeClass != null && SymbolEqualityComparer.Default.Equals(attr.AttributeClass, attributeType)) + .Where(attr => attr.AttributeClass != null && SymbolEqualityComparer.Default.Equals(GetUnboundTypeSymbol(attr.AttributeClass), attributeType)) .ToArray(); public static LanguageVersion GetLanguageVersion(this Compilation compilation) => compilation is CSharpCompilation sharpCompilation ? sharpCompilation.LanguageVersion : LanguageVersion.Default; + + private static INamedTypeSymbol? GetUnboundTypeSymbol(INamedTypeSymbol? typeSymbol) => + typeSymbol is null + ? typeSymbol : typeSymbol.IsGenericType + ? typeSymbol.ConstructUnboundGenericType() + : typeSymbol; } \ No newline at end of file diff --git a/src/Pure.DI.Core/Core/ImplementationDependencyNodeBuilder.cs b/src/Pure.DI.Core/Core/ImplementationDependencyNodeBuilder.cs index f4747a97d..4d4f9da67 100644 --- a/src/Pure.DI.Core/Core/ImplementationDependencyNodeBuilder.cs +++ b/src/Pure.DI.Core/Core/ImplementationDependencyNodeBuilder.cs @@ -4,6 +4,8 @@ // ReSharper disable ClassNeverInstantiated.Global namespace Pure.DI.Core; +using ITypeSymbol = Microsoft.CodeAnalysis.ITypeSymbol; + internal sealed class ImplementationDependencyNodeBuilder( ILogger logger, IBuilder> implementationVariantsBuilder) @@ -224,8 +226,18 @@ private T GetAttribute( switch (attributeData.Count) { case 1: - var args = attributeData[0].ConstructorArguments; - if (attribute.ArgumentPosition > args.Length) + var attr = attributeData[0]; + if (typeof(ITypeSymbol).IsAssignableFrom(typeof(T)) && attr.AttributeClass is { IsGenericType: true, TypeArguments.Length: > 0 } attributeClass) + { + if (attribute.ArgumentPosition < attributeClass.TypeArguments.Length + && attributeClass.TypeArguments[attribute.ArgumentPosition] is { } typeSymbol) + { + return (T)typeSymbol; + } + } + + var args = attr.ConstructorArguments; + if (attribute.ArgumentPosition >= args.Length) { logger.CompileError($"The argument position {attribute.ArgumentPosition.ToString()} of attribute {attribute.Source} is out of range [0..{args.Length.ToString()}].", attribute.Source.GetLocation(), LogId.ErrorInvalidMetadata); } diff --git a/src/Pure.DI.Core/Core/Models/IMdAttribute.cs b/src/Pure.DI.Core/Core/Models/IMdAttribute.cs index 27eeba2db..9c7ef1c4d 100644 --- a/src/Pure.DI.Core/Core/Models/IMdAttribute.cs +++ b/src/Pure.DI.Core/Core/Models/IMdAttribute.cs @@ -7,7 +7,7 @@ internal interface IMdAttribute SyntaxNode Source { get; } - ITypeSymbol AttributeType { get; } + INamedTypeSymbol AttributeType { get; } int ArgumentPosition { get; } } \ No newline at end of file diff --git a/src/Pure.DI.Core/Core/Models/MdOrdinalAttribute.cs b/src/Pure.DI.Core/Core/Models/MdOrdinalAttribute.cs index 01f51328c..b13bc80a4 100644 --- a/src/Pure.DI.Core/Core/Models/MdOrdinalAttribute.cs +++ b/src/Pure.DI.Core/Core/Models/MdOrdinalAttribute.cs @@ -5,7 +5,7 @@ namespace Pure.DI.Core.Models; internal readonly record struct MdOrdinalAttribute( SemanticModel SemanticModel, SyntaxNode Source, - ITypeSymbol AttributeType, + INamedTypeSymbol AttributeType, int ArgumentPosition) : IMdAttribute { public override string ToString() => $".OrdinalAttribute<{AttributeType}>({(ArgumentPosition != 0 ? ArgumentPosition.ToString() : string.Empty)})"; diff --git a/src/Pure.DI.Core/Core/Models/MdTagAttribute.cs b/src/Pure.DI.Core/Core/Models/MdTagAttribute.cs index a97d667dd..935ac0072 100644 --- a/src/Pure.DI.Core/Core/Models/MdTagAttribute.cs +++ b/src/Pure.DI.Core/Core/Models/MdTagAttribute.cs @@ -5,7 +5,7 @@ namespace Pure.DI.Core.Models; internal readonly record struct MdTagAttribute( SemanticModel SemanticModel, SyntaxNode Source, - ITypeSymbol AttributeType, + INamedTypeSymbol AttributeType, int ArgumentPosition) : IMdAttribute { public override string ToString() => $".TagAttribute<{AttributeType}>({(ArgumentPosition != 0 ? ArgumentPosition.ToString() : string.Empty)})"; diff --git a/src/Pure.DI.Core/Core/Models/MdTypeAttribute.cs b/src/Pure.DI.Core/Core/Models/MdTypeAttribute.cs index a5a24e6fd..408373299 100644 --- a/src/Pure.DI.Core/Core/Models/MdTypeAttribute.cs +++ b/src/Pure.DI.Core/Core/Models/MdTypeAttribute.cs @@ -4,7 +4,7 @@ namespace Pure.DI.Core.Models; internal readonly record struct MdTypeAttribute( SemanticModel SemanticModel, SyntaxNode Source, - ITypeSymbol AttributeType, + INamedTypeSymbol AttributeType, int ArgumentPosition) : IMdAttribute { public override string ToString() => $".TypeAttribute<{AttributeType}>({(ArgumentPosition != 0 ? ArgumentPosition.ToString() : string.Empty)})"; diff --git a/src/Pure.DI.Core/Pure.DI.Core.csproj b/src/Pure.DI.Core/Pure.DI.Core.csproj index d2ca82c56..41499ad2c 100644 --- a/src/Pure.DI.Core/Pure.DI.Core.csproj +++ b/src/Pure.DI.Core/Pure.DI.Core.csproj @@ -21,6 +21,7 @@ True GenericTypeArguments.g.tt + diff --git a/tests/Pure.DI.IntegrationTests/TypeAttributeTests.cs b/tests/Pure.DI.IntegrationTests/TypeAttributeTests.cs new file mode 100644 index 000000000..308a77c09 --- /dev/null +++ b/tests/Pure.DI.IntegrationTests/TypeAttributeTests.cs @@ -0,0 +1,152 @@ +namespace Pure.DI.IntegrationTests; + +public class TypeAttributeTests +{ + [Fact] + public async Task ShouldSupportTypeAttribute() + { + // Given + + // When + var result = await """ +using System; +using Pure.DI; + +namespace Sample +{ + interface IDependency { } + + class AbcDependency : IDependency { } + + class XyzDependency : IDependency { } + + interface IService + { + IDependency Dependency1 { get; } + + IDependency Dependency2 { get; } + } + + class Service : IService + { + public Service( + [Type(typeof(AbcDependency))] IDependency dependency1, + [Type(typeof(XyzDependency))] IDependency dependency2) + { + Dependency1 = dependency1; + Dependency2 = dependency2; + } + + public IDependency Dependency1 { get; } + + public IDependency Dependency2 { get; } + } + + partial class Composition + { + private static void SetupComposition() + { + DI.Setup(nameof(Composition)) + .Bind().To() + + // Composition root + .Root("Root"); + } + } + + public class Program + { + public static void Main() + { + var composition = new Composition(); + var service = composition.Root; + Console.WriteLine(service.Dependency1); + Console.WriteLine(service.Dependency2); + } + } +} +""".RunAsync(); + + // Then + result.Success.ShouldBeTrue(result); + result.StdOut.ShouldBe(["Sample.AbcDependency", "Sample.XyzDependency"], result); + } + +#if ROSLYN4_8_OR_GREATER + [Fact] + public async Task ShouldSupportGenericTypeAttribute() + { + // Given + + // When + var result = await """ +using System; +using Pure.DI; + +namespace Sample +{ + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)] + internal class TypeAttribute : Attribute + { + } + + interface IDependency { } + + class AbcDependency : IDependency { } + + class XyzDependency : IDependency { } + + interface IService + { + IDependency Dependency1 { get; } + + IDependency Dependency2 { get; } + } + + class Service : IService + { + public Service( + [Type] IDependency dependency1, + [Type] IDependency dependency2) + { + Dependency1 = dependency1; + Dependency2 = dependency2; + } + + public IDependency Dependency1 { get; } + + public IDependency Dependency2 { get; } + } + + partial class Composition + { + private static void SetupComposition() + { + DI.Setup(nameof(Composition)) + .TypeAttribute>() + .Bind().To() + + // Composition root + .Root("Root"); + } + } + + public class Program + { + public static void Main() + { + var composition = new Composition(); + var service = composition.Root; + Console.WriteLine(service.Dependency1); + Console.WriteLine(service.Dependency2); + } + } +} +""".RunAsync(new Options(LanguageVersion.CSharp11)); + + // Then + result.Success.ShouldBeTrue(result); + result.StdOut.ShouldBe(["Sample.AbcDependency", "Sample.XyzDependency"], result); + } +#endif +} \ No newline at end of file diff --git a/tests/Pure.DI.UsageTests/Attributes/CustomAttributesScenario.cs b/tests/Pure.DI.UsageTests/Attributes/CustomAttributesScenario.cs index 8e67fe288..a01bd4b79 100644 --- a/tests/Pure.DI.UsageTests/Attributes/CustomAttributesScenario.cs +++ b/tests/Pure.DI.UsageTests/Attributes/CustomAttributesScenario.cs @@ -14,6 +14,7 @@ // ReSharper disable UnusedParameter.Local // ReSharper disable ClassNeverInstantiated.Global // ReSharper disable ArrangeTypeModifiers +// ReSharper disable MemberCanBePrivate.Global #pragma warning disable CS9113 // Parameter is unread. namespace Pure.DI.UsageTests.Attributes.CustomAttributesScenario; @@ -39,15 +40,27 @@ class MyTagAttribute(object tag) : Attribute; | AttributeTargets.Field)] class MyTypeAttribute(Type type) : Attribute; +[AttributeUsage( + AttributeTargets.Parameter + | AttributeTargets.Property + | AttributeTargets.Field)] +class MyGenericTypeAttribute : Attribute; + interface IPerson; class Person([MyTag("NikName")] string name) : IPerson { + private object? _state; + [MyOrdinal(1)] [MyType(typeof(int))] internal object Id = ""; - public override string ToString() => $"{Id} {name}"; + [MyOrdinal(2)] + public void Initialize([MyGenericType] object state) => + _state = state; + + public override string ToString() => $"{Id} {name} {_state}"; } // } @@ -62,16 +75,18 @@ public void Run() .TagAttribute() .OrdinalAttribute() .TypeAttribute() + .TypeAttribute>() .Arg("personId") - .Bind("NikName").To(_ => "Nik") - .Bind().To() + .Bind().To(_ => new Uri("https://github.com/DevTeam/Pure.DI")) + .Bind("NikName").To(_ => "Nik") + .Bind().To() // Composition root .Root("Person"); var composition = new PersonComposition(personId: 123); var person = composition.Person; - person.ToString().ShouldBe("123 Nik"); + person.ToString().ShouldBe("123 Nik https://github.com/DevTeam/Pure.DI"); // } composition.SaveClassDiagram(); }