From f2e81a4e599b9cf0134b058cfc6718435dc3d995 Mon Sep 17 00:00:00 2001 From: latonz Date: Fri, 12 Jul 2024 22:01:08 -0500 Subject: [PATCH] feat: support private constructors --- .../configuration/private-member-mapping.md | 115 ----------------- docs/docs/configuration/private-members.mdx | 114 +++++++++++++++++ .../MapperAttribute.cs | 5 + .../PublicAPI.Shipped.txt | 2 + .../Configuration/MapperConfiguration.cs | 3 + .../MapperConfigurationMerger.cs | 3 + .../MapperConfigurationReader.cs | 1 + .../Constructors/IInstanceConstructor.cs | 24 ++++ .../Constructors/InstanceConstructor.cs | 17 +++ .../InstanceConstructorExtensions.cs | 72 +++++++++++ .../InstanceConstructorFactory.cs | 83 ++++++++++++ .../Descriptors/DescriptorBuilder.cs | 24 ++-- .../Descriptors/Enumerables/CollectionInfo.cs | 2 +- .../Enumerables/CollectionInfoBuilder.cs | 47 ++++--- .../EnsureCapacity/EnsureCapacityBuilder.cs | 11 +- .../Descriptors/MapperDescriptor.cs | 5 + .../MembersContainerBuilderContext.cs | 8 +- .../NewInstanceBuilderContext.cs | 12 +- .../NewInstanceContainerBuilderContext.cs | 12 +- .../EnumerableMappingBodyBuilder.cs | 6 +- ...wInstanceObjectMemberMappingBodyBuilder.cs | 12 +- .../Descriptors/MappingBuilderContext.cs | 10 +- .../MappingBuilders/CtorMappingBuilder.cs | 8 +- .../DictionaryMappingBuilder.cs | 65 +++------- .../EnumerableMappingBuilder.cs | 50 ++------ .../NewInstanceObjectMemberMappingBuilder.cs | 46 +++---- .../MappingBuilders/SpanMappingBuilder.cs | 68 +++------- .../Descriptors/Mappings/CtorMapping.cs | 7 +- .../Mappings/ForEachAddEnumerableMapping.cs | 29 +++-- ...orEachAddEnumerableObjectFactoryMapping.cs | 39 ------ .../Mappings/ForEachSetDictionaryMapping.cs | 31 +++-- ...orEachSetDictionaryObjectFactoryMapping.cs | 41 ------ .../INewInstanceObjectMemberMapping.cs | 5 + .../MemberNullAssignmentInitializerMapping.cs | 10 +- ...dMemberNullAssignmentInitializerMapping.cs | 29 +++-- .../NewInstanceObjectFactoryMemberMapping.cs | 39 ------ .../NewInstanceObjectMemberMapping.cs | 33 +++-- .../NewInstanceObjectMemberMethodMapping.cs | 59 ++++----- .../GenericSourceObjectFactory.cs | 2 +- .../GenericSourceTargetObjectFactory.cs | 2 +- .../GenericTargetObjectFactory.cs | 2 +- .../GenericTargetObjectFactoryWithSource.cs | 5 +- .../ObjectFactories/ObjectFactory.cs | 42 +------ .../ObjectFactoryCollection.cs | 2 +- .../ObjectFactoryConstructorAdapter.cs | 25 ++++ .../ObjectFactories/SimpleObjectFactory.cs | 2 +- .../SimpleObjectFactoryWithSource.cs | 5 +- .../Descriptors/SymbolAccessor.cs | 40 ++++-- .../UnsafeAccess/UnsafeAccessorContext.cs | 39 +++--- .../UnsafeAccess/UnsafeConstructorAccessor.cs | 33 +++++ .../ReferenceHandlingSyntaxFactoryHelper.cs | 4 +- .../Syntax/SyntaxFactoryHelper.Attribute.cs | 16 +-- .../Syntax/SyntaxFactoryHelper.Condition.cs | 2 - .../Syntax/SyntaxFactoryHelper.Invocation.cs | 33 ++++- .../Syntax/SyntaxFactoryHelper.Literal.cs | 45 ++++++- .../Emit/Syntax/SyntaxFactoryHelper.New.cs | 5 +- .../Emit/Syntax/SyntaxFactoryHelper.Null.cs | 7 +- .../Emit/UnsafeAccessorEmitter.cs | 5 +- .../Symbols/Members/MappableMember.cs | 2 +- .../Symbols/Members/PropertyMember.cs | 4 +- .../Dto/PrivateCtorDto.cs | 17 +++ .../Mapper/TestMapper.cs | 8 +- .../MapperTest.cs | 10 ++ .../Models/PrivateCtorObject.cs | 20 +++ ...erTest.SnapshotGeneratedSource.verified.cs | 2 +- ...SnapshotGeneratedSource_NET6_0.verified.cs | 2 +- ...SnapshotGeneratedSource_NET8_0.verified.cs | 26 +++- ...erTest.SnapshotGeneratedSource.verified.cs | 2 +- ...SnapshotGeneratedSource_NET6_0.verified.cs | 2 +- test/Riok.Mapperly.Tests/Mapping/CtorTest.cs | 25 ++++ .../Mapping/DictionaryCustomTest.cs | 8 +- .../Mapping/ObjectPropertyFlatteningTest.cs | 52 ++++++-- .../Mapping/ObjectPropertyValueMethodTest.cs | 10 +- .../Mapping/ObjectPropertyValueTest.cs | 2 +- .../Mapping/ReferenceHandlingTest.cs | 5 +- .../Mapping/UnsafeAccessorTest.cs | 118 +++++++++++++++++- test/Riok.Mapperly.Tests/TestSourceBuilder.cs | 1 + .../TestSourceBuilderOptions.cs | 4 + ...rivateCtorCustomClass#Mapper.g.verified.cs | 19 +++ ...sWithCustomClassParam#Mapper.g.verified.cs | 19 +++ ...tomDictionaryWithList#Mapper.g.verified.cs | 2 +- ...eCtorShouldDiagnostic#Mapper.g.verified.cs | 9 +- ...thPrivateCtorShouldDiagnostic.verified.txt | 18 +-- ...meterlessCtorShouldDiagnostic.verified.txt | 4 +- ...ideAssemblyDefault#MyMapper.g.verified.cs} | 2 +- ...athPrivateConstructor#Mapper.g.verified.cs | 27 ++++ ...ithPrivateConstructor#Mapper.g.verified.cs | 34 +++++ ...st.PrivateConstructor#Mapper.g.verified.cs | 20 +++ ...eConstructorAndMember#Mapper.g.verified.cs | 33 +++++ 89 files changed, 1270 insertions(+), 700 deletions(-) delete mode 100644 docs/docs/configuration/private-member-mapping.md create mode 100644 docs/docs/configuration/private-members.mdx create mode 100644 src/Riok.Mapperly/Descriptors/Constructors/IInstanceConstructor.cs create mode 100644 src/Riok.Mapperly/Descriptors/Constructors/InstanceConstructor.cs create mode 100644 src/Riok.Mapperly/Descriptors/Constructors/InstanceConstructorExtensions.cs create mode 100644 src/Riok.Mapperly/Descriptors/Constructors/InstanceConstructorFactory.cs delete mode 100644 src/Riok.Mapperly/Descriptors/Mappings/ForEachAddEnumerableObjectFactoryMapping.cs delete mode 100644 src/Riok.Mapperly/Descriptors/Mappings/ForEachSetDictionaryObjectFactoryMapping.cs delete mode 100644 src/Riok.Mapperly/Descriptors/Mappings/NewInstanceObjectFactoryMemberMapping.cs create mode 100644 src/Riok.Mapperly/Descriptors/ObjectFactories/ObjectFactoryConstructorAdapter.cs create mode 100644 src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeConstructorAccessor.cs create mode 100644 test/Riok.Mapperly.IntegrationTests/Dto/PrivateCtorDto.cs create mode 100644 test/Riok.Mapperly.IntegrationTests/Models/PrivateCtorObject.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/CtorTest.PrivateCtorCustomClass#Mapper.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/CtorTest.PrivateCtorCustomClassWithCustomClassParam#Mapper.g.verified.cs rename test/Riok.Mapperly.Tests/_snapshots/{UnsafeAccessorTest.statAttributeShouldOverrideAssemblyDefault#MyMapper.g.verified.cs => UnsafeAccessorTest.ClassAttributeShouldOverrideAssemblyDefault#MyMapper.g.verified.cs} (91%) create mode 100644 test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.ManualUnflattenedFieldNullablePathPrivateConstructor#Mapper.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.ManualUnflattenedPropertyNullablePathWithPrivateConstructor#Mapper.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivateConstructor#Mapper.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivateConstructorAndMember#Mapper.g.verified.cs diff --git a/docs/docs/configuration/private-member-mapping.md b/docs/docs/configuration/private-member-mapping.md deleted file mode 100644 index 1d456db6f7..0000000000 --- a/docs/docs/configuration/private-member-mapping.md +++ /dev/null @@ -1,115 +0,0 @@ ---- -sidebar_position: 14 -description: Private member mapping ---- - -# Private member mapping - -As of .NET 8.0, Mapperly supports mapping members that are normally inaccessible like `private` or `protected` properties. This is made possible by using the [UnsafeAccessorAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.unsafeaccessorattribute) which lets Mapperly access normally inaccessible members with zero overhead while being completely AOT safe. - -By default `IncludedMembers` is set to `MemberVisibility.AllAccessible` which will configure Mapperly to map members of all accessibility levels as long as they are ordinarily accessible. To enable unsafe accessor usage, set `IncludedMembers` to `MemberVisibility.All`. Mapperly will then try to map members of all accessibilities, including ones that are not usually visible to external types. - -```csharp -public class Fruit -{ - private bool _isSeeded; - - public string Name { get; set; } - - private int Sweetness { get; set; } -} - -// highlight-start -[Mapper(IncludedMembers = MemberVisibility.All)] -// highlight-end -public partial class FruitMapper -{ - public partial FruitDto ToDto(Fruit source); -} -``` - -## Generated unsafe accessor code - -```csharp -public partial class FruitMapper -{ - private partial global::FruitDto ToDto(global::Fruit source) - { - var target = new global::FruitDto(); - target.GetIsSeeded1() = source.GetIsSeeded(); - target.Name = source.Name; - target.SetSweetness(source.GetSweetness()); - return target; - } -} - -static file class UnsafeAccessor -{ - [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Field, Name = "_isSeeded")] - public static extern ref bool GetSeeded(this global::Fruit target); - - [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Field, Name = "_isSeeded")] - public static extern ref bool GetSeeded1(this global::FruitDto target); - - [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_Sweetness")] - public static extern int GetSweetness(this global::Fruit source); - - [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_Sweetness")] - public static extern void SetSweetness(this global::FruitDto target, int value); -} -``` - -Here Mapperly generates a file scoped class containing extension method for each internal member for both the source and target. Mapperly then uses the extension methods to get and set the members. Note that this uses zero reflection and is as performant as using an ordinary property or field. - -## Controlling member accessibility - -In addition to mapping inaccessible members, `MemberVisbility` can be used to control which members are mapped, depending on their accessibility modifier. For instance `MemberVisibility.Private | MemberVisibility.Protected` will cause mapperly to only map private and protected members, generating an unsafe accessor if needed. - -```csharp -public class Car -{ - private int _cost; - - public string Name { get; set; } - - protected string Engine { get; set; } -} - -// highlight-start -[Mapper(IncludedMembers = MemberVisibility.Private | MemberVisibility.Protected)] -// highlight-end -public partial class CarMapper -{ - public partial CarDto ToDto(Car source); -} -``` - -## Generated member visibility code - -```csharp -public partial class CarMapper -{ - private partial global::CarDto ToDto(global::Car source) - { - var target = new global::CarDto(); - target.GetCost1() = source.GetCost(); - target.SetEngine(source.GetEngine()); - return target; - } -} - -static file class UnsafeAccessor -{ - [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Field, Name = "_cost")] - public static extern ref int GetCost(this global::Car target); - - [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Field, Name = "_cost")] - public static extern ref int GetSeeded1(this global::CarDto target); - - [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_Engine")] - public static extern string GetEngine(this global::Car source); - - [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_Engine")] - public static extern void SetEngine(this global::CarDto target, string value); -} -``` diff --git a/docs/docs/configuration/private-members.mdx b/docs/docs/configuration/private-members.mdx new file mode 100644 index 0000000000..65bc4e15e3 --- /dev/null +++ b/docs/docs/configuration/private-members.mdx @@ -0,0 +1,114 @@ +--- +sidebar_position: 14 +description: Private members +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Private members + +As of .NET 8.0, Mapperly supports mapping members that are normally inaccessible like `private` or `protected` properties. +This is made possible by using the [UnsafeAccessorAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.unsafeaccessorattribute) which lets Mapperly access normally inaccessible members with zero overhead while being completely AOT safe. + +By default `IncludedMembers` and `IncludedConstructors` is set to `MemberVisibility.AllAccessible` which will configure Mapperly to map members of all accessibility levels as long as they are ordinarily accessible. +To enable unsafe accessor usage, set `IncludedMembers` and/or `IncludedConstructors` to `MemberVisibility.All`. +Mapperly will then try to map members of all accessibilities, including ones that are not usually visible to external types. + +`IncludedConstructors` can be used separately from `IncludedMembers`. +This allows you to use inaccessible constructors but only map accessible members or vice versa. + + + + ```csharp + // highlight-start + [Mapper( + IncludedMembers = MemberVisibility.All, + IncludedConstructors = MemberVisibility.All)] + // highlight-end + public partial class FruitMapper + { + public partial FruitDto ToDto(Fruit source); + } + + public class Fruit + { + private bool _isSeeded; + + public string Name { get; set; } + + private int Sweetness { get; set; } + } + + public class FruitDto + { + private FruitDto() {} + + private bool _isSeeded; + + public string Name { get; set; } + + private int Sweetness { get; set; } + } + ``` + + + + Mapperly generates a file scoped class containing an accessor method for each member which cannot be accessed directly. + Mapperly then uses these methods to create the instance, get and set the members as needed. + Note that this uses zero reflection and is as performant as using an ordinary property or field. + + ```csharp + public partial class FruitMapper + { + private partial global::FruitDto ToDto(global::Fruit source) + { + var target = UnsafeAccessor.CreateFruitDto(); + target.GetIsSeeded1() = source.GetIsSeeded(); + target.Name = source.Name; + target.SetSweetness(source.GetSweetness()); + return target; + } + } + + static file class UnsafeAccessor + { + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Constructor)] + public static extern global::FruitDto CreateFruitDto(this global::FruitDto target); + + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Field, Name = "_isSeeded")] + public static extern ref bool GetSeeded(this global::Fruit target); + + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Field, Name = "_isSeeded")] + public static extern ref bool GetSeeded1(this global::FruitDto target); + + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_Sweetness")] + public static extern int GetSweetness(this global::Fruit source); + + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_Sweetness")] + public static extern void SetSweetness(this global::FruitDto target, int value); + } + ``` + + + + +## Controlling member accessibility + +In addition to mapping inaccessible members, +`MemberVisbility` can be used to control which members are considered, depending on their accessibility modifier. +For instance `MemberVisibility.Private | MemberVisibility.Protected` will cause Mapperly to only consider private and protected members, +generating an unsafe accessor if needed. + +`IncludedConstructors` can be used separately from `IncludedMembers`. +This allows you to use inaccessible constructors but only map accessible members or vice versa. + +```csharp +// highlight-start +[Mapper(IncludedMembers = MemberVisibility.Private | MemberVisibility.Protected)] +// highlight-end +public partial class FruitMapper +{ + public partial FruitDto ToDto(Fruit source); +} +``` diff --git a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs index 3a129163bc..84a269ee33 100644 --- a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs +++ b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs @@ -100,6 +100,11 @@ public class MapperAttribute : Attribute /// public MemberVisibility IncludedMembers { get; set; } = MemberVisibility.AllAccessible; + /// + /// Determines the access level of constructors that Mapperly will take into account. + /// + public MemberVisibility IncludedConstructors { get; set; } = MemberVisibility.AllAccessible; + /// /// Controls the priority of constructors used in mapping. /// When true, a parameterless constructor is prioritized over constructors with parameters. diff --git a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt index 3b12015a40..abdaf0953c 100644 --- a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt +++ b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt @@ -186,3 +186,5 @@ Riok.Mapperly.Abstractions.MappingTargetAttribute Riok.Mapperly.Abstractions.MappingTargetAttribute.MappingTargetAttribute() -> void Riok.Mapperly.Abstractions.MapPropertyAttribute.MapPropertyAttribute(string! source, string![]! target) -> void Riok.Mapperly.Abstractions.MapPropertyAttribute.MapPropertyAttribute(string![]! source, string! target) -> void +Riok.Mapperly.Abstractions.MapperAttribute.IncludedConstructors.get -> Riok.Mapperly.Abstractions.MemberVisibility +Riok.Mapperly.Abstractions.MapperAttribute.IncludedConstructors.set -> void diff --git a/src/Riok.Mapperly/Configuration/MapperConfiguration.cs b/src/Riok.Mapperly/Configuration/MapperConfiguration.cs index a82b9a80dc..f2b24c7ec8 100644 --- a/src/Riok.Mapperly/Configuration/MapperConfiguration.cs +++ b/src/Riok.Mapperly/Configuration/MapperConfiguration.cs @@ -104,6 +104,9 @@ public record MapperConfiguration /// public MemberVisibility? IncludedMembers { get; init; } + /// + public MemberVisibility? IncludedConstructors { get; init; } + /// /// Controls the priority of constructors used in mapping. /// When true, a parameterless constructor is prioritized over constructors with parameters. diff --git a/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs b/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs index d85911f016..ee10b1db41 100644 --- a/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs +++ b/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs @@ -54,6 +54,9 @@ public static MapperAttribute Merge(MapperConfiguration mapperConfiguration, Map mapper.IncludedMembers = mapperConfiguration.IncludedMembers ?? defaultMapperConfiguration.IncludedMembers ?? mapper.IncludedMembers; + mapper.IncludedConstructors = + mapperConfiguration.IncludedConstructors ?? defaultMapperConfiguration.IncludedConstructors ?? mapper.IncludedConstructors; + mapper.PreferParameterlessConstructors = mapperConfiguration.PreferParameterlessConstructors ?? defaultMapperConfiguration.PreferParameterlessConstructors diff --git a/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs b/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs index dade4e4398..b99bd14d9d 100644 --- a/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs +++ b/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs @@ -20,6 +20,7 @@ MapperConfiguration defaultMapperConfiguration { _dataAccessor = dataAccessor; _types = types; + var mapperConfiguration = _dataAccessor.AccessSingle(mapperSymbol); var mapper = MapperConfigurationMerger.Merge(mapperConfiguration, defaultMapperConfiguration); diff --git a/src/Riok.Mapperly/Descriptors/Constructors/IInstanceConstructor.cs b/src/Riok.Mapperly/Descriptors/Constructors/IInstanceConstructor.cs new file mode 100644 index 0000000000..1e3cf1a5d9 --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/Constructors/IInstanceConstructor.cs @@ -0,0 +1,24 @@ +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Descriptors.Mappings; + +namespace Riok.Mapperly.Descriptors.Constructors; + +/// +/// An instance constructor represents code-to-be-generated +/// which creates a new object instance. +/// This could happen by calling a C# instance constructor, +/// an unsafe accessor or by calling an object factory. +/// +public interface IInstanceConstructor +{ + /// + /// Whether this constructor supports object initialization blocks to initialize properties. + /// + bool SupportsObjectInitializer { get; } + + ExpressionSyntax CreateInstance( + TypeMappingBuildContext ctx, + IEnumerable args, + InitializerExpressionSyntax? initializer = null + ); +} diff --git a/src/Riok.Mapperly/Descriptors/Constructors/InstanceConstructor.cs b/src/Riok.Mapperly/Descriptors/Constructors/InstanceConstructor.cs new file mode 100644 index 0000000000..121c28de82 --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/Constructors/InstanceConstructor.cs @@ -0,0 +1,17 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Descriptors.Mappings; +using Riok.Mapperly.Emit.Syntax; + +namespace Riok.Mapperly.Descriptors.Constructors; + +public class InstanceConstructor(INamedTypeSymbol type) : IInstanceConstructor +{ + public bool SupportsObjectInitializer => true; + + public ExpressionSyntax CreateInstance( + TypeMappingBuildContext ctx, + IEnumerable args, + InitializerExpressionSyntax? initializer = null + ) => SyntaxFactoryHelper.CreateInstance(type, args).WithInitializer(initializer); +} diff --git a/src/Riok.Mapperly/Descriptors/Constructors/InstanceConstructorExtensions.cs b/src/Riok.Mapperly/Descriptors/Constructors/InstanceConstructorExtensions.cs new file mode 100644 index 0000000000..340949c414 --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/Constructors/InstanceConstructorExtensions.cs @@ -0,0 +1,72 @@ +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Descriptors.Mappings; +using Riok.Mapperly.Descriptors.Mappings.MemberMappings; +using Riok.Mapperly.Emit; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace Riok.Mapperly.Descriptors.Constructors; + +public static class InstanceConstructorExtensions +{ + public static ExpressionSyntax CreateInstance(this IInstanceConstructor ctor, TypeMappingBuildContext ctx) => + ctor.CreateInstance(ctx, []); + + public static ExpressionSyntax CreateInstance( + this IInstanceConstructor ctor, + TypeMappingBuildContext ctx, + IEnumerable args + ) => ctor.CreateInstance(ctx, args.Select(Argument)); + + public static IEnumerable CreateTargetInstance( + this IInstanceConstructor ctor, + TypeMappingBuildContext ctx, + IMapping mapping, + string targetVariableName, + bool enableReferenceHandling, + IReadOnlyCollection ctorParametersMappings, + IReadOnlyCollection? initMemberMappings = null + ) + { + if (enableReferenceHandling) + { + // TryGetReference + yield return ReferenceHandlingSyntaxFactoryHelper.TryGetReference(ctx, mapping); + } + + // new T(ctorArgs) { ... }; + var objectCreationExpression = ctor.CreateInstance(ctx, ctorParametersMappings, initMemberMappings); + + // var target = new T() { ... }; + yield return ctx.SyntaxFactory.DeclareLocalVariable(targetVariableName, objectCreationExpression); + + // set the reference as soon as it is created, + // as property mappings could refer to the same instance. + if (enableReferenceHandling) + { + // SetReference + yield return ctx.SyntaxFactory.ExpressionStatement( + ReferenceHandlingSyntaxFactoryHelper.SetReference(mapping, ctx, IdentifierName(targetVariableName)) + ); + } + } + + public static ExpressionSyntax CreateInstance( + this IInstanceConstructor ctor, + TypeMappingBuildContext ctx, + IReadOnlyCollection ctorParametersMappings, + IReadOnlyCollection? initMemberMappings = null + ) + { + InitializerExpressionSyntax? initializer = null; + if (initMemberMappings is { Count: > 0 }) + { + var initPropertiesContext = ctx.AddIndentation(); + var initMappings = initMemberMappings.Select(x => x.BuildExpression(initPropertiesContext, null)).ToArray(); + initializer = ctx.SyntaxFactory.ObjectInitializer(initMappings); + } + + // new T(ctorArgs) { ... }; + var ctorArgs = ctorParametersMappings.Select(x => x.BuildArgument(ctx)).ToArray(); + return ctor.CreateInstance(ctx, ctorArgs, initializer); + } +} diff --git a/src/Riok.Mapperly/Descriptors/Constructors/InstanceConstructorFactory.cs b/src/Riok.Mapperly/Descriptors/Constructors/InstanceConstructorFactory.cs new file mode 100644 index 0000000000..65d90c85c6 --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/Constructors/InstanceConstructorFactory.cs @@ -0,0 +1,83 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Riok.Mapperly.Descriptors.ObjectFactories; +using Riok.Mapperly.Descriptors.UnsafeAccess; + +namespace Riok.Mapperly.Descriptors.Constructors; + +public class InstanceConstructorFactory( + ObjectFactoryCollection objectFactories, + SymbolAccessor symbolAccessor, + UnsafeAccessorContext unsafeAccessorContext +) +{ + /// + /// Tries to build an . + /// Creates an object factory ctor if possible, + /// tries to use an accessible parameterless ctor otherwise. + /// + public bool TryBuild(ITypeSymbol source, ITypeSymbol target, [NotNullWhen(true)] out IInstanceConstructor? constructor) + { + return TryBuildObjectFactory(source, target, out constructor) || TryBuildParameterless(target, out constructor); + } + + /// + /// Tries to build an object factory constructor. + /// + public bool TryBuildObjectFactory(ITypeSymbol source, ITypeSymbol target, [NotNullWhen(true)] out IInstanceConstructor? constructor) + { + if (objectFactories.TryFindObjectFactory(source, target, out var factory)) + { + constructor = new ObjectFactoryConstructorAdapter(factory, source, target); + return true; + } + + constructor = null; + return false; + } + + /// + /// Tries to build a parameterless constructor. + /// + public bool TryBuildParameterless(ITypeSymbol type, [NotNullWhen(true)] out IInstanceConstructor? ctor) + { + if (type is not INamedTypeSymbol namedType || namedType.IsAbstract) + { + ctor = null; + return false; + } + + var ctorMethod = namedType.InstanceConstructors.FirstOrDefault(x => + x.Parameters.IsDefaultOrEmpty && symbolAccessor.IsConstructorAccessible(x) + ); + if (ctorMethod == null) + { + ctor = null; + return false; + } + + ctor = BuildForConstructor(ctorMethod); + return true; + } + + /// + /// Builds a parameterless ctor, + /// throws if no accessible parameterless ctor is available. + /// + public IInstanceConstructor BuildParameterless(ITypeSymbol type) => + BuildForConstructor(((INamedTypeSymbol)type).InstanceConstructors.First(x => x.Parameters.IsDefaultOrEmpty)); + + /// + /// Builds an for a given constructor method. + /// + public IInstanceConstructor BuildForConstructor(IMethodSymbol ctor) + { + Debug.Assert(ctor.MethodKind == MethodKind.Constructor); + if (symbolAccessor.IsDirectlyAccessible(ctor)) + return new InstanceConstructor(ctor.ContainingType); + + Debug.Assert(symbolAccessor.IsConstructorAccessible(ctor)); + return unsafeAccessorContext.GetOrBuildConstructor(ctor); + } +} diff --git a/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs b/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs index 5731e0e9b5..442f732c6a 100644 --- a/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs @@ -2,6 +2,7 @@ using Riok.Mapperly.Abstractions; using Riok.Mapperly.Abstractions.ReferenceHandling; using Riok.Mapperly.Configuration; +using Riok.Mapperly.Descriptors.Constructors; using Riok.Mapperly.Descriptors.ExternalMappings; using Riok.Mapperly.Descriptors.FormatProviders; using Riok.Mapperly.Descriptors.MappingBodyBuilders; @@ -43,7 +44,7 @@ MapperConfiguration defaultMapperConfiguration _symbolAccessor = symbolAccessor; _types = compilationContext.Types; _mappingBodyBuilder = new MappingBodyBuilder(_mappings); - _unsafeAccessorContext = new UnsafeAccessorContext(_methodNameBuilder, symbolAccessor); + _unsafeAccessorContext = new UnsafeAccessorContext(_methodNameBuilder, symbolAccessor, _mapperDescriptor.UnsafeAccessorName); var attributeAccessor = new AttributeDataAccessor(symbolAccessor); var configurationReader = new MapperConfigurationReader( @@ -78,8 +79,9 @@ MapperConfiguration defaultMapperConfiguration // ExtractObjectFactories needs to be called after ExtractUserMappings due to configuring mapperDescriptor.Static var objectFactories = ExtractObjectFactories(); + var constructorFactory = new InstanceConstructorFactory(objectFactories, _symbolAccessor, _unsafeAccessorContext); var formatProviders = ExtractFormatProviders(); - EnqueueUserMappings(objectFactories, formatProviders); + EnqueueUserMappings(constructorFactory, formatProviders); ExtractExternalMappings(); _mappingBodyBuilder.BuildMappingBodies(cancellationToken); AddUserMappingDiagnostics(); @@ -91,24 +93,30 @@ MapperConfiguration defaultMapperConfiguration } /// - /// If is not set and the roslyn version does not have UnsafeAccessors - /// then emit a diagnostic and update the for . + /// Sets the member and constructor visibility filter on the after validation. + /// If is not set and the compilation does not have UnsafeAccessors, + /// emit a diagnostic and update the to include . /// private void ConfigureMemberVisibility() { var includedMembers = _builderContext.Configuration.Mapper.IncludedMembers; + var includedConstructors = _builderContext.Configuration.Mapper.IncludedConstructors; if (_types.TryGet(UnsafeAccessorName) != null) { _symbolAccessor.SetMemberVisibility(includedMembers); + _symbolAccessor.SetConstructorVisibility(includedConstructors); return; } - if (includedMembers.HasFlag(MemberVisibility.Accessible)) + if (includedMembers.HasFlag(MemberVisibility.Accessible) && includedConstructors.HasFlag(MemberVisibility.Accessible)) + { return; + } _diagnostics.ReportDiagnostic(DiagnosticDescriptors.UnsafeAccessorNotAvailable); _symbolAccessor.SetMemberVisibility(includedMembers | MemberVisibility.Accessible); + _symbolAccessor.SetConstructorVisibility(includedConstructors | MemberVisibility.Accessible); } private void ReserveMethodNames() @@ -156,13 +164,13 @@ private ObjectFactoryCollection ExtractObjectFactories() return ObjectFactoryBuilder.ExtractObjectFactories(_builderContext, _mapperDescriptor.Symbol, _mapperDescriptor.Static); } - private void EnqueueUserMappings(ObjectFactoryCollection objectFactories, FormatProviderCollection formatProviders) + private void EnqueueUserMappings(InstanceConstructorFactory constructorFactory, FormatProviderCollection formatProviders) { foreach (var userMapping in _mappings.UserMappings) { var ctx = new MappingBuilderContext( _builderContext, - objectFactories, + constructorFactory, formatProviders, userMapping, new TypeMappingKey(userMapping.SourceType, userMapping.TargetType) @@ -213,7 +221,7 @@ private void AddMappingsToDescriptor() private void AddAccessorsToDescriptor() { // add generated accessors to the mapper - _mapperDescriptor.AddUnsafeAccessors(_unsafeAccessorContext.UnsafeAccessors); + _mapperDescriptor.AddUnsafeAccessors(_unsafeAccessorContext.Accessors); } private void AddUserMapping(IUserMapping mapping, bool ignoreDuplicates, bool named) diff --git a/src/Riok.Mapperly/Descriptors/Enumerables/CollectionInfo.cs b/src/Riok.Mapperly/Descriptors/Enumerables/CollectionInfo.cs index f3b9d3aedc..2c6aac98c1 100644 --- a/src/Riok.Mapperly/Descriptors/Enumerables/CollectionInfo.cs +++ b/src/Riok.Mapperly/Descriptors/Enumerables/CollectionInfo.cs @@ -10,7 +10,7 @@ public record CollectionInfo( CollectionType ImplementedTypes, ITypeSymbol EnumeratedType, IMappableMember? CountMember, - bool HasImplicitCollectionAddMethod, + string? AddMethodName, bool IsImmutableCollectionType ) { diff --git a/src/Riok.Mapperly/Descriptors/Enumerables/CollectionInfoBuilder.cs b/src/Riok.Mapperly/Descriptors/Enumerables/CollectionInfoBuilder.cs index 8cc63fda71..7eb634a27a 100644 --- a/src/Riok.Mapperly/Descriptors/Enumerables/CollectionInfoBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/Enumerables/CollectionInfoBuilder.cs @@ -11,6 +11,7 @@ public static class CollectionInfoBuilder private readonly record struct CollectionTypeInfo( CollectionType CollectionType, Type? ReflectionType = null, + string? AddMethodName = null, string? TypeFullName = null, bool Immutable = false ) @@ -32,16 +33,16 @@ private readonly record struct CollectionTypeInfo( private static readonly IReadOnlyCollection _collectionTypeInfos = new[] { new CollectionTypeInfo(CollectionType.IEnumerable, typeof(IEnumerable<>)), - new CollectionTypeInfo(CollectionType.List, typeof(List<>)), - new CollectionTypeInfo(CollectionType.Stack, typeof(Stack<>)), - new CollectionTypeInfo(CollectionType.Queue, typeof(Queue<>)), + new CollectionTypeInfo(CollectionType.List, typeof(List<>), nameof(List.Add)), + new CollectionTypeInfo(CollectionType.Stack, typeof(Stack<>), nameof(Stack.Push)), + new CollectionTypeInfo(CollectionType.Queue, typeof(Queue<>), nameof(Queue.Enqueue)), new CollectionTypeInfo(CollectionType.IReadOnlyCollection, typeof(IReadOnlyCollection<>)), - new CollectionTypeInfo(CollectionType.IList, typeof(IList<>)), + new CollectionTypeInfo(CollectionType.IList, typeof(IList<>), nameof(IList.Add)), new CollectionTypeInfo(CollectionType.IReadOnlyList, typeof(IReadOnlyList<>)), - new CollectionTypeInfo(CollectionType.ICollection, typeof(ICollection<>)), - new CollectionTypeInfo(CollectionType.HashSet, typeof(HashSet<>)), - new CollectionTypeInfo(CollectionType.SortedSet, typeof(SortedSet<>)), - new CollectionTypeInfo(CollectionType.ISet, typeof(ISet<>)), + new CollectionTypeInfo(CollectionType.ICollection, typeof(ICollection<>), nameof(ICollection.Add)), + new CollectionTypeInfo(CollectionType.HashSet, typeof(HashSet<>), nameof(HashSet.Add)), + new CollectionTypeInfo(CollectionType.SortedSet, typeof(SortedSet<>), nameof(SortedSet.Add)), + new CollectionTypeInfo(CollectionType.ISet, typeof(ISet<>), nameof(ISet.Add)), new CollectionTypeInfo(CollectionType.IReadOnlySet, TypeFullName: "System.Collections.Generic.IReadOnlySet`1"), new CollectionTypeInfo(CollectionType.IDictionary, typeof(IDictionary<,>)), new CollectionTypeInfo(CollectionType.IReadOnlyDictionary, typeof(IReadOnlyDictionary<,>)), @@ -139,7 +140,7 @@ ITypeSymbol enumeratedType implementedTypes, symbolAccessor.UpgradeNullable(enumeratedType), FindCountMember(symbolAccessor, type, typeInfo), - HasValidAddMethod(wellKnownTypes, type, typeInfo, implementedTypes), + GetAddMethodName(wellKnownTypes, type, implementedTypes, collectionTypeInfo), collectionTypeInfo?.Immutable == true ); } @@ -182,21 +183,15 @@ ITypeSymbol enumeratedType return null; } - private static bool HasValidAddMethod(WellKnownTypes types, ITypeSymbol t, CollectionType typeInfo, CollectionType implementedTypes) + private static string? GetAddMethodName( + WellKnownTypes types, + ITypeSymbol t, + CollectionType implementedTypes, + CollectionTypeInfo? collectionTypeInfo + ) { - if ( - typeInfo - is CollectionType.ICollection - or CollectionType.IList - or CollectionType.List - or CollectionType.ISet - or CollectionType.HashSet - or CollectionType.SortedSet - ) - return true; - - if (typeInfo is not CollectionType.None) - return false; + if (collectionTypeInfo != null) + return collectionTypeInfo.Value.AddMethodName; // has valid add if type implements ICollection and has implicit Add method if ( @@ -204,7 +199,7 @@ or CollectionType.SortedSet && t.HasImplicitGenericImplementation(types.Get(typeof(ICollection<>)), nameof(ICollection.Add)) ) { - return true; + return nameof(ICollection.Add); } // has valid add if type implements ISet and has implicit Add method @@ -213,10 +208,10 @@ or CollectionType.SortedSet && t.HasImplicitGenericImplementation(types.Get(typeof(ISet<>)), nameof(ISet.Add)) ) { - return true; + return nameof(ISet.Add); } - return false; + return null; } private static IMappableMember? FindCountMember(SymbolAccessor symbolAccessor, ITypeSymbol t, CollectionType typeInfo) diff --git a/src/Riok.Mapperly/Descriptors/Enumerables/EnsureCapacity/EnsureCapacityBuilder.cs b/src/Riok.Mapperly/Descriptors/Enumerables/EnsureCapacity/EnsureCapacityBuilder.cs index 50e8a3b1f3..66ee1f98e3 100644 --- a/src/Riok.Mapperly/Descriptors/Enumerables/EnsureCapacity/EnsureCapacityBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/Enumerables/EnsureCapacity/EnsureCapacityBuilder.cs @@ -10,7 +10,11 @@ public static class EnsureCapacityBuilder private const string EnsureCapacityName = "EnsureCapacity"; private const string TryGetNonEnumeratedCountMethodName = "TryGetNonEnumeratedCount"; - public static EnsureCapacityInfo? TryBuildEnsureCapacity(MappingBuilderContext ctx, CollectionInfos collectionInfos) + public static EnsureCapacityInfo? TryBuildEnsureCapacity( + MappingBuilderContext ctx, + CollectionInfos collectionInfos, + bool includeTargetCount + ) { var source = collectionInfos.Source; var target = collectionInfos.Target; @@ -22,10 +26,11 @@ public static class EnsureCapacityBuilder if (capacityMethod == null) return null; + var targetCount = includeTargetCount ? target.CountMember?.BuildGetter(ctx.UnsafeAccessorContext) : null; + // if source count is known, create a simple EnsureCapacity statement if (source.CountIsKnown) { - var targetCount = target.CountMember?.BuildGetter(ctx.UnsafeAccessorContext); var sourceCount = source.CountMember.BuildGetter(ctx.UnsafeAccessorContext); return new EnsureCapacityMember(targetCount, sourceCount); } @@ -44,6 +49,6 @@ public static class EnsureCapacityBuilder return null; // if source does not have a count use GetNonEnumeratedCount, calling EnsureCapacity if count is available - return new EnsureCapacityNonEnumerated(target.CountMember?.BuildGetter(ctx.UnsafeAccessorContext), nonEnumeratedCountMethod); + return new EnsureCapacityNonEnumerated(targetCount, nonEnumeratedCountMethod); } } diff --git a/src/Riok.Mapperly/Descriptors/MapperDescriptor.cs b/src/Riok.Mapperly/Descriptors/MapperDescriptor.cs index a539996c0c..2345b3bb54 100644 --- a/src/Riok.Mapperly/Descriptors/MapperDescriptor.cs +++ b/src/Riok.Mapperly/Descriptors/MapperDescriptor.cs @@ -10,6 +10,8 @@ namespace Riok.Mapperly.Descriptors; public class MapperDescriptor { + private const string AccessorClassName = "UnsafeAccessor"; + private readonly MapperDeclaration _declaration; private readonly List _methodMappings = new(); private readonly List _unsafeAccessors = new(); @@ -21,6 +23,7 @@ public MapperDescriptor(MapperDeclaration declaration, UniqueNameBuilder nameBui _declaration = declaration; NameBuilder = nameBuilder; Name = BuildName(declaration.Symbol); + UnsafeAccessorName = nameBuilder.New(AccessorClassName); if (!Symbol.ContainingNamespace.IsGlobalNamespace) { @@ -32,6 +35,8 @@ public MapperDescriptor(MapperDeclaration declaration, UniqueNameBuilder nameBui public string? Namespace { get; } + public string UnsafeAccessorName { get; } + public ClassDeclarationSyntax Syntax => _declaration.Syntax; public INamedTypeSymbol Symbol => _declaration.Symbol; diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersContainerBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersContainerBuilderContext.cs index a3c5412eed..a16135ce32 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersContainerBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersContainerBuilderContext.cs @@ -66,12 +66,12 @@ private void AddNullMemberInitializers(IMemberAssignmentMappingContainer contain foreach (var nullableTrailPath in path.ObjectPathNullableSubPaths()) { var nullablePath = new NonEmptyMemberPath(path.RootType, nullableTrailPath); - var type = nullablePath.Member.Type; + var type = nullablePath.Member.Type.NonNullable(); if (!nullablePath.Member.CanSet) continue; - if (!BuilderContext.SymbolAccessor.HasDirectlyAccessibleParameterlessConstructor(type)) + if (!BuilderContext.InstanceConstructors.TryBuild(BuilderContext.Source, type, out var ctor)) { BuilderContext.ReportDiagnostic(DiagnosticDescriptors.NoParameterlessConstructorFound, type); continue; @@ -82,12 +82,12 @@ private void AddNullMemberInitializers(IMemberAssignmentMappingContainer contain { var nullablePathGetter = nullablePath.BuildGetter(BuilderContext); container.AddMemberMappingContainer( - new MethodMemberNullAssignmentInitializerMapping(nullablePathSetter, nullablePathGetter) + new MethodMemberNullAssignmentInitializerMapping(nullablePathSetter, nullablePathGetter, ctor) ); continue; } - container.AddMemberMappingContainer(new MemberNullAssignmentInitializerMapping(nullablePathSetter)); + container.AddMemberMappingContainer(new MemberNullAssignmentInitializerMapping(nullablePathSetter, ctor)); } } diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewInstanceBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewInstanceBuilderContext.cs index 51fad24210..c5a8cf89a5 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewInstanceBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewInstanceBuilderContext.cs @@ -16,18 +16,18 @@ public class NewInstanceBuilderContext(MappingBuilderContext builderContext, INewInstanceBuilderContext where T : INewInstanceObjectMemberMapping { - public void AddInitMemberMapping(MemberAssignmentMapping mapping) - { - Mapping.AddInitMemberMapping(mapping); - MappingAdded(mapping.MemberInfo); - } - public void AddConstructorParameterMapping(ConstructorParameterMapping mapping) { Mapping.AddConstructorParameterMapping(mapping); MappingAdded(mapping.MemberInfo, true); } + public void AddInitMemberMapping(MemberAssignmentMapping mapping) + { + Mapping.AddInitMemberMapping(mapping); + MappingAdded(mapping.MemberInfo); + } + public bool TryMatchInitOnlyMember(IMappableMember targetMember, [NotNullWhen(true)] out MemberMappingInfo? memberInfo) { if (TryMatchMember(targetMember, out memberInfo)) diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewInstanceContainerBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewInstanceContainerBuilderContext.cs index e6f3afcd7b..002dfda956 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewInstanceContainerBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewInstanceContainerBuilderContext.cs @@ -17,18 +17,18 @@ public class NewInstanceContainerBuilderContext(MappingBuilderContext builder INewInstanceBuilderContext where T : INewInstanceObjectMemberMapping, IMemberAssignmentTypeMapping { - public void AddInitMemberMapping(MemberAssignmentMapping mapping) - { - Mapping.AddInitMemberMapping(mapping); - MappingAdded(mapping.MemberInfo); - } - public void AddConstructorParameterMapping(ConstructorParameterMapping mapping) { Mapping.AddConstructorParameterMapping(mapping); MappingAdded(mapping.MemberInfo, true); } + public void AddInitMemberMapping(MemberAssignmentMapping mapping) + { + Mapping.AddInitMemberMapping(mapping); + MappingAdded(mapping.MemberInfo); + } + public bool TryMatchInitOnlyMember(IMappableMember targetMember, [NotNullWhen(true)] out MemberMappingInfo? memberInfo) { if (TryMatchMember(targetMember, out memberInfo)) diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/EnumerableMappingBodyBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/EnumerableMappingBodyBuilder.cs index d36cd06907..cb9520a3d1 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/EnumerableMappingBodyBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/EnumerableMappingBodyBuilder.cs @@ -25,7 +25,8 @@ public static void BuildMappingBody(MappingBuilderContext ctx, IEnumerableMappin var mappingCtx = new MembersContainerBuilderContext(ctx, mapping); InitContext(mappingCtx); - if (EnsureCapacityBuilder.TryBuildEnsureCapacity(ctx, mapping.CollectionInfos) is { } ensureCapacity) + // include the target count as the target could already include elements + if (EnsureCapacityBuilder.TryBuildEnsureCapacity(ctx, mapping.CollectionInfos, true) is { } ensureCapacity) { mapping.AddEnsureCapacity(ensureCapacity); } @@ -83,9 +84,10 @@ private static void BuildConstructorMapping(INewInstanceBuilderContext BuildConstructorMapping bool? preferParameterlessConstructor = null ) { + if (ctx.Mapping.HasConstructor) + return []; + if (ctx.Mapping.TargetType is not INamedTypeSymbol namedTargetType) { ctx.BuilderContext.ReportDiagnostic(DiagnosticDescriptors.NoConstructorFound, ctx.BuilderContext.Target); @@ -47,7 +51,7 @@ public static IReadOnlyList BuildConstructorMapping // the reverse if preferParameterlessConstructors is false , descending parameter count is prio2 then parameterless ctor // ctors annotated with [Obsolete] are considered last unless they have a MapperConstructor attribute set var ctorCandidates = namedTargetType - .InstanceConstructors.Where(ctor => ctx.BuilderContext.SymbolAccessor.IsDirectlyAccessible(ctor)) + .InstanceConstructors.Where(ctor => ctx.BuilderContext.SymbolAccessor.IsConstructorAccessible(ctor)) .OrderByDescending(x => ctx.BuilderContext.SymbolAccessor.HasAttribute(x)) .ThenBy(x => ctx.BuilderContext.SymbolAccessor.HasAttribute(x)); @@ -76,6 +80,8 @@ public static IReadOnlyList BuildConstructorMapping continue; } + ctx.Mapping.Constructor = ctx.BuilderContext.InstanceConstructors.BuildForConstructor(ctorCandidate); + foreach (var mapping in constructorParameterMappings) { ctx.AddConstructorParameterMapping(mapping); @@ -85,6 +91,7 @@ public static IReadOnlyList BuildConstructorMapping } ctx.BuilderContext.ReportDiagnostic(DiagnosticDescriptors.NoConstructorFound, ctx.BuilderContext.Target); + ctx.Mapping.Constructor = new InstanceConstructor(namedTargetType); return []; } @@ -93,6 +100,9 @@ public static void BuildInitMemberMappings( bool includeAllMembers = false ) { + if (!ctx.Mapping.Constructor.SupportsObjectInitializer) + return; + var initOnlyTargetMembers = includeAllMembers ? ctx.EnumerateUnmappedTargetMembers().ToArray() : ctx.EnumerateUnmappedTargetMembers().Where(x => x.CanOnlySetViaInitializer()).ToArray(); diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs index 98e1b49d00..a0d0fc1ae3 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs @@ -1,12 +1,12 @@ using System.Diagnostics; using Microsoft.CodeAnalysis; using Riok.Mapperly.Configuration; +using Riok.Mapperly.Descriptors.Constructors; using Riok.Mapperly.Descriptors.Enumerables; using Riok.Mapperly.Descriptors.FormatProviders; using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Descriptors.Mappings.ExistingTarget; using Riok.Mapperly.Descriptors.Mappings.UserMappings; -using Riok.Mapperly.Descriptors.ObjectFactories; using Riok.Mapperly.Diagnostics; using Riok.Mapperly.Helpers; @@ -21,7 +21,7 @@ public class MappingBuilderContext : SimpleMappingBuilderContext public MappingBuilderContext( SimpleMappingBuilderContext parentCtx, - ObjectFactoryCollection objectFactories, + InstanceConstructorFactory instanceConstructors, FormatProviderCollection formatProviders, IUserMapping? userMapping, TypeMappingKey mappingKey, @@ -29,7 +29,7 @@ public MappingBuilderContext( ) : base(parentCtx, diagnosticLocation ?? userMapping?.Method.GetSyntaxLocation()) { - ObjectFactories = objectFactories; + InstanceConstructors = instanceConstructors; _formatProviders = formatProviders; UserMapping = userMapping; MappingKey = mappingKey; @@ -43,7 +43,7 @@ protected MappingBuilderContext( TypeMappingKey mappingKey, bool ignoreDerivedTypes ) - : this(ctx, ctx.ObjectFactories, ctx._formatProviders, userMapping, mappingKey, diagnosticLocation) + : this(ctx, ctx.InstanceConstructors, ctx._formatProviders, userMapping, mappingKey, diagnosticLocation) { if (ignoreDerivedTypes) { @@ -72,7 +72,7 @@ bool ignoreDerivedTypes /// public virtual bool IsExpression => false; - public ObjectFactoryCollection ObjectFactories { get; } + public InstanceConstructorFactory InstanceConstructors { get; } /// public IReadOnlyDictionary NewInstanceMappings => MappingBuilder.NewInstanceMappings; diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/CtorMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/CtorMappingBuilder.cs index e5fa0ae49f..9be80744ff 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/CtorMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/CtorMappingBuilder.cs @@ -16,14 +16,16 @@ public static class CtorMappingBuilder return null; // resolve ctors which have the source as single argument - var ctorMethod = namedTarget - .InstanceConstructors.Where(ctx.SymbolAccessor.IsDirectlyAccessible) + var ctor = namedTarget + .InstanceConstructors.Where(ctx.SymbolAccessor.IsConstructorAccessible) .FirstOrDefault(m => m.Parameters.Length == 1 && SymbolEqualityComparer.Default.Equals(m.Parameters[0].Type.NonNullable(), ctx.Source.NonNullable()) && ctx.Source.HasSameOrStricterNullability(m.Parameters[0].Type) ); + if (ctor == null) + return null; - return ctorMethod == null ? null : new CtorMapping(ctx.Source, ctx.Target); + return new CtorMapping(ctx.Source, ctx.Target, ctx.InstanceConstructors.BuildForConstructor(ctor)); } } diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/DictionaryMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/DictionaryMappingBuilder.cs index 95fb48ee0d..e3c5fef456 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/DictionaryMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/DictionaryMappingBuilder.cs @@ -53,42 +53,25 @@ or CollectionType.IReadOnlyDictionary INewInstanceMapping valueMapping ) { - // the target is not a well known dictionary type - // it should have a an object factory or a parameterless public ctor - var hasObjectFactory = ctx.ObjectFactories.TryFindObjectFactory(ctx.Source, ctx.Target, out var objectFactory); - if (!hasObjectFactory && !ctx.SymbolAccessor.HasDirectlyAccessibleParameterlessConstructor(ctx.Target)) - { - ctx.ReportDiagnostic(DiagnosticDescriptors.NoParameterlessConstructorFound, ctx.Target); - return null; - } - if (!ctx.CollectionInfos!.Target.ImplementedTypes.HasFlag(CollectionType.IDictionary)) return null; var collectionInfos = ctx.CollectionInfos; - if (!hasObjectFactory) + + // the target is not a well known dictionary type + // it should have a an object factory or a parameterless public ctor + if (!ctx.InstanceConstructors.TryBuildObjectFactory(ctx.Source, ctx.Target, out var constructor)) { collectionInfos = collectionInfos with { Source = BuildCollectionTypeForSourceIDictionary(ctx) }; var existingMapping = ctx.BuildDelegatedMapping(collectionInfos.Source.Type, ctx.Target); if (existingMapping != null) return existingMapping; - hasObjectFactory = ctx.ObjectFactories.TryFindObjectFactory(ctx.Source, ctx.Target, out objectFactory); - } - - if (hasObjectFactory) - { - return new ForEachSetDictionaryObjectFactoryMapping( - collectionInfos, - keyMapping, - valueMapping, - GetExplicitIndexer(ctx), - objectFactory!, - ctx.Configuration.Mapper.UseReferenceHandling - ); + ctx.InstanceConstructors.TryBuildObjectFactory(collectionInfos.Source.Type, ctx.Target, out constructor); } return new ForEachSetDictionaryMapping( + constructor, collectionInfos, keyMapping, valueMapping, @@ -110,12 +93,11 @@ INewInstanceMapping valueMapping if (TryGetFromEnumerable(ctx, keyMapping, valueMapping) is { } toDictionary) return toDictionary; - // there might be an object factory for the exact types - var hasObjectFactory = ctx.ObjectFactories.TryFindObjectFactory(ctx.Source, ctx.Target, out var objectFactory); - // use generalized types to reuse generated mappings var collectionInfos = ctx.CollectionInfos!; - if (!hasObjectFactory) + + // there might be an object factory for the exact types + if (!ctx.InstanceConstructors.TryBuildObjectFactory(ctx.Source, ctx.Target, out var constructor)) { collectionInfos = new CollectionInfos( BuildCollectionTypeForSourceIDictionary(ctx), @@ -126,26 +108,11 @@ INewInstanceMapping valueMapping if (delegateMapping != null) return delegateMapping; - hasObjectFactory = ctx.ObjectFactories.TryFindObjectFactory( - collectionInfos.Source.Type, - collectionInfos.Target.Type, - out objectFactory - ); - } - - if (hasObjectFactory) - { - return new ForEachSetDictionaryObjectFactoryMapping( - collectionInfos, - keyMapping, - valueMapping, - GetExplicitIndexer(ctx), - objectFactory!, - ctx.Configuration.Mapper.UseReferenceHandling - ); + ctx.InstanceConstructors.TryBuildObjectFactory(collectionInfos.Source.Type, collectionInfos.Target.Type, out constructor); } return new ForEachSetDictionaryMapping( + constructor, collectionInfos, keyMapping, valueMapping, @@ -216,10 +183,12 @@ INewInstanceMapping valueMapping if (fromEnumerableCtor != null) { - var constructedDictionary = dictionaryType - .Construct(keyMapping.TargetType, valueMapping.TargetType) - .WithNullableAnnotation(NullableAnnotation.NotAnnotated); - return new CtorMapping(ctx.Source, constructedDictionary); + var constructedDictionary = (INamedTypeSymbol) + dictionaryType + .Construct(keyMapping.TargetType, valueMapping.TargetType) + .WithNullableAnnotation(NullableAnnotation.NotAnnotated); + var ctor = ctx.InstanceConstructors.BuildParameterless(constructedDictionary); + return new CtorMapping(ctx.Source, constructedDictionary, ctor); } return null; diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumerableMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumerableMappingBuilder.cs index 76c2a38b61..60be3e9d05 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumerableMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumerableMappingBuilder.cs @@ -15,7 +15,6 @@ public static class EnumerableMappingBuilder private const string ToArrayMethodName = "global::System.Linq.Enumerable.ToArray"; private const string ToListMethodName = "global::System.Linq.Enumerable.ToList"; private const string ToHashSetMethodName = "ToHashSet"; - private const string AddMethodName = nameof(ICollection.Add); private const string ToImmutableArrayMethodName = "global::System.Collections.Immutable.ImmutableArray.ToImmutableArray"; private const string ToImmutableListMethodName = "global::System.Collections.Immutable.ImmutableList.ToImmutableList"; @@ -79,17 +78,11 @@ public static class EnumerableMappingBuilder if (elementMapping == null) return null; - if (ctx.CollectionInfos.Target.CollectionType == CollectionType.Stack) - return CreateForEach(nameof(Stack.Push)); - - if (ctx.CollectionInfos.Target.CollectionType == CollectionType.Queue) - return CreateForEach(nameof(Queue.Enqueue)); - - // create a foreach loop with add calls if source is not an array - // and has an implicit .Add() method - // the implicit check is an easy way to exclude for example immutable types. - if (ctx.CollectionInfos.Target.CollectionType != CollectionType.Array && ctx.CollectionInfos.Target.HasImplicitCollectionAddMethod) - return CreateForEach(AddMethodName); + var addMethodName = ctx.CollectionInfos.Target.AddMethodName; + if (addMethodName != null) + { + return new ForEachAddEnumerableExistingTargetMapping(ctx.CollectionInfos, elementMapping, addMethodName); + } if (ctx.CollectionInfos.Target.IsImmutableCollectionType) { @@ -97,11 +90,6 @@ public static class EnumerableMappingBuilder } return null; - - ForEachAddEnumerableExistingTargetMapping CreateForEach(string methodName) - { - return new ForEachAddEnumerableExistingTargetMapping(ctx.CollectionInfos, elementMapping, methodName); - } } private static NewInstanceMapping? TryBuildCastMapping(MappingBuilderContext ctx, ITypeMapping elementMapping) @@ -195,10 +183,11 @@ or CollectionType.IEnumerable return new DelegateMapping(ctx.Source, ctx.Target, existingMapping); return new ForEachAddEnumerableMapping( + null, collectionInfos, elementMapping, ctx.Configuration.Mapper.UseReferenceHandling, - AddMethodName + collectionInfos.Target.AddMethodName! ); } @@ -294,47 +283,32 @@ INewInstanceMapping elementMapping private static INewInstanceMapping? BuildCustomTypeMapping(MappingBuilderContext ctx, INewInstanceMapping elementMapping) { - var hasObjectFactory = ctx.ObjectFactories.TryFindObjectFactory(ctx.Source, ctx.Target, out var objectFactory); - // create a foreach loop with add calls if source is not an array // and has an implicit .Add() method // the implicit check is an easy way to exclude for example immutable types. - if ( - ctx.CollectionInfos!.Target.CollectionType == CollectionType.Array - || !ctx.CollectionInfos.Target.HasImplicitCollectionAddMethod - ) + if (ctx.CollectionInfos?.Target.AddMethodName == null) { return null; } // try to reuse an existing mapping var collectionInfos = ctx.CollectionInfos; - if (!hasObjectFactory) + if (!ctx.InstanceConstructors.TryBuildObjectFactory(ctx.Source, ctx.Target, out var constructor)) { collectionInfos = collectionInfos with { Source = BuildCollectionTypeForICollection(ctx, collectionInfos.Source) }; var existingMapping = ctx.BuildDelegatedMapping(collectionInfos.Source.Type, ctx.Target); if (existingMapping != null) return existingMapping; - hasObjectFactory = ctx.ObjectFactories.TryFindObjectFactory(ctx.Source, ctx.Target, out objectFactory); - } - - if (hasObjectFactory) - { - return new ForEachAddEnumerableObjectFactoryMapping( - collectionInfos, - elementMapping, - objectFactory!, - ctx.Configuration.Mapper.UseReferenceHandling, - AddMethodName - ); + ctx.InstanceConstructors.TryBuildObjectFactory(collectionInfos.Source.Type, ctx.Target, out constructor); } return new ForEachAddEnumerableMapping( + constructor, collectionInfos, elementMapping, ctx.Configuration.Mapper.UseReferenceHandling, - AddMethodName + collectionInfos.Target.AddMethodName ); } diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/NewInstanceObjectMemberMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/NewInstanceObjectMemberMappingBuilder.cs index 5f4bbd92c2..22b401bde5 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/NewInstanceObjectMemberMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/NewInstanceObjectMemberMappingBuilder.cs @@ -16,37 +16,26 @@ public static class NewInstanceObjectMemberMappingBuilder if (ctx.Source.IsDelegate() || ctx.Target.IsDelegate()) return null; - if (ctx.ObjectFactories.TryFindObjectFactory(ctx.Source, ctx.Target, out var objectFactory)) - return new NewInstanceObjectFactoryMemberMapping( + if (ctx.InstanceConstructors.TryBuildObjectFactory(ctx.Source, ctx.Target, out var constructor)) + { + return new NewInstanceObjectMemberMethodMapping( ctx.Source, ctx.Target.NonNullable(), - objectFactory, ctx.Configuration.Mapper.UseReferenceHandling - ); + ) + { + Constructor = constructor + }; + } - if ( - ctx.Target is not INamedTypeSymbol namedTarget - || namedTarget.Constructors.All(x => !ctx.SymbolAccessor.IsDirectlyAccessible(x)) - ) + if (!ctx.SymbolAccessor.HasAnyAccessibleConstructor(ctx.Target)) return null; if (ctx.Source.IsEnum() || ctx.Target.IsEnum()) return null; if (ctx.Target.IsTupleType) - { - if (!ctx.IsConversionEnabled(MappingConversionType.Tuple)) - return null; - - // inline expressions don't support tuple expressions so ValueTuple is used instead - if (ctx.IsExpression) - { - return new NewValueTupleConstructorMapping(ctx.Source, ctx.Target); - } - - var expectedArgumentCount = (ctx.Target as INamedTypeSymbol)!.TupleElements.Length; - return new NewValueTupleExpressionMapping(ctx.Source, ctx.Target, expectedArgumentCount); - } + return BuildTupleMapping(ctx); // inline expressions don't support method property mappings // and can only map to properties via object initializers. @@ -65,4 +54,19 @@ ctx.Target is not INamedTypeSymbol namedTarget return new ObjectMemberExistingTargetMapping(ctx.Source, ctx.Target); } + + private static INewInstanceMapping? BuildTupleMapping(MappingBuilderContext ctx) + { + if (!ctx.IsConversionEnabled(MappingConversionType.Tuple)) + return null; + + // inline expressions don't support tuple expressions so ValueTuple is used instead + if (ctx.IsExpression) + { + return new NewValueTupleConstructorMapping(ctx.Source, ctx.Target); + } + + var expectedArgumentCount = (ctx.Target as INamedTypeSymbol)!.TupleElements.Length; + return new NewValueTupleExpressionMapping(ctx.Source, ctx.Target, expectedArgumentCount); + } } diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/SpanMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/SpanMappingBuilder.cs index c14c665ad5..0a3d00b8df 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/SpanMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/SpanMappingBuilder.cs @@ -3,7 +3,6 @@ using Riok.Mapperly.Descriptors.Enumerables; using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Descriptors.Mappings.ExistingTarget; -using Riok.Mapperly.Descriptors.ObjectFactories; using Riok.Mapperly.Diagnostics; namespace Riok.Mapperly.Descriptors.MappingBuilders; @@ -11,7 +10,6 @@ namespace Riok.Mapperly.Descriptors.MappingBuilders; public static class SpanMappingBuilder { private const string ToArrayMethodName = nameof(Enumerable.ToArray); - private const string AddMethodName = nameof(ICollection.Add); public static INewInstanceMapping? TryBuildMapping(MappingBuilderContext ctx) { @@ -81,17 +79,10 @@ when elementMapping.IsSynthetic && !ctx.Configuration.Mapper.UseDeepCloning if (ctx.FindOrBuildMapping(source.EnumeratedType, target.EnumeratedType) is not { } elementMapping) return null; - if (target.CollectionType is CollectionType.Stack) - return CreateForEach(nameof(Stack.Push)); - - if (target.CollectionType is CollectionType.Queue) - return CreateForEach(nameof(Queue.Enqueue)); - - // create a foreach loop with add calls if source is not an array - // and ICollection.Add(T): void is implemented and not explicit - // ensures add is not called and immutable types - if (target.CollectionType is not CollectionType.Array && target.HasImplicitCollectionAddMethod) - return CreateForEach(AddMethodName); + if (target.AddMethodName != null) + { + return new ForEachAddEnumerableExistingTargetMapping(ctx.CollectionInfos, elementMapping, target.AddMethodName); + } // if a mapping could be created for an immutable collection // we diagnostic when it is an existing target mapping @@ -101,11 +92,6 @@ when elementMapping.IsSynthetic && !ctx.Configuration.Mapper.UseDeepCloning } return null; - - ForEachAddEnumerableExistingTargetMapping CreateForEach(string methodName) - { - return new ForEachAddEnumerableExistingTargetMapping(ctx.CollectionInfos, elementMapping, methodName); - } } private static INewInstanceMapping? BuildSpanToEnumerable(MappingBuilderContext ctx, INewInstanceMapping elementMapping) @@ -121,47 +107,23 @@ ForEachAddEnumerableExistingTargetMapping CreateForEach(string methodName) return BuildSpanToList(ctx, elementMapping); if ( - !ctx.ObjectFactories.TryFindObjectFactory(ctx.Source, ctx.Target, out var objectFactory) - && !ctx.SymbolAccessor.HasDirectlyAccessibleParameterlessConstructor(ctx.Target) + !ctx.InstanceConstructors.TryBuildObjectFactory(ctx.Source, ctx.Target, out var constructor) + && !ctx.SymbolAccessor.HasAccessibleParameterlessConstructor(ctx.Target) ) { return MapSpanArrayToEnumerableMethod(ctx); } - if (target.CollectionType is CollectionType.Stack) - return CreateForEach(nameof(Stack.Push), objectFactory); - - if (target.CollectionType is CollectionType.Queue) - return CreateForEach(nameof(Queue.Enqueue), objectFactory); - - // create a foreach loop with add calls if source is not an array - // and ICollection.Add(T): void is implemented and not explicit - // ensures add is not called and immutable types - if (target.CollectionType is not CollectionType.Array && target.HasImplicitCollectionAddMethod) - return CreateForEach(AddMethodName, objectFactory); - - return MapSpanArrayToEnumerableMethod(ctx); + if (target.AddMethodName == null) + return MapSpanArrayToEnumerableMethod(ctx); - INewInstanceMapping CreateForEach(string methodName, ObjectFactory? factory) - { - if (factory != null) - { - return new ForEachAddEnumerableObjectFactoryMapping( - ctx.CollectionInfos, - elementMapping, - factory, - ctx.Configuration.Mapper.UseReferenceHandling, - methodName - ); - } - - return new ForEachAddEnumerableMapping( - ctx.CollectionInfos, - elementMapping, - ctx.Configuration.Mapper.UseReferenceHandling, - methodName - ); - } + return new ForEachAddEnumerableMapping( + constructor, + ctx.CollectionInfos, + elementMapping, + ctx.Configuration.Mapper.UseReferenceHandling, + target.AddMethodName + ); } private static INewInstanceMapping BuildToArrayOrMap(MappingBuilderContext ctx, INewInstanceMapping elementMapping) diff --git a/src/Riok.Mapperly/Descriptors/Mappings/CtorMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/CtorMapping.cs index 1773dba296..0b99e43f79 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/CtorMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/CtorMapping.cs @@ -1,13 +1,14 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; -using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; +using Riok.Mapperly.Descriptors.Constructors; namespace Riok.Mapperly.Descriptors.Mappings; /// /// Represents a mapping where the target type has the source as single ctor argument. /// -public class CtorMapping(ITypeSymbol sourceType, ITypeSymbol targetType) : NewInstanceMapping(sourceType, targetType) +public class CtorMapping(ITypeSymbol sourceType, ITypeSymbol targetType, IInstanceConstructor constructor) + : NewInstanceMapping(sourceType, targetType) { - public override ExpressionSyntax Build(TypeMappingBuildContext ctx) => CreateInstance(TargetType, ctx.Source); + public override ExpressionSyntax Build(TypeMappingBuildContext ctx) => constructor.CreateInstance(ctx, [ctx.Source]); } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/ForEachAddEnumerableMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/ForEachAddEnumerableMapping.cs index 4f6da827b0..a0e8cd2aa0 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/ForEachAddEnumerableMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/ForEachAddEnumerableMapping.cs @@ -1,4 +1,5 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Descriptors.Constructors; using Riok.Mapperly.Descriptors.Enumerables; using Riok.Mapperly.Descriptors.Enumerables.EnsureCapacity; using Riok.Mapperly.Descriptors.Mappings.ExistingTarget; @@ -9,17 +10,25 @@ namespace Riok.Mapperly.Descriptors.Mappings; /// Represents a foreach enumerable mapping which works by creating a new target instance, /// looping through the source, mapping each element and adding it to the target collection. /// -public class ForEachAddEnumerableMapping( - CollectionInfos collectionInfos, - INewInstanceMapping elementMapping, - bool enableReferenceHandling, - string insertMethodName -) - : NewInstanceObjectMemberMethodMapping(collectionInfos.Source.Type, collectionInfos.Target.Type, enableReferenceHandling), - INewInstanceEnumerableMapping +public class ForEachAddEnumerableMapping : NewInstanceObjectMemberMethodMapping, INewInstanceEnumerableMapping { - private readonly ForEachAddEnumerableExistingTargetMapping _existingTargetMapping = - new(collectionInfos, elementMapping, insertMethodName); + private readonly ForEachAddEnumerableExistingTargetMapping _existingTargetMapping; + + public ForEachAddEnumerableMapping( + IInstanceConstructor? constructor, + CollectionInfos collectionInfos, + INewInstanceMapping elementMapping, + bool enableReferenceHandling, + string insertMethodName + ) + : base(collectionInfos.Source.Type, collectionInfos.Target.Type, enableReferenceHandling) + { + _existingTargetMapping = new(collectionInfos, elementMapping, insertMethodName); + if (constructor != null) + { + Constructor = constructor; + } + } public CollectionInfos CollectionInfos => _existingTargetMapping.CollectionInfos; diff --git a/src/Riok.Mapperly/Descriptors/Mappings/ForEachAddEnumerableObjectFactoryMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/ForEachAddEnumerableObjectFactoryMapping.cs deleted file mode 100644 index 21af2ed599..0000000000 --- a/src/Riok.Mapperly/Descriptors/Mappings/ForEachAddEnumerableObjectFactoryMapping.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Riok.Mapperly.Descriptors.Enumerables; -using Riok.Mapperly.Descriptors.Enumerables.EnsureCapacity; -using Riok.Mapperly.Descriptors.Mappings.ExistingTarget; -using Riok.Mapperly.Descriptors.ObjectFactories; - -namespace Riok.Mapperly.Descriptors.Mappings; - -/// -/// Represents a foreach enumerable mapping which works by creating a new target instance via an object factory, -/// looping through the source, mapping each element and adding it to the target collection. -/// -public class ForEachAddEnumerableObjectFactoryMapping( - CollectionInfos collectionInfos, - INewInstanceMapping elementMapping, - ObjectFactory objectFactory, - bool enableReferenceHandling, - string insertMethodName -) - : NewInstanceObjectFactoryMemberMapping( - collectionInfos.Source.Type, - collectionInfos.Target.Type, - objectFactory, - enableReferenceHandling - ), - IEnumerableMapping -{ - private readonly ForEachAddEnumerableExistingTargetMapping _existingTargetMapping = - new(collectionInfos, elementMapping, insertMethodName); - - public CollectionInfos CollectionInfos => _existingTargetMapping.CollectionInfos; - - public void AddEnsureCapacity(EnsureCapacityInfo ensureCapacityInfo) => _existingTargetMapping.AddEnsureCapacity(ensureCapacityInfo); - - protected override IEnumerable BuildBody(TypeMappingBuildContext ctx, ExpressionSyntax target) - { - return base.BuildBody(ctx, target).Concat(_existingTargetMapping.Build(ctx, target)); - } -} diff --git a/src/Riok.Mapperly/Descriptors/Mappings/ForEachSetDictionaryMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/ForEachSetDictionaryMapping.cs index 2e33778f32..72bcc84fa6 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/ForEachSetDictionaryMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/ForEachSetDictionaryMapping.cs @@ -1,5 +1,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Descriptors.Constructors; using Riok.Mapperly.Descriptors.Enumerables; using Riok.Mapperly.Descriptors.Enumerables.EnsureCapacity; using Riok.Mapperly.Descriptors.Mappings.ExistingTarget; @@ -10,18 +11,26 @@ namespace Riok.Mapperly.Descriptors.Mappings; /// Represents a foreach dictionary mapping which works by creating a new target instance, /// looping through the source, mapping each element and setting it to the target collection. /// -public class ForEachSetDictionaryMapping( - CollectionInfos collectionInfos, - INewInstanceMapping keyMapping, - INewInstanceMapping valueMapping, - INamedTypeSymbol? explicitCast, - bool enableReferenceHandling -) - : NewInstanceObjectMemberMethodMapping(collectionInfos.Source.Type, collectionInfos.Target.Type, enableReferenceHandling), - INewInstanceEnumerableMapping +public class ForEachSetDictionaryMapping : NewInstanceObjectMemberMethodMapping, INewInstanceEnumerableMapping { - private readonly ForEachSetDictionaryExistingTargetMapping _existingTargetMapping = - new(collectionInfos, keyMapping, valueMapping, explicitCast); + private readonly ForEachSetDictionaryExistingTargetMapping _existingTargetMapping; + + public ForEachSetDictionaryMapping( + IInstanceConstructor? constructor, + CollectionInfos collectionInfos, + INewInstanceMapping keyMapping, + INewInstanceMapping valueMapping, + INamedTypeSymbol? explicitCast, + bool enableReferenceHandling + ) + : base(collectionInfos.Source.Type, collectionInfos.Target.Type, enableReferenceHandling) + { + _existingTargetMapping = new(collectionInfos, keyMapping, valueMapping, explicitCast); + if (constructor != null) + { + Constructor = constructor; + } + } public CollectionInfos CollectionInfos => _existingTargetMapping.CollectionInfos; diff --git a/src/Riok.Mapperly/Descriptors/Mappings/ForEachSetDictionaryObjectFactoryMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/ForEachSetDictionaryObjectFactoryMapping.cs deleted file mode 100644 index e247e80247..0000000000 --- a/src/Riok.Mapperly/Descriptors/Mappings/ForEachSetDictionaryObjectFactoryMapping.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Riok.Mapperly.Descriptors.Enumerables; -using Riok.Mapperly.Descriptors.Enumerables.EnsureCapacity; -using Riok.Mapperly.Descriptors.Mappings.ExistingTarget; -using Riok.Mapperly.Descriptors.ObjectFactories; - -namespace Riok.Mapperly.Descriptors.Mappings; - -/// -/// Represents a foreach dictionary mapping which works by creating a new target instance via an object factory, -/// looping through the source, mapping each element and adding it to the target collection. -/// -public class ForEachSetDictionaryObjectFactoryMapping( - CollectionInfos collectionInfos, - INewInstanceMapping keyMapping, - INewInstanceMapping valueMapping, - INamedTypeSymbol? explicitCast, - ObjectFactory objectFactory, - bool enableReferenceHandling -) - : NewInstanceObjectFactoryMemberMapping( - collectionInfos.Source.Type, - collectionInfos.Target.Type, - objectFactory, - enableReferenceHandling - ), - IEnumerableMapping -{ - private readonly ForEachSetDictionaryExistingTargetMapping _existingTargetMapping = - new(collectionInfos, keyMapping, valueMapping, explicitCast); - - public CollectionInfos CollectionInfos => _existingTargetMapping.CollectionInfos; - - public void AddEnsureCapacity(EnsureCapacityInfo ensureCapacityInfo) => _existingTargetMapping.AddEnsureCapacity(ensureCapacityInfo); - - protected override IEnumerable BuildBody(TypeMappingBuildContext ctx, ExpressionSyntax target) - { - return base.BuildBody(ctx, target).Concat(_existingTargetMapping.Build(ctx, target)); - } -} diff --git a/src/Riok.Mapperly/Descriptors/Mappings/INewInstanceObjectMemberMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/INewInstanceObjectMemberMapping.cs index 26e1e8ef2c..2b161d7c88 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/INewInstanceObjectMemberMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/INewInstanceObjectMemberMapping.cs @@ -1,3 +1,4 @@ +using Riok.Mapperly.Descriptors.Constructors; using Riok.Mapperly.Descriptors.Mappings.MemberMappings; namespace Riok.Mapperly.Descriptors.Mappings; @@ -7,6 +8,10 @@ namespace Riok.Mapperly.Descriptors.Mappings; /// public interface INewInstanceObjectMemberMapping : INewInstanceMapping { + IInstanceConstructor Constructor { get; set; } + + bool HasConstructor { get; } + void AddConstructorParameterMapping(ConstructorParameterMapping mapping); void AddInitMemberMapping(MemberAssignmentMapping mapping); diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberNullAssignmentInitializerMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberNullAssignmentInitializerMapping.cs index 713d5a88f0..65b619c795 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberNullAssignmentInitializerMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberNullAssignmentInitializerMapping.cs @@ -1,7 +1,7 @@ using System.Diagnostics; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Descriptors.Constructors; using Riok.Mapperly.Symbols.Members; -using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings; @@ -9,16 +9,16 @@ namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings; /// A member initializer which initializes null members to new objects. /// [DebuggerDisplay("MemberNullAssignmentInitializerMapping({_pathToInitialize} ??= new())")] -public class MemberNullAssignmentInitializerMapping(MemberPathSetter pathToInitialize) : MemberAssignmentMappingContainer +public class MemberNullAssignmentInitializerMapping(MemberPathSetter pathToInitialize, IInstanceConstructor constructor) + : MemberAssignmentMappingContainer { private readonly MemberPathSetter _pathToInitialize = pathToInitialize; public override IEnumerable Build(TypeMappingBuildContext ctx, ExpressionSyntax targetAccess) { // source.Value ??= new(); - var initializer = ctx.SyntaxFactory.ExpressionStatement( - _pathToInitialize.BuildAssignment(targetAccess, ImplicitObjectCreationExpression(), true) - ); + var newObj = constructor.CreateInstance(ctx); + var initializer = ctx.SyntaxFactory.ExpressionStatement(_pathToInitialize.BuildAssignment(targetAccess, newObj, true)); return base.Build(ctx, targetAccess).Prepend(initializer); } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MethodMemberNullAssignmentInitializerMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MethodMemberNullAssignmentInitializerMapping.cs index 3de286b088..ea38744d0a 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MethodMemberNullAssignmentInitializerMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MethodMemberNullAssignmentInitializerMapping.cs @@ -1,7 +1,6 @@ using System.Diagnostics; -using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Riok.Mapperly.Emit.Syntax; +using Riok.Mapperly.Descriptors.Constructors; using Riok.Mapperly.Symbols.Members; namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings; @@ -10,22 +9,26 @@ namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings; /// A member initializer which initializes null members to new objects. /// [DebuggerDisplay("MemberNullAssignmentInitializerMapping({_targetPathToInitialize} ??= new())")] -public class MethodMemberNullAssignmentInitializerMapping(MemberPathSetter targetPathToInitialize, MemberPathGetter sourcePathToInitialize) - : MemberAssignmentMappingContainer +public class MethodMemberNullAssignmentInitializerMapping( + MemberPathSetter targetPathToInitialize, + MemberPathGetter sourcePathToInitialize, + IInstanceConstructor constructor +) : MemberAssignmentMappingContainer { private readonly MemberPathSetter _targetPathToInitialize = targetPathToInitialize; public override IEnumerable Build(TypeMappingBuildContext ctx, ExpressionSyntax targetAccess) { - // target.Value ?? new() - var initializer = SyntaxFactoryHelper.Coalesce( - sourcePathToInitialize.BuildAccess(targetAccess), - SyntaxFactory.ImplicitObjectCreationExpression() - ); - - // target.SetValue(source.Value ?? new()); - var setTarget = ctx.SyntaxFactory.ExpressionStatement(_targetPathToInitialize.BuildAssignment(targetAccess, initializer)); - return base.Build(ctx, targetAccess).Prepend(setTarget); + // new T(); + var newTarget = constructor.CreateInstance(ctx); + + // target.Value = new T(); + var setTarget = _targetPathToInitialize.BuildAssignment(targetAccess, newTarget); + + // if (target.Value == null) target.Value = new T(); + var setTargetIfNull = ctx.SyntaxFactory.IfNull(sourcePathToInitialize.BuildAccess(targetAccess), setTarget); + + return base.Build(ctx, targetAccess).Prepend(setTargetIfNull); } public override bool Equals(object? obj) diff --git a/src/Riok.Mapperly/Descriptors/Mappings/NewInstanceObjectFactoryMemberMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/NewInstanceObjectFactoryMemberMapping.cs deleted file mode 100644 index defe1891f9..0000000000 --- a/src/Riok.Mapperly/Descriptors/Mappings/NewInstanceObjectFactoryMemberMapping.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Riok.Mapperly.Descriptors.ObjectFactories; -using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; - -namespace Riok.Mapperly.Descriptors.Mappings; - -/// -/// An object mapping creating the target instance via an object factory. -/// -public class NewInstanceObjectFactoryMemberMapping( - ITypeSymbol sourceType, - ITypeSymbol targetType, - ObjectFactory objectFactory, - bool enableReferenceHandling -) : ObjectMemberMethodMapping(sourceType, targetType) -{ - private const string TargetVariableName = "target"; - - public override IEnumerable BuildBody(TypeMappingBuildContext ctx) - { - var targetVariableName = ctx.NameBuilder.New(TargetVariableName); - - // create instance - foreach (var statement in objectFactory.CreateInstance(ctx, this, enableReferenceHandling, targetVariableName)) - { - yield return statement; - } - - // map properties - foreach (var expression in BuildBody(ctx, IdentifierName(targetVariableName))) - { - yield return expression; - } - - // return target; - yield return ctx.SyntaxFactory.ReturnVariable(targetVariableName); - } -} diff --git a/src/Riok.Mapperly/Descriptors/Mappings/NewInstanceObjectMemberMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/NewInstanceObjectMemberMapping.cs index 5c1b32fabf..67c7ff82da 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/NewInstanceObjectMemberMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/NewInstanceObjectMemberMapping.cs @@ -1,7 +1,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Descriptors.Constructors; using Riok.Mapperly.Descriptors.Mappings.MemberMappings; -using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; namespace Riok.Mapperly.Descriptors.Mappings; @@ -14,27 +14,22 @@ public class NewInstanceObjectMemberMapping(ITypeSymbol sourceType, ITypeSymbol : NewInstanceMapping(sourceType, targetType), INewInstanceObjectMemberMapping { - private readonly HashSet _constructorPropertyMappings = new(); - private readonly HashSet _initPropertyMappings = new(); + private IInstanceConstructor? _constructor; + private readonly HashSet _constructorMemberMappings = new(); + private readonly HashSet _initMemberMappings = new(); - public void AddConstructorParameterMapping(ConstructorParameterMapping mapping) => _constructorPropertyMappings.Add(mapping); + public IInstanceConstructor Constructor + { + get => _constructor ?? throw new InvalidOperationException("constructor is not set"); + set => _constructor = value; + } - public void AddInitMemberMapping(MemberAssignmentMapping mapping) => _initPropertyMappings.Add(mapping); + public bool HasConstructor => _constructor != null; - public override ExpressionSyntax Build(TypeMappingBuildContext ctx) - { - // new T(ctorArgs) { ... }; - var ctorArgs = _constructorPropertyMappings.Select(x => x.BuildArgument(ctx)).ToArray(); - var objectCreationExpression = CreateInstance(TargetType, ctorArgs); + public void AddConstructorParameterMapping(ConstructorParameterMapping mapping) => _constructorMemberMappings.Add(mapping); - // add initializer - if (_initPropertyMappings.Count > 0) - { - var initPropertiesContext = ctx.AddIndentation(); - var initMappings = _initPropertyMappings.Select(x => x.BuildExpression(initPropertiesContext, null)).ToArray(); - objectCreationExpression = objectCreationExpression.WithInitializer(ctx.SyntaxFactory.ObjectInitializer(initMappings)); - } + public void AddInitMemberMapping(MemberAssignmentMapping mapping) => _initMemberMappings.Add(mapping); - return objectCreationExpression; - } + public override ExpressionSyntax Build(TypeMappingBuildContext ctx) => + Constructor.CreateInstance(ctx, _constructorMemberMappings, _initMemberMappings); } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/NewInstanceObjectMemberMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/NewInstanceObjectMemberMethodMapping.cs index fc98495cfe..a635550ff8 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/NewInstanceObjectMemberMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/NewInstanceObjectMemberMethodMapping.cs @@ -1,9 +1,8 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Descriptors.Constructors; using Riok.Mapperly.Descriptors.Mappings.MemberMappings; -using Riok.Mapperly.Emit; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; -using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; namespace Riok.Mapperly.Descriptors.Mappings; @@ -16,12 +15,22 @@ public class NewInstanceObjectMemberMethodMapping(ITypeSymbol sourceType, ITypeS INewInstanceObjectMemberMapping { private const string TargetVariableName = "target"; - private readonly HashSet _constructorPropertyMappings = new(); - private readonly HashSet _initPropertyMappings = new(); - public void AddConstructorParameterMapping(ConstructorParameterMapping mapping) => _constructorPropertyMappings.Add(mapping); + private IInstanceConstructor? _constructor; + private readonly HashSet _constructorMemberMappings = new(); + private readonly HashSet _initMemberMappings = new(); - public void AddInitMemberMapping(MemberAssignmentMapping mapping) => _initPropertyMappings.Add(mapping); + public IInstanceConstructor Constructor + { + get => _constructor ?? throw new InvalidOperationException("constructor is not set"); + set => _constructor = value; + } + + public bool HasConstructor => _constructor != null; + + public void AddConstructorParameterMapping(ConstructorParameterMapping mapping) => _constructorMemberMappings.Add(mapping); + + public void AddInitMemberMapping(MemberAssignmentMapping mapping) => _initMemberMappings.Add(mapping); public override IEnumerable BuildBody(TypeMappingBuildContext ctx) { @@ -45,35 +54,13 @@ public override IEnumerable BuildBody(TypeMappingBuildContext c private IEnumerable CreateTargetInstance(TypeMappingBuildContext ctx, string targetVariableName) { - if (enableReferenceHandling) - { - // TryGetReference - yield return ReferenceHandlingSyntaxFactoryHelper.TryGetReference(ctx, this); - } - - // new T(ctorArgs) { ... }; - var ctorArgs = _constructorPropertyMappings.Select(x => x.BuildArgument(ctx)).ToArray(); - var objectCreationExpression = CreateInstance(TargetType, ctorArgs); - - // add initializer - if (_initPropertyMappings.Count > 0) - { - var initPropertiesContext = ctx.AddIndentation(); - var initMappings = _initPropertyMappings.Select(x => x.BuildExpression(initPropertiesContext, null)).ToArray(); - objectCreationExpression = objectCreationExpression.WithInitializer(ctx.SyntaxFactory.ObjectInitializer(initMappings)); - } - - // var target = new T() { ... }; - yield return ctx.SyntaxFactory.DeclareLocalVariable(targetVariableName, objectCreationExpression); - - // set the reference as soon as it is created, - // as property mappings could refer to the same instance. - if (enableReferenceHandling) - { - // SetReference - yield return ctx.SyntaxFactory.ExpressionStatement( - ReferenceHandlingSyntaxFactoryHelper.SetReference(this, ctx, IdentifierName(targetVariableName)) - ); - } + return Constructor.CreateTargetInstance( + ctx, + this, + targetVariableName, + enableReferenceHandling, + _constructorMemberMappings, + _initMemberMappings + ); } } diff --git a/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericSourceObjectFactory.cs b/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericSourceObjectFactory.cs index a86e366525..a34abb4022 100644 --- a/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericSourceObjectFactory.cs +++ b/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericSourceObjectFactory.cs @@ -13,7 +13,7 @@ namespace Riok.Mapperly.Descriptors.ObjectFactories; public class GenericSourceObjectFactory(GenericTypeChecker typeChecker, SymbolAccessor symbolAccessor, IMethodSymbol method) : ObjectFactory(symbolAccessor, method) { - public override bool CanCreateType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate) => + public override bool CanCreateInstanceOfType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate) => SymbolEqualityComparer.Default.Equals(Method.ReturnType, targetTypeToCreate) && typeChecker.CheckTypes((Method.TypeParameters[0], sourceType)); diff --git a/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericSourceTargetObjectFactory.cs b/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericSourceTargetObjectFactory.cs index 1b541f9567..a3f58d8cdb 100644 --- a/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericSourceTargetObjectFactory.cs +++ b/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericSourceTargetObjectFactory.cs @@ -14,7 +14,7 @@ int sourceTypeParameterIndex { private readonly int _targetTypeParameterIndex = (sourceTypeParameterIndex + 1) % 2; - public override bool CanCreateType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate) => + public override bool CanCreateInstanceOfType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate) => typeChecker.CheckTypes( (Method.TypeParameters[sourceTypeParameterIndex], sourceType), (Method.TypeParameters[_targetTypeParameterIndex], targetTypeToCreate) diff --git a/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericTargetObjectFactory.cs b/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericTargetObjectFactory.cs index 29a9d82c81..a1a1f1a199 100644 --- a/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericTargetObjectFactory.cs +++ b/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericTargetObjectFactory.cs @@ -13,7 +13,7 @@ namespace Riok.Mapperly.Descriptors.ObjectFactories; public class GenericTargetObjectFactory(GenericTypeChecker typeChecker, SymbolAccessor symbolAccessor, IMethodSymbol method) : ObjectFactory(symbolAccessor, method) { - public override bool CanCreateType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate) => + public override bool CanCreateInstanceOfType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate) => typeChecker.CheckTypes((Method.TypeParameters[0], targetTypeToCreate)); protected override ExpressionSyntax BuildCreateType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate, ExpressionSyntax source) => diff --git a/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericTargetObjectFactoryWithSource.cs b/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericTargetObjectFactoryWithSource.cs index 3da7353c59..1369a82fe0 100644 --- a/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericTargetObjectFactoryWithSource.cs +++ b/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericTargetObjectFactoryWithSource.cs @@ -13,8 +13,9 @@ namespace Riok.Mapperly.Descriptors.ObjectFactories; public class GenericTargetObjectFactoryWithSource(GenericTypeChecker typeChecker, SymbolAccessor symbolAccessor, IMethodSymbol method) : GenericTargetObjectFactory(typeChecker, symbolAccessor, method) { - public override bool CanCreateType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate) => - base.CanCreateType(sourceType, targetTypeToCreate) && SymbolEqualityComparer.Default.Equals(Method.Parameters[0].Type, sourceType); + public override bool CanCreateInstanceOfType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate) => + base.CanCreateInstanceOfType(sourceType, targetTypeToCreate) + && SymbolEqualityComparer.Default.Equals(Method.Parameters[0].Type, sourceType); protected override ExpressionSyntax BuildCreateType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate, ExpressionSyntax source) => GenericInvocation(Method.Name, new[] { NonNullableIdentifier(targetTypeToCreate) }, source); diff --git a/src/Riok.Mapperly/Descriptors/ObjectFactories/ObjectFactory.cs b/src/Riok.Mapperly/Descriptors/ObjectFactories/ObjectFactory.cs index 5afe53edb4..94e7280073 100644 --- a/src/Riok.Mapperly/Descriptors/ObjectFactories/ObjectFactory.cs +++ b/src/Riok.Mapperly/Descriptors/ObjectFactories/ObjectFactory.cs @@ -1,10 +1,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Riok.Mapperly.Descriptors.Mappings; -using Riok.Mapperly.Emit; -using Riok.Mapperly.Emit.Syntax; using Riok.Mapperly.Helpers; -using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; namespace Riok.Mapperly.Descriptors.ObjectFactories; @@ -16,43 +12,13 @@ public abstract class ObjectFactory(SymbolAccessor symbolAccessor, IMethodSymbol { protected IMethodSymbol Method { get; } = method; - public IEnumerable CreateInstance( - TypeMappingBuildContext ctx, - INewInstanceMapping mapping, - bool enableReferenceHandling, - string targetVariableName - ) - { - if (enableReferenceHandling) - { - // TryGetReference - yield return ReferenceHandlingSyntaxFactoryHelper.TryGetReference(ctx, mapping); - } - - // var target = CreateMyObject(); - yield return ctx.SyntaxFactory.DeclareLocalVariable( - targetVariableName, - CreateType(mapping.SourceType, mapping.TargetType, ctx.Source) - ); - - // set the reference as soon as it is created, - // as property mappings could refer to the same instance. - if (enableReferenceHandling) - { - // SetReference - yield return ctx.SyntaxFactory.ExpressionStatement( - ReferenceHandlingSyntaxFactoryHelper.SetReference(mapping, ctx, IdentifierName(targetVariableName)) - ); - } - } + public ExpressionSyntax CreateType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate, ExpressionSyntax source) => + HandleNull(BuildCreateType(sourceType, targetTypeToCreate, source), targetTypeToCreate); - public abstract bool CanCreateType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate); + public abstract bool CanCreateInstanceOfType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate); protected abstract ExpressionSyntax BuildCreateType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate, ExpressionSyntax source); - private ExpressionSyntax CreateType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate, ExpressionSyntax source) => - HandleNull(BuildCreateType(sourceType, targetTypeToCreate, source), targetTypeToCreate); - /// /// Wraps the in null handling. /// If the returns a nullable type, but the is not nullable, @@ -68,7 +34,7 @@ private ExpressionSyntax HandleNull(ExpressionSyntax expression, ITypeSymbol typ return expression; ExpressionSyntax nullFallback = symbolAccessor.HasDirectlyAccessibleParameterlessConstructor(typeToCreate) - ? SyntaxFactoryHelper.CreateInstance(typeToCreate) + ? CreateInstance(typeToCreate) : ThrowNullReferenceException($"The object factory {Method.Name} returned null"); return Coalesce(expression, nullFallback); diff --git a/src/Riok.Mapperly/Descriptors/ObjectFactories/ObjectFactoryCollection.cs b/src/Riok.Mapperly/Descriptors/ObjectFactories/ObjectFactoryCollection.cs index a863645abe..b6446bf32d 100644 --- a/src/Riok.Mapperly/Descriptors/ObjectFactories/ObjectFactoryCollection.cs +++ b/src/Riok.Mapperly/Descriptors/ObjectFactories/ObjectFactoryCollection.cs @@ -13,7 +13,7 @@ public bool TryFindObjectFactory(ITypeSymbol sourceType, ITypeSymbol targetType, if (_concreteObjectFactories.TryGetValue(key, out objectFactory)) return true; - objectFactory = objectFactories.FirstOrDefault(f => f.CanCreateType(sourceType, targetType)); + objectFactory = objectFactories.FirstOrDefault(f => f.CanCreateInstanceOfType(sourceType, targetType)); if (objectFactory == null) return false; diff --git a/src/Riok.Mapperly/Descriptors/ObjectFactories/ObjectFactoryConstructorAdapter.cs b/src/Riok.Mapperly/Descriptors/ObjectFactories/ObjectFactoryConstructorAdapter.cs new file mode 100644 index 0000000000..041e586c2a --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/ObjectFactories/ObjectFactoryConstructorAdapter.cs @@ -0,0 +1,25 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Descriptors.Constructors; +using Riok.Mapperly.Descriptors.Mappings; + +namespace Riok.Mapperly.Descriptors.ObjectFactories; + +/// +/// An adapter to adapt a general as a +/// for a concrete type pair. +/// +public class ObjectFactoryConstructorAdapter(ObjectFactory objectFactory, ITypeSymbol sourceType, ITypeSymbol targetType) + : IInstanceConstructor +{ + public bool SupportsObjectInitializer => false; + + public ExpressionSyntax CreateInstance( + TypeMappingBuildContext ctx, + IEnumerable args, + InitializerExpressionSyntax? initializer = null + ) + { + return objectFactory.CreateType(sourceType, targetType, ctx.Source); + } +} diff --git a/src/Riok.Mapperly/Descriptors/ObjectFactories/SimpleObjectFactory.cs b/src/Riok.Mapperly/Descriptors/ObjectFactories/SimpleObjectFactory.cs index 6b6927271d..818da3f61b 100644 --- a/src/Riok.Mapperly/Descriptors/ObjectFactories/SimpleObjectFactory.cs +++ b/src/Riok.Mapperly/Descriptors/ObjectFactories/SimpleObjectFactory.cs @@ -10,7 +10,7 @@ namespace Riok.Mapperly.Descriptors.ObjectFactories; /// public class SimpleObjectFactory(SymbolAccessor symbolAccessor, IMethodSymbol method) : ObjectFactory(symbolAccessor, method) { - public override bool CanCreateType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate) => + public override bool CanCreateInstanceOfType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate) => SymbolEqualityComparer.Default.Equals(Method.ReturnType, targetTypeToCreate); protected override ExpressionSyntax BuildCreateType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate, ExpressionSyntax source) => diff --git a/src/Riok.Mapperly/Descriptors/ObjectFactories/SimpleObjectFactoryWithSource.cs b/src/Riok.Mapperly/Descriptors/ObjectFactories/SimpleObjectFactoryWithSource.cs index f9635f33f7..121a0b5a66 100644 --- a/src/Riok.Mapperly/Descriptors/ObjectFactories/SimpleObjectFactoryWithSource.cs +++ b/src/Riok.Mapperly/Descriptors/ObjectFactories/SimpleObjectFactoryWithSource.cs @@ -12,8 +12,9 @@ namespace Riok.Mapperly.Descriptors.ObjectFactories; public class SimpleObjectFactoryWithSource(SymbolAccessor symbolAccessor, IMethodSymbol method) : SimpleObjectFactory(symbolAccessor, method) { - public override bool CanCreateType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate) => - base.CanCreateType(sourceType, targetTypeToCreate) && SymbolEqualityComparer.Default.Equals(sourceType, Method.Parameters[0].Type); + public override bool CanCreateInstanceOfType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate) => + base.CanCreateInstanceOfType(sourceType, targetTypeToCreate) + && SymbolEqualityComparer.Default.Equals(sourceType, Method.Parameters[0].Type); protected override ExpressionSyntax BuildCreateType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate, ExpressionSyntax source) => Invocation(Method.Name, source); diff --git a/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs b/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs index 0960a06646..10cb64ea0a 100644 --- a/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs +++ b/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using Microsoft.CodeAnalysis; @@ -26,32 +27,55 @@ public class SymbolAccessor(CompilationContext compilationContext, INamedTypeSym new(SymbolEqualityComparer.Default); private MemberVisibility _memberVisibility = MemberVisibility.AllAccessible; + private MemberVisibility _constructorVisibility = MemberVisibility.AllAccessible; private Compilation Compilation => compilationContext.Compilation; internal void SetMemberVisibility(MemberVisibility visibility) => _memberVisibility = visibility; + internal void SetConstructorVisibility(MemberVisibility visibility) => _constructorVisibility = visibility; + public bool HasDirectlyAccessibleParameterlessConstructor(ITypeSymbol symbol) => symbol is INamedTypeSymbol { IsAbstract: false } namedTypeSymbol && namedTypeSymbol.InstanceConstructors.Any(c => c.Parameters.IsDefaultOrEmpty && IsDirectlyAccessible(c)); + public bool HasAccessibleParameterlessConstructor(ITypeSymbol symbol) => + symbol is INamedTypeSymbol { IsAbstract: false } namedTypeSymbol + && namedTypeSymbol.InstanceConstructors.Any(x => x.Parameters.IsDefaultOrEmpty && IsConstructorAccessible(x)); + + public bool HasAnyAccessibleConstructor(ITypeSymbol symbol) => + symbol is INamedTypeSymbol { IsAbstract: false } namedTypeSymbol + && namedTypeSymbol.InstanceConstructors.Any(IsConstructorAccessible); + public bool IsDirectlyAccessible(ISymbol symbol) => Compilation.IsSymbolAccessibleWithin(symbol, mapperSymbol); - public bool IsAccessible(ISymbol symbol) + public bool IsMemberAccessible(ISymbol symbol) + { + Debug.Assert(symbol is not IMethodSymbol { MethodKind: MethodKind.Constructor }); + return IsAccessible(symbol, _memberVisibility); + } + + public bool IsConstructorAccessible(IMethodSymbol symbol) + { + Debug.Assert(symbol.MethodKind == MethodKind.Constructor); + return IsAccessible(symbol, _constructorVisibility); + } + + private bool IsAccessible(ISymbol symbol, MemberVisibility visibility) { - if (_memberVisibility.HasFlag(MemberVisibility.Accessible) && !IsDirectlyAccessible(symbol)) + if (visibility.HasFlag(MemberVisibility.Accessible) && !IsDirectlyAccessible(symbol)) return false; return symbol.DeclaredAccessibility switch { - Accessibility.Private => _memberVisibility.HasFlag(MemberVisibility.Private), + Accessibility.Private => visibility.HasFlag(MemberVisibility.Private), Accessibility.ProtectedAndInternal - => _memberVisibility.HasFlag(MemberVisibility.Protected) && _memberVisibility.HasFlag(MemberVisibility.Internal), - Accessibility.Protected => _memberVisibility.HasFlag(MemberVisibility.Protected), - Accessibility.Internal => _memberVisibility.HasFlag(MemberVisibility.Internal), + => visibility.HasFlag(MemberVisibility.Protected) && visibility.HasFlag(MemberVisibility.Internal), + Accessibility.Protected => visibility.HasFlag(MemberVisibility.Protected), + Accessibility.Internal => visibility.HasFlag(MemberVisibility.Internal), Accessibility.ProtectedOrInternal - => _memberVisibility.HasFlag(MemberVisibility.Protected) || _memberVisibility.HasFlag(MemberVisibility.Internal), - Accessibility.Public => _memberVisibility.HasFlag(MemberVisibility.Public), + => visibility.HasFlag(MemberVisibility.Protected) || visibility.HasFlag(MemberVisibility.Internal), + Accessibility.Public => visibility.HasFlag(MemberVisibility.Public), _ => false, }; } diff --git a/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeAccessorContext.cs b/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeAccessorContext.cs index acfe6402db..e9a2e71664 100644 --- a/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeAccessorContext.cs +++ b/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeAccessorContext.cs @@ -6,48 +6,50 @@ namespace Riok.Mapperly.Descriptors.UnsafeAccess; -public class UnsafeAccessorContext(UniqueNameBuilder nameBuilder, SymbolAccessor symbolAccessor) +public class UnsafeAccessorContext(UniqueNameBuilder nameBuilder, SymbolAccessor symbolAccessor, string className) { private readonly List _unsafeAccessors = new(); private readonly Dictionary _unsafeAccessorsBySymbol = new(); private readonly UniqueNameBuilder _nameBuilder = nameBuilder.NewScope(); - public IReadOnlyCollection UnsafeAccessors => _unsafeAccessors; + + public IReadOnlyCollection Accessors => _unsafeAccessors; public UnsafeSetPropertyAccessor GetOrBuildPropertySetter(PropertyMember member) { return GetOrBuild( - "Set", UnsafeAccessorType.SetProperty, member.Symbol, - static (m, methodName) => new UnsafeSetPropertyAccessor(m, methodName) + static (m, _, methodName) => new UnsafeSetPropertyAccessor(m, methodName) ); } public UnsafeGetPropertyAccessor GetOrBuildPropertyGetter(PropertyMember member) { return GetOrBuild( - "Get", UnsafeAccessorType.GetProperty, member.Symbol, - static (m, methodName) => new UnsafeGetPropertyAccessor(m, methodName) + static (m, _, methodName) => new UnsafeGetPropertyAccessor(m, methodName) ); } public UnsafeFieldAccessor GetOrBuildFieldGetter(FieldMember member) + { + return GetOrBuild(UnsafeAccessorType.GetField, member.Symbol, static (m, _, methodName) => new UnsafeFieldAccessor(m, methodName)); + } + + public UnsafeConstructorAccessor GetOrBuildConstructor(IMethodSymbol ctorSymbol) { return GetOrBuild( - "Get", - UnsafeAccessorType.GetField, - member.Symbol, - static (m, methodName) => new UnsafeFieldAccessor(m, methodName) + UnsafeAccessorType.Constructor, + ctorSymbol, + static (s, className, methodName) => new UnsafeConstructorAccessor(s, className, methodName) ); } private TAccessor GetOrBuild( - string methodNamePrefix, UnsafeAccessorType type, TSymbol symbol, - Func factory + Func factory ) where TAccessor : IUnsafeAccessor where TSymbol : ISymbol @@ -56,8 +58,14 @@ Func factory if (TryGetAccessor(key, out var accessor)) return accessor; - var methodName = BuildMethodName(methodNamePrefix, symbol); - return CacheAccessor(key, factory(symbol, methodName)); + var methodName = type switch + { + UnsafeAccessorType.GetProperty or UnsafeAccessorType.GetField => BuildExtensionMethodName("Get", symbol), + UnsafeAccessorType.SetProperty => BuildExtensionMethodName("Set", symbol), + UnsafeAccessorType.Constructor => _nameBuilder.New("Create" + symbol.ContainingType.Name), + _ => throw new ArgumentOutOfRangeException(nameof(type), type, "Unknown type") + }; + return CacheAccessor(key, factory(symbol, className, methodName)); } private T CacheAccessor(UnsafeAccessorKey key, T accessor) @@ -81,7 +89,7 @@ private bool TryGetAccessor(UnsafeAccessorKey key, [NotNullWhen(true)] out T? return false; } - private string BuildMethodName(string prefix, ISymbol symbol) + private string BuildExtensionMethodName(string prefix, ISymbol symbol) { var methodName = prefix + FormatAccessorName(symbol.Name); return GetUniqueMethodName(symbol.ContainingType, methodName); @@ -112,6 +120,7 @@ private enum UnsafeAccessorType GetProperty, SetProperty, GetField, + Constructor, } private readonly struct UnsafeAccessorKey(ISymbol member, UnsafeAccessorType type) : IEquatable diff --git a/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeConstructorAccessor.cs b/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeConstructorAccessor.cs new file mode 100644 index 0000000000..3f39cfb854 --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeConstructorAccessor.cs @@ -0,0 +1,33 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Descriptors.Constructors; +using Riok.Mapperly.Descriptors.Mappings; +using Riok.Mapperly.Emit; +using Riok.Mapperly.Emit.Syntax; +using Riok.Mapperly.Helpers; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; + +namespace Riok.Mapperly.Descriptors.UnsafeAccess; + +public class UnsafeConstructorAccessor(IMethodSymbol symbol, string className, string methodName) : IUnsafeAccessor, IInstanceConstructor +{ + public bool SupportsObjectInitializer => false; + + public MethodDeclarationSyntax BuildAccessorMethod(SourceEmitterContext ctx) + { + var typeToCreate = IdentifierName(symbol.ContainingType.FullyQualifiedIdentifierName()).AddTrailingSpace(); + var parameters = ParameterList(symbol.Parameters); + var attributeList = ctx.SyntaxFactory.UnsafeAccessorAttributeList(UnsafeAccessorType.Constructor); + return PublicStaticExternMethod(ctx, typeToCreate, methodName, parameters, attributeList); + } + + public ExpressionSyntax CreateInstance( + TypeMappingBuildContext ctx, + IEnumerable args, + InitializerExpressionSyntax? initializer = null + ) + { + return StaticInvocation(className, methodName, args); + } +} diff --git a/src/Riok.Mapperly/Emit/ReferenceHandlingSyntaxFactoryHelper.cs b/src/Riok.Mapperly/Emit/ReferenceHandlingSyntaxFactoryHelper.cs index e81c60a582..518db4ea4e 100644 --- a/src/Riok.Mapperly/Emit/ReferenceHandlingSyntaxFactoryHelper.cs +++ b/src/Riok.Mapperly/Emit/ReferenceHandlingSyntaxFactoryHelper.cs @@ -11,7 +11,7 @@ public static class ReferenceHandlingSyntaxFactoryHelper { private const string ExistingTargetVariableName = "existingTargetReference"; - public static IfStatementSyntax TryGetReference(TypeMappingBuildContext ctx, INewInstanceMapping mapping) + public static IfStatementSyntax TryGetReference(TypeMappingBuildContext ctx, IMapping mapping) { // GetReference var refHandler = ctx.ReferenceHandler ?? throw new ArgumentException("Reference handler is not set", nameof(ctx)); @@ -36,7 +36,7 @@ public static IfStatementSyntax TryGetReference(TypeMappingBuildContext ctx, INe return ctx.SyntaxFactory.If(invocation, ctx.SyntaxFactory.AddIndentation().Return(IdentifierName(existingTargetVariableName))); } - public static ExpressionSyntax SetReference(INewInstanceMapping mapping, TypeMappingBuildContext ctx, ExpressionSyntax target) + public static ExpressionSyntax SetReference(IMapping mapping, TypeMappingBuildContext ctx, ExpressionSyntax target) { // SetReference var refHandler = ctx.ReferenceHandler ?? throw new ArgumentException("Reference handler is not set", nameof(ctx)); diff --git a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Attribute.cs b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Attribute.cs index eeec430e25..829774c676 100644 --- a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Attribute.cs +++ b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Attribute.cs @@ -22,25 +22,27 @@ public SyntaxList AttributeList(string name, params Express return SingletonList(SyntaxFactory.AttributeList(SingletonSeparatedList(attribute)).AddTrailingLineFeed(Indentation)); } - public SyntaxList UnsafeAccessorAttributeList(UnsafeAccessorType type, string name) + public SyntaxList UnsafeAccessorAttributeList(UnsafeAccessorType type, string? name = null) { var unsafeAccessType = type switch { UnsafeAccessorType.Field => "Field", UnsafeAccessorType.Method => "Method", + UnsafeAccessorType.Constructor => "Constructor", _ => throw new ArgumentOutOfRangeException(nameof(type), type, $"Unknown {nameof(UnsafeAccessorType)}"), }; - return AttributeList( - UnsafeAccessorName, - MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, _unsafeAccessorKindName, IdentifierName(unsafeAccessType)), - Assignment(IdentifierName(UnsafeAccessorNameArgument), StringLiteral(name)) - ); + var kind = MemberAccess(_unsafeAccessorKindName, IdentifierName(unsafeAccessType)); + if (name == null) + return AttributeList(UnsafeAccessorName, kind); + + return AttributeList(UnsafeAccessorName, kind, Assignment(IdentifierName(UnsafeAccessorNameArgument), StringLiteral(name))); } public enum UnsafeAccessorType { Method, - Field + Field, + Constructor, } } diff --git a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Condition.cs b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Condition.cs index e1fc13c903..4983f5377d 100644 --- a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Condition.cs +++ b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Condition.cs @@ -80,8 +80,6 @@ public static BinaryExpressionSyntax Is(ExpressionSyntax left, ExpressionSyntax public static ExpressionSyntax Or(IEnumerable values) => BinaryExpression(SyntaxKind.LogicalOrExpression, values); - public static ExpressionSyntax And(params ExpressionSyntax?[] values) => And((IEnumerable)values); - public static ExpressionSyntax And(IEnumerable values) => BinaryExpression(SyntaxKind.LogicalAndExpression, values); public static ExpressionSyntax Add(ExpressionSyntax one, ExpressionSyntax two) => BinaryExpression(SyntaxKind.AddExpression, one, two); diff --git a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Invocation.cs b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Invocation.cs index 7f0f89eb0f..d109b818fa 100644 --- a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Invocation.cs +++ b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Invocation.cs @@ -74,6 +74,12 @@ public static InvocationExpressionSyntax StaticInvocation(IMethodSymbol method, return InvocationExpression(methodAccess).WithArgumentList(ArgumentList(arguments)); } + public static ParameterListSyntax ParameterList(IEnumerable parameters) + { + var parameterSyntaxes = parameters.Select(Parameter); + return SyntaxFactory.ParameterList(CommaSeparatedList(parameterSyntaxes)); + } + public static ParameterListSyntax ParameterList(bool extensionMethod, IEnumerable parameters) { var parameterSyntaxes = parameters @@ -89,6 +95,25 @@ private static ParameterSyntax Parameter(bool addThisKeyword, MethodParameter pa return Parameter(parameter.Type.FullyQualifiedIdentifierName(), parameter.Name, addThisKeyword); } + private static ParameterSyntax Parameter(IParameterSymbol symbol) + { + var type = IdentifierName(symbol.Type.WithNullableAnnotation(symbol.NullableAnnotation.Upgrade()).FullyQualifiedIdentifierName()) + .AddTrailingSpace(); + var param = SyntaxFactory.Parameter(Identifier(symbol.Name)).WithType(type); + + if (symbol.IsThis) + { + param = param.WithModifiers(TokenList(TrailingSpacedToken(SyntaxKind.ThisKeyword))); + } + + if (symbol.HasExplicitDefaultValue) + { + param = param.WithDefault(EqualsValueClause(Literal(symbol.ExplicitDefaultValue))); + } + + return param; + } + public static ParameterSyntax Parameter(string type, string identifier, bool addThisKeyword = false) { var param = SyntaxFactory.Parameter(Identifier(identifier)).WithType(IdentifierName(type).AddTrailingSpace()); @@ -101,7 +126,13 @@ public static ParameterSyntax Parameter(string type, string identifier, bool add return param; } - public static InvocationExpressionSyntax StaticInvocation(string receiverType, string methodName, params ExpressionSyntax[] arguments) + public static InvocationExpressionSyntax StaticInvocation( + string receiverType, + string methodName, + params ExpressionSyntax[] arguments + ) => StaticInvocation(receiverType, methodName, arguments.Select(Argument).ToArray()); + + public static InvocationExpressionSyntax StaticInvocation(string receiverType, string methodName, IEnumerable arguments) { var receiverTypeIdentifier = IdentifierName(receiverType); var methodAccess = MemberAccessExpression( diff --git a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Literal.cs b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Literal.cs index 6e748d8518..16eb67a3d0 100644 --- a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Literal.cs +++ b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Literal.cs @@ -13,8 +13,49 @@ public partial struct SyntaxFactoryHelper public static LiteralExpressionSyntax BooleanLiteral(bool b) => LiteralExpression(b ? SyntaxKind.TrueLiteralExpression : SyntaxKind.FalseLiteralExpression); - public static LiteralExpressionSyntax IntLiteral(int i) => LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(i)); + public static LiteralExpressionSyntax IntLiteral(int i) => + LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(i)); + + public static LiteralExpressionSyntax IntLiteral(uint i) => + LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(i)); public static LiteralExpressionSyntax StringLiteral(string content) => - LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(content)); + LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(content)); + + public static LiteralExpressionSyntax CharLiteral(char content) => + LiteralExpression(SyntaxKind.CharacterLiteralExpression, SyntaxFactory.Literal(content)); + + public static LiteralExpressionSyntax LongLiteral(long content) => + LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(content)); + + public static LiteralExpressionSyntax LongLiteral(ulong content) => + LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(content)); + + public static LiteralExpressionSyntax DecimalLiteral(decimal content) => + LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(content)); + + public static LiteralExpressionSyntax DoubleLiteral(double content) => + LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(content)); + + public static LiteralExpressionSyntax FloatLiteral(float content) => + LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(content)); + + public static LiteralExpressionSyntax Literal(object? obj) + { + return obj switch + { + null => DefaultLiteral(), + int i => IntLiteral(i), + uint i => IntLiteral(i), + bool b => BooleanLiteral(b), + string s => StringLiteral(s), + char c => CharLiteral(c), + long l => LongLiteral(l), + ulong l => LongLiteral(l), + decimal d => DecimalLiteral(d), + double d => DoubleLiteral(d), + float f => FloatLiteral(f), + _ => throw new ArgumentOutOfRangeException(nameof(obj), obj, "Unsupported literal type " + obj.GetType()) + }; + } } diff --git a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.New.cs b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.New.cs index 5dfe848f12..59e4f9189a 100644 --- a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.New.cs +++ b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.New.cs @@ -35,7 +35,10 @@ public static ObjectCreationExpressionSyntax CreateInstance(ITypeSymbol typeSymb return CreateObject(type, ArgumentList(args)); } - public static ObjectCreationExpressionSyntax CreateInstance(ITypeSymbol typeSymbol, params ArgumentSyntax[] args) + public static ObjectCreationExpressionSyntax CreateInstance(ITypeSymbol typeSymbol, params ArgumentSyntax[] args) => + CreateInstance(typeSymbol, (IEnumerable)args); + + public static ObjectCreationExpressionSyntax CreateInstance(ITypeSymbol typeSymbol, IEnumerable args) { var type = NonNullableIdentifier(typeSymbol); return CreateObject(type, ArgumentList(args)); diff --git a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Null.cs b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Null.cs index 1c293ceec2..3a65a571aa 100644 --- a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Null.cs +++ b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Null.cs @@ -50,6 +50,11 @@ _ when argument is InvocationExpressionSyntax invocation }; } + public IfStatementSyntax IfNull(ExpressionSyntax value, ExpressionSyntax body) => + IfNull(value, AddIndentation().ExpressionStatement(body)); + + public IfStatementSyntax IfNull(ExpressionSyntax value, StatementSyntax statement) => If(IsNull(value), statement); + public StatementSyntax IfNullReturnOrThrow(ExpressionSyntax expression, ExpressionSyntax? returnOrThrowExpression = null) { StatementSyntax ifExpression = returnOrThrowExpression switch @@ -58,7 +63,7 @@ public StatementSyntax IfNullReturnOrThrow(ExpressionSyntax expression, Expressi _ => AddIndentation().Return(returnOrThrowExpression), }; - return If(IsNull(expression), ifExpression); + return IfNull(expression, ifExpression); } public static ExpressionSyntax SuppressNullableWarning(ExpressionSyntax expression) => diff --git a/src/Riok.Mapperly/Emit/UnsafeAccessorEmitter.cs b/src/Riok.Mapperly/Emit/UnsafeAccessorEmitter.cs index 4be10c4469..ff1ad75043 100644 --- a/src/Riok.Mapperly/Emit/UnsafeAccessorEmitter.cs +++ b/src/Riok.Mapperly/Emit/UnsafeAccessorEmitter.cs @@ -10,8 +10,6 @@ namespace Riok.Mapperly.Emit; public static class UnsafeAccessorEmitter { - private const string AccessorClassName = "UnsafeAccessor"; - public static MemberDeclarationSyntax BuildUnsafeAccessorClass( MapperDescriptor descriptor, CancellationToken cancellationToken, @@ -19,11 +17,10 @@ SourceEmitterContext ctx ) { var accessorCtx = ctx.AddIndentation(); - var accessorClassName = descriptor.NameBuilder.New(AccessorClassName); var accessors = BuildUnsafeAccessors(accessorCtx, descriptor, cancellationToken); accessors = accessors.SeparateByLineFeed(accessorCtx.SyntaxFactory.Indentation); return ctx.SyntaxFactory.Class( - accessorClassName, + descriptor.UnsafeAccessorName, TokenList(TrailingSpacedToken(SyntaxKind.StaticKeyword), TrailingSpacedToken(SyntaxKind.FileKeyword)), List(accessors) ); diff --git a/src/Riok.Mapperly/Symbols/Members/MappableMember.cs b/src/Riok.Mapperly/Symbols/Members/MappableMember.cs index e52c2b1bfa..e9ccd5bd07 100644 --- a/src/Riok.Mapperly/Symbols/Members/MappableMember.cs +++ b/src/Riok.Mapperly/Symbols/Members/MappableMember.cs @@ -7,7 +7,7 @@ internal static class MappableMember { public static IMappableMember? Create(SymbolAccessor accessor, ISymbol symbol) { - if (!accessor.IsAccessible(symbol) || !symbol.CanBeReferencedByName) + if (!symbol.CanBeReferencedByName || !accessor.IsMemberAccessible(symbol)) return null; return symbol switch diff --git a/src/Riok.Mapperly/Symbols/Members/PropertyMember.cs b/src/Riok.Mapperly/Symbols/Members/PropertyMember.cs index 282603f76b..79da441bad 100644 --- a/src/Riok.Mapperly/Symbols/Members/PropertyMember.cs +++ b/src/Riok.Mapperly/Symbols/Members/PropertyMember.cs @@ -20,12 +20,12 @@ public class PropertyMember(IPropertySymbol symbol, SymbolAccessor symbolAccesso public ITypeSymbol Type { get; } = symbolAccessor.UpgradeNullable(symbol.Type); public bool IsNullable => Type.IsNullable(); - public bool CanGet => !Symbol.IsWriteOnly && (Symbol.GetMethod == null || symbolAccessor.IsAccessible(Symbol.GetMethod)); + public bool CanGet => !Symbol.IsWriteOnly && (Symbol.GetMethod == null || symbolAccessor.IsMemberAccessible(Symbol.GetMethod)); public bool CanGetDirectly => !Symbol.IsWriteOnly && (Symbol.GetMethod == null || symbolAccessor.IsDirectlyAccessible(Symbol.GetMethod)); - public bool CanSet => !Symbol.IsReadOnly && (Symbol.SetMethod == null || symbolAccessor.IsAccessible(Symbol.SetMethod)); + public bool CanSet => !Symbol.IsReadOnly && (Symbol.SetMethod == null || symbolAccessor.IsMemberAccessible(Symbol.SetMethod)); public bool CanSetDirectly => !Symbol.IsReadOnly && (Symbol.SetMethod == null || symbolAccessor.IsDirectlyAccessible(Symbol.SetMethod)); diff --git a/test/Riok.Mapperly.IntegrationTests/Dto/PrivateCtorDto.cs b/test/Riok.Mapperly.IntegrationTests/Dto/PrivateCtorDto.cs new file mode 100644 index 0000000000..af0dabd721 --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/Dto/PrivateCtorDto.cs @@ -0,0 +1,17 @@ +namespace Riok.Mapperly.IntegrationTests.Dto +{ + public class PrivateCtorDto + { + private PrivateCtorDto(int intValue) + { + this.intValue = intValue; + } + + private int intValue; + private string stringValue = string.Empty; + + public int ExposeIntValue() => intValue; + + public string ExposeStringValue() => stringValue; + } +} diff --git a/test/Riok.Mapperly.IntegrationTests/Mapper/TestMapper.cs b/test/Riok.Mapperly.IntegrationTests/Mapper/TestMapper.cs index ff9423ae6e..fa3c552f9e 100644 --- a/test/Riok.Mapperly.IntegrationTests/Mapper/TestMapper.cs +++ b/test/Riok.Mapperly.IntegrationTests/Mapper/TestMapper.cs @@ -12,7 +12,11 @@ namespace Riok.Mapperly.IntegrationTests.Mapper { #if NET8_0_OR_GREATER - [Mapper(IncludedMembers = MemberVisibility.All, EnumMappingStrategy = EnumMappingStrategy.ByValue)] + [Mapper( + IncludedMembers = MemberVisibility.All, + IncludedConstructors = MemberVisibility.All, + EnumMappingStrategy = EnumMappingStrategy.ByValue + )] #else [Mapper(EnumMappingStrategy = EnumMappingStrategy.ByValue)] #endif @@ -102,6 +106,8 @@ public TestObjectDto MapToDto(TestObject src) private int ComputeSum(TestObject testObject) => testObject.SumComponent1 + testObject.SumComponent2; #if NET8_0_OR_GREATER + public partial PrivateCtorDto MapPrivateDto(PrivateCtorObject testObject); + public partial AliasedTupleTarget MapAliasedTuple(AliasedTupleSource source); #endif } diff --git a/test/Riok.Mapperly.IntegrationTests/MapperTest.cs b/test/Riok.Mapperly.IntegrationTests/MapperTest.cs index b500f32ec9..a116673b74 100644 --- a/test/Riok.Mapperly.IntegrationTests/MapperTest.cs +++ b/test/Riok.Mapperly.IntegrationTests/MapperTest.cs @@ -5,6 +5,7 @@ using Xunit; #if NET8_0_OR_GREATER using FluentAssertions; +using Riok.Mapperly.IntegrationTests.Models; #endif namespace Riok.Mapperly.IntegrationTests @@ -29,6 +30,15 @@ public Task RunMappingShouldWork() } #if NET8_0_OR_GREATER + [Fact] + public void RunMappingPrivateCtor() + { + var source = PrivateCtorObject.CreateObject(11, "fooBar"); + var target = new TestMapper().MapPrivateDto(source); + target.ExposeIntValue().Should().Be(11); + target.ExposeStringValue().Should().Be("fooBar"); + } + [Fact] public void RunMappingAliasedTuple() { diff --git a/test/Riok.Mapperly.IntegrationTests/Models/PrivateCtorObject.cs b/test/Riok.Mapperly.IntegrationTests/Models/PrivateCtorObject.cs new file mode 100644 index 0000000000..857f079c89 --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/Models/PrivateCtorObject.cs @@ -0,0 +1,20 @@ +namespace Riok.Mapperly.IntegrationTests.Models +{ + public class PrivateCtorObject + { + private PrivateCtorObject() { } + + private int intValue; + private string stringValue = string.Empty; + + public static PrivateCtorObject CreateObject(int intValue, string stringValue) + { + var obj = new PrivateCtorObject { intValue = intValue, stringValue = stringValue }; + return obj; + } + + public int ExposeIntValue() => intValue; + + public string ExposeStringValue() => stringValue; + } +} diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.SnapshotGeneratedSource.verified.cs index 7da7d8c5bb..c1608efdf0 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.SnapshotGeneratedSource.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.SnapshotGeneratedSource.verified.cs @@ -70,7 +70,7 @@ public partial int ParseableInt(string value) } if (testObject.NullableUnflatteningIdValue != null) { - target.NullableUnflattening ??= new(); + target.NullableUnflattening ??= new global::Riok.Mapperly.IntegrationTests.Dto.IdObjectDto(); target.NullableUnflattening.IdValue = DirectInt(testObject.NullableUnflatteningIdValue.Value); } if (testObject.NestedNullable != null) diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.SnapshotGeneratedSource_NET6_0.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.SnapshotGeneratedSource_NET6_0.verified.cs index fd552eb6e5..d7847e7830 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.SnapshotGeneratedSource_NET6_0.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.SnapshotGeneratedSource_NET6_0.verified.cs @@ -70,7 +70,7 @@ public partial int ParseableInt(string value) } if (testObject.NullableUnflatteningIdValue != null) { - target.NullableUnflattening ??= new(); + target.NullableUnflattening ??= new global::Riok.Mapperly.IntegrationTests.Dto.IdObjectDto(); target.NullableUnflattening.IdValue = DirectInt(testObject.NullableUnflatteningIdValue.Value); } if (testObject.NestedNullable != null) diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.SnapshotGeneratedSource_NET8_0.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.SnapshotGeneratedSource_NET8_0.verified.cs index a8f16200af..bf84bdd8b1 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.SnapshotGeneratedSource_NET8_0.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.SnapshotGeneratedSource_NET8_0.verified.cs @@ -70,7 +70,7 @@ public partial int ParseableInt(string value) } if (testObject.NullableUnflatteningIdValue != null) { - target.NullableUnflattening ??= new(); + target.NullableUnflattening ??= new global::Riok.Mapperly.IntegrationTests.Dto.IdObjectDto(); target.NullableUnflattening.IdValue = DirectInt(testObject.NullableUnflatteningIdValue.Value); } if (testObject.NestedNullable != null) @@ -419,6 +419,14 @@ private partial int PrivateDirectInt(int value) return value; } + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + public partial global::Riok.Mapperly.IntegrationTests.Dto.PrivateCtorDto MapPrivateDto(global::Riok.Mapperly.IntegrationTests.Models.PrivateCtorObject testObject) + { + var target = UnsafeAccessor.CreatePrivateCtorDto(DirectInt(testObject.GetIntValue())); + target.GetStringValue1() = testObject.GetStringValue(); + return target; + } + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] public partial (string X, string Y) MapAliasedTuple((int X, int Y) source) { @@ -598,5 +606,21 @@ static file class UnsafeAccessor [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_PrivateValue")] public static extern void SetPrivateValue1(this global::Riok.Mapperly.IntegrationTests.Models.TestObject target, int value); + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Field, Name = "intValue")] + public static extern ref int GetIntValue(this global::Riok.Mapperly.IntegrationTests.Models.PrivateCtorObject target); + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Constructor)] + public static extern global::Riok.Mapperly.IntegrationTests.Dto.PrivateCtorDto CreatePrivateCtorDto(int intValue); + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Field, Name = "stringValue")] + public static extern ref string GetStringValue(this global::Riok.Mapperly.IntegrationTests.Models.PrivateCtorObject target); + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Field, Name = "stringValue")] + public static extern ref string GetStringValue1(this global::Riok.Mapperly.IntegrationTests.Dto.PrivateCtorDto target); } } \ No newline at end of file diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource.verified.cs index 4e89086b66..331ab7b0fc 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource.verified.cs @@ -196,7 +196,7 @@ public static partial void MapIdTargetFirst(global::Riok.Mapperly.IntegrationTes } if (testObject.NullableUnflatteningIdValue != null) { - target.NullableUnflattening ??= new(); + target.NullableUnflattening ??= new global::Riok.Mapperly.IntegrationTests.Dto.IdObjectDto(); target.NullableUnflattening.IdValue = DirectInt(testObject.NullableUnflatteningIdValue.Value); } if (testObject.NestedNullable != null) diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs index ed077de2ea..cf707c0373 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs @@ -198,7 +198,7 @@ public static partial void MapIdTargetFirst(global::Riok.Mapperly.IntegrationTes } if (testObject.NullableUnflatteningIdValue != null) { - target.NullableUnflattening ??= new(); + target.NullableUnflattening ??= new global::Riok.Mapperly.IntegrationTests.Dto.IdObjectDto(); target.NullableUnflattening.IdValue = DirectInt(testObject.NullableUnflatteningIdValue.Value); } if (testObject.NestedNullable != null) diff --git a/test/Riok.Mapperly.Tests/Mapping/CtorTest.cs b/test/Riok.Mapperly.Tests/Mapping/CtorTest.cs index 0cda39539d..10aeb8a2f9 100644 --- a/test/Riok.Mapperly.Tests/Mapping/CtorTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/CtorTest.cs @@ -108,4 +108,29 @@ class B """ ); } + + [Fact] + public Task PrivateCtorCustomClass() + { + var source = TestSourceBuilder.Mapping( + "string", + "A", + TestSourceBuilderOptions.WithConstructorVisibility(MemberVisibility.All), + "class A { private A(string x) {} }" + ); + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task PrivateCtorCustomClassWithCustomClassParam() + { + var source = TestSourceBuilder.Mapping( + "A", + "B", + TestSourceBuilderOptions.WithConstructorVisibility(MemberVisibility.All), + "class A;", + "class B { private B(A source) {} }" + ); + return TestHelper.VerifyGenerator(source); + } } diff --git a/test/Riok.Mapperly.Tests/Mapping/DictionaryCustomTest.cs b/test/Riok.Mapperly.Tests/Mapping/DictionaryCustomTest.cs index a64c048c60..075dde3d9a 100644 --- a/test/Riok.Mapperly.Tests/Mapping/DictionaryCustomTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/DictionaryCustomTest.cs @@ -34,7 +34,7 @@ public void DictionaryToCustomDictionary() .HaveSingleMethodBody( """ var target = new global::A(); - target.EnsureCapacity(source.Count + target.Count); + target.EnsureCapacity(source.Count); foreach (var item in source) { target[item.Key] = item.Value; @@ -60,7 +60,7 @@ public void CustomDictionaryToCustomDictionary() """ var target = new global::B(); target.Value = source.Value; - target.EnsureCapacity(source.Count + target.Count); + target.EnsureCapacity(source.Count); foreach (var item in source) { target[item.Key] = item.Value; @@ -87,7 +87,7 @@ public void DictionaryToCustomDictionaryWithObjectFactory() .HaveSingleMethodBody( """ var target = CreateA(); - target.EnsureCapacity(source.Count + target.Count); + target.EnsureCapacity(source.Count); foreach (var item in source) { target[item.Key] = item.Value; @@ -116,7 +116,7 @@ public void CustomDictionaryToCustomDictionaryWithObjectFactory() """ var target = CreateB(); target.Value = source.Value; - target.EnsureCapacity(source.Count + target.Count); + target.EnsureCapacity(source.Count); foreach (var item in source) { target[item.Key] = item.Value; diff --git a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyFlatteningTest.cs b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyFlatteningTest.cs index f6ef73f61f..bf20f64c19 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyFlatteningTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyFlatteningTest.cs @@ -439,7 +439,7 @@ public void ManualUnflattenedPropertyInterpolatedFullNameOfTarget() public void ManualUnflattenedPropertyNullablePath() { var source = TestSourceBuilder.MapperWithBodyAndTypes( - "[MapProperty($\"MyValueId\", \"Value.Id\"), MapProperty($\"MyValueId2\", \"Value.Id2\")] partial B Map(A source);", + "[MapProperty(\"MyValueId\", \"Value.Id\"), MapProperty(\"MyValueId2\", \"Value.Id2\")] partial B Map(A source);", "class A { public string MyValueId { get; set; } public string MyValueId2 { get; set; } }", "class B { public C? Value { get; set; } }", "class C { public string Id { get; set; } public string Id2 { get; set; } }" @@ -451,7 +451,7 @@ public void ManualUnflattenedPropertyNullablePath() .HaveSingleMethodBody( """ var target = new global::B(); - target.Value ??= new(); + target.Value ??= new global::C(); target.Value.Id = source.MyValueId; target.Value.Id2 = source.MyValueId2; return target; @@ -485,7 +485,7 @@ public void ManualUnflattenedPropertyReadOnlyNullablePath() public void ManualUnflattenedPropertyDeepNullablePath() { var source = TestSourceBuilder.MapperWithBodyAndTypes( - "[MapProperty($\"MyValueId\", \"Value.Nested.Id\"), MapProperty($\"MyValueId2\", \"Value.Nested.Id2\")] partial B Map(A source);", + "[MapProperty(\"MyValueId\", \"Value.Nested.Id\"), MapProperty(\"MyValueId2\", \"Value.Nested.Id2\")] partial B Map(A source);", "class A { public string MyValueId { get; set; } public string MyValueId2 { get; set; } }", "class B { public C? Value { get; set; } }", "class C { public D? Nested { get; set; } }", @@ -498,8 +498,44 @@ public void ManualUnflattenedPropertyDeepNullablePath() .HaveSingleMethodBody( """ var target = new global::B(); - target.Value ??= new(); - target.Value.Nested ??= new(); + target.Value ??= new global::C(); + target.Value.Nested ??= new global::D(); + target.Value.Nested.Id = source.MyValueId; + target.Value.Nested.Id2 = source.MyValueId2; + return target; + """ + ); + } + + [Fact] + public void ManualUnflattenedPropertyDeepNullablePathObjectFactory() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [ObjectFactory] + C CreateMyC() => new C(); + + [ObjectFactory] + D CreateMyD() => new D(); + + [MapProperty("MyValueId", "Value.Nested.Id")] + [MapProperty("MyValueId2", "Value.Nested.Id2")] + partial B Map(A source); + """, + "class A { public string MyValueId { get; set; } public string MyValueId2 { get; set; } }", + "class B { public C? Value { get; set; } }", + "class C { public D? Nested { get; set; } }", + "class D { public string Id { get; set; } public string Id2 { get; set; } }" + ); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.Value ??= CreateMyC(); + target.Value.Nested ??= CreateMyD(); target.Value.Nested.Id = source.MyValueId; target.Value.Nested.Id2 = source.MyValueId2; return target; @@ -511,7 +547,7 @@ public void ManualUnflattenedPropertyDeepNullablePath() public Task ManualUnflattenedPropertyNullablePathNoParameterlessCtorShouldDiagnostic() { var source = TestSourceBuilder.MapperWithBodyAndTypes( - "[MapProperty($\"MyValueId\", \"Value.Id\")] private partial B Map(A source);", + "[MapProperty(\"MyValueId\", \"Value.Id\")] private partial B Map(A source);", "class A { public string MyValueId { get; set; } }", "class B { public C? Value { get; set; } }", "class C { public C(string arg) {} public string Id { get; set; } }" @@ -583,10 +619,10 @@ public void ManualNestedPropertyNullablePath() var target = new global::B(); if (source.Value1 != null) { - target.Value2 ??= new(); + target.Value2 ??= new global::E(); if (source.Value1.Value1 != null) { - target.Value2.Value2 ??= new(); + target.Value2.Value2 ??= new global::F(); target.Value2.Value2.Id2 = source.Value1.Value1.Id1; target.Value2.Value2.Id20 = source.Value1.Value1.Id10; } diff --git a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyValueMethodTest.cs b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyValueMethodTest.cs index 99abcacf95..76969470f4 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyValueMethodTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyValueMethodTest.cs @@ -62,10 +62,7 @@ public void MethodToPrivateProperty() [MapValue("GuidValue", Use = nameof(NewGuid))] partial B Map(A source); Guid NewGuid() => Guid.NewGuid(); """, - TestSourceBuilderOptions.Default with - { - IncludedMembers = MemberVisibility.Private - }, + TestSourceBuilderOptions.WithMemberVisibility(MemberVisibility.Private), "class A;", "class B { private Guid GuidValue { get; set; } }" ); @@ -90,10 +87,7 @@ public void MethodToNestedPrivateProperty() [MapValue("Nested.Value", Use = nameof(NewGuid))] partial B Map(A source); Guid NewGuid() => Guid.NewGuid(); """, - TestSourceBuilderOptions.Default with - { - IncludedMembers = MemberVisibility.All - }, + TestSourceBuilderOptions.WithMemberVisibility(MemberVisibility.All), "class A;", "class B { private C Nested { get; set; } }", "class C { public Guid Value { get; set; } }" diff --git a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyValueTest.cs b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyValueTest.cs index addecad59f..a5781d9eff 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyValueTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyValueTest.cs @@ -107,7 +107,7 @@ public void StringToNestedNullableProperty() .HaveSingleMethodBody( """ var target = new global::B(); - target.Nested ??= new(); + target.Nested ??= new global::C(); target.Nested.Value = "fooBar"; return target; """ diff --git a/test/Riok.Mapperly.Tests/Mapping/ReferenceHandlingTest.cs b/test/Riok.Mapperly.Tests/Mapping/ReferenceHandlingTest.cs index 352cba347b..0a4a0f3fba 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ReferenceHandlingTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ReferenceHandlingTest.cs @@ -89,7 +89,10 @@ public Task ExistingInstanceShouldWork() public Task ObjectFactoryShouldWork() { var source = TestSourceBuilder.MapperWithBodyAndTypes( - "[ObjectFactory] B CreateB() => new B();" + "private partial B Map(A a);", + """ + [ObjectFactory] B CreateB() => new B(); + private partial B Map(A a); + """, TestSourceBuilderOptions.WithReferenceHandling, "class A { public A Parent { get; set; } public C Value { get; set; } }", "class B { public B Parent { get; set; } public D Value { get; set; } }", diff --git a/test/Riok.Mapperly.Tests/Mapping/UnsafeAccessorTest.cs b/test/Riok.Mapperly.Tests/Mapping/UnsafeAccessorTest.cs index d66d09aa52..98f3c551e3 100644 --- a/test/Riok.Mapperly.Tests/Mapping/UnsafeAccessorTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/UnsafeAccessorTest.cs @@ -132,7 +132,10 @@ public void ManualUnflattenedPropertyNullablePath() .HaveMapMethodBody( """ var target = new global::B(); - target.SetValue(target.GetValue() ?? new()); + if (target.GetValue() == null) + { + target.SetValue(new global::C()); + } target.GetValue().Id = source.MyValueId; target.GetValue().Id2 = source.MyValueId2; return target; @@ -140,6 +143,63 @@ public void ManualUnflattenedPropertyNullablePath() ); } + [Fact] + public void ManualUnflattenedPropertyNullablePathWithWithObjectFactory() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [ObjectFactory] + C CreateMyC() => new C(); + + [MapProperty("MyValueId", "Value.Id")] + [MapProperty("MyValueId2", "Value.Id2")] + partial B Map(A source); + """, + TestSourceBuilderOptions.WithMemberVisibility(MemberVisibility.All), + "class A { public string MyValueId { get; set; } public string MyValueId2 { get; set; } }", + "class B { private C? Value { get; set; } }", + "class C { public string Id { get; set; } public string Id2 { get; set; } }" + ); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveMapMethodBody( + """ + var target = new global::B(); + if (target.GetValue() == null) + { + target.SetValue(CreateMyC()); + } + target.GetValue().Id = source.MyValueId; + target.GetValue().Id2 = source.MyValueId2; + return target; + """ + ); + } + + [Fact] + public Task ManualUnflattenedPropertyNullablePathWithPrivateConstructor() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapProperty("MyValueId", "Value.Id")] + [MapProperty("MyValueId2", "Value.Id2")] + partial B Map(A source); + """, + TestSourceBuilderOptions.Default with + { + IncludedMembers = MemberVisibility.All, + IncludedConstructors = MemberVisibility.All + }, + "class A { public string MyValueId { get; set; } public string MyValueId2 { get; set; } }", + "class B { private C? Value { get; set; } }", + "class C { private C() {} public string Id { get; set; } public string Id2 { get; set; } }" + ); + + return TestHelper.VerifyGenerator(source); + } + [Fact] public void PropertyWithPrivateSetter() { @@ -212,7 +272,7 @@ public void ManualUnflattenedFieldNullablePath() .HaveMapMethodBody( """ var target = new global::B(); - target.GetValue() ??= new(); + target.GetValue() ??= new global::C(); target.GetValue().Id = source.MyValueId; target.GetValue().Id2 = source.MyValueId2; return target; @@ -220,6 +280,28 @@ public void ManualUnflattenedFieldNullablePath() ); } + [Fact] + public Task ManualUnflattenedFieldNullablePathPrivateConstructor() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapProperty("MyValueId", "Value.Id")] + [MapProperty("MyValueId2", "Value.Id2")] + partial B Map(A source); + """, + TestSourceBuilderOptions.Default with + { + IncludedMembers = MemberVisibility.All, + IncludedConstructors = MemberVisibility.All, + }, + "class A { public string MyValueId { get; set; } public string MyValueId2 { get; set; } }", + "class B { private C? Value }", + "class C { private C() {} public string Id { get; set; } public string Id2 { get; set; } }" + ); + + return TestHelper.VerifyGenerator(source); + } + [Fact] public Task GeneratedMethodShouldNotHaveConflictingName() { @@ -398,6 +480,36 @@ public void MemberVisibilityPublic() ); } + [Fact] + public Task PrivateConstructor() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "partial B Map(A source);", + TestSourceBuilderOptions.WithConstructorVisibility(MemberVisibility.All), + "class A { public int IntValue { get; set; } private string _stringValue { get; set; } }", + "class B { private B(int intValue) {} private string _stringValue { get; set; } }" + ); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task PrivateConstructorAndMember() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "partial B Map(A source);", + TestSourceBuilderOptions.Default with + { + IncludedMembers = MemberVisibility.All, + IncludedConstructors = MemberVisibility.All, + }, + "class A { private int _intValue { get; set; } private string _stringValue { get; set; } }", + "class B { private B(int _intValue) {} private string _stringValue { get; set; } }" + ); + + return TestHelper.VerifyGenerator(source); + } + [Fact] public void MemberVisibilityAllWithoutPublic() { @@ -447,7 +559,7 @@ class B { private int value { get; set; } } } [Fact] - public Task statAttributeShouldOverrideAssemblyDefault() + public Task ClassAttributeShouldOverrideAssemblyDefault() { var source = TestSourceBuilder.CSharp( """ diff --git a/test/Riok.Mapperly.Tests/TestSourceBuilder.cs b/test/Riok.Mapperly.Tests/TestSourceBuilder.cs index 9d393a31c6..ed94b083fd 100644 --- a/test/Riok.Mapperly.Tests/TestSourceBuilder.cs +++ b/test/Riok.Mapperly.Tests/TestSourceBuilder.cs @@ -95,6 +95,7 @@ private static string BuildAttribute(TestSourceBuilderOptions options) Attribute(options.IgnoreObsoleteMembersStrategy), Attribute(options.RequiredMappingStrategy), Attribute(options.IncludedMembers), + Attribute(options.IncludedConstructors), Attribute(options.PreferParameterlessConstructors), Attribute(options.AutoUserMappings), }.WhereNotNull(); diff --git a/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs b/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs index 43f6431b67..758b1e202c 100644 --- a/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs +++ b/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs @@ -18,6 +18,7 @@ public record TestSourceBuilderOptions( IgnoreObsoleteMembersStrategy? IgnoreObsoleteMembersStrategy = null, RequiredMappingStrategy? RequiredMappingStrategy = null, MemberVisibility? IncludedMembers = null, + MemberVisibility? IncludedConstructors = null, bool Static = false, bool PreferParameterlessConstructors = true, bool AutoUserMappings = true @@ -44,6 +45,9 @@ public static TestSourceBuilderOptions WithRequiredMappingStrategy(RequiredMappi public static TestSourceBuilderOptions WithMemberVisibility(MemberVisibility memberVisibility) => new(IncludedMembers: memberVisibility); + public static TestSourceBuilderOptions WithConstructorVisibility(MemberVisibility memberVisibility) => + new(IncludedConstructors: memberVisibility); + public static TestSourceBuilderOptions WithDisabledMappingConversion(params MappingConversionType[] conversionTypes) { var enabled = MappingConversionType.All; diff --git a/test/Riok.Mapperly.Tests/_snapshots/CtorTest.PrivateCtorCustomClass#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/CtorTest.PrivateCtorCustomClass#Mapper.g.verified.cs new file mode 100644 index 0000000000..1f44f47d2d --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/CtorTest.PrivateCtorCustomClass#Mapper.g.verified.cs @@ -0,0 +1,19 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial global::A Map(string source) + { + return UnsafeAccessor.CreateA(source); + } +} + +[global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] +static file class UnsafeAccessor +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Constructor)] + public static extern global::A CreateA(string x); +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/CtorTest.PrivateCtorCustomClassWithCustomClassParam#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/CtorTest.PrivateCtorCustomClassWithCustomClassParam#Mapper.g.verified.cs new file mode 100644 index 0000000000..f79526470c --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/CtorTest.PrivateCtorCustomClassWithCustomClassParam#Mapper.g.verified.cs @@ -0,0 +1,19 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial global::B Map(global::A source) + { + return UnsafeAccessor.CreateB(source); + } +} + +[global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] +static file class UnsafeAccessor +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Constructor)] + public static extern global::B CreateB(global::A source); +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/DictionaryCustomTest.CustomDictionaryWithList#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/DictionaryCustomTest.CustomDictionaryWithList#Mapper.g.verified.cs index ab244d17d0..0c4365e227 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/DictionaryCustomTest.CustomDictionaryWithList#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/DictionaryCustomTest.CustomDictionaryWithList#Mapper.g.verified.cs @@ -7,7 +7,7 @@ public partial class Mapper private partial global::B Map(global::A source) { var target = new global::B(); - target.EnsureCapacity(source.Count + target.Count); + target.EnsureCapacity(source.Count); foreach (var item in source) { target[item.Key] = MapToListOfD(item.Value); diff --git a/test/Riok.Mapperly.Tests/_snapshots/DictionaryCustomTest.DictionaryToCustomDictionaryWithPrivateCtorShouldDiagnostic#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/DictionaryCustomTest.DictionaryToCustomDictionaryWithPrivateCtorShouldDiagnostic#Mapper.g.verified.cs index 76efbe6c2f..b2eeb67988 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/DictionaryCustomTest.DictionaryToCustomDictionaryWithPrivateCtorShouldDiagnostic#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/DictionaryCustomTest.DictionaryToCustomDictionaryWithPrivateCtorShouldDiagnostic#Mapper.g.verified.cs @@ -6,7 +6,12 @@ public partial class Mapper [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] private partial global::A Map(global::System.Collections.Generic.IDictionary source) { - // Could not generate mapping - throw new System.NotImplementedException(); + var target = new global::A(); + target.EnsureCapacity(source.Count); + foreach (var item in source) + { + target[item.Key] = item.Value; + } + return target; } } \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/DictionaryCustomTest.DictionaryToCustomDictionaryWithPrivateCtorShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/DictionaryCustomTest.DictionaryToCustomDictionaryWithPrivateCtorShouldDiagnostic.verified.txt index c08fa1c5f2..4cc99f1d01 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/DictionaryCustomTest.DictionaryToCustomDictionaryWithPrivateCtorShouldDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/DictionaryCustomTest.DictionaryToCustomDictionaryWithPrivateCtorShouldDiagnostic.verified.txt @@ -1,23 +1,13 @@ { Diagnostics: [ { - Id: RMG002, - Title: No accessible parameterless constructor found, + Id: RMG013, + Title: No accessible constructor with mappable arguments found, Severity: Error, WarningLevel: 0, Location: : (11,4)-(11,59), - MessageFormat: {0} has no accessible parameterless constructor, - Message: A has no accessible parameterless constructor, - Category: Mapper - }, - { - Id: RMG008, - Title: Could not create mapping, - Severity: Error, - WarningLevel: 0, - Location: : (11,4)-(11,59), - MessageFormat: Could not create mapping from {0} to {1}. Consider implementing the mapping manually., - Message: Could not create mapping from System.Collections.Generic.IDictionary to A. Consider implementing the mapping manually., + MessageFormat: {0} has no accessible constructor with mappable arguments, + Message: A has no accessible constructor with mappable arguments, Category: Mapper } ] diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyFlatteningTest.ManualUnflattenedPropertyNullablePathNoParameterlessCtorShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyFlatteningTest.ManualUnflattenedPropertyNullablePathNoParameterlessCtorShouldDiagnostic.verified.txt index f220cca154..f2556c605e 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyFlatteningTest.ManualUnflattenedPropertyNullablePathNoParameterlessCtorShouldDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyFlatteningTest.ManualUnflattenedPropertyNullablePathNoParameterlessCtorShouldDiagnostic.verified.txt @@ -5,9 +5,9 @@ Title: No accessible parameterless constructor found, Severity: Error, WarningLevel: 0, - Location: : (11,4)-(11,76), + Location: : (11,4)-(11,75), MessageFormat: {0} has no accessible parameterless constructor, - Message: C? has no accessible parameterless constructor, + Message: C has no accessible parameterless constructor, Category: Mapper } ] diff --git a/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.statAttributeShouldOverrideAssemblyDefault#MyMapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.ClassAttributeShouldOverrideAssemblyDefault#MyMapper.g.verified.cs similarity index 91% rename from test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.statAttributeShouldOverrideAssemblyDefault#MyMapper.g.verified.cs rename to test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.ClassAttributeShouldOverrideAssemblyDefault#MyMapper.g.verified.cs index a62aa0f230..0dbc7c4312 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.statAttributeShouldOverrideAssemblyDefault#MyMapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.ClassAttributeShouldOverrideAssemblyDefault#MyMapper.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: MyMapper.g.cs +//HintName: MyMapper.g.cs // #nullable enable public partial class MyMapper diff --git a/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.ManualUnflattenedFieldNullablePathPrivateConstructor#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.ManualUnflattenedFieldNullablePathPrivateConstructor#Mapper.g.verified.cs new file mode 100644 index 0000000000..41bd27eb9a --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.ManualUnflattenedFieldNullablePathPrivateConstructor#Mapper.g.verified.cs @@ -0,0 +1,27 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + partial global::B Map(global::A source) + { + var target = new global::B(); + target.GetValue() ??= UnsafeAccessor.CreateC(); + target.GetValue().Id = source.MyValueId; + target.GetValue().Id2 = source.MyValueId2; + return target; + } +} + +[global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] +static file class UnsafeAccessor +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Field, Name = "Value")] + public static extern ref global::C? GetValue(this global::B target); + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Constructor)] + public static extern global::C CreateC(); +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.ManualUnflattenedPropertyNullablePathWithPrivateConstructor#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.ManualUnflattenedPropertyNullablePathWithPrivateConstructor#Mapper.g.verified.cs new file mode 100644 index 0000000000..9a16ad6ff6 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.ManualUnflattenedPropertyNullablePathWithPrivateConstructor#Mapper.g.verified.cs @@ -0,0 +1,34 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + partial global::B Map(global::A source) + { + var target = new global::B(); + if (target.GetValue() == null) + { + target.SetValue(UnsafeAccessor.CreateC()); + } + target.GetValue().Id = source.MyValueId; + target.GetValue().Id2 = source.MyValueId2; + return target; + } +} + +[global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] +static file class UnsafeAccessor +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_Value")] + public static extern global::C? GetValue(this global::B source); + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Constructor)] + public static extern global::C CreateC(); + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_Value")] + public static extern void SetValue(this global::B target, global::C? value); +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivateConstructor#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivateConstructor#Mapper.g.verified.cs new file mode 100644 index 0000000000..4777cd0031 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivateConstructor#Mapper.g.verified.cs @@ -0,0 +1,20 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + partial global::B Map(global::A source) + { + var target = UnsafeAccessor.CreateB(source.IntValue); + return target; + } +} + +[global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] +static file class UnsafeAccessor +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Constructor)] + public static extern global::B CreateB(int intValue); +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivateConstructorAndMember#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivateConstructorAndMember#Mapper.g.verified.cs new file mode 100644 index 0000000000..c7bb60c14f --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivateConstructorAndMember#Mapper.g.verified.cs @@ -0,0 +1,33 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + partial global::B Map(global::A source) + { + var target = UnsafeAccessor.CreateB(source.GetIntValue()); + target.SetStringValue(source.GetStringValue()); + return target; + } +} + +[global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] +static file class UnsafeAccessor +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get__intValue")] + public static extern int GetIntValue(this global::A source); + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Constructor)] + public static extern global::B CreateB(int _intValue); + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get__stringValue")] + public static extern string GetStringValue(this global::A source); + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set__stringValue")] + public static extern void SetStringValue(this global::B target, string value); +} \ No newline at end of file