From c69c43a4477d41c0e0ffc39da82be85935acbd35 Mon Sep 17 00:00:00 2001 From: Nikolay Pianikov Date: Thu, 14 Mar 2024 12:05:52 +0300 Subject: [PATCH] #42 Generic type composition root - support of type parameter constraints --- .../Core/Code/RootMethodsBuilder.cs | 50 +++++- src/Pure.DI.Core/Core/Code/TypeDescription.cs | 3 +- src/Pure.DI.Core/Core/Code/TypeResolver.cs | 39 ++--- .../GenericRootsTests.cs | 149 ++++++++++++++++++ ...WithConstraintsCompositionRootsScenario.cs | 80 ++++++++++ 5 files changed, 300 insertions(+), 21 deletions(-) create mode 100644 tests/Pure.DI.UsageTests/Basics/GenericsWithConstraintsCompositionRootsScenario.cs diff --git a/src/Pure.DI.Core/Core/Code/RootMethodsBuilder.cs b/src/Pure.DI.Core/Core/Code/RootMethodsBuilder.cs index beec81990..4b1600ebd 100644 --- a/src/Pure.DI.Core/Core/Code/RootMethodsBuilder.cs +++ b/src/Pure.DI.Core/Core/Code/RootMethodsBuilder.cs @@ -80,6 +80,7 @@ private void BuildRoot(CompositionCode composition, Root root) name.Append(root.DisplayName); var typeArgs = root.TypeDescription.TypeArgs; + var constraints = new List(); if (typeArgs.Count > 0) { name.Append('<'); @@ -88,8 +89,55 @@ private void BuildRoot(CompositionCode composition, Root root) } name.Append(rootArgsStr); - code.AppendLine(name.ToString()); + + if (typeArgs.Count > 0) + { + using (code.Indent()) + { + foreach (var typeArg in typeArgs) + { + if (typeArg.TypeParam is not { } typeParam) + { + continue; + } + + var constrains = typeParam.ConstraintTypes.Select(i => typeResolver.Resolve(i).Name).ToList(); + if (typeParam.HasReferenceTypeConstraint) + { + constrains.Add("class"); + } + + if (typeParam.HasUnmanagedTypeConstraint) + { + constrains.Add("unmanaged"); + } + + if (typeParam.HasNotNullConstraint) + { + constrains.Add("notnull"); + } + + if (typeParam.HasValueTypeConstraint) + { + constrains.Add("struct"); + } + + if (typeParam.HasConstructorConstraint) + { + constrains.Add("new()"); + } + + if (constrains.Count == 0) + { + continue; + } + + code.AppendLine($"where {typeArg.Name}: {string.Join(", ", constrains)}"); + } + } + } + code.AppendLine("{"); using (code.Indent()) { diff --git a/src/Pure.DI.Core/Core/Code/TypeDescription.cs b/src/Pure.DI.Core/Core/Code/TypeDescription.cs index 6e827bda2..4e4d1fbc1 100644 --- a/src/Pure.DI.Core/Core/Code/TypeDescription.cs +++ b/src/Pure.DI.Core/Core/Code/TypeDescription.cs @@ -2,7 +2,8 @@ public readonly record struct TypeDescription( string Name, - IReadOnlyCollection TypeArgs) + IReadOnlyCollection TypeArgs, + ITypeParameterSymbol? TypeParam) { public override string ToString() => Name; }; \ No newline at end of file diff --git a/src/Pure.DI.Core/Core/Code/TypeResolver.cs b/src/Pure.DI.Core/Core/Code/TypeResolver.cs index 92d95aa00..ae8c0f5e4 100644 --- a/src/Pure.DI.Core/Core/Code/TypeResolver.cs +++ b/src/Pure.DI.Core/Core/Code/TypeResolver.cs @@ -3,28 +3,31 @@ internal class TypeResolver(IMarker marker) : ITypeResolver { - private readonly Dictionary _typeParameters = new(SymbolEqualityComparer.Default); + private readonly Dictionary _names = new(SymbolEqualityComparer.Default); private int _markerCounter; - public TypeDescription Resolve(ITypeSymbol type) + public TypeDescription Resolve(ITypeSymbol type) => Resolve(type, default); + + private TypeDescription Resolve(ITypeSymbol type, ITypeParameterSymbol? typeParam) { - if (_typeParameters.TryGetValue(type, out var description)) - { - return description; - } - + TypeDescription description; switch (type) { case INamedTypeSymbol { IsGenericType: false }: if (marker.IsMarker(type)) { - var typeName = _markerCounter == 0 ? "T" : $"T{_markerCounter}"; + if (!_names.TryGetValue(type, out var typeName)) + { + typeName = _markerCounter == 0 ? "T" : $"T{_markerCounter}"; + _names.Add(type, typeName); + } + _markerCounter++; - description = new TypeDescription(typeName, ImmutableArray.Create(new TypeDescription(typeName, ImmutableArray.Empty))); + description = new TypeDescription(typeName, ImmutableArray.Create(new TypeDescription(typeName, ImmutableArray.Empty, typeParam)), typeParam); } else { - description = new TypeDescription(type.ToString(), ImmutableArray.Empty); + description = new TypeDescription(type.ToString(), ImmutableArray.Empty, typeParam); } break; @@ -33,14 +36,13 @@ public TypeDescription Resolve(ITypeSymbol type) { var elements = new List(); var args = new List(); - foreach (var tupleElement in tupleTypeSymbol.TupleElements) + foreach (var item in tupleTypeSymbol.TupleElements.Zip(tupleTypeSymbol.TypeParameters, (element, parameter) => (description: Resolve(element.Type, parameter), element))) { - var tupleElementDescription = Resolve(tupleElement.Type); - elements.Add($"{tupleElementDescription} {tupleElement.Name}"); - args.AddRange(tupleElementDescription.TypeArgs); + elements.Add($"{item.description} {item.element.Name}"); + args.AddRange(item.description.TypeArgs); } - description = new TypeDescription($"({string.Join(", ", elements)})", args.Distinct().ToImmutableArray()); + description = new TypeDescription($"({string.Join(", ", elements)})", args.Distinct().ToImmutableArray(), typeParam); } break; @@ -48,14 +50,14 @@ public TypeDescription Resolve(ITypeSymbol type) { var types = new List(); var args = new List(); - foreach (var typeArgDescription in namedTypeSymbol.TypeArguments.Select(Resolve)) + foreach (var typeArgDescription in namedTypeSymbol.TypeArguments.Zip(namedTypeSymbol.TypeParameters, Resolve)) { args.AddRange(typeArgDescription.TypeArgs); types.Add(typeArgDescription.Name); } var name = string.Join("", namedTypeSymbol.ToDisplayParts().TakeWhile(i => i.ToString() != "<")); - description = new TypeDescription($"{name}<{string.Join(", ", types)}>", args.Distinct().ToImmutableArray()); + description = new TypeDescription($"{name}<{string.Join(", ", types)}>", args.Distinct().ToImmutableArray(), typeParam); } break; @@ -65,11 +67,10 @@ public TypeDescription Resolve(ITypeSymbol type) break; default: - description = new TypeDescription(type.ToString(), ImmutableArray.Empty); + description = new TypeDescription(type.ToString(), ImmutableArray.Empty, typeParam); break; } - _typeParameters.Add(type, description); return description; } } \ No newline at end of file diff --git a/tests/Pure.DI.IntegrationTests/GenericRootsTests.cs b/tests/Pure.DI.IntegrationTests/GenericRootsTests.cs index f37ea99dd..5a41c7b8e 100644 --- a/tests/Pure.DI.IntegrationTests/GenericRootsTests.cs +++ b/tests/Pure.DI.IntegrationTests/GenericRootsTests.cs @@ -15,10 +15,16 @@ public async Task ShouldSupportGenericRoot() namespace Sample { + class Dep { } + interface IBox { T? Content { get; set; } } class CardboardBox : IBox { + public CardboardBox(Dep dep) + { + } + public T? Content { get; set; } } @@ -176,4 +182,147 @@ public static void Main() result.Success.ShouldBeTrue(result); result.StdOut.ShouldBe(ImmutableArray.Create("Sample.Consumer`1[System.Int32]"), result); } + + [Fact] + public async Task ShouldSupportGenericRootWhenTypeConstraint() + { + // Given + + // When + var result = await """ +using System; +using Pure.DI; +using static Pure.DI.Lifetime; + +namespace Sample +{ + class Disposable: IDisposable + { + public void Dispose() + { + } + } + + class Dep + where T: IDisposable + { + } + + interface IBox + where T: IDisposable + where TStruct: struct + { + T? Content { get; set; } + } + + class CardboardBox : IBox + where T: IDisposable + where TStruct: struct + { + public CardboardBox(Dep dep) + { + } + + public T? Content { get; set; } + } + + internal partial class Composition + { + private static void Setup() + { + DI.Setup(nameof(Composition)) + .Bind>().To>() + // Composition Root + .Root>("GetRoot"); + } + } + + public class Program + { + public static void Main() + { + var composition = new Composition(); + var root = composition.GetRoot(); + Console.WriteLine(root); + } + } +} +""".RunAsync(new Options(LanguageVersion.CSharp9)); + + // Then + result.Success.ShouldBeTrue(result); + result.StdOut.ShouldBe(ImmutableArray.Create("Sample.CardboardBox`2[Sample.Disposable,System.Int32]"), result); + } + + [Fact] + public async Task ShouldSupportGenericRootWhenStructConstraint() + { + // Given + + // When + var result = await """ +using System; +using Pure.DI; +using static Pure.DI.Lifetime; + +namespace Sample +{ + interface IDependency { } + + class Dependency : IDependency { } + + interface IService + where TStruct: struct + { + } + + class Service: IService + where TStruct: struct + { + public Service(IDependency dependency) + { + } + } + + class OtherService: IService + where TStruct: struct + { + public OtherService(IDependency dependency) + { + } + } + + internal partial class Composition + { + private static void Setup() + { + DI.Setup(nameof(Composition)) + .Bind().To>() + .Bind().To>() + .Bind("Other").To(ctx => + { + ctx.Inject(out IDependency dependency); + return new OtherService(dependency); + }) + .Root>("GetMyRoot") + .Root>("GetOtherService", "Other"); + } + } + + public class Program + { + public static void Main() + { + var composition = new Composition(); + var service = composition.GetMyRoot(); + Console.WriteLine(service); + } + } +} +""".RunAsync(new Options(LanguageVersion.CSharp9)); + + // Then + result.Success.ShouldBeTrue(result); + result.StdOut.ShouldBe(ImmutableArray.Create("Sample.Service`2[System.Int32,System.Double]"), result); + } } \ No newline at end of file diff --git a/tests/Pure.DI.UsageTests/Basics/GenericsWithConstraintsCompositionRootsScenario.cs b/tests/Pure.DI.UsageTests/Basics/GenericsWithConstraintsCompositionRootsScenario.cs new file mode 100644 index 000000000..6a5f872bd --- /dev/null +++ b/tests/Pure.DI.UsageTests/Basics/GenericsWithConstraintsCompositionRootsScenario.cs @@ -0,0 +1,80 @@ +/* +$v=true +$p=4 +$d=Generic with constraints composition roots +*/ + +// ReSharper disable ClassNeverInstantiated.Local +// ReSharper disable CheckNamespace +// ReSharper disable UnusedParameter.Local +// ReSharper disable ArrangeTypeModifiers +// ReSharper disable ClassNeverInstantiated.Global +// ReSharper disable UnusedVariable +// ReSharper disable UnusedTypeParameter +#pragma warning disable CS9113 // Parameter is unread. +namespace Pure.DI.UsageTests.Basics.GenericsWithConstraintsCompositionRootsScenario; + +using Shouldly; +using Xunit; + +// { +interface IDependency + where T: IDisposable; + +class Dependency : IDependency + where T: IDisposable; + +interface IService + where T: IDisposable + where TStruct: struct; + +class Service(IDependency dependency) : IService + where T: IDisposable + where TStruct: struct; + +class OtherService(IDependency dependency) : IService + where T: IDisposable + where TStruct: struct; +// } + +public class Scenario +{ + [Fact] + public void Run() + { +// { + DI.Setup(nameof(Composition)) + .Bind().To>() + .Bind().To>() + // Creates OtherService manually, + // just for the sake of example + .Bind("Other").To(ctx => + { + ctx.Inject(out IDependency dependency); + return new OtherService(dependency); + }) + + // Specifies to create a regular public method + // to get a composition root of type Service + // with the name "GetMyRoot" + .Root>("GetMyRoot") + + // Specifies to create a regular public method + // to get a composition root of type OtherService + // with the name "GetOtherService" + // using the "Other" tag + .Root>("GetOtherService", "Other"); + + var composition = new Composition(); + + // service = new Service(new Dependency()); + var service = composition.GetMyRoot(); + + // someOtherService = new OtherService(new Dependency()); + var someOtherService = composition.GetOtherService(); +// } + service.ShouldBeOfType>(); + someOtherService.ShouldBeOfType>(); + composition.SaveClassDiagram(); + } +} \ No newline at end of file