diff --git a/docs/docs/configuration/private-member-mapping.md b/docs/docs/configuration/private-member-mapping.md deleted file mode 100644 index 1d456db6f70..00000000000 --- 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 00000000000..65bc4e15e38 --- /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 3a129163bc3..84a269ee338 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 3b12015a40a..abdaf0953c0 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 a82b9a80dc6..f2b24c7ec88 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 d85911f0163..ee10b1db418 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 dade4e43986..b99bd14d9dd 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 00000000000..1e3cf1a5d9b --- /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 00000000000..121c28de82c --- /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 00000000000..340949c4141 --- /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 00000000000..65d90c85c6b --- /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 5731e0e9b56..442f732c6a3 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 f3b9d3aedc1..2c6aac98c19 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 8cc63fda717..7eb634a27a7 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 50e8a3b1f3a..66ee1f98e37 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 a539996c0ca..2345b3bb54c 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 a3c5412eedd..a16135ce326 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 51fad242108..c5a8cf89a5f 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 e6f3afcd7b7..002dfda956e 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 d36cd069071..cb9520a3d1a 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 98e1b49d004..a0d0fc1ae32 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 e5fa0ae49f3..9be80744ffd 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 95fb48ee0db..e3c5fef4560 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 76c2a38b61b..60be3e9d05e 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 5f4bbd92c27..22b401bde5b 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 c14c665ad57..0a3d00b8df3 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 1773dba2961..0b99e43f79d 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 4f6da827b01..a0e8cd2aa04 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 21af2ed5998..00000000000 --- 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 2e33778f32a..72bcc84fa6c 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 e247e80247c..00000000000 --- 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 26e1e8ef2c4..2b161d7c88e 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 713d5a88f0c..65b619c795e 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 3de286b0886..ea38744d0ac 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 defe1891f94..00000000000 --- 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 5c1b32fabf8..67c7ff82daf 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 fc98495cfe3..a635550ff85 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 a86e366525d..a34abb4022b 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 1b541f9567e..a3f58d8cdbc 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 29a9d82c814..a1a1f1a199d 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 3da7353c59f..1369a82fe0b 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 5afe53edb4a..94e72800738 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 a863645abec..b6446bf32d0 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 00000000000..041e586c2a4 --- /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 6b6927271de..818da3f61b6 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 f9635f33f7e..121a0b5a66d 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 0960a066467..10cb64ea0ad 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 acfe6402db1..e9a2e716649 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 00000000000..3f39cfb854b --- /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 e81c60a5826..518db4ea4e5 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 eeec430e252..829774c6764 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 e1fc13c903b..4983f5377d6 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 7f0f89eb0fc..d109b818fad 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 6e748d85184..16eb67a3d0f 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 5dfe848f12f..59e4f9189a8 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 1c293ceec2d..3a65a571aa0 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 4be10c4469b..ff1ad75043e 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 e52c2b1bfa5..e9ccd5bd07a 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 282603f76b1..79da441bad4 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 00000000000..af0dabd7212 --- /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 ff9423ae6e1..fa3c552f9e0 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 b500f32ec9b..a116673b745 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 00000000000..857f079c89f --- /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 7da7d8c5bb5..c1608efdf03 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 fd552eb6e5e..d7847e7830a 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 a8f16200af8..bf84bdd8b1d 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 4e89086b66f..331ab7b0fcb 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 ed077de2ead..cf707c03730 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 0cda39539d0..10aeb8a2f9d 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 a64c048c609..075dde3d9a4 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 f6ef73f61fa..bf20f64c19d 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 99abcacf95e..76969470f4d 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 addecad59fd..a5781d9eff3 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 352cba347be..0a4a0f3fbaa 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 d66d09aa52b..98f3c551e3f 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 9d393a31c62..ed94b083fd0 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 43f6431b67d..758b1e202c6 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 00000000000..1f44f47d2d6 --- /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 00000000000..f79526470c8 --- /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 ab244d17d07..0c4365e227f 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 76efbe6c2f9..b2eeb679883 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 c08fa1c5f25..4cc99f1d01c 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 f220cca154b..f2556c605ef 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 a62aa0f2304..0dbc7c43126 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 00000000000..41bd27eb9a6 --- /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 00000000000..9a16ad6ff6e --- /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 00000000000..4777cd0031b --- /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 00000000000..c7bb60c14fc --- /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