From fd18a62ee6d9a2c7b0b1c4f46c3477a32ac9c696 Mon Sep 17 00:00:00 2001 From: Lars Date: Wed, 11 Jan 2023 19:41:18 +0100 Subject: [PATCH] feat: support reference loops (#218) --- Riok.Mapperly.sln | 2 +- .../09-void-mapping-methods.md | 2 + .../02-configuration/10-reference-handling.md | 40 +++ .../{10-conversions.md => 11-conversions.md} | 0 ...ostics.mdx => 12-analyzer-diagnostics.mdx} | 0 ...ted-source.mdx => 13-generated-source.mdx} | 0 .../MapperAttribute.cs | 10 + .../PublicAPI.Shipped.txt | 11 + .../ReferenceHandling/IReferenceHandler.cs | 38 +++ .../Internal/PreserveReferenceHandler.cs | 64 +++++ .../Internal/ReferenceEqualityComparer.cs | 27 ++ .../ReferenceHandlerAttribute.cs | 10 + src/Riok.Mapperly/AnalyzerReleases.Shipped.md | 2 + .../Descriptors/DescriptorBuilder.cs | 90 +++---- .../Descriptors/MapperDescriptor.cs | 4 +- .../DictionaryMappingBuilder.cs | 23 +- .../EnumerableMappingBuilder.cs | 26 +- ...NewInstanceObjectPropertyMappingBuilder.cs | 13 +- .../MappingBuilder/NullableMappingBuilder.cs | 2 +- .../UserMethodMappingBuilder.cs | 147 ++++++++--- .../Descriptors/MappingBuilderContext.cs | 24 +- .../Descriptors/MappingCollection.cs | 90 +++++++ .../Descriptors/Mappings/ArrayCloneMapping.cs | 4 +- .../Descriptors/Mappings/ArrayForMapping.cs | 13 +- .../Descriptors/Mappings/CastMapping.cs | 11 +- .../Descriptors/Mappings/CtorMapping.cs | 7 +- .../Mappings/DirectAssignmentMapping.cs | 4 +- .../Mappings/EnumFromStringMapping.cs | 6 +- .../Descriptors/Mappings/EnumNameMapping.cs | 6 +- .../Mappings/EnumToStringMapping.cs | 6 +- .../Mappings/ForEachAddDictionaryMapping.cs | 20 +- .../Mappings/ForEachAddEnumerableMapping.cs | 12 +- .../Descriptors/Mappings/ITypeMapping.cs | 27 ++ .../Descriptors/Mappings/IUserMapping.cs | 2 +- .../Mappings/LinqEnumerableMapping.cs | 12 +- .../Descriptors/Mappings/MethodMapping.cs | 64 ++--- ...NewInstanceObjectFactoryPropertyMapping.cs | 29 ++- .../NewInstanceObjectPropertyMapping.cs | 33 ++- .../Mappings/NullDelegateMapping.cs | 22 +- .../Mappings/NullDelegateMethodMapping.cs | 6 +- .../Mappings/ObjectPropertyMapping.cs | 13 +- .../ConstructorParameterMapping.cs | 4 +- .../IPropertyAssignmentMapping.cs | 2 +- .../PropertyMappings/IPropertyMapping.cs | 2 +- .../PropertyMappings/NullPropertyMapping.cs | 21 +- .../PropertyAssignmentMapping.cs | 8 +- .../PropertyMappings/PropertyMapping.cs | 11 +- ...ropertyNullAssignmentInitializerMapping.cs | 2 +- .../PropertyNullDelegateAssignmentMapping.cs | 6 +- .../Mappings/SourceObjectMethodMapping.cs | 4 +- .../Mappings/StaticMethodMapping.cs | 4 +- .../Descriptors/Mappings/TypeMapping.cs | 17 +- .../Mappings/TypeMappingBuildContext.cs | 34 +++ ...serDefinedExistingInstanceMethodMapping.cs | 61 +++-- .../UserDefinedNewInstanceMethodMapping.cs | 57 ++++- .../Mappings/UserImplementedMethodMapping.cs | 21 +- .../ObjectFactories/ObjectFactoryBuilder.cs | 5 +- .../ObjectFactories/SimpleObjectFactory.cs | 2 +- .../SimpleMappingBuilderContext.cs | 7 +- .../Descriptors/WellKnownTypes.cs | 53 ++++ .../Diagnostics/DiagnosticDescriptors.cs | 19 +- .../ReferenceHandlingSyntaxFactoryHelper.cs | 54 ++++ .../Emit/Symbols/MappingMethodParameters.cs | 20 ++ .../Emit/Symbols/MethodArgument.cs | 19 ++ .../Emit/Symbols/MethodParameter.cs | 32 +++ src/Riok.Mapperly/Emit/SyntaxFactoryHelper.cs | 43 +++- .../Helpers/EnumerableExtensions.cs | 8 + src/Riok.Mapperly/Helpers/SymbolExtensions.cs | 3 + .../MapPropertyAttributeTest.cs | 2 +- .../Internal/PreserveReferenceHandlerTest.cs | 41 +++ .../Internal/ReferenceEqualityComparerTest.cs | 54 ++++ .../Riok.Mapperly.Abstractions.Tests.csproj} | 0 .../CircularReferenceMapperTest.cs | 37 +++ .../Dto/CircularReferenceDto.cs | 9 + .../Mapper/CircularReferenceMapper.cs | 12 + .../Models/CircularReferenceObject.cs | 9 + ...erTest.SnapshotGeneratedSource.verified.cs | 26 ++ ...erTest.SnapshotGeneratedSource.verified.cs | 236 +++++++++--------- ...erTest.SnapshotGeneratedSource.verified.cs | 234 ++++++++--------- .../Descriptors/MethodNameBuilderTest.cs | 2 +- test/Riok.Mapperly.Tests/DiagnosticMatcher.cs | 15 +- test/Riok.Mapperly.Tests/GeneratedMethod.cs | 31 +++ .../Helpers/EnumerableExtensionsTest.cs | 9 + .../MapperGenerationResult.cs | 4 +- .../MapperGenerationResultAssertions.cs | 33 ++- .../Mapping/ObjectPropertyInitPropertyTest.cs | 17 +- .../Mapping/ReferenceHandlingTest.cs | 231 +++++++++++++++++ .../Mapping/UserMethodTest.cs | 18 +- test/Riok.Mapperly.Tests/StringSyntax.cs | 6 + test/Riok.Mapperly.Tests/TestHelper.cs | 17 +- test/Riok.Mapperly.Tests/TestSourceBuilder.cs | 31 +-- .../TestSourceBuilderOptions.cs | 5 +- ...thPrivateCtorShouldDiagnostic.verified.txt | 6 +- ...OtherEnumByNameWithoutOverlap.verified.txt | 4 +- ...ccessibleCtorShouldDiagnostic.verified.txt | 4 +- ...ssPrivateCtorShouldDiagnostic.verified.txt | 2 +- ...ldDiagnosticAndUseAlternative.verified.txt | 2 +- ...UnmatchedCtorShouldDiagnostic.verified.txt | 4 +- ...ourcePropertyShouldDiagnostic.verified.txt | 2 +- ...meterlessCtorShouldDiagnostic.verified.txt | 2 +- ...pertyNotFoundShouldDiagnostic.verified.txt | 4 +- ...pertyNotFoundShouldDiagnostic.verified.txt | 4 +- ...PathWriteOnlyShouldDiagnostic.verified.txt | 4 +- ...iagnosticOnVoidMethod#Mapper.g.verified.cs | 8 - ...yShouldDiagnosticOnVoidMethod.verified.txt | 28 --- ...FoundShouldDiagnostic#Mapper.g.verified.cs | 11 - ...ourceNotFoundShouldDiagnostic.verified.txt | 28 --- ...ourcePropertyShouldDiagnostic.verified.txt | 4 +- ...onfigurationsShouldDiagnostic.verified.txt | 4 +- ...onfigurationsShouldDiagnostic.verified.txt | 4 +- ...ourceNotFoundShouldDiagnostic.verified.txt | 4 +- ...ropertyOnTargetWithDiagnostic.verified.txt | 4 +- ...opertyOnSourceWithDiagnostics.verified.txt | 4 +- ...dIgnoreAndGenerateDiagnostics.verified.txt | 2 +- ...dIgnoreAndGenerateDiagnostics.verified.txt | 2 +- ...ourcePropertyShouldDiagnostic.verified.txt | 4 +- ...argetPropertyShouldDiagnostic.verified.txt | 6 +- ...ourcePropertyShouldDiagnostic.verified.txt | 4 +- ...ibutePropertyShouldDiagnostic.verified.txt | 2 +- ...ourcePropertyShouldDiagnostic.verified.txt | 2 +- ...argetPropertyShouldDiagnostic.verified.txt | 2 +- ...dIgnoreAndGenerateDiagnostics.verified.txt | 2 +- ...tterShouldIgnoreAndDiagnostic.verified.txt | 4 +- ...tterShouldIgnoreAndDiagnostic.verified.txt | 4 +- ...tterShouldIgnoreAndDiagnostic.verified.txt | 4 +- ...tterShouldIgnoreAndDiagnostic.verified.txt | 4 +- ...eMappingStrategyCaseSensitive.verified.txt | 4 +- ...pablePropertyShouldDiagnostic.verified.txt | 4 +- ...tchedPropertyShouldDiagnostic.verified.txt | 4 +- ...gTest.ArrayShouldWork#Mapper.g.verified.cs | 41 +++ ...lerAtIndex0ShouldWork#Mapper.g.verified.cs | 25 ++ ...ingInstanceShouldWork#Mapper.g.verified.cs | 31 +++ ...stomHandlerShouldWork#Mapper.g.verified.cs | 25 ++ ...ingInstanceShouldWork#Mapper.g.verified.cs | 31 +++ ...jectFactoryShouldWork#Mapper.g.verified.cs | 25 ++ ....EnumerableShouldWork#Mapper.g.verified.cs | 30 +++ ...ingInstanceShouldWork#Mapper.g.verified.cs | 32 +++ ...dPropertiesShouldWork#Mapper.g.verified.cs | 46 ++++ ...llyMappedPropertiesShouldWork.verified.txt | 28 +++ ...jectFactoryShouldWork#Mapper.g.verified.cs | 30 +++ ...ndlingTest.ShouldWork#Mapper.g.verified.cs | 30 +++ ...enceHandlerShouldWork#Mapper.g.verified.cs | 30 +++ ...sReferenceHandlerTODO#Mapper.g.verified.cs | 0 ...enceHandlerShouldWork#Mapper.g.verified.cs | 30 +++ ...ericSignatureShouldDiagnostic.verified.txt | 16 -- ...alidSignatureShouldDiagnostic.verified.txt | 16 -- 146 files changed, 2392 insertions(+), 785 deletions(-) create mode 100644 docs/docs/02-configuration/10-reference-handling.md rename docs/docs/02-configuration/{10-conversions.md => 11-conversions.md} (100%) rename docs/docs/02-configuration/{11-analyzer-diagnostics.mdx => 12-analyzer-diagnostics.mdx} (100%) rename docs/docs/02-configuration/{12-generated-source.mdx => 13-generated-source.mdx} (100%) create mode 100644 src/Riok.Mapperly.Abstractions/ReferenceHandling/IReferenceHandler.cs create mode 100644 src/Riok.Mapperly.Abstractions/ReferenceHandling/Internal/PreserveReferenceHandler.cs create mode 100644 src/Riok.Mapperly.Abstractions/ReferenceHandling/Internal/ReferenceEqualityComparer.cs create mode 100644 src/Riok.Mapperly.Abstractions/ReferenceHandling/ReferenceHandlerAttribute.cs create mode 100644 src/Riok.Mapperly/Descriptors/MappingCollection.cs create mode 100644 src/Riok.Mapperly/Descriptors/Mappings/ITypeMapping.cs create mode 100644 src/Riok.Mapperly/Descriptors/Mappings/TypeMappingBuildContext.cs create mode 100644 src/Riok.Mapperly/Descriptors/WellKnownTypes.cs create mode 100644 src/Riok.Mapperly/Emit/ReferenceHandlingSyntaxFactoryHelper.cs create mode 100644 src/Riok.Mapperly/Emit/Symbols/MappingMethodParameters.cs create mode 100644 src/Riok.Mapperly/Emit/Symbols/MethodArgument.cs create mode 100644 src/Riok.Mapperly/Emit/Symbols/MethodParameter.cs rename test/{Riok.Mapperly.Abstractions.Test => Riok.Mapperly.Abstractions.Tests}/MapPropertyAttributeTest.cs (91%) create mode 100644 test/Riok.Mapperly.Abstractions.Tests/ReferenceHandling/Internal/PreserveReferenceHandlerTest.cs create mode 100644 test/Riok.Mapperly.Abstractions.Tests/ReferenceHandling/Internal/ReferenceEqualityComparerTest.cs rename test/{Riok.Mapperly.Abstractions.Test/Riok.Mapperly.Abstractions.Test.csproj => Riok.Mapperly.Abstractions.Tests/Riok.Mapperly.Abstractions.Tests.csproj} (100%) create mode 100644 test/Riok.Mapperly.IntegrationTests/CircularReferenceMapperTest.cs create mode 100644 test/Riok.Mapperly.IntegrationTests/Dto/CircularReferenceDto.cs create mode 100644 test/Riok.Mapperly.IntegrationTests/Mapper/CircularReferenceMapper.cs create mode 100644 test/Riok.Mapperly.IntegrationTests/Models/CircularReferenceObject.cs create mode 100644 test/Riok.Mapperly.IntegrationTests/_snapshots/CircularReferenceMapperTest.SnapshotGeneratedSource.verified.cs create mode 100644 test/Riok.Mapperly.Tests/GeneratedMethod.cs create mode 100644 test/Riok.Mapperly.Tests/Mapping/ReferenceHandlingTest.cs create mode 100644 test/Riok.Mapperly.Tests/StringSyntax.cs delete mode 100644 test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertyShouldDiagnosticOnVoidMethod#Mapper.g.verified.cs delete mode 100644 test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertyShouldDiagnosticOnVoidMethod.verified.txt delete mode 100644 test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertySourceNotFoundShouldDiagnostic#Mapper.g.verified.cs delete mode 100644 test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertySourceNotFoundShouldDiagnostic.verified.txt create mode 100644 test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ArrayShouldWork#Mapper.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.CustomHandlerAtIndex0ShouldWork#Mapper.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.CustomHandlerAtIndex1WithExistingInstanceShouldWork#Mapper.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.CustomHandlerShouldWork#Mapper.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.CustomHandlerWithExistingInstanceShouldWork#Mapper.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.CustomHandlerWithObjectFactoryShouldWork#Mapper.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.EnumerableShouldWork#Mapper.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ExistingInstanceShouldWork#Mapper.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ManuallyMappedPropertiesShouldWork#Mapper.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ManuallyMappedPropertiesShouldWork.verified.txt create mode 100644 test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ObjectFactoryShouldWork#Mapper.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ShouldWork#Mapper.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.UserImplementedWithReferenceHandlerShouldWork#Mapper.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.UserImplementedWithoutReferenceHandlerButNestedRequiresReferenceHandlerTODO#Mapper.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.UserImplementedWithoutReferenceHandlerShouldWork#Mapper.g.verified.cs delete mode 100644 test/Riok.Mapperly.Tests/_snapshots/UserMethodTest.WithInvalidGenericSignatureShouldDiagnostic.verified.txt delete mode 100644 test/Riok.Mapperly.Tests/_snapshots/UserMethodTest.WithInvalidSignatureShouldDiagnostic.verified.txt diff --git a/Riok.Mapperly.sln b/Riok.Mapperly.sln index f52f25c37b..4ad9377ba8 100644 --- a/Riok.Mapperly.sln +++ b/Riok.Mapperly.sln @@ -15,7 +15,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Riok.Mapperly.Abstractions" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Riok.Mapperly.Tests", "test\Riok.Mapperly.Tests\Riok.Mapperly.Tests.csproj", "{284E2122-CE48-4A5A-A045-3A3F941DA5C3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Riok.Mapperly.Abstractions.Test", "test\Riok.Mapperly.Abstractions.Test\Riok.Mapperly.Abstractions.Test.csproj", "{C3C40A0A-168F-4A66-B9F9-FC80D2F26306}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Riok.Mapperly.Abstractions.Tests", "test\Riok.Mapperly.Abstractions.Tests\Riok.Mapperly.Abstractions.Tests.csproj", "{C3C40A0A-168F-4A66-B9F9-FC80D2F26306}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/docs/docs/02-configuration/09-void-mapping-methods.md b/docs/docs/02-configuration/09-void-mapping-methods.md index f03e0939eb..138fea3cb4 100644 --- a/docs/docs/02-configuration/09-void-mapping-methods.md +++ b/docs/docs/02-configuration/09-void-mapping-methods.md @@ -6,7 +6,9 @@ If an existing object instance should be used as target, you can define the mapp [Mapper] public partial class CarMapper { + // highlight-start public partial void CarToCarDto(Car car, CarDto dto); + // highlight-end } ``` diff --git a/docs/docs/02-configuration/10-reference-handling.md b/docs/docs/02-configuration/10-reference-handling.md new file mode 100644 index 0000000000..03cc44ca3c --- /dev/null +++ b/docs/docs/02-configuration/10-reference-handling.md @@ -0,0 +1,40 @@ +# Reference handling + +Mapperly can support mapping object structures with circular references. +To opt in for reference handling set `UseReferenceHandling` to `true`: +```csharp +// highlight-start +[Mapper(UseReferenceHandling = true)] +// highlight-end +public partial class CarMapper +{ + public partial void CarToCarDto(Car car, CarDto dto); +} +``` + +This enables the usage of a default reference handler +which reuses the same target object instance if encountered the same source object instance. + +## Custom reference handler + +To use a custom `IReferenceHandler` implementation, +a parameter of the type `Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler` +annotated with the `Riok.Mapperly.Abstractions.ReferenceHandling.ReferenceHandlerAttribute` +can be added to the mapping method. + +```csharp +// highlight-start +[Mapper(UseReferenceHandling = true)] +// highlight-end +public partial class CarMapper +{ + // highlight-start + public partial void CarToCarDto(Car car, CarDto dto, [ReferenceHandler] IReferenceHandler myRefHandler); + // highlight-end +} +``` + +## User implemented mappings + +To make use of the `IReferenceHandler` in a user implemented mapping method, +add a parameter as described in the section "[Custom reference handler](#custom-reference-handler)". diff --git a/docs/docs/02-configuration/10-conversions.md b/docs/docs/02-configuration/11-conversions.md similarity index 100% rename from docs/docs/02-configuration/10-conversions.md rename to docs/docs/02-configuration/11-conversions.md diff --git a/docs/docs/02-configuration/11-analyzer-diagnostics.mdx b/docs/docs/02-configuration/12-analyzer-diagnostics.mdx similarity index 100% rename from docs/docs/02-configuration/11-analyzer-diagnostics.mdx rename to docs/docs/02-configuration/12-analyzer-diagnostics.mdx diff --git a/docs/docs/02-configuration/12-generated-source.mdx b/docs/docs/02-configuration/13-generated-source.mdx similarity index 100% rename from docs/docs/02-configuration/12-generated-source.mdx rename to docs/docs/02-configuration/13-generated-source.mdx diff --git a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs index 1dd8f9fbe5..4a05773258 100644 --- a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs +++ b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs @@ -1,3 +1,5 @@ +using Riok.Mapperly.Abstractions.ReferenceHandling; + namespace Riok.Mapperly.Abstractions; /// @@ -60,4 +62,12 @@ public sealed class MapperAttribute : Attribute /// /// public MappingConversionType EnabledConversions { get; set; } = MappingConversionType.All; + + /// + /// Enables the reference handling feature. + /// Disabled by default for performance reasons. + /// When enabled, an instance is passed through the mapping methods + /// to keep track of and reuse existing target object instances. + /// + public bool UseReferenceHandling { get; set; } } diff --git a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt index 67cebe031c..c9377fa143 100644 --- a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt +++ b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt @@ -58,3 +58,14 @@ Riok.Mapperly.Abstractions.MappingConversionType.None = 0 -> Riok.Mapperly.Abstr Riok.Mapperly.Abstractions.MappingConversionType.ParseMethod = 8 -> Riok.Mapperly.Abstractions.MappingConversionType Riok.Mapperly.Abstractions.MappingConversionType.StringToEnum = 32 -> Riok.Mapperly.Abstractions.MappingConversionType Riok.Mapperly.Abstractions.MappingConversionType.ToStringMethod = 16 -> Riok.Mapperly.Abstractions.MappingConversionType +Riok.Mapperly.Abstractions.MapperAttribute.UseReferenceHandling.get -> bool +Riok.Mapperly.Abstractions.MapperAttribute.UseReferenceHandling.set -> void +Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler +Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler.PreserveReferenceHandler() -> void +Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler.SetReference(TSource source, TTarget target) -> void +Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler.TryGetReference(TSource source, out TTarget? target) -> bool +Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler +Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler.SetReference(TSource source, TTarget target) -> void +Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler.TryGetReference(TSource source, out TTarget? target) -> bool +Riok.Mapperly.Abstractions.ReferenceHandling.ReferenceHandlerAttribute +Riok.Mapperly.Abstractions.ReferenceHandling.ReferenceHandlerAttribute.ReferenceHandlerAttribute() -> void diff --git a/src/Riok.Mapperly.Abstractions/ReferenceHandling/IReferenceHandler.cs b/src/Riok.Mapperly.Abstractions/ReferenceHandling/IReferenceHandler.cs new file mode 100644 index 0000000000..143cabb1de --- /dev/null +++ b/src/Riok.Mapperly.Abstractions/ReferenceHandling/IReferenceHandler.cs @@ -0,0 +1,38 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Riok.Mapperly.Abstractions.ReferenceHandling; + +/// +/// A reference handler can store and resolve references +/// of mapping target objects. +/// +public interface IReferenceHandler +{ + /// + /// Before an object is created by Mapperly this method is called. + /// It can attempt to resolve existing target object instances based on the source object instance. + /// If false is returned, Mapperly creates a new instance of the target class. + /// If true is returned, target has to be non-null. + /// Mapperly then uses the target instance. + /// + /// The source object instance. + /// The resolved target object instance or null if none could be resolved. + /// The type of the source object. + /// The target object type. + /// + bool TryGetReference(TSource source, [NotNullWhen(true)] out TTarget? target) + where TSource : notnull + where TTarget : notnull; + + /// + /// Stores the created target instance. + /// Called by Mapperly just after a new target object instance is created. + /// + /// The source object instance. + /// The target object instance. + /// The type of the source object. + /// The type of the target object. + void SetReference(TSource source, TTarget target) + where TSource : notnull + where TTarget : notnull; +} diff --git a/src/Riok.Mapperly.Abstractions/ReferenceHandling/Internal/PreserveReferenceHandler.cs b/src/Riok.Mapperly.Abstractions/ReferenceHandling/Internal/PreserveReferenceHandler.cs new file mode 100644 index 0000000000..0b738876bf --- /dev/null +++ b/src/Riok.Mapperly.Abstractions/ReferenceHandling/Internal/PreserveReferenceHandler.cs @@ -0,0 +1,64 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Riok.Mapperly.Abstractions.ReferenceHandling.Internal; + +/// +/// A implementation +/// which returns the same target object instance if encountered the same source object instance. +/// Do not use directly. Should only be used by Mapperly generated code. +/// API surface is not subject to semantic releases and may break in any release. +/// +public sealed class PreserveReferenceHandler : IReferenceHandler +{ + private readonly Dictionary<(Type, Type), ReferenceHolder> _referenceHolders = new(); + + /// + public bool TryGetReference(TSource source, [NotNullWhen(true)] out TTarget? target) + where TSource : notnull + where TTarget : notnull + { + var refHolder = GetReferenceHolder(); + return refHolder.TryGetRef(source, out target); + } + + /// + public void SetReference(TSource source, TTarget target) + where TSource : notnull + where TTarget : notnull + => GetReferenceHolder().SetRef(source, target); + + private ReferenceHolder GetReferenceHolder() + { + var mapping = (typeof(TSource), typeof(TTarget)); + if (_referenceHolders.TryGetValue(mapping, out var refHolder)) + return refHolder; + + return _referenceHolders[mapping] = new(); + } + + private class ReferenceHolder + { + private readonly Dictionary _references = new(ReferenceEqualityComparer.Instance); + + public bool TryGetRef(TSource source, [NotNullWhen(true)] out TTarget? target) + where TSource : notnull + where TTarget : notnull + { + if (_references.TryGetValue(source, out var targetObj)) + { + target = (TTarget)targetObj; + return true; + } + + target = default; + return false; + } + + public void SetRef(TSource source, TTarget target) + where TSource : notnull + where TTarget : notnull + { + _references[source] = target; + } + } +} diff --git a/src/Riok.Mapperly.Abstractions/ReferenceHandling/Internal/ReferenceEqualityComparer.cs b/src/Riok.Mapperly.Abstractions/ReferenceHandling/Internal/ReferenceEqualityComparer.cs new file mode 100644 index 0000000000..85fe36c389 --- /dev/null +++ b/src/Riok.Mapperly.Abstractions/ReferenceHandling/Internal/ReferenceEqualityComparer.cs @@ -0,0 +1,27 @@ +using System.Runtime.CompilerServices; + +namespace Riok.Mapperly.Abstractions.ReferenceHandling.Internal; + +/// +/// Defines methods to support the comparison of objects for reference equality. +/// +/// The type of objects to compare. +internal sealed class ReferenceEqualityComparer : IEqualityComparer +{ + // cannot use System.Collections.Generic.ReferenceEqualityComparer since it is not available in netstandard2.0 + + /// + /// A instance. + /// + public static readonly IEqualityComparer Instance = new ReferenceEqualityComparer(); + + private ReferenceEqualityComparer() + { + } + + bool IEqualityComparer.Equals(T? x, T? y) + => ReferenceEquals(x, y); + + int IEqualityComparer.GetHashCode(T obj) + => RuntimeHelpers.GetHashCode(obj); +} diff --git a/src/Riok.Mapperly.Abstractions/ReferenceHandling/ReferenceHandlerAttribute.cs b/src/Riok.Mapperly.Abstractions/ReferenceHandling/ReferenceHandlerAttribute.cs new file mode 100644 index 0000000000..3c5718f763 --- /dev/null +++ b/src/Riok.Mapperly.Abstractions/ReferenceHandling/ReferenceHandlerAttribute.cs @@ -0,0 +1,10 @@ +namespace Riok.Mapperly.Abstractions.ReferenceHandling; + +/// +/// Marks a mapping method parameter as a . +/// The type of the parameter needs to be . +/// +[AttributeUsage(AttributeTargets.Parameter)] +public sealed class ReferenceHandlerAttribute : Attribute +{ +} diff --git a/src/Riok.Mapperly/AnalyzerReleases.Shipped.md b/src/Riok.Mapperly/AnalyzerReleases.Shipped.md index 082109219a..1cf24e3b0b 100644 --- a/src/Riok.Mapperly/AnalyzerReleases.Shipped.md +++ b/src/Riok.Mapperly/AnalyzerReleases.Shipped.md @@ -55,3 +55,5 @@ RMG022 | Mapper | Error | Invalid object factory signature Rule ID | Category | Severity | Notes --------|----------|----------|-------------------- RMG023 | Mapper | Error | Mapping source property for a required target property not found +RMG024 | Mapper | Error | The reference handler parameter is not of the correct type +RMG025 | Mapper | Error | To use reference handling it needs to be enabled on the mapper attribute diff --git a/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs b/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs index 57f8e7a07d..22d460778d 100644 --- a/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs @@ -11,7 +11,7 @@ namespace Riok.Mapperly.Descriptors; public class DescriptorBuilder { - private delegate TypeMapping? MappingBuilder(MappingBuilderContext context); + private delegate ITypeMapping? MappingBuilder(MappingBuilderContext context); private static readonly IReadOnlyCollection _mappingBuilders = new MappingBuilder[] { @@ -40,15 +40,10 @@ public class DescriptorBuilder // Usually these are derived from the mapper attribute or default values. private readonly Dictionary _defaultConfigurations = new(); - // this includes mappings to build and already built mappings - private readonly Dictionary<(ITypeSymbol SourceType, ITypeSymbol TargetType), TypeMapping> _mappings = new(new MappingTupleEqualityComparer()); - - // additional user defined mappings - // (with same signature as already defined mappings but with different names) - private readonly List _extraMappings = new(); - // queue of mappings which don't have the body built yet - private readonly Queue<(TypeMapping, MappingBuilderContext)> _mappingsToBuildBody = new(); + private readonly Queue<(ITypeMapping, MappingBuilderContext)> _mappingsToBuildBody = new(); + + private readonly MappingCollection _mappings = new(); private readonly MethodNameBuilder _methodNameBuilder = new(); @@ -61,6 +56,7 @@ public DescriptorBuilder( _mapperSymbol = mapperSymbol; _context = sourceContext; Compilation = compilation; + WellKnownTypes = new WellKnownTypes(Compilation); _mapperDescriptor = new MapperDescriptor(mapperSyntax, mapperSymbol.IsStatic); MapperConfiguration = Configure(); } @@ -69,6 +65,8 @@ public DescriptorBuilder( internal Compilation Compilation { get; } + internal WellKnownTypes WellKnownTypes { get; } + internal ObjectFactoryCollection ObjectFactories { get; private set; } = ObjectFactoryCollection.Empty; public MapperAttribute MapperConfiguration { get; } @@ -94,6 +92,7 @@ public MapperDescriptor Build() ExtractUserMappings(); BuildMappingBodies(); BuildMappingMethodNames(); + BuildReferenceHandlingParameters(); AddMappingsToDescriptor(); return _mapperDescriptor; } @@ -103,28 +102,36 @@ private void ExtractObjectFactories() var ctx = new SimpleMappingBuilderContext(this); ObjectFactories = ObjectFactoryBuilder.ExtractObjectFactories(ctx, _mapperSymbol); } + public ITypeMapping? FindMapping(ITypeSymbol sourceType, ITypeSymbol targetType) + => _mappings.FindMapping(sourceType, targetType); - public TypeMapping? FindOrBuildMapping( + public ITypeMapping? FindOrBuildMapping( ITypeSymbol sourceType, ITypeSymbol targetType) { - if (FindMapping(sourceType, targetType) is { } foundMapping) + if (_mappings.FindMapping(sourceType, targetType) is { } foundMapping) return foundMapping; if (BuildDelegateMapping(null, sourceType, targetType) is not { } mapping) return null; - AddMapping(mapping); + _mappings.AddMapping(mapping); return mapping; } - public TypeMapping? FindMapping(ITypeSymbol sourceType, ITypeSymbol targetType) + public ITypeMapping? BuildMappingWithUserSymbol( + ISymbol userSymbol, + ITypeSymbol sourceType, + ITypeSymbol targetType) { - _mappings.TryGetValue((sourceType, targetType), out var mapping); + if (BuildDelegateMapping(userSymbol, sourceType, targetType) is not { } mapping) + return null; + + _mappings.AddMapping(mapping); return mapping; } - public TypeMapping? BuildDelegateMapping( + public ITypeMapping? BuildDelegateMapping( ISymbol? userSymbol, ITypeSymbol sourceType, ITypeSymbol targetType) @@ -150,13 +157,13 @@ private void ExtractUserMappings() var defaultContext = new SimpleMappingBuilderContext(this); foreach (var userMapping in UserMethodMappingBuilder.ExtractUserMappings(defaultContext, _mapperSymbol)) { - AddUserMapping(userMapping); + _mappings.AddMapping(userMapping); var ctx = new MappingBuilderContext( this, userMapping.SourceType, userMapping.TargetType, - (userMapping as IUserMapping)?.Method); + userMapping.Method); _mappingsToBuildBody.Enqueue((userMapping, ctx)); } } @@ -188,60 +195,31 @@ private void BuildMappingBodies() } } - private void AddMapping(TypeMapping mapping) - => _mappings.Add((mapping.SourceType, mapping.TargetType), mapping); - - private void AddUserMapping(TypeMapping mapping) - { - if (mapping.CallableByOtherMappings && FindMapping(mapping.SourceType, mapping.TargetType) is null) - { - AddMapping(mapping); - return; - } - - _extraMappings.Add(mapping); - } - private void BuildMappingMethodNames() { - foreach (var methodMapping in _mappings.Values.Concat(_extraMappings).OfType()) + foreach (var methodMapping in _mappings.MethodMappings) { methodMapping.SetMethodNameIfNeeded(_methodNameBuilder.Build); } } - private void AddMappingsToDescriptor() + private void BuildReferenceHandlingParameters() { - // add generated mappings to the mapper - foreach (var mapping in _mappings.Values) - { - _mapperDescriptor.AddTypeMapping(mapping); - } + if (!MapperConfiguration.UseReferenceHandling) + return; - // add extra mappings to the mapper - foreach (var extraMapping in _extraMappings) + foreach (var methodMapping in _mappings.MethodMappings) { - _mapperDescriptor.AddTypeMapping(extraMapping); + methodMapping.EnableReferenceHandling(WellKnownTypes.IReferenceHandler); } } - private class MappingTupleEqualityComparer : IEqualityComparer<(ITypeSymbol Source, ITypeSymbol Target)> + private void AddMappingsToDescriptor() { - public bool Equals((ITypeSymbol Source, ITypeSymbol Target) x, (ITypeSymbol Source, ITypeSymbol Target) y) - { - return Equals(x.Source, y.Source) - && Equals(x.Target, y.Target); - } - - public int GetHashCode((ITypeSymbol Source, ITypeSymbol Target) obj) + // add generated mappings to the mapper + foreach (var mapping in _mappings.All) { - unchecked - { - return (SymbolEqualityComparer.Default.GetHashCode(obj.Source) * 397) ^ SymbolEqualityComparer.Default.GetHashCode(obj.Target); - } + _mapperDescriptor.AddTypeMapping(mapping); } - - private bool Equals(ITypeSymbol x, ITypeSymbol y) - => SymbolEqualityComparer.IncludeNullability.Equals(x, y); } } diff --git a/src/Riok.Mapperly/Descriptors/MapperDescriptor.cs b/src/Riok.Mapperly/Descriptors/MapperDescriptor.cs index fecf41a8a0..a4ed590f30 100644 --- a/src/Riok.Mapperly/Descriptors/MapperDescriptor.cs +++ b/src/Riok.Mapperly/Descriptors/MapperDescriptor.cs @@ -6,7 +6,7 @@ namespace Riok.Mapperly.Descriptors; public class MapperDescriptor { - private readonly List _mappings = new(); + private readonly List _mappings = new(); public MapperDescriptor(ClassDeclarationSyntax syntax, bool isStatic) { @@ -23,6 +23,6 @@ public MapperDescriptor(ClassDeclarationSyntax syntax, bool isStatic) public IEnumerable MethodTypeMappings => _mappings.OfType(); - public void AddTypeMapping(TypeMapping mapping) + public void AddTypeMapping(ITypeMapping mapping) => _mappings.Add(mapping); } diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/DictionaryMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/DictionaryMappingBuilder.cs index d9e9eb1262..ee9acc4a08 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilder/DictionaryMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/DictionaryMappingBuilder.cs @@ -11,7 +11,7 @@ public static class DictionaryMappingBuilder public static TypeMapping? TryBuildMapping(MappingBuilderContext ctx) { - if (GetDictionaryKeyValueTypes(ctx.Target, ctx.GetTypeSymbol(typeof(IDictionary<,>)), ctx.GetTypeSymbol(typeof(IReadOnlyDictionary<,>))) is not var (targetKeyType, targetValueType)) + if (GetDictionaryKeyValueTypes(ctx, ctx.Target) is not var (targetKeyType, targetValueType)) return null; if (GetEnumerableKeyValueTypes(ctx, ctx.Source) is not var (sourceKeyType, sourceValueType)) @@ -33,7 +33,7 @@ public static class DictionaryMappingBuilder .OfType() .Any(x => !x.IsStatic && !x.IsIndexer && !x.IsWriteOnly && x.Type.SpecialType == SpecialType.System_Int32); - var targetDictionarySymbol = ctx.GetTypeSymbol(typeof(Dictionary<,>)).Construct(targetKeyType, targetValueType); + var targetDictionarySymbol = ctx.Types.Dictionary.Construct(targetKeyType, targetValueType); ctx.ObjectFactories.TryFindObjectFactory(ctx.Source, ctx.Target, out var dictionaryObjectFactory); return new ForEachAddDictionaryMapping(ctx.Source, ctx.Target, keyMapping, valueMapping, sourceHasCount, targetDictionarySymbol, dictionaryObjectFactory); } @@ -54,22 +54,19 @@ private static bool IsDictionaryType(MappingBuilderContext ctx, ITypeSymbol symb if (symbol is not INamedTypeSymbol namedSymbol) return false; - return SymbolEqualityComparer.Default.Equals(namedSymbol.ConstructedFrom, ctx.GetTypeSymbol(typeof(Dictionary<,>))) - || SymbolEqualityComparer.Default.Equals(namedSymbol.ConstructedFrom, ctx.GetTypeSymbol(typeof(IDictionary<,>))) - || SymbolEqualityComparer.Default.Equals(namedSymbol.ConstructedFrom, ctx.GetTypeSymbol(typeof(IReadOnlyDictionary<,>))); + return SymbolEqualityComparer.Default.Equals(namedSymbol.ConstructedFrom, ctx.Types.Dictionary) + || SymbolEqualityComparer.Default.Equals(namedSymbol.ConstructedFrom, ctx.Types.IDictionary) + || SymbolEqualityComparer.Default.Equals(namedSymbol.ConstructedFrom, ctx.Types.IReadOnlyDictionary); } - private static (ITypeSymbol, ITypeSymbol)? GetDictionaryKeyValueTypes( - ITypeSymbol t, - INamedTypeSymbol dictionarySymbol, - INamedTypeSymbol readOnlyDictionarySymbol) + private static (ITypeSymbol, ITypeSymbol)? GetDictionaryKeyValueTypes(MappingBuilderContext ctx, ITypeSymbol t) { - if (t.ImplementsGeneric(dictionarySymbol, out var dictionaryImpl)) + if (t.ImplementsGeneric(ctx.Types.IDictionary, out var dictionaryImpl)) { return (dictionaryImpl.TypeArguments[0], dictionaryImpl.TypeArguments[1]); } - if (t.ImplementsGeneric(readOnlyDictionarySymbol, out var readOnlyDictionaryImpl)) + if (t.ImplementsGeneric(ctx.Types.IReadOnlyDictionary, out var readOnlyDictionaryImpl)) { return (readOnlyDictionaryImpl.TypeArguments[0], readOnlyDictionaryImpl.TypeArguments[1]); } @@ -79,13 +76,13 @@ private static (ITypeSymbol, ITypeSymbol)? GetDictionaryKeyValueTypes( private static (ITypeSymbol, ITypeSymbol)? GetEnumerableKeyValueTypes(MappingBuilderContext ctx, ITypeSymbol t) { - if (!t.ImplementsGeneric(ctx.GetTypeSymbol(typeof(IEnumerable<>)), out var enumerableImpl)) + if (!t.ImplementsGeneric(ctx.Types.IEnumerable, out var enumerableImpl)) return null; if (enumerableImpl.TypeArguments[0] is not INamedTypeSymbol enumeratedType) return null; - if (!SymbolEqualityComparer.Default.Equals(enumeratedType.ConstructedFrom, ctx.GetTypeSymbol(typeof(KeyValuePair<,>)))) + if (!SymbolEqualityComparer.Default.Equals(enumeratedType.ConstructedFrom, ctx.Types.KeyValuePair)) return null; return (enumeratedType.TypeArguments[0], enumeratedType.TypeArguments[1]); diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/EnumerableMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/EnumerableMappingBuilder.cs index d072d501a0..0b4ad410e2 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilder/EnumerableMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/EnumerableMappingBuilder.cs @@ -27,12 +27,12 @@ public static class EnumerableMappingBuilder // if element mapping is synthetic // and target is an IEnumerable, there is no mapping needed at all. - if (elementMapping.IsSynthetic && ctx.IsType(ctx.Target.OriginalDefinition, typeof(IEnumerable<>))) + if (elementMapping.IsSynthetic && SymbolEqualityComparer.Default.Equals(ctx.Target.OriginalDefinition, ctx.Types.IEnumerable)) return new CastMapping(ctx.Source, ctx.Target); // if source is an array and target is an array or IReadOnlyCollection faster mappings can be applied if (ctx.Source.IsArrayType() - && (ctx.Target.IsArrayType() || ctx.IsType(ctx.Target.OriginalDefinition, typeof(IReadOnlyCollection<>)))) + && (ctx.Target.IsArrayType() || SymbolEqualityComparer.Default.Equals(ctx.Target.OriginalDefinition, ctx.Types.IReadOnlyCollection))) { // if element mapping is synthetic // a single Array.Clone / cast mapping call should be sufficient and fast, @@ -56,7 +56,7 @@ public static class EnumerableMappingBuilder private static LinqEnumerableMapping BuildLinqMapping( MappingBuilderContext ctx, - TypeMapping elementMapping, + ITypeMapping elementMapping, string? collectMethodName) { var collectMethod = collectMethodName == null @@ -72,7 +72,7 @@ private static LinqEnumerableMapping BuildLinqMapping( private static ForEachAddEnumerableMapping? BuildCustomTypeMapping( MappingBuilderContext ctx, - TypeMapping elementMapping) + ITypeMapping elementMapping) { if (!ctx.ObjectFactories.TryFindObjectFactory(ctx.Source, ctx.Target, out var objectFactory) && !ctx.Target.HasAccessibleParameterlessConstructor()) { @@ -80,7 +80,7 @@ private static LinqEnumerableMapping BuildLinqMapping( return null; } - return ctx.Target.ImplementsGeneric(ctx.GetTypeSymbol(typeof(ICollection<>)), out _) + return ctx.Target.ImplementsGeneric(ctx.Types.ICollection, out _) ? new ForEachAddEnumerableMapping(ctx.Source, ctx.Target, elementMapping, objectFactory) : null; } @@ -92,31 +92,31 @@ private static (bool CanMapWithLinq, string? CollectMethod) ResolveCollectMethod return (true, ToArrayMethodName); // if the target is an IEnumerable don't collect at all. - if (ctx.IsType(ctx.Target.OriginalDefinition, typeof(IEnumerable<>))) + if (SymbolEqualityComparer.Default.Equals(ctx.Target.OriginalDefinition, ctx.Types.IEnumerable)) return (true, null); // if the target is IReadOnlyCollection // and the count of the source is known (array, IReadOnlyCollection, ICollection) we collect to array // for performance/space reasons - var targetIsReadOnlyCollection = ctx.IsType(ctx.Target.OriginalDefinition, typeof(IReadOnlyCollection<>)); + var targetIsReadOnlyCollection = SymbolEqualityComparer.Default.Equals(ctx.Target.OriginalDefinition, ctx.Types.IReadOnlyCollection); var sourceCountIsKnown = ctx.Source.IsArrayType() - || ctx.Source.ImplementsGeneric(ctx.GetTypeSymbol(typeof(IReadOnlyCollection<>)), out _) - || ctx.Source.ImplementsGeneric(ctx.GetTypeSymbol(typeof(ICollection<>)), out _); + || ctx.Source.ImplementsGeneric(ctx.Types.IReadOnlyCollection, out _) + || ctx.Source.ImplementsGeneric(ctx.Types.ICollection, out _); if (targetIsReadOnlyCollection && sourceCountIsKnown) return (true, ToArrayMethodName); // if target is a list, ICollection or IReadOnlyCollection collect with ToList() return targetIsReadOnlyCollection - || ctx.IsType(ctx.Target.OriginalDefinition, typeof(ICollection<>)) - || ctx.IsType(ctx.Target.OriginalDefinition, typeof(List<>)) + || SymbolEqualityComparer.Default.Equals(ctx.Target.OriginalDefinition, ctx.Types.ICollection) + || targetIsReadOnlyCollection ? (true, ToListMethodName) : (false, null); } private static IMethodSymbol? ResolveLinqMethod(MappingBuilderContext ctx, string methodName) { - var method = ctx.GetTypeSymbol(typeof(Enumerable)) + var method = ctx.Types.Enumerable .GetMembers(methodName) .OfType() .FirstOrDefault(m => @@ -128,7 +128,7 @@ private static (bool CanMapWithLinq, string? CollectMethod) ResolveCollectMethod private static ITypeSymbol? GetEnumeratedType(MappingBuilderContext ctx, ITypeSymbol type) { - return type.ImplementsGeneric(ctx.GetTypeSymbol(typeof(IEnumerable<>)), out var enumerableIntf) + return type.ImplementsGeneric(ctx.Types.IEnumerable, out var enumerableIntf) ? enumerableIntf.TypeArguments[0] : null; } diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/NewInstanceObjectPropertyMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/NewInstanceObjectPropertyMappingBuilder.cs index 2f64e5cb1f..8bf53bd9a7 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilder/NewInstanceObjectPropertyMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/NewInstanceObjectPropertyMappingBuilder.cs @@ -16,7 +16,7 @@ public static class NewInstanceObjectPropertyMappingBuilder return null; if (ctx.ObjectFactories.TryFindObjectFactory(ctx.Source, ctx.Target, out var objectFactory)) - return new NewInstanceObjectFactoryPropertyMapping(ctx.Source, ctx.Target.NonNullable(), objectFactory); + return new NewInstanceObjectFactoryPropertyMapping(ctx.Source, ctx.Target.NonNullable(), objectFactory, ctx.MapperConfiguration.UseReferenceHandling); if (ctx.Target is not INamedTypeSymbol namedTarget || namedTarget.Constructors.All(x => !x.IsAccessible())) return null; @@ -24,7 +24,7 @@ public static class NewInstanceObjectPropertyMappingBuilder if (ctx.Source.IsEnum() || ctx.Target.IsEnum()) return null; - return new NewInstanceObjectPropertyMapping(ctx.Source, ctx.Target.NonNullable()); + return new NewInstanceObjectPropertyMapping(ctx.Source, ctx.Target.NonNullable(), ctx.MapperConfiguration.UseReferenceHandling); } public static void BuildMappingBody(MappingBuilderContext ctx, NewInstanceObjectPropertyMapping mapping) @@ -162,17 +162,14 @@ private static bool TryBuildConstructorMapping( return false; } - var mapperConstructorAttribute = ctx.BuilderContext.GetTypeSymbol(typeof(MapperConstructorAttribute)); - var obsoleteAttribute = ctx.BuilderContext.GetTypeSymbol(typeof(ObsoleteAttribute)); - // attributed ctor is prio 1 // parameterless ctor is prio 2 // then by descending parameter count // ctors annotated with [Obsolete] are considered last unless they have a MapperConstructor attribute set var ctorCandidates = namedTargetType.Constructors .Where(ctor => ctor.IsAccessible()) - .OrderByDescending(x => x.HasAttribute(mapperConstructorAttribute)) - .ThenBy(x => x.HasAttribute(obsoleteAttribute)) + .OrderByDescending(x => x.HasAttribute(ctx.BuilderContext.Types.MapperConstructorAttribute)) + .ThenBy(x => x.HasAttribute(ctx.BuilderContext.Types.ObsoleteAttribute)) .ThenByDescending(x => x.Parameters.Length == 0) .ThenByDescending(x => x.Parameters.Length); foreach (var ctorCandidate in ctorCandidates) @@ -183,7 +180,7 @@ private static bool TryBuildConstructorMapping( out mappedTargetPropertyNames, out var constructorParameterMappings)) { - if (ctorCandidate.HasAttribute(mapperConstructorAttribute)) + if (ctorCandidate.HasAttribute(ctx.BuilderContext.Types.MapperConstructorAttribute)) { ctx.BuilderContext.ReportDiagnostic( DiagnosticDescriptors.CannotMapToConfiguredConstructor, diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/NullableMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/NullableMappingBuilder.cs index 100df94cf1..95526572bc 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilder/NullableMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/NullableMappingBuilder.cs @@ -18,7 +18,7 @@ public static class NullableMappingBuilder : BuildNullDelegateMapping(ctx, delegateMapping); } - private static TypeMapping BuildNullDelegateMapping(MappingBuilderContext ctx, TypeMapping mapping) + private static TypeMapping BuildNullDelegateMapping(MappingBuilderContext ctx, ITypeMapping mapping) { var nullFallback = ctx.GetNullFallbackValue(); return mapping switch diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/UserMethodMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/UserMethodMappingBuilder.cs index 99833a6e80..1db68ee710 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilder/UserMethodMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/UserMethodMappingBuilder.cs @@ -1,13 +1,14 @@ using Microsoft.CodeAnalysis; using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Diagnostics; +using Riok.Mapperly.Emit.Symbols; using Riok.Mapperly.Helpers; namespace Riok.Mapperly.Descriptors.MappingBuilder; public static class UserMethodMappingBuilder { - public static IEnumerable ExtractUserMappings( + public static IEnumerable ExtractUserMappings( SimpleMappingBuilderContext ctx, ITypeSymbol mapperSymbol) { @@ -15,7 +16,7 @@ public static IEnumerable ExtractUserMappings( foreach (var methodSymbol in ExtractMethods(mapperSymbol)) { var mapping = BuilderUserDefinedMapping(ctx, methodSymbol, mapperSymbol.IsStatic) - ?? BuildUserImplementedMapping(methodSymbol, false, mapperSymbol.IsStatic); + ?? BuildUserImplementedMapping(ctx, methodSymbol, false, mapperSymbol.IsStatic); if (mapping != null) yield return mapping; } @@ -29,9 +30,9 @@ public static IEnumerable ExtractUserMappings( { // Partial method declarations are allowed for base classes, // but still treated as user implemented methods, - // since the user should provide an implementation elsewhere. - // This is the case if a partial mapper class is extended. - var mapping = BuildUserImplementedMapping(method, true, mapperSymbol.IsStatic); + // since the user should provide an implementation elsewhere. + // This is the case if a partial mapper class is extended. + var mapping = BuildUserImplementedMapping(ctx, method, true, mapperSymbol.IsStatic); if (mapping != null) yield return mapping; } @@ -39,7 +40,9 @@ public static IEnumerable ExtractUserMappings( public static void BuildMappingBody(MappingBuilderContext ctx, UserDefinedNewInstanceMethodMapping mapping) { - var delegateMapping = ctx.BuildDelegateMapping(mapping.SourceType, mapping.TargetType); + var delegateMapping = mapping.CallableByOtherMappings + ? ctx.BuildDelegateMapping(mapping.SourceType, mapping.TargetType) + : ctx.BuildMappingWithUserSymbol(mapping.SourceType, mapping.TargetType); if (delegateMapping != null) { mapping.SetDelegateMapping(delegateMapping); @@ -70,14 +73,22 @@ private static IEnumerable ExtractBaseMethods(INamedTypeSymbol ob && !SymbolEqualityComparer.Default.Equals(x.ReceiverType, objectType)); } - private static TypeMapping? BuildUserImplementedMapping(IMethodSymbol m, bool allowPartial, bool isStatic) + private static IUserMapping? BuildUserImplementedMapping( + SimpleMappingBuilderContext ctx, + IMethodSymbol method, + bool allowPartial, + bool isStatic) { - return IsNewInstanceMappingMethod(m) && (allowPartial || !m.IsPartialDefinition) && (isStatic == m.IsStatic) - ? new UserImplementedMethodMapping(m) + var valid = BuildParameters(ctx, method, out var parameters) + && !method.ReturnsVoid + && (allowPartial || !method.IsPartialDefinition) + && isStatic == method.IsStatic; + return valid + ? new UserImplementedMethodMapping(method, parameters.Source, parameters.ReferenceHandler) : null; } - private static TypeMapping? BuilderUserDefinedMapping( + private static IUserMapping? BuilderUserDefinedMapping( SimpleMappingBuilderContext ctx, IMethodSymbol methodSymbol, bool isStatic) @@ -94,28 +105,106 @@ private static IEnumerable ExtractBaseMethods(INamedTypeSymbol ob return null; } - if (IsExistingInstanceMappingMethod(methodSymbol)) - return new UserDefinedExistingInstanceMethodMapping(methodSymbol); + if (methodSymbol.IsAsync || methodSymbol.IsGenericMethod) + { + ctx.ReportDiagnostic( + DiagnosticDescriptors.UnsupportedMappingMethodSignature, + methodSymbol, + methodSymbol.Name); + return null; + } + + if (!BuildParameters(ctx, methodSymbol, out var parameters)) + { + ctx.ReportDiagnostic( + DiagnosticDescriptors.UnsupportedMappingMethodSignature, + methodSymbol, + methodSymbol.Name); + return null; + } - if (IsNewInstanceMappingMethod(methodSymbol)) - return new UserDefinedNewInstanceMethodMapping(methodSymbol); + if (parameters.Target.HasValue) + { + return new UserDefinedExistingInstanceMethodMapping( + methodSymbol, + parameters.Source, + parameters.Target.Value, + parameters.ReferenceHandler, + ctx.MapperConfiguration.UseReferenceHandling, + ctx.Types.PreserveReferenceHandler); + } - ctx.ReportDiagnostic( - DiagnosticDescriptors.UnsupportedMappingMethodSignature, + return new UserDefinedNewInstanceMethodMapping( methodSymbol, - methodSymbol.Name); - return null; + parameters.Source, + parameters.ReferenceHandler, + ctx.MapperConfiguration.UseReferenceHandling, + ctx.Types.PreserveReferenceHandler); } - private static bool IsExistingInstanceMappingMethod(IMethodSymbol m) - => m.Parameters.Length == 2 - && m.ReturnsVoid - && !m.IsAsync - && !m.IsGenericMethod; - - private static bool IsNewInstanceMappingMethod(IMethodSymbol m) - => m.Parameters.Length == 1 - && !m.ReturnsVoid - && !m.IsAsync - && !m.IsGenericMethod; + private static bool BuildParameters( + SimpleMappingBuilderContext ctx, + IMethodSymbol method, + out MappingMethodParameters parameters) + { + // reference handler parameter is always annotated + var refHandlerParameter = BuildReferenceHandlerParameter(ctx, method); + var refHandlerParameterOrdinal = refHandlerParameter?.Ordinal ?? -1; + + // source parameter is the first parameter (except if the reference handler is the first parameter) + var sourceParameter = MethodParameter.Wrap(method.Parameters.FirstOrDefault(p => p.Ordinal != refHandlerParameterOrdinal)); + if (sourceParameter == null) + { + parameters = default; + return false; + } + + // target parameter is the second parameter (except if the reference handler is the first or the second parameter) + // if the method returns void, a target parameter is required + // if the method doesnt return void, a target parameter is not allowed + var targetParameter = MethodParameter.Wrap(method.Parameters.FirstOrDefault(p => p.Ordinal != sourceParameter.Value.Ordinal && p.Ordinal != refHandlerParameterOrdinal)); + if (method.ReturnsVoid == (targetParameter == null)) + { + parameters = default; + return false; + } + + parameters = new MappingMethodParameters( + sourceParameter.Value, + targetParameter, + refHandlerParameter); + return true; + } + + private static MethodParameter? BuildReferenceHandlerParameter( + SimpleMappingBuilderContext ctx, + IMethodSymbol method) + { + var refHandlerParameterSymbol = method.Parameters.FirstOrDefault(p => p.HasAttribute(ctx.Types.ReferenceHandlerAttribute)); + if (refHandlerParameterSymbol == null) + return null; + + var refHandlerParameter = new MethodParameter(refHandlerParameterSymbol); + if (!SymbolEqualityComparer.Default.Equals(ctx.Types.IReferenceHandler, refHandlerParameter.Type)) + { + ctx.ReportDiagnostic( + DiagnosticDescriptors.ReferenceHandlerParameterWrongType, + refHandlerParameterSymbol, + method.ContainingType.ToDisplayString(), + method.Name, + ctx.Types.IReferenceHandler.ToDisplayString(), + refHandlerParameterSymbol.ToDisplayString()); + } + + if (!ctx.MapperConfiguration.UseReferenceHandling) + { + ctx.ReportDiagnostic( + DiagnosticDescriptors.ReferenceHandlingNotEnabled, + refHandlerParameterSymbol, + method.ContainingType.ToDisplayString(), + method.Name); + } + + return refHandlerParameter; + } } diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs index e351c209c7..bf7148ca0c 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs @@ -29,7 +29,7 @@ public MappingBuilderContext( public ITypeSymbol Target { get; } - public TypeMapping? FindMapping(ITypeSymbol sourceType, ITypeSymbol targetType) + public ITypeMapping? FindMapping(ITypeSymbol sourceType, ITypeSymbol targetType) => _builder.FindMapping(sourceType.UpgradeNullable(), targetType.UpgradeNullable()); /// @@ -44,7 +44,7 @@ public MappingBuilderContext( /// The source type. /// The target type. /// The found or created mapping, or null if no mapping could be created. - public TypeMapping? FindOrBuildMapping(ITypeSymbol sourceType, ITypeSymbol targetType) + public ITypeMapping? FindOrBuildMapping(ITypeSymbol sourceType, ITypeSymbol targetType) => _builder.FindOrBuildMapping(sourceType.UpgradeNullable(), targetType.UpgradeNullable()); /// @@ -56,9 +56,27 @@ public MappingBuilderContext( /// The source type. /// The target type. /// The created mapping or null if none could be created. - public TypeMapping? BuildDelegateMapping(ITypeSymbol source, ITypeSymbol target) + public ITypeMapping? BuildDelegateMapping(ITypeSymbol source, ITypeSymbol target) => _builder.BuildDelegateMapping(_userSymbol, source.UpgradeNullable(), target.UpgradeNullable()); + /// + /// Tries to build a new mapping for the given types while keeping the current user symbol reference. + /// This reuses configurations on the user symbol for the to be built mapping (eg. ). + /// If no mapping is possible for the provided types, + /// null is returned. + /// If a new mapping is created, it is added to the mapping descriptor + /// and returned in further calls. + /// + /// + /// + /// + /// + public ITypeMapping? BuildMappingWithUserSymbol(ITypeSymbol source, ITypeSymbol target) + => _builder.BuildMappingWithUserSymbol( + _userSymbol ?? throw new InvalidOperationException(nameof(BuildMappingWithUserSymbol) + " can only be called for contexts with a user symbol"), + source.UpgradeNullable(), + target.UpgradeNullable()); + public T GetConfigurationOrDefault() where T : Attribute { return ListConfiguration().FirstOrDefault() diff --git a/src/Riok.Mapperly/Descriptors/MappingCollection.cs b/src/Riok.Mapperly/Descriptors/MappingCollection.cs new file mode 100644 index 0000000000..7f64e13ef3 --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/MappingCollection.cs @@ -0,0 +1,90 @@ +using Microsoft.CodeAnalysis; +using Riok.Mapperly.Descriptors.Mappings; + +namespace Riok.Mapperly.Descriptors; + +public class MappingCollection +{ + // this includes mappings to build and already built mappings + private readonly Dictionary _mappings = new(); + + // additional user defined mappings + // (with same signature as already defined mappings but with different names) + private readonly List _extraMappings = new(); + + // a list of all method mappings (extra mappings and mappings) + private readonly List _methodMappings = new(); + + // a list of all mappings (extra mappings and mappings) + private readonly List _allMappings = new(); + + public IReadOnlyCollection MethodMappings => _methodMappings; + + public IReadOnlyCollection All => _allMappings; + + public ITypeMapping? FindMapping(ITypeSymbol sourceType, ITypeSymbol targetType) + { + _mappings.TryGetValue(new TypeMappingKey(sourceType, targetType), out var mapping); + return mapping; + } + + public void AddMapping(ITypeMapping mapping) + { + _allMappings.Add(mapping); + if (mapping is MethodMapping methodMapping) + { + _methodMappings.Add(methodMapping); + } + + if (mapping.CallableByOtherMappings && FindMapping(mapping.SourceType, mapping.TargetType) is null) + { + _mappings.Add(new TypeMappingKey(mapping), mapping); + return; + } + + _extraMappings.Add(mapping); + } + + private readonly struct TypeMappingKey + { + private static readonly IEqualityComparer _comparer = SymbolEqualityComparer.IncludeNullability; + + private readonly ITypeSymbol _source; + private readonly ITypeSymbol _target; + + public TypeMappingKey(ITypeMapping mapping) + { + _source = mapping.SourceType; + _target = mapping.TargetType; + } + + public TypeMappingKey(ITypeSymbol source, ITypeSymbol target) + { + _source = source; + _target = target; + } + + private bool Equals(TypeMappingKey other) + => _comparer.Equals(_source, other._source) + && _comparer.Equals(_target, other._target); + + public override bool Equals(object? obj) + => obj is TypeMappingKey other && Equals(other); + + public override int GetHashCode() + { + unchecked + { + var hashCode = _comparer.GetHashCode(_source); + hashCode = (hashCode * 397) ^ _comparer.GetHashCode(_target); + return hashCode; + } + } + + public static bool operator ==(TypeMappingKey left, TypeMappingKey right) + => left.Equals(right); + + public static bool operator !=(TypeMappingKey left, TypeMappingKey right) + => !left.Equals(right); + } +} diff --git a/src/Riok.Mapperly/Descriptors/Mappings/ArrayCloneMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/ArrayCloneMapping.cs index 2da9d023e7..ea0c627a4d 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/ArrayCloneMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/ArrayCloneMapping.cs @@ -19,10 +19,10 @@ public ArrayCloneMapping( { } - public override ExpressionSyntax Build(ExpressionSyntax source) + public override ExpressionSyntax Build(TypeMappingBuildContext ctx) { return CastExpression( IdentifierName(TargetType.ToDisplayString()), - InvocationExpression(MemberAccess(source, CloneMethodName))); + InvocationExpression(MemberAccess(ctx.Source, CloneMethodName))); } } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/ArrayForMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/ArrayForMapping.cs index a372e9a259..2778614dd2 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/ArrayForMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/ArrayForMapping.cs @@ -12,30 +12,31 @@ public class ArrayForMapping : MethodMapping private const string LoopCounterName = "i"; private const string ArrayLengthProperty = nameof(Array.Length); - private readonly TypeMapping _elementMapping; + private readonly ITypeMapping _elementMapping; private readonly ITypeSymbol _targetArrayElementType; public ArrayForMapping( ITypeSymbol sourceType, ITypeSymbol targetType, - TypeMapping elementMapping, + ITypeMapping elementMapping, ITypeSymbol targetArrayElementType) : base(sourceType, targetType) { _elementMapping = elementMapping; _targetArrayElementType = targetArrayElementType; } - public override IEnumerable BuildBody(ExpressionSyntax source) + public override IEnumerable BuildBody(TypeMappingBuildContext ctx) { // var target = new T[source.Length]; - var sourceLengthArrayRank = ArrayRankSpecifier(SingletonSeparatedList(MemberAccess(source, ArrayLengthProperty))); + var sourceLengthArrayRank = ArrayRankSpecifier(SingletonSeparatedList(MemberAccess(ctx.Source, ArrayLengthProperty))); var targetInitializationValue = ArrayCreationExpression( ArrayType(IdentifierName(_targetArrayElementType.ToDisplayString())) .WithRankSpecifiers(SingletonList(sourceLengthArrayRank))); yield return DeclareLocalVariable(TargetVariableName, targetInitializationValue); // target[i] = Map(source[i]); - var mappedIndexedSourceValue = _elementMapping.Build(ElementAccess(source, IdentifierName(LoopCounterName))); + var forLoopBuilderCtx = ctx.WithSource(ElementAccess(ctx.Source, IdentifierName(LoopCounterName))); + var mappedIndexedSourceValue = _elementMapping.Build(forLoopBuilderCtx); var assignment = AssignmentExpression( SyntaxKind.SimpleAssignmentExpression, ElementAccess(IdentifierName(TargetVariableName), IdentifierName(LoopCounterName)), @@ -44,7 +45,7 @@ public override IEnumerable BuildBody(ExpressionSyntax source) // for(var i = 0; i < source.Length; i++) // target[i] = Map(source[i]); - yield return IncrementalForLoop(LoopCounterName, assignmentBlock, MemberAccess(source, ArrayLengthProperty)); + yield return IncrementalForLoop(LoopCounterName, assignmentBlock, MemberAccess(ctx.Source, ArrayLengthProperty)); // return target; yield return ReturnVariable(TargetVariableName); diff --git a/src/Riok.Mapperly/Descriptors/Mappings/CastMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/CastMapping.cs index 35e4aa97d3..c260c64977 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/CastMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/CastMapping.cs @@ -9,16 +9,19 @@ namespace Riok.Mapperly.Descriptors.Mappings; /// public class CastMapping : TypeMapping { - private readonly TypeMapping? _delegateMapping; + private readonly ITypeMapping? _delegateMapping; - public CastMapping(ITypeSymbol sourceType, ITypeSymbol targetType, TypeMapping? delegateMapping = null) + public CastMapping(ITypeSymbol sourceType, ITypeSymbol targetType, ITypeMapping? delegateMapping = null) : base(sourceType, targetType) { _delegateMapping = delegateMapping; } - public override ExpressionSyntax Build(ExpressionSyntax source) + public override ExpressionSyntax Build(TypeMappingBuildContext ctx) { - return CastExpression(IdentifierName(TargetType.ToDisplayString()), _delegateMapping != null ? _delegateMapping.Build(source) : source); + var objToCast = _delegateMapping != null + ? _delegateMapping.Build(ctx) + : ctx.Source; + return CastExpression(IdentifierName(TargetType.ToDisplayString()), objToCast); } } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/CtorMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/CtorMapping.cs index 11b547fe71..faac3e3c34 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/CtorMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/CtorMapping.cs @@ -11,13 +11,14 @@ namespace Riok.Mapperly.Descriptors.Mappings; /// public class CtorMapping : TypeMapping { - public CtorMapping(ITypeSymbol sourceType, ITypeSymbol targetType) : base(sourceType, targetType) + public CtorMapping(ITypeSymbol sourceType, ITypeSymbol targetType) + : base(sourceType, targetType) { } - public override ExpressionSyntax Build(ExpressionSyntax source) + public override ExpressionSyntax Build(TypeMappingBuildContext ctx) { var type = IdentifierName(TargetType.NonNullable().ToDisplayString()); - return ObjectCreationExpression(type).WithArgumentList(ArgumentList(source)); + return ObjectCreationExpression(type).WithArgumentList(ArgumentList(ctx.Source)); } } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/DirectAssignmentMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/DirectAssignmentMapping.cs index 47af49e836..fad59c53f8 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/DirectAssignmentMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/DirectAssignmentMapping.cs @@ -13,8 +13,8 @@ public DirectAssignmentMapping(ITypeSymbol type) : base(type, type) { } - public override ExpressionSyntax Build(ExpressionSyntax source) - => source; + public override ExpressionSyntax Build(TypeMappingBuildContext ctx) + => ctx.Source; public override bool IsSynthetic => true; } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/EnumFromStringMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/EnumFromStringMapping.cs index 9711c3a617..0b2494757f 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/EnumFromStringMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/EnumFromStringMapping.cs @@ -32,12 +32,12 @@ public EnumFromStringMapping( _ignoreCase = ignoreCase; } - public override IEnumerable BuildBody(ExpressionSyntax source) + public override IEnumerable BuildBody(TypeMappingBuildContext ctx) { // fallback switch arm: _ => (TargetType)System.Enum.Parse(typeof(TargetType), source, ignoreCase) var enumParseInvocation = Invocation( MemberAccess(EnumClassName, ParseMethodName), - TypeOfExpression(IdentifierName(TargetType.ToDisplayString())), source, BooleanLiteral(_ignoreCase)); + TypeOfExpression(IdentifierName(TargetType.ToDisplayString())), ctx.Source, BooleanLiteral(_ignoreCase)); var fallbackArm = SwitchExpressionArm( DiscardPattern(), CastExpression(IdentifierName(TargetType.ToDisplayString()), enumParseInvocation)); @@ -48,7 +48,7 @@ public override IEnumerable BuildBody(ExpressionSyntax source) : _enumMembers.Select(BuildArm); arms = arms.Append(fallbackArm); - var switchExpr = SwitchExpression(source) + var switchExpr = SwitchExpression(ctx.Source) .WithArms(CommaSeparatedList(arms, true)); yield return ReturnStatement(switchExpr); diff --git a/src/Riok.Mapperly/Descriptors/Mappings/EnumNameMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/EnumNameMapping.cs index 921db83500..55c3c22e06 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/EnumNameMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/EnumNameMapping.cs @@ -23,12 +23,12 @@ public EnumNameMapping( _enumMemberMappings = enumMemberMappings; } - public override IEnumerable BuildBody(ExpressionSyntax source) + public override IEnumerable BuildBody(TypeMappingBuildContext ctx) { // fallback switch arm: _ => throw new ArgumentOutOfRangeException("source"); var fallbackArm = SwitchExpressionArm( DiscardPattern(), - ThrowArgumentOutOfRangeException(source)); + ThrowArgumentOutOfRangeException(ctx.Source)); // switch for each name to the enum value // eg: Enum1.Value1 => Enum2.Value1, @@ -36,7 +36,7 @@ public override IEnumerable BuildBody(ExpressionSyntax source) .Select(BuildArm) .Append(fallbackArm); - var switchExpr = SwitchExpression(source) + var switchExpr = SwitchExpression(ctx.Source) .WithArms(CommaSeparatedList(arms, true)); yield return ReturnStatement(switchExpr); diff --git a/src/Riok.Mapperly/Descriptors/Mappings/EnumToStringMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/EnumToStringMapping.cs index b1a463887b..8887c852f9 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/EnumToStringMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/EnumToStringMapping.cs @@ -26,12 +26,12 @@ public EnumToStringMapping( _enumMembers = enumMembers; } - public override IEnumerable BuildBody(ExpressionSyntax source) + public override IEnumerable BuildBody(TypeMappingBuildContext ctx) { // fallback switch arm: _ => source.ToString() var fallbackArm = SwitchExpressionArm( DiscardPattern(), - Invocation(MemberAccess(source, ToStringMethodName))); + Invocation(MemberAccess(ctx.Source, ToStringMethodName))); // switch for each name to the enum value // eg: Enum1.Value1 => "Value1" @@ -39,7 +39,7 @@ public override IEnumerable BuildBody(ExpressionSyntax source) .Select(BuildArm) .Append(fallbackArm); - var switchExpr = SwitchExpression(source) + var switchExpr = SwitchExpression(ctx.Source) .WithArms(CommaSeparatedList(arms, true)); yield return ReturnStatement(switchExpr); diff --git a/src/Riok.Mapperly/Descriptors/Mappings/ForEachAddDictionaryMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/ForEachAddDictionaryMapping.cs index 40b3a1fe12..a8ebdf9ae1 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/ForEachAddDictionaryMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/ForEachAddDictionaryMapping.cs @@ -19,8 +19,8 @@ public class ForEachAddDictionaryMapping : MethodMapping private const string KeyValueValuePropertyName = "Value"; private const string CountPropertyName = "Count"; - private readonly TypeMapping _keyMapping; - private readonly TypeMapping _valueMapping; + private readonly ITypeMapping _keyMapping; + private readonly ITypeMapping _valueMapping; private readonly bool _sourceHasCount; private readonly ObjectFactory? _objectFactory; private readonly ITypeSymbol _typeToInstantiate; @@ -28,8 +28,8 @@ public class ForEachAddDictionaryMapping : MethodMapping public ForEachAddDictionaryMapping( ITypeSymbol sourceType, ITypeSymbol targetType, - TypeMapping keyMapping, - TypeMapping valueMapping, + ITypeMapping keyMapping, + ITypeMapping valueMapping, bool sourceHasCount, ITypeSymbol? typeToInstantiate = null, ObjectFactory? objectFactory = null) @@ -42,18 +42,18 @@ public ForEachAddDictionaryMapping( _typeToInstantiate = typeToInstantiate ?? targetType; } - public override IEnumerable BuildBody(ExpressionSyntax source) + public override IEnumerable BuildBody(TypeMappingBuildContext ctx) { - var convertedKeyExpression = _keyMapping.Build(MemberAccess(LoopItemVariableName, KeyValueKeyPropertyName)); - var convertedValueExpression = _valueMapping.Build(MemberAccess(LoopItemVariableName, KeyValueValuePropertyName)); + var convertedKeyExpression = _keyMapping.Build(ctx.WithSource(MemberAccess(LoopItemVariableName, KeyValueKeyPropertyName))); + var convertedValueExpression = _valueMapping.Build(ctx.WithSource(MemberAccess(LoopItemVariableName, KeyValueValuePropertyName))); if (_objectFactory != null) { - yield return DeclareLocalVariable(TargetVariableName, _objectFactory.CreateType(SourceType, _typeToInstantiate, source)); + yield return DeclareLocalVariable(TargetVariableName, _objectFactory.CreateType(SourceType, _typeToInstantiate, ctx.Source)); } else if (_sourceHasCount) { - yield return CreateInstance(TargetVariableName, _typeToInstantiate, MemberAccess(source, CountPropertyName)); + yield return CreateInstance(TargetVariableName, _typeToInstantiate, MemberAccess(ctx.Source, CountPropertyName)); } else { @@ -64,7 +64,7 @@ public override IEnumerable BuildBody(ExpressionSyntax source) yield return ForEachStatement( VarIdentifier, Identifier(LoopItemVariableName), - source, + ctx.Source, Block(ExpressionStatement(Invocation(addMethod, convertedKeyExpression, convertedValueExpression)))); yield return ReturnVariable(TargetVariableName); } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/ForEachAddEnumerableMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/ForEachAddEnumerableMapping.cs index 38728454b9..c6c4129d08 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/ForEachAddEnumerableMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/ForEachAddEnumerableMapping.cs @@ -16,13 +16,13 @@ public class ForEachAddEnumerableMapping : MethodMapping private const string LoopItemVariableName = "item"; private const string AddMethodName = "Add"; - private readonly TypeMapping _elementMapping; + private readonly ITypeMapping _elementMapping; private readonly ObjectFactory? _objectFactory; public ForEachAddEnumerableMapping( ITypeSymbol sourceType, ITypeSymbol targetType, - TypeMapping elementMapping, + ITypeMapping elementMapping, ObjectFactory? objectFactory) : base(sourceType, targetType) { @@ -30,18 +30,18 @@ public ForEachAddEnumerableMapping( _objectFactory = objectFactory; } - public override IEnumerable BuildBody(ExpressionSyntax source) + public override IEnumerable BuildBody(TypeMappingBuildContext ctx) { yield return _objectFactory == null ? CreateInstance(TargetVariableName, TargetType) - : DeclareLocalVariable(TargetVariableName, _objectFactory.CreateType(SourceType, TargetType, source)); + : DeclareLocalVariable(TargetVariableName, _objectFactory.CreateType(SourceType, TargetType, ctx.Source)); - var convertedSourceItemExpression = _elementMapping.Build(IdentifierName(LoopItemVariableName)); + var convertedSourceItemExpression = _elementMapping.Build(ctx.WithSource(LoopItemVariableName)); var addMethod = MemberAccess(TargetVariableName, AddMethodName); yield return ForEachStatement( VarIdentifier, Identifier(LoopItemVariableName), - source, + ctx.Source, Block(ExpressionStatement(Invocation(addMethod, convertedSourceItemExpression)))); yield return ReturnVariable(TargetVariableName); } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/ITypeMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/ITypeMapping.cs new file mode 100644 index 0000000000..9bbae194bc --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/Mappings/ITypeMapping.cs @@ -0,0 +1,27 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Riok.Mapperly.Descriptors.Mappings; + +/// +/// Represents a mapping from one type to another. +/// +public interface ITypeMapping +{ + ITypeSymbol SourceType { get; } + + ITypeSymbol TargetType { get; } + + /// + /// Gets a value indicating if this mapping can be called / built by another mapping. + /// This should be true for most mappings. + /// + bool CallableByOtherMappings { get; } + + /// + /// Gets a value indicating whether this mapping produces any code or can be omitted completely (eg. direct assignments or delegate mappings). + /// + bool IsSynthetic { get; } + + ExpressionSyntax Build(TypeMappingBuildContext ctx); +} diff --git a/src/Riok.Mapperly/Descriptors/Mappings/IUserMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/IUserMapping.cs index 53b696806b..466fb75f90 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/IUserMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/IUserMapping.cs @@ -5,7 +5,7 @@ namespace Riok.Mapperly.Descriptors.Mappings; /// /// A user defined / implemented mapping. /// -public interface IUserMapping +public interface IUserMapping : ITypeMapping { IMethodSymbol Method { get; } } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/LinqEnumerableMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/LinqEnumerableMapping.cs index a401317521..66d6bc41c2 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/LinqEnumerableMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/LinqEnumerableMapping.cs @@ -12,14 +12,14 @@ public class LinqEnumerableMapping : TypeMapping { private const string LambdaParamName = "x"; - private readonly TypeMapping _elementMapping; + private readonly ITypeMapping _elementMapping; private readonly IMethodSymbol? _selectMethod; private readonly IMethodSymbol? _collectMethod; public LinqEnumerableMapping( ITypeSymbol sourceType, ITypeSymbol targetType, - TypeMapping elementMapping, + ITypeMapping elementMapping, IMethodSymbol? selectMethod, IMethodSymbol? collectMethod) : base(sourceType, targetType) @@ -29,21 +29,21 @@ public LinqEnumerableMapping( _collectMethod = collectMethod; } - public override ExpressionSyntax Build(ExpressionSyntax source) + public override ExpressionSyntax Build(TypeMappingBuildContext ctx) { ExpressionSyntax mappedSource; // Select / Map if needed if (_selectMethod != null) { - var sourceMapExpression = _elementMapping.Build(IdentifierName(LambdaParamName)); + var sourceMapExpression = _elementMapping.Build(ctx.WithSource(LambdaParamName)); var convertLambda = SimpleLambdaExpression(Parameter(Identifier(LambdaParamName))) .WithExpressionBody(sourceMapExpression); - mappedSource = StaticInvocation(_selectMethod, source, convertLambda); + mappedSource = StaticInvocation(_selectMethod, ctx.Source, convertLambda); } else { - mappedSource = _elementMapping.Build(source); + mappedSource = _elementMapping.Build(ctx); } return _collectMethod == null diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs index 03e77a48a8..d9a8c96f63 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs @@ -2,6 +2,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Riok.Mapperly.Emit; +using Riok.Mapperly.Emit.Symbols; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using static Riok.Mapperly.Emit.SyntaxFactoryHelper; @@ -12,11 +13,23 @@ namespace Riok.Mapperly.Descriptors.Mappings; /// public abstract class MethodMapping : TypeMapping { - private const string DefaultSourceParamName = "source"; + protected const string DefaultReferenceHandlerParameterName = "refHandler"; + private const string DefaultSourceParameterName = "source"; + + private const int SourceParameterIndex = 0; + private const int ReferenceHandlerParameterIndex = 1; + private string? _methodName; - protected MethodMapping(ITypeSymbol sourceType, ITypeSymbol targetType) : base(sourceType, targetType) + protected MethodMapping(ITypeSymbol sourceType, ITypeSymbol targetType) + : this(new MethodParameter(SourceParameterIndex, DefaultSourceParameterName, sourceType), targetType) + { + } + + protected MethodMapping(MethodParameter sourceParameter, ITypeSymbol targetType) + : base(sourceParameter.Type, targetType) { + SourceParameter = sourceParameter; } protected Accessibility Accessibility { get; set; } = Accessibility.Private; @@ -31,51 +44,43 @@ protected string MethodName set => _methodName = value; } - protected string MappingSourceParameterName - { - get; - set; - } = DefaultSourceParamName; + protected MethodParameter SourceParameter { get; } + + protected MethodParameter? ReferenceHandlerParameter { get; set; } + + protected virtual ITypeSymbol? ReturnType => TargetType; - public override ExpressionSyntax Build(ExpressionSyntax source) - => Invocation(MethodName, source); + public override ExpressionSyntax Build(TypeMappingBuildContext ctx) + => Invocation(MethodName, SourceParameter.WithArgument(ctx.Source), ReferenceHandlerParameter?.WithArgument(ctx.ReferenceHandler)); - public MethodDeclarationSyntax BuildMethod(SourceEmitterContext context) + public MethodDeclarationSyntax BuildMethod(SourceEmitterContext ctx) { TypeSyntax returnType = ReturnType == null ? PredefinedType(Token(SyntaxKind.VoidKeyword)) : IdentifierName(TargetType.ToDisplayString()); + var typeMappingBuildContext = new TypeMappingBuildContext(SourceParameter.Name, ReferenceHandlerParameter?.Name); + return MethodDeclaration(returnType, Identifier(MethodName)) - .WithModifiers(TokenList(BuildModifiers(context.IsStatic))) + .WithModifiers(TokenList(BuildModifiers(ctx.IsStatic))) .WithParameterList(BuildParameterList()) - .WithBody(Block(BuildBody(IdentifierName(MappingSourceParameterName)))); + .WithBody(Block(BuildBody(typeMappingBuildContext))); } - public abstract IEnumerable BuildBody(ExpressionSyntax source); + public abstract IEnumerable BuildBody(TypeMappingBuildContext ctx); internal void SetMethodNameIfNeeded(Func methodNameBuilder) { _methodName ??= methodNameBuilder(this); } - protected virtual ITypeSymbol? ReturnType => TargetType; - - protected virtual IEnumerable BuildParameters() + internal virtual void EnableReferenceHandling(INamedTypeSymbol iReferenceHandlerType) { - return new[] - { - Parameter(Identifier(MappingSourceParameterName)) - .WithType(IdentifierName(SourceType.ToDisplayString())) - .WithModifiers(TokenList(BuildParameterModifiers())), - }; + ReferenceHandlerParameter ??= new MethodParameter(ReferenceHandlerParameterIndex, DefaultReferenceHandlerParameterName, iReferenceHandlerType); } - private IEnumerable BuildParameterModifiers() - { - if (IsExtensionMethod) - yield return Token(SyntaxKind.ThisKeyword); - } + protected virtual ParameterListSyntax BuildParameterList() + => ParameterList(IsExtensionMethod, SourceParameter, ReferenceHandlerParameter); private IEnumerable BuildModifiers(bool isStatic) { @@ -87,9 +92,4 @@ private IEnumerable BuildModifiers(bool isStatic) if (IsPartial) yield return Token(SyntaxKind.PartialKeyword); } - - private ParameterListSyntax BuildParameterList() - { - return ParameterList(CommaSeparatedList(BuildParameters())); - } } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/NewInstanceObjectFactoryPropertyMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/NewInstanceObjectFactoryPropertyMapping.cs index 06251c4d0d..d71d72e023 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/NewInstanceObjectFactoryPropertyMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/NewInstanceObjectFactoryPropertyMapping.cs @@ -1,6 +1,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Riok.Mapperly.Descriptors.ObjectFactories; +using Riok.Mapperly.Emit; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using static Riok.Mapperly.Emit.SyntaxFactoryHelper; @@ -13,23 +14,43 @@ public class NewInstanceObjectFactoryPropertyMapping : ObjectPropertyMapping { private const string TargetVariableName = "target"; private readonly ObjectFactory _objectFactory; + private readonly bool _enableReferenceHandling; public NewInstanceObjectFactoryPropertyMapping( ITypeSymbol sourceType, ITypeSymbol targetType, - ObjectFactory objectFactory) + ObjectFactory objectFactory, + bool enableReferenceHandling) : base(sourceType, targetType) { _objectFactory = objectFactory; + _enableReferenceHandling = enableReferenceHandling; } - public override IEnumerable BuildBody(ExpressionSyntax source) + public override IEnumerable BuildBody(TypeMappingBuildContext ctx) { + if (_enableReferenceHandling) + { + // TryGetReference + yield return ReferenceHandlingSyntaxFactoryHelper.TryGetReference(this, ctx); + } + // var target = CreateMyObject(); - yield return DeclareLocalVariable(TargetVariableName, _objectFactory.CreateType(SourceType, TargetType, source)); + yield return DeclareLocalVariable(TargetVariableName, _objectFactory.CreateType(SourceType, 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 ExpressionStatement(ReferenceHandlingSyntaxFactoryHelper.SetReference( + this, + ctx, + IdentifierName(TargetVariableName))); + } // map properties - foreach (var expression in BuildBody(source, IdentifierName(TargetVariableName))) + foreach (var expression in BuildBody(ctx, IdentifierName(TargetVariableName))) { yield return expression; } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/NewInstanceObjectPropertyMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/NewInstanceObjectPropertyMapping.cs index 1e9355f031..8d469c9e1f 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/NewInstanceObjectPropertyMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/NewInstanceObjectPropertyMapping.cs @@ -1,6 +1,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Riok.Mapperly.Descriptors.Mappings.PropertyMappings; +using Riok.Mapperly.Emit; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using static Riok.Mapperly.Emit.SyntaxFactoryHelper; @@ -14,12 +15,15 @@ public class NewInstanceObjectPropertyMapping : ObjectPropertyMapping private const string TargetVariableName = "target"; private readonly HashSet _constructorPropertyMappings = new(); private readonly HashSet _initPropertyMappings = new(); + private readonly bool _enableReferenceHandling; public NewInstanceObjectPropertyMapping( ITypeSymbol sourceType, - ITypeSymbol targetType) + ITypeSymbol targetType, + bool enableReferenceHandling) : base(sourceType, targetType) { + _enableReferenceHandling = enableReferenceHandling; } public void AddConstructorParameterMapping(ConstructorParameterMapping mapping) @@ -28,17 +32,23 @@ public void AddConstructorParameterMapping(ConstructorParameterMapping mapping) public void AddInitPropertyMapping(PropertyAssignmentMapping mapping) => _initPropertyMappings.Add(mapping); - public override IEnumerable BuildBody(ExpressionSyntax source) + public override IEnumerable BuildBody(TypeMappingBuildContext ctx) { - // new T() { ... }; - var ctorArgs = _constructorPropertyMappings.Select(x => x.BuildArgument(source)).ToArray(); + if (_enableReferenceHandling) + { + // TryGetReference + yield return ReferenceHandlingSyntaxFactoryHelper.TryGetReference(this, ctx); + } + + // new T(ctorArgs) { ... }; + var ctorArgs = _constructorPropertyMappings.Select(x => x.BuildArgument(ctx)).ToArray(); var objectCreationExpression = CreateInstance(TargetType, ctorArgs); // add initializer if (_initPropertyMappings.Count > 0) { var initMappings = _initPropertyMappings - .Select(x => x.BuildExpression(source, null)) + .Select(x => x.BuildExpression(ctx, null)) .ToArray(); objectCreationExpression = objectCreationExpression.WithInitializer(ObjectInitializer(initMappings)); } @@ -46,8 +56,19 @@ public override IEnumerable BuildBody(ExpressionSyntax source) // var target = new T() { ... }; yield return 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 ExpressionStatement(ReferenceHandlingSyntaxFactoryHelper.SetReference( + this, + ctx, + IdentifierName(TargetVariableName))); + } + // map properties - foreach (var expression in BuildBody(source, IdentifierName(TargetVariableName))) + foreach (var expression in BuildBody(ctx, IdentifierName(TargetVariableName))) { yield return expression; } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/NullDelegateMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/NullDelegateMapping.cs index e9da8b1965..c19964e94a 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/NullDelegateMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/NullDelegateMapping.cs @@ -13,13 +13,13 @@ public class NullDelegateMapping : TypeMapping { private const string NullableValueProperty = "Value"; - private readonly TypeMapping _delegateMapping; + private readonly ITypeMapping _delegateMapping; private readonly NullFallbackValue _nullFallbackValue; public NullDelegateMapping( ITypeSymbol nullableSourceType, ITypeSymbol nullableTargetType, - TypeMapping delegateMapping, + ITypeMapping delegateMapping, NullFallbackValue nullFallbackValue) : base(nullableSourceType, nullableTargetType) { @@ -37,18 +37,18 @@ public NullDelegateMapping( public override bool IsSynthetic { get; } - public override ExpressionSyntax Build(ExpressionSyntax source) + public override ExpressionSyntax Build(TypeMappingBuildContext ctx) { if (_delegateMapping.SourceType.IsNullable()) - return _delegateMapping.Build(source); + return _delegateMapping.Build(ctx); if (!SourceType.IsNullable()) { // if the target type is a nullable value type, there needs to be an additional cast // eg. int => int? needs to be casted. return TargetType.IsNullableValueType() - ? CastExpression(IdentifierName(TargetType.ToDisplayString()), _delegateMapping.Build(source)) - : _delegateMapping.Build(source); + ? CastExpression(IdentifierName(TargetType.ToDisplayString()), _delegateMapping.Build(ctx)) + : _delegateMapping.Build(ctx); } // source is nullable and the mapping method cannot handle nulls, @@ -57,12 +57,12 @@ public override ExpressionSyntax Build(ExpressionSyntax source) // or for nullable value types: // source == null ? : Map(source.Value) var sourceValue = SourceType.IsNullableValueType() - ? MemberAccess(source, NullableValueProperty) - : source; + ? MemberAccess(ctx.Source, NullableValueProperty) + : ctx.Source; return ConditionalExpression( - IsNull(source), - NullSubstitute(TargetType.NonNullable(), source, _nullFallbackValue), - _delegateMapping.Build(sourceValue)); + IsNull(ctx.Source), + NullSubstitute(TargetType.NonNullable(), ctx.Source, _nullFallbackValue), + _delegateMapping.Build(ctx.WithSource(sourceValue))); } } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/NullDelegateMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/NullDelegateMethodMapping.cs index dac5c589a9..24653ed03b 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/NullDelegateMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/NullDelegateMethodMapping.cs @@ -25,10 +25,10 @@ public NullDelegateMethodMapping( _nullFallbackValue = nullFallbackValue; } - public override IEnumerable BuildBody(ExpressionSyntax source) + public override IEnumerable BuildBody(TypeMappingBuildContext ctx) { - var body = _delegateMapping.BuildBody(source); - return AddPreNullHandling(source, body); + var body = _delegateMapping.BuildBody(ctx); + return AddPreNullHandling(ctx.Source, body); } private IEnumerable AddPreNullHandling(ExpressionSyntax source, IEnumerable body) diff --git a/src/Riok.Mapperly/Descriptors/Mappings/ObjectPropertyMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/ObjectPropertyMapping.cs index 672a6085c6..626c6b6c4e 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/ObjectPropertyMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/ObjectPropertyMapping.cs @@ -1,6 +1,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Riok.Mapperly.Descriptors.Mappings.PropertyMappings; +using Riok.Mapperly.Emit.Symbols; namespace Riok.Mapperly.Descriptors.Mappings; @@ -12,7 +13,13 @@ public abstract class ObjectPropertyMapping : MethodMapping, IPropertyAssignment { private readonly HashSet _mappings = new(); - protected ObjectPropertyMapping(ITypeSymbol sourceType, ITypeSymbol targetType) : base(sourceType, targetType) + protected ObjectPropertyMapping(ITypeSymbol sourceType, ITypeSymbol targetType) + : base(sourceType, targetType) + { + } + + protected ObjectPropertyMapping(MethodParameter sourceParameter, ITypeSymbol targetType) + : base(sourceParameter, targetType) { } @@ -30,6 +37,6 @@ public void AddPropertyMappings(IEnumerable mappings public bool HasPropertyMapping(IPropertyAssignmentMapping mapping) => _mappings.Contains(mapping); - protected IEnumerable BuildBody(ExpressionSyntax source, ExpressionSyntax target) - => _mappings.Select(x => x.Build(source, target)); + protected IEnumerable BuildBody(TypeMappingBuildContext ctx, ExpressionSyntax target) + => _mappings.Select(x => x.Build(ctx, target)); } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/ConstructorParameterMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/ConstructorParameterMapping.cs index e0e468bf5d..410a137c2d 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/ConstructorParameterMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/ConstructorParameterMapping.cs @@ -21,9 +21,9 @@ public ConstructorParameterMapping( public NullPropertyMapping DelegateMapping { get; } - public ArgumentSyntax BuildArgument(ExpressionSyntax source) + public ArgumentSyntax BuildArgument(TypeMappingBuildContext ctx) { - var argumentExpression = DelegateMapping.Build(source); + var argumentExpression = DelegateMapping.Build(ctx); var arg = Argument(argumentExpression); return _selfOrPreviousIsUnmappedOptional ? arg.WithNameColon(NameColon(_parameter.Name)) diff --git a/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/IPropertyAssignmentMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/IPropertyAssignmentMapping.cs index dccaf806c2..5168c35d8f 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/IPropertyAssignmentMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/IPropertyAssignmentMapping.cs @@ -8,6 +8,6 @@ namespace Riok.Mapperly.Descriptors.Mappings.PropertyMappings; public interface IPropertyAssignmentMapping { StatementSyntax Build( - ExpressionSyntax sourceAccess, + TypeMappingBuildContext ctx, ExpressionSyntax targetAccess); } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/IPropertyMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/IPropertyMapping.cs index 08cdf878aa..a381cf5055 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/IPropertyMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/IPropertyMapping.cs @@ -10,5 +10,5 @@ public interface IPropertyMapping { PropertyPath SourcePath { get; } - ExpressionSyntax Build(ExpressionSyntax source); + ExpressionSyntax Build(TypeMappingBuildContext ctx); } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/NullPropertyMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/NullPropertyMapping.cs index 644f843c4a..4742e75f98 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/NullPropertyMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/NullPropertyMapping.cs @@ -13,10 +13,10 @@ namespace Riok.Mapperly.Descriptors.Mappings.PropertyMappings; [DebuggerDisplay("NullPropertyMapping({SourcePath}: {_delegateMapping})")] public class NullPropertyMapping : IPropertyMapping { - private readonly TypeMapping _delegateMapping; + private readonly ITypeMapping _delegateMapping; private readonly NullFallbackValue _nullFallback; - public NullPropertyMapping(TypeMapping delegateMapping, PropertyPath sourcePath, NullFallbackValue nullFallback) + public NullPropertyMapping(ITypeMapping delegateMapping, PropertyPath sourcePath, NullFallbackValue nullFallback) { SourcePath = sourcePath; _delegateMapping = delegateMapping; @@ -25,10 +25,13 @@ public NullPropertyMapping(TypeMapping delegateMapping, PropertyPath sourcePath, public PropertyPath SourcePath { get; } - public ExpressionSyntax Build(ExpressionSyntax source) + public ExpressionSyntax Build(TypeMappingBuildContext ctx) { if (_delegateMapping.SourceType.IsNullable() || !SourcePath.IsAnyNullable()) - return _delegateMapping.Build(SourcePath.BuildAccess(source, nullConditional: true)); + { + ctx = ctx.WithSource(SourcePath.BuildAccess(ctx.Source, nullConditional: true)); + return _delegateMapping.Build(ctx); + } // source is nullable and the mapping method cannot handle nulls, // call mapping only if source is not null. @@ -39,18 +42,18 @@ public ExpressionSyntax Build(ExpressionSyntax source) // source.A?.B ?? if (_delegateMapping.IsSynthetic) { - var nullConditionalSourceAccess = SourcePath.BuildAccess(source, nullConditional: true); + var nullConditionalSourceAccess = SourcePath.BuildAccess(ctx.Source, nullConditional: true); return Coalesce( - _delegateMapping.Build(nullConditionalSourceAccess), + _delegateMapping.Build(ctx.WithSource(nullConditionalSourceAccess)), NullSubstitute(_delegateMapping.TargetType, nullConditionalSourceAccess, _nullFallback)); } - var nullCheckPath = SourcePath.BuildAccess(source, nullConditional: true, skipTrailingNonNullable: true); - var sourcePropertyAccess = SourcePath.BuildAccess(source, true); + var nullCheckPath = SourcePath.BuildAccess(ctx.Source, nullConditional: true, skipTrailingNonNullable: true); + var sourcePropertyAccess = SourcePath.BuildAccess(ctx.Source, true); return ConditionalExpression( IsNull(nullCheckPath), NullSubstitute(_delegateMapping.TargetType, sourcePropertyAccess, _nullFallback), - _delegateMapping.Build(sourcePropertyAccess)); + _delegateMapping.Build(ctx.WithSource(sourcePropertyAccess))); } protected bool Equals(NullPropertyMapping other) diff --git a/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/PropertyAssignmentMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/PropertyAssignmentMapping.cs index 32e0489342..1d429c6943 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/PropertyAssignmentMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/PropertyAssignmentMapping.cs @@ -27,18 +27,18 @@ public PropertyAssignmentMapping( public PropertyPath TargetPath { get; } public StatementSyntax Build( - ExpressionSyntax sourceAccess, + TypeMappingBuildContext ctx, ExpressionSyntax targetAccess) { - return ExpressionStatement(BuildExpression(sourceAccess, targetAccess)); + return ExpressionStatement(BuildExpression(ctx, targetAccess)); } public ExpressionSyntax BuildExpression( - ExpressionSyntax sourceAccess, + TypeMappingBuildContext ctx, ExpressionSyntax? targetAccess) { var targetPropertyAccess = TargetPath.BuildAccess(targetAccess); - var mappedValue = _mapping.Build(sourceAccess); + var mappedValue = _mapping.Build(ctx); // target.Property = mappedValue; return AssignmentExpression( diff --git a/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/PropertyMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/PropertyMapping.cs index 1166bd4de9..fdfc23810b 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/PropertyMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/PropertyMapping.cs @@ -8,11 +8,11 @@ namespace Riok.Mapperly.Descriptors.Mappings.PropertyMappings; /// public class PropertyMapping : IPropertyMapping { - private readonly TypeMapping _delegateMapping; + private readonly ITypeMapping _delegateMapping; private readonly bool _nullConditionalAccess; private readonly bool _addValuePropertyOnNullable; - public PropertyMapping(TypeMapping delegateMapping, PropertyPath sourcePath, bool nullConditionalAccess, bool addValuePropertyOnNullable) + public PropertyMapping(ITypeMapping delegateMapping, PropertyPath sourcePath, bool nullConditionalAccess, bool addValuePropertyOnNullable) { _delegateMapping = delegateMapping; SourcePath = sourcePath; @@ -22,6 +22,9 @@ public PropertyMapping(TypeMapping delegateMapping, PropertyPath sourcePath, boo public PropertyPath SourcePath { get; } - public ExpressionSyntax Build(ExpressionSyntax source) - => _delegateMapping.Build(SourcePath.BuildAccess(source, _addValuePropertyOnNullable, _nullConditionalAccess)); + public ExpressionSyntax Build(TypeMappingBuildContext ctx) + { + ctx = ctx.WithSource(SourcePath.BuildAccess(ctx.Source, _addValuePropertyOnNullable, _nullConditionalAccess)); + return _delegateMapping.Build(ctx); + } } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/PropertyNullAssignmentInitializerMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/PropertyNullAssignmentInitializerMapping.cs index eb8bf2e853..0d0e4b90dc 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/PropertyNullAssignmentInitializerMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/PropertyNullAssignmentInitializerMapping.cs @@ -18,7 +18,7 @@ public PropertyNullAssignmentInitializerMapping(PropertyPath pathToInitialize) _pathToInitialize = pathToInitialize; } - public StatementSyntax Build(ExpressionSyntax sourceAccess, ExpressionSyntax targetAccess) + public StatementSyntax Build(TypeMappingBuildContext ctx, ExpressionSyntax targetAccess) { // source.Value ??= new(); return ExpressionStatement( diff --git a/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/PropertyNullDelegateAssignmentMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/PropertyNullDelegateAssignmentMapping.cs index 9c8668d984..07e749e926 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/PropertyNullDelegateAssignmentMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/PropertyMappings/PropertyNullDelegateAssignmentMapping.cs @@ -27,20 +27,20 @@ public PropertyNullDelegateAssignmentMapping( } public StatementSyntax Build( - ExpressionSyntax sourceAccess, + TypeMappingBuildContext ctx, ExpressionSyntax targetAccess) { // if (source.Value != null) // target.Value = Map(Source.Name); // else // throw ... - var sourceNullConditionalAccess = _nullConditionalSourcePath.BuildAccess(sourceAccess, true, true, true); + var sourceNullConditionalAccess = _nullConditionalSourcePath.BuildAccess(ctx.Source, true, true, true); var condition = IsNotNull(sourceNullConditionalAccess); var elseClause = _throwInsteadOfConditionalNullMapping ? ElseClause(Block(ExpressionStatement(ThrowArgumentNullException(sourceNullConditionalAccess)))) : null; - var mappings = _delegateMappings.Select(m => m.Build(sourceAccess, targetAccess)).ToList(); + var mappings = _delegateMappings.Select(m => m.Build(ctx, targetAccess)).ToList(); return IfStatement(condition, Block(mappings), elseClause); } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/SourceObjectMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/SourceObjectMethodMapping.cs index 0af7209a87..fc74f17a71 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/SourceObjectMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/SourceObjectMethodMapping.cs @@ -17,6 +17,6 @@ public SourceObjectMethodMapping(ITypeSymbol sourceType, ITypeSymbol targetType, _methodName = methodName; } - public override ExpressionSyntax Build(ExpressionSyntax source) - => InvocationExpression(MemberAccess(source, _methodName)); + public override ExpressionSyntax Build(TypeMappingBuildContext ctx) + => InvocationExpression(MemberAccess(ctx.Source, _methodName)); } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/StaticMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/StaticMethodMapping.cs index 4173922e68..c312cf4eab 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/StaticMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/StaticMethodMapping.cs @@ -16,6 +16,6 @@ public StaticMethodMapping(IMethodSymbol method) : base(method.Parameters.Single _method = method; } - public override ExpressionSyntax Build(ExpressionSyntax source) - => StaticInvocation(_method, source); + public override ExpressionSyntax Build(TypeMappingBuildContext ctx) + => StaticInvocation(_method, ctx.Source); } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/TypeMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/TypeMapping.cs index 89b18539b4..46a0b6a5d9 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/TypeMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/TypeMapping.cs @@ -4,11 +4,9 @@ namespace Riok.Mapperly.Descriptors.Mappings; -/// -/// Represents a mapping to map from one type to another. -/// +/// [DebuggerDisplay("{GetType()}({SourceType.Name} => {TargetType.Name})")] -public abstract class TypeMapping +public abstract class TypeMapping : ITypeMapping { protected TypeMapping(ITypeSymbol sourceType, ITypeSymbol targetType) { @@ -20,16 +18,11 @@ protected TypeMapping(ITypeSymbol sourceType, ITypeSymbol targetType) public ITypeSymbol TargetType { get; } - /// - /// Gets a value indicating if this mapping can be called / built by another mapping. - /// This should be true for most mappings. - /// + /// public virtual bool CallableByOtherMappings => true; - /// - /// Gets a value indicating whether this mapping produces any code or can be omitted completely (eg. direct assignments or delegate mappings). - /// + /// public virtual bool IsSynthetic => false; - public abstract ExpressionSyntax Build(ExpressionSyntax source); + public abstract ExpressionSyntax Build(TypeMappingBuildContext ctx); } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/TypeMappingBuildContext.cs b/src/Riok.Mapperly/Descriptors/Mappings/TypeMappingBuildContext.cs new file mode 100644 index 0000000000..99dfa0c94c --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/Mappings/TypeMappingBuildContext.cs @@ -0,0 +1,34 @@ +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace Riok.Mapperly.Descriptors.Mappings; + +public readonly struct TypeMappingBuildContext +{ + public TypeMappingBuildContext(string source, string? referenceHandler) + : this(IdentifierName(source), referenceHandler == null ? null : IdentifierName(referenceHandler)) + { + } + + private TypeMappingBuildContext(ExpressionSyntax source, ExpressionSyntax? referenceHandler) + { + Source = source; + ReferenceHandler = referenceHandler; + } + + public ExpressionSyntax Source { get; } + + public ExpressionSyntax? ReferenceHandler { get; } + + public TypeMappingBuildContext WithSource(ExpressionSyntax source) + => new(source, ReferenceHandler); + + public TypeMappingBuildContext WithSource(string source) + => WithSource(IdentifierName(source)); + + public TypeMappingBuildContext WithRefHandler(string refHandler) + => WithRefHandler(IdentifierName(refHandler)); + + public TypeMappingBuildContext WithRefHandler(ExpressionSyntax refHandler) + => new(Source, refHandler); +} diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserDefinedExistingInstanceMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserDefinedExistingInstanceMethodMapping.cs index a87c369ce5..6b577a6dc8 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserDefinedExistingInstanceMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserDefinedExistingInstanceMethodMapping.cs @@ -1,5 +1,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Emit.Symbols; using Riok.Mapperly.Helpers; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using static Riok.Mapperly.Emit.SyntaxFactoryHelper; @@ -12,44 +13,70 @@ namespace Riok.Mapperly.Descriptors.Mappings; /// public class UserDefinedExistingInstanceMethodMapping : ObjectPropertyMapping, IUserMapping { + private readonly bool _enableReferenceHandling; + private readonly INamedTypeSymbol _referenceHandlerType; + public UserDefinedExistingInstanceMethodMapping( - IMethodSymbol method) - : base(method.Parameters[0].Type.UpgradeNullable(), method.Parameters[1].Type.UpgradeNullable()) + IMethodSymbol method, + MethodParameter sourceParameter, + MethodParameter targetParameter, + MethodParameter? referenceHandlerParameter, + bool enableReferenceHandling, + INamedTypeSymbol referenceHandlerType) + : base(sourceParameter, targetParameter.Type) { + _enableReferenceHandling = enableReferenceHandling; + _referenceHandlerType = referenceHandlerType; IsPartial = true; IsExtensionMethod = method.IsExtensionMethod; Accessibility = method.DeclaredAccessibility; - MappingSourceParameterName = method.Parameters[0].Name; Method = method; MethodName = method.Name; + TargetParameter = targetParameter; + ReferenceHandlerParameter = referenceHandlerParameter; } public IMethodSymbol Method { get; } - private IParameterSymbol TargetParameter => Method.Parameters[1]; + private MethodParameter TargetParameter { get; } public override bool CallableByOtherMappings => false; - public override ExpressionSyntax Build(ExpressionSyntax source) + protected override ITypeSymbol? ReturnType => null; // return type is always void. + + public override ExpressionSyntax Build(TypeMappingBuildContext ctx) => throw new InvalidOperationException($"{nameof(UserDefinedExistingInstanceMethodMapping)} does not support {nameof(Build)}"); - public override IEnumerable BuildBody(ExpressionSyntax source) + public override IEnumerable BuildBody(TypeMappingBuildContext ctx) { - var body = base.BuildBody(source, IdentifierName(TargetParameter.Name)); - // if the source type is nullable, add a null guard. - return SourceType.IsNullable() - ? body.Prepend(IfNullReturn(source)) - : body; + if (SourceType.IsNullable()) + { + yield return IfNullReturn(ctx.Source); + } + + // if reference handling is enabled and no reference handler parameter is declared + // a new reference handler is instantiated and used. + if (_enableReferenceHandling && ReferenceHandlerParameter == null) + { + // var refHandler = new RefHandler(); + var createRefHandler = CreateInstance(_referenceHandlerType); + yield return DeclareLocalVariable(DefaultReferenceHandlerParameterName, createRefHandler); + ctx = ctx.WithRefHandler(DefaultReferenceHandlerParameterName); + } + + foreach (var body in base.BuildBody(ctx, IdentifierName(TargetParameter.Name))) + { + yield return body; + } } - protected override ITypeSymbol? ReturnType => null; // return type is always void. + protected override ParameterListSyntax BuildParameterList() + // needs to include the target parameter + => ParameterList(IsExtensionMethod, SourceParameter, TargetParameter, ReferenceHandlerParameter); - protected override IEnumerable BuildParameters() + internal override void EnableReferenceHandling(INamedTypeSymbol iReferenceHandlerType) { - var targetParam = Parameter(Identifier(TargetParameter.Name)) - .WithType(IdentifierName(TargetType.ToDisplayString())); - - return base.BuildParameters().Append(targetParam); + // the parameters of user defined methods should not be manipulated } } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserDefinedNewInstanceMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserDefinedNewInstanceMethodMapping.cs index 9c3b1ffe2d..b3502b6591 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserDefinedNewInstanceMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserDefinedNewInstanceMethodMapping.cs @@ -1,5 +1,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Emit.Symbols; using Riok.Mapperly.Helpers; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using static Riok.Mapperly.Emit.SyntaxFactoryHelper; @@ -12,40 +13,76 @@ namespace Riok.Mapperly.Descriptors.Mappings; public class UserDefinedNewInstanceMethodMapping : MethodMapping, IUserMapping { private const string NoMappingComment = "// Could not generate mapping"; - private TypeMapping? _delegateMapping; - public UserDefinedNewInstanceMethodMapping(IMethodSymbol method) - : base(method.Parameters.Single().Type.UpgradeNullable(), method.ReturnType.UpgradeNullable()) + private readonly bool _enableReferenceHandling; + private readonly INamedTypeSymbol _referenceHandlerType; + + private ITypeMapping? _delegateMapping; + + public UserDefinedNewInstanceMethodMapping( + IMethodSymbol method, + MethodParameter sourceParameter, + MethodParameter? referenceHandlerParameter, + bool enableReferenceHandling, + INamedTypeSymbol referenceHandlerType) + : base(sourceParameter, method.ReturnType.UpgradeNullable()) { + _enableReferenceHandling = enableReferenceHandling; + _referenceHandlerType = referenceHandlerType; IsPartial = true; IsExtensionMethod = method.IsExtensionMethod; Accessibility = method.DeclaredAccessibility; - MappingSourceParameterName = method.Parameters[0].Name; Method = method; MethodName = method.Name; + ReferenceHandlerParameter = referenceHandlerParameter; } public IMethodSymbol Method { get; } - public void SetDelegateMapping(TypeMapping delegateMapping) + public void SetDelegateMapping(ITypeMapping delegateMapping) => _delegateMapping = delegateMapping; - public override IEnumerable BuildBody(ExpressionSyntax source) + public override IEnumerable BuildBody(TypeMappingBuildContext ctx) { if (_delegateMapping == null) { - return new StatementSyntax[] + return new[] { ThrowStatement(ThrowNotImplementedException()) .WithLeadingTrivia(TriviaList(Comment(NoMappingComment))), }; } - if (_delegateMapping is MethodMapping delegateMethodMapping) + // if reference handling is enabled and no reference handler parameter is declared + // the generated mapping method is called with a new reference handler instance + // otherwise the generated method is embedded + if (_enableReferenceHandling && ReferenceHandlerParameter == null) { - return delegateMethodMapping.BuildBody(source); + // new RefHandler(); + var createRefHandler = CreateInstance(_referenceHandlerType); + ctx = ctx.WithRefHandler(createRefHandler); + return new[] { ReturnStatement(_delegateMapping.Build(ctx)) }; } - return new[] { ReturnStatement(_delegateMapping.Build(source)) }; + if (_delegateMapping is MethodMapping delegateMethodMapping) + return delegateMethodMapping.BuildBody(ctx); + + return new[] { ReturnStatement(_delegateMapping.Build(ctx)) }; + } + + /// + /// A is callable by other mappings + /// if either reference handling is not activated, or the user defined a reference handler parameter. + /// + public override bool CallableByOtherMappings => !_enableReferenceHandling || ReferenceHandlerParameter != null; + + internal override void EnableReferenceHandling(INamedTypeSymbol iReferenceHandlerType) + { + // the parameters of user defined methods should not be manipulated + // if the user did not define a parameter a new reference handler is initialized + if (_delegateMapping is MethodMapping methodMapping) + { + methodMapping.EnableReferenceHandling(iReferenceHandlerType); + } } } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserImplementedMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserImplementedMethodMapping.cs index 237ba4cc58..9dd5b8b3c2 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserImplementedMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserImplementedMethodMapping.cs @@ -1,5 +1,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Emit.Symbols; using Riok.Mapperly.Helpers; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using static Riok.Mapperly.Emit.SyntaxFactoryHelper; @@ -11,25 +12,29 @@ namespace Riok.Mapperly.Descriptors.Mappings; /// public class UserImplementedMethodMapping : TypeMapping, IUserMapping { - public UserImplementedMethodMapping(IMethodSymbol method) - : base(method.Parameters.Single().Type.UpgradeNullable(), method.ReturnType.UpgradeNullable()) + public UserImplementedMethodMapping(IMethodSymbol method, MethodParameter sourceParameter, MethodParameter? referenceHandlerParameter) + : base(method.Parameters[0].Type.UpgradeNullable(), method.ReturnType.UpgradeNullable()) { Method = method; + SourceParameter = sourceParameter; + ReferenceHandlerParameter = referenceHandlerParameter; } public IMethodSymbol Method { get; } - public override ExpressionSyntax Build(ExpressionSyntax source) + private MethodParameter SourceParameter { get; } + + public MethodParameter? ReferenceHandlerParameter { get; } + + public override ExpressionSyntax Build(TypeMappingBuildContext ctx) { // if the user implemented method is on an interface, - // we explicitly cast to be able to use the default interface implementation + // we explicitly cast to be able to use the default interface implementation or explicit implementations if (Method.ReceiverType?.TypeKind != TypeKind.Interface) - return Invocation(Method.Name, source); + return Invocation(Method.Name, SourceParameter.WithArgument(ctx.Source), ReferenceHandlerParameter?.WithArgument(ctx.ReferenceHandler)); var castedThis = CastExpression(IdentifierName(Method.ReceiverType!.ToDisplayString()), ThisExpression()); var method = MemberAccess(ParenthesizedExpression(castedThis), Method.Name); - return Invocation( - method, - source); + return Invocation(method, SourceParameter.WithArgument(ctx.Source), ReferenceHandlerParameter?.WithArgument(ctx.ReferenceHandler)); } } diff --git a/src/Riok.Mapperly/Descriptors/ObjectFactories/ObjectFactoryBuilder.cs b/src/Riok.Mapperly/Descriptors/ObjectFactories/ObjectFactoryBuilder.cs index 2cb3b656fb..531bab7236 100644 --- a/src/Riok.Mapperly/Descriptors/ObjectFactories/ObjectFactoryBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/ObjectFactories/ObjectFactoryBuilder.cs @@ -1,5 +1,4 @@ using Microsoft.CodeAnalysis; -using Riok.Mapperly.Abstractions; using Riok.Mapperly.Diagnostics; using Riok.Mapperly.Helpers; @@ -9,11 +8,9 @@ public static class ObjectFactoryBuilder { public static ObjectFactoryCollection ExtractObjectFactories(SimpleMappingBuilderContext ctx, ITypeSymbol mapperSymbol) { - var objectFactoryAttribute = ctx.GetTypeSymbol(typeof(ObjectFactoryAttribute)); - var objectFactories = mapperSymbol.GetMembers() .OfType() - .Where(m => m.HasAttribute(objectFactoryAttribute)) + .Where(m => m.HasAttribute(ctx.Types.ObjectFactoryAttribute)) .Select(x => BuildObjectFactory(ctx, x)) .WhereNotNull() .ToList(); diff --git a/src/Riok.Mapperly/Descriptors/ObjectFactories/SimpleObjectFactory.cs b/src/Riok.Mapperly/Descriptors/ObjectFactories/SimpleObjectFactory.cs index 1af50720d5..76c5dae9c1 100644 --- a/src/Riok.Mapperly/Descriptors/ObjectFactories/SimpleObjectFactory.cs +++ b/src/Riok.Mapperly/Descriptors/ObjectFactories/SimpleObjectFactory.cs @@ -18,5 +18,5 @@ public override bool CanCreateType(ITypeSymbol sourceType, ITypeSymbol targetTyp => SymbolEqualityComparer.Default.Equals(Method.ReturnType, targetTypeToCreate); protected override ExpressionSyntax BuildCreateType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate, ExpressionSyntax source) - => Invocation(Method.Name); + => Invocation(Method.Name, Array.Empty()); } diff --git a/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs index 34f7044e17..95ebfa373b 100644 --- a/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs @@ -22,12 +22,7 @@ public SimpleMappingBuilderContext(DescriptorBuilder builder) public bool IsConversionEnabled(MappingConversionType conversionType) => MapperConfiguration.EnabledConversions.HasFlag(conversionType); - public INamedTypeSymbol GetTypeSymbol(Type type) - => Compilation.GetTypeByMetadataName(type.FullName ?? throw new InvalidOperationException("Could not get name of type " + type)) - ?? throw new InvalidOperationException("Could not get type " + type.FullName); - - public bool IsType(ITypeSymbol symbol, Type type) - => SymbolEqualityComparer.Default.Equals(symbol, GetTypeSymbol(type)); + public WellKnownTypes Types => _builder.WellKnownTypes; public void ReportDiagnostic(DiagnosticDescriptor descriptor, ISymbol? location, params object[] messageArgs) => ReportDiagnostic(descriptor, location?.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax(), messageArgs); diff --git a/src/Riok.Mapperly/Descriptors/WellKnownTypes.cs b/src/Riok.Mapperly/Descriptors/WellKnownTypes.cs new file mode 100644 index 0000000000..33ec6d531c --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/WellKnownTypes.cs @@ -0,0 +1,53 @@ +using Microsoft.CodeAnalysis; +using Riok.Mapperly.Abstractions; +using Riok.Mapperly.Abstractions.ReferenceHandling; +using Riok.Mapperly.Abstractions.ReferenceHandling.Internal; + +namespace Riok.Mapperly.Descriptors; + +public class WellKnownTypes +{ + private readonly Compilation _compilation; + + private INamedTypeSymbol? _referenceHandlerAttribute; + private INamedTypeSymbol? _objectFactoryAttribute; + private INamedTypeSymbol? _mapperConstructorAttribute; + + private INamedTypeSymbol? _obsoleteAttribute; + + private INamedTypeSymbol? _iReferenceHandler; + private INamedTypeSymbol? _preserveReferenceHandler; + + private INamedTypeSymbol? _iDictionary; + private INamedTypeSymbol? _iReadOnlyDictionary; + private INamedTypeSymbol? _iEnumerable; + private INamedTypeSymbol? _enumerable; + private INamedTypeSymbol? _iCollection; + private INamedTypeSymbol? _iReadOnlyCollection; + private INamedTypeSymbol? _keyValuePair; + private INamedTypeSymbol? _dictionary; + + internal WellKnownTypes(Compilation compilation) + { + _compilation = compilation; + } + + public INamedTypeSymbol ReferenceHandlerAttribute => _referenceHandlerAttribute ??= GetTypeSymbol(typeof(ReferenceHandlerAttribute)); + public INamedTypeSymbol ObjectFactoryAttribute => _objectFactoryAttribute ??= GetTypeSymbol(typeof(ObjectFactoryAttribute)); + public INamedTypeSymbol MapperConstructorAttribute => _mapperConstructorAttribute ??= GetTypeSymbol(typeof(MapperConstructorAttribute)); + public INamedTypeSymbol ObsoleteAttribute => _obsoleteAttribute ??= GetTypeSymbol(typeof(ObsoleteAttribute)); + public INamedTypeSymbol IReferenceHandler => _iReferenceHandler ??= GetTypeSymbol(typeof(IReferenceHandler)); + public INamedTypeSymbol PreserveReferenceHandler => _preserveReferenceHandler ??= GetTypeSymbol(typeof(PreserveReferenceHandler)); + public INamedTypeSymbol IDictionary => _iDictionary ??= GetTypeSymbol(typeof(IDictionary<,>)); + public INamedTypeSymbol IReadOnlyDictionary => _iReadOnlyDictionary ??= GetTypeSymbol(typeof(IReadOnlyDictionary<,>)); + public INamedTypeSymbol IEnumerable => _iEnumerable ??= GetTypeSymbol(typeof(IEnumerable<>)); + public INamedTypeSymbol Enumerable => _enumerable ??= GetTypeSymbol(typeof(Enumerable)); + public INamedTypeSymbol ICollection => _iCollection ??= GetTypeSymbol(typeof(ICollection<>)); + public INamedTypeSymbol IReadOnlyCollection => _iReadOnlyCollection ??= GetTypeSymbol(typeof(IReadOnlyCollection<>)); + public INamedTypeSymbol KeyValuePair => _keyValuePair ??= GetTypeSymbol(typeof(KeyValuePair<,>)); + public INamedTypeSymbol Dictionary => _dictionary ??= GetTypeSymbol(typeof(Dictionary<,>)); + + private INamedTypeSymbol GetTypeSymbol(Type type) + => _compilation.GetTypeByMetadataName(type.FullName ?? throw new InvalidOperationException("Could not get name of type " + type)) + ?? throw new InvalidOperationException("Could not get type " + type.FullName); +} diff --git a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs index b3a854ebc8..f24fa033c7 100644 --- a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs +++ b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs @@ -1,4 +1,5 @@ using Microsoft.CodeAnalysis; +using Riok.Mapperly.Abstractions; namespace Riok.Mapperly.Diagnostics; @@ -184,7 +185,23 @@ internal static class DiagnosticDescriptors public static readonly DiagnosticDescriptor RequiredPropertyNotMapped = new DiagnosticDescriptor( "RMG023", "Source property was not found for required target property", - "Required property {0} on mapping target type was not found on the mapping source type {1}", + "Required property {0} on mapping target type was not found on the mapping source type {1}", + DiagnosticCategories.Mapper, + DiagnosticSeverity.Error, + true); + + public static readonly DiagnosticDescriptor ReferenceHandlerParameterWrongType = new DiagnosticDescriptor( + "RMG024", + "The reference handler parameter is not of the correct type", + "The reference handler parameter of {0}.{1} needs to be of type {2} but is {3}", + DiagnosticCategories.Mapper, + DiagnosticSeverity.Error, + true); + + public static readonly DiagnosticDescriptor ReferenceHandlingNotEnabled = new DiagnosticDescriptor( + "RMG025", + "To use reference handling it needs to be enabled on the mapper attribute", + $"{{0}}.{{1}} uses reference handling, but it is not enabled on the mapper attribute, to enable reference handling set {nameof(MapperAttribute.UseReferenceHandling)} to true", DiagnosticCategories.Mapper, DiagnosticSeverity.Error, true); diff --git a/src/Riok.Mapperly/Emit/ReferenceHandlingSyntaxFactoryHelper.cs b/src/Riok.Mapperly/Emit/ReferenceHandlingSyntaxFactoryHelper.cs new file mode 100644 index 0000000000..e59c7831e0 --- /dev/null +++ b/src/Riok.Mapperly/Emit/ReferenceHandlingSyntaxFactoryHelper.cs @@ -0,0 +1,54 @@ +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Abstractions.ReferenceHandling; +using Riok.Mapperly.Descriptors.Mappings; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static Riok.Mapperly.Emit.SyntaxFactoryHelper; + +namespace Riok.Mapperly.Emit; + +public static class ReferenceHandlingSyntaxFactoryHelper +{ + private const string ExistingTargetVariableName = "existingTargetReference"; + + public static IfStatementSyntax TryGetReference( + ITypeMapping mapping, + TypeMappingBuildContext ctx) + { + // GetReference + var refHandler = ctx.ReferenceHandler ?? throw new ArgumentNullException(nameof(ctx.ReferenceHandler)); + var methodName = GenericName(Identifier(nameof(IReferenceHandler.TryGetReference))) + .WithTypeArgumentList(TypeArgumentList(IdentifierName(mapping.SourceType.ToDisplayString()), IdentifierName(mapping.TargetType.ToDisplayString()))); + var method = MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, refHandler, methodName); + + // out var target + var targetArgument = Argument( + DeclarationExpression( + VarIdentifier, + SingleVariableDesignation(Identifier(ExistingTargetVariableName)))) + .WithRefOrOutKeyword(Token(SyntaxKind.OutKeyword)); + + // GetReference(source, out var target) + var invocation = Invocation(method, Argument(ctx.Source), targetArgument); + + // if (_referenceHandler.GetReference(source, out var target)) + // return target; + return IfStatement( + invocation, + ReturnStatement(IdentifierName(ExistingTargetVariableName))); + } + + public static ExpressionSyntax SetReference( + ITypeMapping mapping, + TypeMappingBuildContext ctx, + ExpressionSyntax target) + { + // SetReference + var refHandler = ctx.ReferenceHandler ?? throw new ArgumentNullException(nameof(ctx.ReferenceHandler)); + var methodName = GenericName(Identifier(nameof(IReferenceHandler.SetReference))) + .WithTypeArgumentList(TypeArgumentList(IdentifierName(mapping.SourceType.ToDisplayString()), IdentifierName(mapping.TargetType.ToDisplayString()))); + var method = MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, refHandler, methodName); + + return Invocation(method, ctx.Source, target); + } +} diff --git a/src/Riok.Mapperly/Emit/Symbols/MappingMethodParameters.cs b/src/Riok.Mapperly/Emit/Symbols/MappingMethodParameters.cs new file mode 100644 index 0000000000..df3de7c395 --- /dev/null +++ b/src/Riok.Mapperly/Emit/Symbols/MappingMethodParameters.cs @@ -0,0 +1,20 @@ +namespace Riok.Mapperly.Emit.Symbols; + +/// +/// Well known mapping method parameters. +/// +public readonly struct MappingMethodParameters +{ + public MappingMethodParameters(MethodParameter source, MethodParameter? target, MethodParameter? referenceHandler) + { + Source = source; + Target = target; + ReferenceHandler = referenceHandler; + } + + public MethodParameter Source { get; } + + public MethodParameter? Target { get; } + + public MethodParameter? ReferenceHandler { get; } +} diff --git a/src/Riok.Mapperly/Emit/Symbols/MethodArgument.cs b/src/Riok.Mapperly/Emit/Symbols/MethodArgument.cs new file mode 100644 index 0000000000..6724a7b7ec --- /dev/null +++ b/src/Riok.Mapperly/Emit/Symbols/MethodArgument.cs @@ -0,0 +1,19 @@ +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Riok.Mapperly.Emit.Symbols; + +/// +/// A method argument (a parameter and an argument value). +/// +public readonly struct MethodArgument +{ + public MethodArgument(MethodParameter parameter, ExpressionSyntax argument) + { + Parameter = parameter; + Argument = argument; + } + + public MethodParameter Parameter { get; } + + public ExpressionSyntax Argument { get; } +} diff --git a/src/Riok.Mapperly/Emit/Symbols/MethodParameter.cs b/src/Riok.Mapperly/Emit/Symbols/MethodParameter.cs new file mode 100644 index 0000000000..77d9531544 --- /dev/null +++ b/src/Riok.Mapperly/Emit/Symbols/MethodParameter.cs @@ -0,0 +1,32 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Helpers; + +namespace Riok.Mapperly.Emit.Symbols; + +public readonly struct MethodParameter +{ + public MethodParameter(int ordinal, string name, ITypeSymbol type) + { + Ordinal = ordinal; + Name = name; + Type = type; + } + + public MethodParameter(IParameterSymbol symbol) + : this(symbol.Ordinal, symbol.Name, symbol.Type.UpgradeNullable()) + { + } + + public int Ordinal { get; } + + public string Name { get; } + + public ITypeSymbol Type { get; } + + public MethodArgument WithArgument(ExpressionSyntax? argument) + => new(this, argument ?? throw new ArgumentNullException(nameof(argument))); + + public static MethodParameter? Wrap(IParameterSymbol? symbol) + => symbol == null ? null : new(symbol); +} diff --git a/src/Riok.Mapperly/Emit/SyntaxFactoryHelper.cs b/src/Riok.Mapperly/Emit/SyntaxFactoryHelper.cs index 58dee4e02c..1b039296e1 100644 --- a/src/Riok.Mapperly/Emit/SyntaxFactoryHelper.cs +++ b/src/Riok.Mapperly/Emit/SyntaxFactoryHelper.cs @@ -2,6 +2,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Riok.Mapperly.Descriptors.Mappings; +using Riok.Mapperly.Emit.Symbols; using Riok.Mapperly.Helpers; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; @@ -134,11 +135,17 @@ public static ThrowExpressionSyntax ThrowNotImplementedException() public static InvocationExpressionSyntax GenericInvocation(string methodName, IEnumerable typeParams, params ExpressionSyntax[] arguments) { var method = GenericName(methodName) - .WithTypeArgumentList(TypeArgumentList(CommaSeparatedList(typeParams))); + .WithTypeArgumentList(TypeArgumentList(typeParams.ToArray())); return InvocationExpression(method) .WithArgumentList(ArgumentList(arguments)); } + public static InvocationExpressionSyntax Invocation(string methodName, params MethodArgument?[] arguments) + => Invocation(IdentifierName(methodName), arguments); + + public static InvocationExpressionSyntax Invocation(ExpressionSyntax method, params MethodArgument?[] arguments) + => Invocation(method, arguments.WhereNotNull().OrderBy(x => x.Parameter.Ordinal).Select(x => x.Argument).ToArray()); + public static InvocationExpressionSyntax Invocation(string methodName, params ExpressionSyntax[] arguments) => Invocation(IdentifierName(methodName), arguments); @@ -148,6 +155,37 @@ public static InvocationExpressionSyntax Invocation(ExpressionSyntax method, par .WithArgumentList(ArgumentList(arguments)); } + public static InvocationExpressionSyntax Invocation(ExpressionSyntax method) + => Invocation(method, Array.Empty()); + + public static InvocationExpressionSyntax Invocation(ExpressionSyntax method, params ArgumentSyntax[] arguments) + { + return InvocationExpression(method) + .WithArgumentList(ArgumentList(arguments)); + } + + public static ParameterListSyntax ParameterList(bool extensionMethod, params MethodParameter?[] parameters) + { + var parameterSyntaxes = parameters + .WhereNotNull() + .OrderBy(x => x.Ordinal) + .Select(p => Parameter(extensionMethod, p)); + return SyntaxFactory.ParameterList(CommaSeparatedList(parameterSyntaxes)); + } + + public static ParameterSyntax Parameter(bool addThisKeyword, MethodParameter parameter) + { + var param = SyntaxFactory.Parameter(Identifier(parameter.Name)) + .WithType(IdentifierName(parameter.Type.ToDisplayString())); + + if (addThisKeyword && parameter.Ordinal == 0) + { + param = param.WithModifiers(TokenList(Token(SyntaxKind.ThisKeyword))); + } + + return param; + } + public static InvocationExpressionSyntax StaticInvocation(IMethodSymbol method, params ExpressionSyntax[] arguments) { var receiverType = method.ReceiverType ?? throw new ArgumentNullException(nameof(method.ReceiverType)); @@ -234,6 +272,9 @@ public static NamespaceDeclarationSyntax Namespace(string ns) public static ArgumentListSyntax ArgumentList(params ExpressionSyntax[] argSyntaxes) => SyntaxFactory.ArgumentList(CommaSeparatedList(argSyntaxes.Select(Argument))); + public static TypeArgumentListSyntax TypeArgumentList(params TypeSyntax[] argSyntaxes) + => SyntaxFactory.TypeArgumentList(CommaSeparatedList(argSyntaxes)); + public static ArgumentListSyntax ArgumentList(params ArgumentSyntax[] args) => SyntaxFactory.ArgumentList(CommaSeparatedList(args)); diff --git a/src/Riok.Mapperly/Helpers/EnumerableExtensions.cs b/src/Riok.Mapperly/Helpers/EnumerableExtensions.cs index 894774e8e3..048380e42c 100644 --- a/src/Riok.Mapperly/Helpers/EnumerableExtensions.cs +++ b/src/Riok.Mapperly/Helpers/EnumerableExtensions.cs @@ -2,6 +2,14 @@ namespace Riok.Mapperly.Helpers; public static class EnumerableExtensions { + public static IEnumerable WhereNotNull(this IEnumerable enumerable) + where T : struct + { +#nullable disable + return enumerable.Where(x => x != null).Select(x => x.Value); +#nullable restore + } + public static IEnumerable WhereNotNull(this IEnumerable enumerable) where T : notnull { diff --git a/src/Riok.Mapperly/Helpers/SymbolExtensions.cs b/src/Riok.Mapperly/Helpers/SymbolExtensions.cs index 08d4d4e6ba..f0e1055977 100644 --- a/src/Riok.Mapperly/Helpers/SymbolExtensions.cs +++ b/src/Riok.Mapperly/Helpers/SymbolExtensions.cs @@ -78,6 +78,9 @@ internal static bool ImplementsGeneric( return genericIntf != null; } + internal static bool Implements(this ITypeSymbol t, INamedTypeSymbol interfaceSymbol) + => t.AllInterfaces.Any(x => SymbolEqualityComparer.Default.Equals(interfaceSymbol, x)); + internal static bool CanConsumeType(this ITypeParameterSymbol typeParameter, Compilation compilation, ITypeSymbol type) { if (typeParameter.HasConstructorConstraint && !type.HasAccessibleParameterlessConstructor()) diff --git a/test/Riok.Mapperly.Abstractions.Test/MapPropertyAttributeTest.cs b/test/Riok.Mapperly.Abstractions.Tests/MapPropertyAttributeTest.cs similarity index 91% rename from test/Riok.Mapperly.Abstractions.Test/MapPropertyAttributeTest.cs rename to test/Riok.Mapperly.Abstractions.Tests/MapPropertyAttributeTest.cs index bb647fba2e..471ce32531 100644 --- a/test/Riok.Mapperly.Abstractions.Test/MapPropertyAttributeTest.cs +++ b/test/Riok.Mapperly.Abstractions.Tests/MapPropertyAttributeTest.cs @@ -1,4 +1,4 @@ -namespace Riok.Mapperly.Abstractions.Test; +namespace Riok.Mapperly.Abstractions.Tests; public class MapPropertyAttributeTest { diff --git a/test/Riok.Mapperly.Abstractions.Tests/ReferenceHandling/Internal/PreserveReferenceHandlerTest.cs b/test/Riok.Mapperly.Abstractions.Tests/ReferenceHandling/Internal/PreserveReferenceHandlerTest.cs new file mode 100644 index 0000000000..f2650ad057 --- /dev/null +++ b/test/Riok.Mapperly.Abstractions.Tests/ReferenceHandling/Internal/PreserveReferenceHandlerTest.cs @@ -0,0 +1,41 @@ +using Riok.Mapperly.Abstractions.ReferenceHandling; +using Riok.Mapperly.Abstractions.ReferenceHandling.Internal; + +namespace Riok.Mapperly.Abstractions.Tests.ReferenceHandling.Internal; + +public class PreserveReferenceHandlerTest +{ + private readonly IReferenceHandler _handler = new PreserveReferenceHandler(); + + [Fact] + public void EmptyReferenceHandlerShouldReturnFalse() + { + _handler.TryGetReference(new MyObj(), out MyDto _) + .Should() + .BeFalse(); + } + + [Fact] + public void SetReferenceShouldBeReturned() + { + var myObj = new MyObj { Value = 1 }; + var myDto = new MyDto { Value = 2 }; + _handler.SetReference(myObj, myDto); + _handler.TryGetReference(myObj, out MyDto? mySecondDto) + .Should() + .BeTrue(); + myDto + .Should() + .Be(mySecondDto); + } + + class MyDto + { + public int Value { get; set; } + } + + class MyObj + { + public int Value { get; set; } + } +} diff --git a/test/Riok.Mapperly.Abstractions.Tests/ReferenceHandling/Internal/ReferenceEqualityComparerTest.cs b/test/Riok.Mapperly.Abstractions.Tests/ReferenceHandling/Internal/ReferenceEqualityComparerTest.cs new file mode 100644 index 0000000000..b7628073fb --- /dev/null +++ b/test/Riok.Mapperly.Abstractions.Tests/ReferenceHandling/Internal/ReferenceEqualityComparerTest.cs @@ -0,0 +1,54 @@ +using Riok.Mapperly.Abstractions.ReferenceHandling.Internal; + +namespace Riok.Mapperly.Abstractions.Tests.ReferenceHandling.Internal; + +public class ReferenceEqualityComparerTest +{ + [Fact] + public void PrimitivesShouldNotBeEqual() + { + ReferenceEqualityComparer.Instance.Equals(20, 10) + .Should() + .BeFalse(); + ReferenceEqualityComparer.Instance.GetHashCode(10) + .Should() + .NotBe(ReferenceEqualityComparer.Instance.GetHashCode(10)); + } + + [Fact] + public void InternedStringsShouldBeEqual() + { + ReferenceEqualityComparer.Instance.Equals(string.Intern("fooBar"), string.Intern("fooBar")) + .Should() + .BeTrue(); + ReferenceEqualityComparer.Instance.GetHashCode(string.Intern("fooBar")) + .Should() + .Be(ReferenceEqualityComparer.Instance.GetHashCode(string.Intern("fooBar"))); + } + + [Fact] + public void SameObjectRefShouldBeEqual() + { + var obj = new object(); + + ReferenceEqualityComparer.Instance.Equals(obj, obj) + .Should() + .BeTrue(); + + ReferenceEqualityComparer.Instance.GetHashCode(obj) + .Should() + .Be(ReferenceEqualityComparer.Instance.GetHashCode(obj)); + } + + [Fact] + public void DifferentObjectRefShouldNotBeEqual() + { + ReferenceEqualityComparer.Instance.Equals(new object(), new object()) + .Should() + .BeFalse(); + + ReferenceEqualityComparer.Instance.GetHashCode(new object()) + .Should() + .NotBe(ReferenceEqualityComparer.Instance.GetHashCode(new object())); + } +} diff --git a/test/Riok.Mapperly.Abstractions.Test/Riok.Mapperly.Abstractions.Test.csproj b/test/Riok.Mapperly.Abstractions.Tests/Riok.Mapperly.Abstractions.Tests.csproj similarity index 100% rename from test/Riok.Mapperly.Abstractions.Test/Riok.Mapperly.Abstractions.Test.csproj rename to test/Riok.Mapperly.Abstractions.Tests/Riok.Mapperly.Abstractions.Tests.csproj diff --git a/test/Riok.Mapperly.IntegrationTests/CircularReferenceMapperTest.cs b/test/Riok.Mapperly.IntegrationTests/CircularReferenceMapperTest.cs new file mode 100644 index 0000000000..154bf0d53c --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/CircularReferenceMapperTest.cs @@ -0,0 +1,37 @@ +using System.Threading.Tasks; +using FluentAssertions; +using Riok.Mapperly.IntegrationTests.Mapper; +using Riok.Mapperly.IntegrationTests.Models; +using VerifyXunit; +using Xunit; + +namespace Riok.Mapperly.IntegrationTests +{ + [UsesVerify] + public class CircularReferenceMapperTest : BaseMapperTest + { + [Fact] + public void ShouldMapCircularReference() + { + var obj = new CircularReferenceObject + { + Value = 1, + Parent = new() { Value = 2 }, + }; + obj.Parent.Parent = obj; + + var dto = CircularReferenceMapper.ToDto(obj); + dto.Value.Should().Be(1); + dto.Parent.Should().NotBeNull(); + dto.Parent!.Value.Should().Be(2); + dto.Parent.Parent.Should().Be(dto); + } + + [Fact] + public Task SnapshotGeneratedSource() + { + var path = GetGeneratedMapperFilePath(nameof(CircularReferenceMapper)); + return Verifier.VerifyFile(path); + } + } +} diff --git a/test/Riok.Mapperly.IntegrationTests/Dto/CircularReferenceDto.cs b/test/Riok.Mapperly.IntegrationTests/Dto/CircularReferenceDto.cs new file mode 100644 index 0000000000..d53f48a91d --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/Dto/CircularReferenceDto.cs @@ -0,0 +1,9 @@ +namespace Riok.Mapperly.IntegrationTests.Dto +{ + public class CircularReferenceDto + { + public int Value { get; set; } + + public CircularReferenceDto? Parent { get; set; } + } +} diff --git a/test/Riok.Mapperly.IntegrationTests/Mapper/CircularReferenceMapper.cs b/test/Riok.Mapperly.IntegrationTests/Mapper/CircularReferenceMapper.cs new file mode 100644 index 0000000000..95d6fd02bd --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/Mapper/CircularReferenceMapper.cs @@ -0,0 +1,12 @@ +using Riok.Mapperly.Abstractions; +using Riok.Mapperly.IntegrationTests.Dto; +using Riok.Mapperly.IntegrationTests.Models; + +namespace Riok.Mapperly.IntegrationTests.Mapper +{ + [Mapper(UseReferenceHandling = true)] + public static partial class CircularReferenceMapper + { + public static partial CircularReferenceDto ToDto(CircularReferenceObject obj); + } +} diff --git a/test/Riok.Mapperly.IntegrationTests/Models/CircularReferenceObject.cs b/test/Riok.Mapperly.IntegrationTests/Models/CircularReferenceObject.cs new file mode 100644 index 0000000000..c0d91342ed --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/Models/CircularReferenceObject.cs @@ -0,0 +1,9 @@ +namespace Riok.Mapperly.IntegrationTests.Models +{ + public class CircularReferenceObject + { + public int Value { get; set; } + + public CircularReferenceObject? Parent { get; set; } + } +} diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/CircularReferenceMapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/CircularReferenceMapperTest.SnapshotGeneratedSource.verified.cs new file mode 100644 index 0000000000..a490cfc25f --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/CircularReferenceMapperTest.SnapshotGeneratedSource.verified.cs @@ -0,0 +1,26 @@ +#nullable enable +namespace Riok.Mapperly.IntegrationTests.Mapper +{ + public static partial class CircularReferenceMapper + { + public static partial Riok.Mapperly.IntegrationTests.Dto.CircularReferenceDto ToDto(Riok.Mapperly.IntegrationTests.Models.CircularReferenceObject obj) + { + return MapToCircularReferenceDto(obj, new Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler()); + } + + private static Riok.Mapperly.IntegrationTests.Dto.CircularReferenceDto MapToCircularReferenceDto(Riok.Mapperly.IntegrationTests.Models.CircularReferenceObject source, Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + { + if (refHandler.TryGetReference(source, out var existingTargetReference)) + return existingTargetReference; + var target = new Riok.Mapperly.IntegrationTests.Dto.CircularReferenceDto(); + refHandler.SetReference(source, target); + target.Value = source.Value; + if (source.Parent != null) + { + target.Parent = MapToCircularReferenceDto(source.Parent, refHandler); + } + + return target; + } + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.SnapshotGeneratedSource.verified.cs index 4ceda595ff..e1206250e4 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.SnapshotGeneratedSource.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.SnapshotGeneratedSource.verified.cs @@ -43,6 +43,66 @@ public partial System.DateTime DirectDateTime(System.DateTime dateTime) return System.Linq.Enumerable.Select(objects, x => MapToDto(x)); } + private partial Riok.Mapperly.IntegrationTests.Dto.TestObjectDto MapToDtoInternal(Riok.Mapperly.IntegrationTests.Models.TestObject testObject) + { + var target = new Riok.Mapperly.IntegrationTests.Dto.TestObjectDto(DirectInt(testObject.CtorValue), ctorValue2: DirectInt(testObject.CtorValue2)) + {IntInitOnlyValue = DirectInt(testObject.IntInitOnlyValue), RequiredValue = DirectInt(testObject.RequiredValue)}; + target.IntValue = DirectInt(testObject.IntValue); + target.StringValue = testObject.StringValue; + target.RenamedStringValue2 = testObject.RenamedStringValue; + target.FlatteningIdValue = DirectInt(testObject.Flattening.IdValue); + if (testObject.NullableFlattening != null) + { + target.NullableFlatteningIdValue = CastIntNullable(testObject.NullableFlattening.IdValue); + } + + target.Unflattening.IdValue = DirectInt(testObject.UnflatteningIdValue); + if (testObject.NullableUnflatteningIdValue != null) + { + target.NullableUnflattening ??= new(); + target.NullableUnflattening.IdValue = DirectInt(testObject.NullableUnflatteningIdValue.Value); + } + + if (testObject.NestedNullable != null) + { + target.NestedNullableIntValue = DirectInt(testObject.NestedNullable.IntValue); + target.NestedNullable = MapToTestObjectNestedDto(testObject.NestedNullable); + } + + if (testObject.NestedNullableTargetNotNullable != null) + { + target.NestedNullableTargetNotNullable = MapToTestObjectNestedDto(testObject.NestedNullableTargetNotNullable); + } + + if (testObject.StringNullableTargetNotNullable != null) + { + target.StringNullableTargetNotNullable = testObject.StringNullableTargetNotNullable; + } + + if (testObject.RecursiveObject != null) + { + target.RecursiveObject = MapToDto(testObject.RecursiveObject); + } + + target.SourceTargetSameObjectType = testObject.SourceTargetSameObjectType; + if (testObject.NullableReadOnlyObjectCollection != null) + { + target.NullableReadOnlyObjectCollection = System.Linq.Enumerable.ToArray(System.Linq.Enumerable.Select(testObject.NullableReadOnlyObjectCollection, x => MapToTestObjectNestedDto(x))); + } + + target.EnumValue = (Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue)testObject.EnumValue; + target.EnumName = MapToEnumDtoByName(testObject.EnumName); + target.EnumRawValue = (byte)testObject.EnumRawValue; + target.EnumStringValue = MapToString(testObject.EnumStringValue); + target.EnumReverseStringValue = MapToTestEnumDtoByValue(testObject.EnumReverseStringValue); + if (testObject.SubObject != null) + { + target.SubObject = MapToInheritanceSubObjectDto(testObject.SubObject); + } + + return target; + } + public partial Riok.Mapperly.IntegrationTests.Models.TestObject MapFromDto(Riok.Mapperly.IntegrationTests.Dto.TestObjectDto dto) { var target = new Riok.Mapperly.IntegrationTests.Models.TestObject(DirectInt(dto.CtorValue), ctorValue2: DirectInt(dto.CtorValue2)) @@ -97,6 +157,63 @@ public partial Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName MapToEnumDto }; } + public partial void UpdateDto(Riok.Mapperly.IntegrationTests.Models.TestObject source, Riok.Mapperly.IntegrationTests.Dto.TestObjectDto target) + { + target.CtorValue = DirectInt(source.CtorValue); + target.CtorValue2 = DirectInt(source.CtorValue2); + target.IntValue = DirectInt(source.IntValue); + target.StringValue = source.StringValue; + target.FlatteningIdValue = DirectInt(source.Flattening.IdValue); + if (source.NullableFlattening != null) + { + target.NullableFlatteningIdValue = CastIntNullable(source.NullableFlattening.IdValue); + } + + if (source.NestedNullable != null) + { + target.NestedNullableIntValue = DirectInt(source.NestedNullable.IntValue); + target.NestedNullable = MapToTestObjectNestedDto(source.NestedNullable); + } + + if (source.NestedNullableTargetNotNullable != null) + { + target.NestedNullableTargetNotNullable = MapToTestObjectNestedDto(source.NestedNullableTargetNotNullable); + } + + if (source.StringNullableTargetNotNullable != null) + { + target.StringNullableTargetNotNullable = source.StringNullableTargetNotNullable; + } + + if (source.RecursiveObject != null) + { + target.RecursiveObject = MapToDto(source.RecursiveObject); + } + + target.SourceTargetSameObjectType = source.SourceTargetSameObjectType; + if (source.NullableReadOnlyObjectCollection != null) + { + target.NullableReadOnlyObjectCollection = System.Linq.Enumerable.ToArray(System.Linq.Enumerable.Select(source.NullableReadOnlyObjectCollection, x => MapToTestObjectNestedDto(x))); + } + + target.EnumValue = (Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue)source.EnumValue; + target.EnumName = MapToEnumDtoByName(source.EnumName); + target.EnumRawValue = (byte)source.EnumRawValue; + target.EnumStringValue = MapToString(source.EnumStringValue); + target.EnumReverseStringValue = MapToTestEnumDtoByValue(source.EnumReverseStringValue); + if (source.SubObject != null) + { + target.SubObject = MapToInheritanceSubObjectDto(source.SubObject); + } + + target.IgnoredStringValue = source.IgnoredStringValue; + } + + private partial int PrivateDirectInt(int value) + { + return value; + } + private Riok.Mapperly.IntegrationTests.Dto.TestObjectNestedDto MapToTestObjectNestedDto(Riok.Mapperly.IntegrationTests.Models.TestObjectNested source) { var target = new Riok.Mapperly.IntegrationTests.Dto.TestObjectNestedDto(); @@ -181,122 +298,5 @@ private Riok.Mapperly.IntegrationTests.Models.InheritanceSubObject MapToInherita target.BaseIntValue = DirectInt(source.BaseIntValue); return target; } - - private partial Riok.Mapperly.IntegrationTests.Dto.TestObjectDto MapToDtoInternal(Riok.Mapperly.IntegrationTests.Models.TestObject testObject) - { - var target = new Riok.Mapperly.IntegrationTests.Dto.TestObjectDto(DirectInt(testObject.CtorValue), ctorValue2: DirectInt(testObject.CtorValue2)) - {IntInitOnlyValue = DirectInt(testObject.IntInitOnlyValue), RequiredValue = DirectInt(testObject.RequiredValue)}; - target.IntValue = DirectInt(testObject.IntValue); - target.StringValue = testObject.StringValue; - target.RenamedStringValue2 = testObject.RenamedStringValue; - target.FlatteningIdValue = DirectInt(testObject.Flattening.IdValue); - if (testObject.NullableFlattening != null) - { - target.NullableFlatteningIdValue = CastIntNullable(testObject.NullableFlattening.IdValue); - } - - target.Unflattening.IdValue = DirectInt(testObject.UnflatteningIdValue); - if (testObject.NullableUnflatteningIdValue != null) - { - target.NullableUnflattening ??= new(); - target.NullableUnflattening.IdValue = DirectInt(testObject.NullableUnflatteningIdValue.Value); - } - - if (testObject.NestedNullable != null) - { - target.NestedNullableIntValue = DirectInt(testObject.NestedNullable.IntValue); - target.NestedNullable = MapToTestObjectNestedDto(testObject.NestedNullable); - } - - if (testObject.NestedNullableTargetNotNullable != null) - { - target.NestedNullableTargetNotNullable = MapToTestObjectNestedDto(testObject.NestedNullableTargetNotNullable); - } - - if (testObject.StringNullableTargetNotNullable != null) - { - target.StringNullableTargetNotNullable = testObject.StringNullableTargetNotNullable; - } - - if (testObject.RecursiveObject != null) - { - target.RecursiveObject = MapToDto(testObject.RecursiveObject); - } - - target.SourceTargetSameObjectType = testObject.SourceTargetSameObjectType; - if (testObject.NullableReadOnlyObjectCollection != null) - { - target.NullableReadOnlyObjectCollection = System.Linq.Enumerable.ToArray(System.Linq.Enumerable.Select(testObject.NullableReadOnlyObjectCollection, x => MapToTestObjectNestedDto(x))); - } - - target.EnumValue = (Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue)testObject.EnumValue; - target.EnumName = MapToEnumDtoByName(testObject.EnumName); - target.EnumRawValue = (byte)testObject.EnumRawValue; - target.EnumStringValue = MapToString(testObject.EnumStringValue); - target.EnumReverseStringValue = MapToTestEnumDtoByValue(testObject.EnumReverseStringValue); - if (testObject.SubObject != null) - { - target.SubObject = MapToInheritanceSubObjectDto(testObject.SubObject); - } - - return target; - } - - public partial void UpdateDto(Riok.Mapperly.IntegrationTests.Models.TestObject source, Riok.Mapperly.IntegrationTests.Dto.TestObjectDto target) - { - target.CtorValue = DirectInt(source.CtorValue); - target.CtorValue2 = DirectInt(source.CtorValue2); - target.IntValue = DirectInt(source.IntValue); - target.StringValue = source.StringValue; - target.FlatteningIdValue = DirectInt(source.Flattening.IdValue); - if (source.NullableFlattening != null) - { - target.NullableFlatteningIdValue = CastIntNullable(source.NullableFlattening.IdValue); - } - - if (source.NestedNullable != null) - { - target.NestedNullableIntValue = DirectInt(source.NestedNullable.IntValue); - target.NestedNullable = MapToTestObjectNestedDto(source.NestedNullable); - } - - if (source.NestedNullableTargetNotNullable != null) - { - target.NestedNullableTargetNotNullable = MapToTestObjectNestedDto(source.NestedNullableTargetNotNullable); - } - - if (source.StringNullableTargetNotNullable != null) - { - target.StringNullableTargetNotNullable = source.StringNullableTargetNotNullable; - } - - if (source.RecursiveObject != null) - { - target.RecursiveObject = MapToDto(source.RecursiveObject); - } - - target.SourceTargetSameObjectType = source.SourceTargetSameObjectType; - if (source.NullableReadOnlyObjectCollection != null) - { - target.NullableReadOnlyObjectCollection = System.Linq.Enumerable.ToArray(System.Linq.Enumerable.Select(source.NullableReadOnlyObjectCollection, x => MapToTestObjectNestedDto(x))); - } - - target.EnumValue = (Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue)source.EnumValue; - target.EnumName = MapToEnumDtoByName(source.EnumName); - target.EnumRawValue = (byte)source.EnumRawValue; - target.EnumStringValue = MapToString(source.EnumStringValue); - target.EnumReverseStringValue = MapToTestEnumDtoByValue(source.EnumReverseStringValue); - if (source.SubObject != null) - { - target.SubObject = MapToInheritanceSubObjectDto(source.SubObject); - } - - target.IgnoredStringValue = source.IgnoredStringValue; - } - - private partial int PrivateDirectInt(int value) - { - return value; - } } -} +} \ 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 a3d1ec12d1..7be62ef9f3 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource.verified.cs @@ -96,6 +96,66 @@ public static partial Riok.Mapperly.IntegrationTests.Dto.TestObjectDto MapToDtoE return target; } + private static partial Riok.Mapperly.IntegrationTests.Dto.TestObjectDto MapToDtoInternal(Riok.Mapperly.IntegrationTests.Models.TestObject testObject) + { + var target = new Riok.Mapperly.IntegrationTests.Dto.TestObjectDto(DirectInt(testObject.CtorValue), ctorValue2: DirectInt(testObject.CtorValue2)) + {IntInitOnlyValue = DirectInt(testObject.IntInitOnlyValue), RequiredValue = DirectInt(testObject.RequiredValue)}; + target.IntValue = DirectInt(testObject.IntValue); + target.StringValue = testObject.StringValue; + target.RenamedStringValue2 = testObject.RenamedStringValue; + target.FlatteningIdValue = DirectInt(testObject.Flattening.IdValue); + if (testObject.NullableFlattening != null) + { + target.NullableFlatteningIdValue = CastIntNullable(testObject.NullableFlattening.IdValue); + } + + target.Unflattening.IdValue = DirectInt(testObject.UnflatteningIdValue); + if (testObject.NullableUnflatteningIdValue != null) + { + target.NullableUnflattening ??= new(); + target.NullableUnflattening.IdValue = DirectInt(testObject.NullableUnflatteningIdValue.Value); + } + + if (testObject.NestedNullable != null) + { + target.NestedNullableIntValue = DirectInt(testObject.NestedNullable.IntValue); + target.NestedNullable = MapToTestObjectNestedDto(testObject.NestedNullable); + } + + if (testObject.NestedNullableTargetNotNullable != null) + { + target.NestedNullableTargetNotNullable = MapToTestObjectNestedDto(testObject.NestedNullableTargetNotNullable); + } + + if (testObject.StringNullableTargetNotNullable != null) + { + target.StringNullableTargetNotNullable = testObject.StringNullableTargetNotNullable; + } + + if (testObject.RecursiveObject != null) + { + target.RecursiveObject = MapToDtoExt(testObject.RecursiveObject); + } + + target.SourceTargetSameObjectType = testObject.SourceTargetSameObjectType; + if (testObject.NullableReadOnlyObjectCollection != null) + { + target.NullableReadOnlyObjectCollection = System.Linq.Enumerable.ToArray(System.Linq.Enumerable.Select(testObject.NullableReadOnlyObjectCollection, x => MapToTestObjectNestedDto(x))); + } + + target.EnumValue = (Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue)testObject.EnumValue; + target.EnumName = MapToEnumDtoByName(testObject.EnumName); + target.EnumRawValue = (byte)testObject.EnumRawValue; + target.EnumStringValue = MapToString(testObject.EnumStringValue); + target.EnumReverseStringValue = MapToTestEnumDtoByValue(testObject.EnumReverseStringValue); + if (testObject.SubObject != null) + { + target.SubObject = MapToInheritanceSubObjectDto(testObject.SubObject); + } + + return target; + } + public static partial Riok.Mapperly.IntegrationTests.Models.TestObject MapFromDto(Riok.Mapperly.IntegrationTests.Dto.TestObjectDto dto) { var target = new Riok.Mapperly.IntegrationTests.Models.TestObject(DirectInt(dto.CtorValue), ctorValue2: DirectInt(dto.CtorValue2)) @@ -150,6 +210,63 @@ public static partial Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName MapTo }; } + public static partial void UpdateDto(Riok.Mapperly.IntegrationTests.Models.TestObject source, Riok.Mapperly.IntegrationTests.Dto.TestObjectDto target) + { + target.CtorValue = DirectInt(source.CtorValue); + target.CtorValue2 = DirectInt(source.CtorValue2); + target.IntValue = DirectInt(source.IntValue); + target.StringValue = source.StringValue; + target.FlatteningIdValue = DirectInt(source.Flattening.IdValue); + if (source.NullableFlattening != null) + { + target.NullableFlatteningIdValue = CastIntNullable(source.NullableFlattening.IdValue); + } + + if (source.NestedNullable != null) + { + target.NestedNullableIntValue = DirectInt(source.NestedNullable.IntValue); + target.NestedNullable = MapToTestObjectNestedDto(source.NestedNullable); + } + + if (source.NestedNullableTargetNotNullable != null) + { + target.NestedNullableTargetNotNullable = MapToTestObjectNestedDto(source.NestedNullableTargetNotNullable); + } + + if (source.StringNullableTargetNotNullable != null) + { + target.StringNullableTargetNotNullable = source.StringNullableTargetNotNullable; + } + + if (source.RecursiveObject != null) + { + target.RecursiveObject = MapToDtoExt(source.RecursiveObject); + } + + target.SourceTargetSameObjectType = source.SourceTargetSameObjectType; + if (source.NullableReadOnlyObjectCollection != null) + { + target.NullableReadOnlyObjectCollection = System.Linq.Enumerable.ToArray(System.Linq.Enumerable.Select(source.NullableReadOnlyObjectCollection, x => MapToTestObjectNestedDto(x))); + } + + target.EnumValue = (Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue)source.EnumValue; + target.EnumName = MapToEnumDtoByName(source.EnumName); + target.EnumRawValue = (byte)source.EnumRawValue; + target.EnumStringValue = MapToString(source.EnumStringValue); + target.EnumReverseStringValue = MapToTestEnumDtoByValue(source.EnumReverseStringValue); + if (source.SubObject != null) + { + target.SubObject = MapToInheritanceSubObjectDto(source.SubObject); + } + + target.IgnoredStringValue = source.IgnoredStringValue; + } + + private static partial int PrivateDirectInt(int value) + { + return value; + } + private static Riok.Mapperly.IntegrationTests.Dto.TestObjectNestedDto MapToTestObjectNestedDto(Riok.Mapperly.IntegrationTests.Models.TestObjectNested source) { var target = new Riok.Mapperly.IntegrationTests.Dto.TestObjectNestedDto(); @@ -234,122 +351,5 @@ private static Riok.Mapperly.IntegrationTests.Models.InheritanceSubObject MapToI target.BaseIntValue = DirectInt(source.BaseIntValue); return target; } - - private static partial Riok.Mapperly.IntegrationTests.Dto.TestObjectDto MapToDtoInternal(Riok.Mapperly.IntegrationTests.Models.TestObject testObject) - { - var target = new Riok.Mapperly.IntegrationTests.Dto.TestObjectDto(DirectInt(testObject.CtorValue), ctorValue2: DirectInt(testObject.CtorValue2)) - {IntInitOnlyValue = DirectInt(testObject.IntInitOnlyValue), RequiredValue = DirectInt(testObject.RequiredValue)}; - target.IntValue = DirectInt(testObject.IntValue); - target.StringValue = testObject.StringValue; - target.RenamedStringValue2 = testObject.RenamedStringValue; - target.FlatteningIdValue = DirectInt(testObject.Flattening.IdValue); - if (testObject.NullableFlattening != null) - { - target.NullableFlatteningIdValue = CastIntNullable(testObject.NullableFlattening.IdValue); - } - - target.Unflattening.IdValue = DirectInt(testObject.UnflatteningIdValue); - if (testObject.NullableUnflatteningIdValue != null) - { - target.NullableUnflattening ??= new(); - target.NullableUnflattening.IdValue = DirectInt(testObject.NullableUnflatteningIdValue.Value); - } - - if (testObject.NestedNullable != null) - { - target.NestedNullableIntValue = DirectInt(testObject.NestedNullable.IntValue); - target.NestedNullable = MapToTestObjectNestedDto(testObject.NestedNullable); - } - - if (testObject.NestedNullableTargetNotNullable != null) - { - target.NestedNullableTargetNotNullable = MapToTestObjectNestedDto(testObject.NestedNullableTargetNotNullable); - } - - if (testObject.StringNullableTargetNotNullable != null) - { - target.StringNullableTargetNotNullable = testObject.StringNullableTargetNotNullable; - } - - if (testObject.RecursiveObject != null) - { - target.RecursiveObject = MapToDtoExt(testObject.RecursiveObject); - } - - target.SourceTargetSameObjectType = testObject.SourceTargetSameObjectType; - if (testObject.NullableReadOnlyObjectCollection != null) - { - target.NullableReadOnlyObjectCollection = System.Linq.Enumerable.ToArray(System.Linq.Enumerable.Select(testObject.NullableReadOnlyObjectCollection, x => MapToTestObjectNestedDto(x))); - } - - target.EnumValue = (Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue)testObject.EnumValue; - target.EnumName = MapToEnumDtoByName(testObject.EnumName); - target.EnumRawValue = (byte)testObject.EnumRawValue; - target.EnumStringValue = MapToString(testObject.EnumStringValue); - target.EnumReverseStringValue = MapToTestEnumDtoByValue(testObject.EnumReverseStringValue); - if (testObject.SubObject != null) - { - target.SubObject = MapToInheritanceSubObjectDto(testObject.SubObject); - } - - return target; - } - - public static partial void UpdateDto(Riok.Mapperly.IntegrationTests.Models.TestObject source, Riok.Mapperly.IntegrationTests.Dto.TestObjectDto target) - { - target.CtorValue = DirectInt(source.CtorValue); - target.CtorValue2 = DirectInt(source.CtorValue2); - target.IntValue = DirectInt(source.IntValue); - target.StringValue = source.StringValue; - target.FlatteningIdValue = DirectInt(source.Flattening.IdValue); - if (source.NullableFlattening != null) - { - target.NullableFlatteningIdValue = CastIntNullable(source.NullableFlattening.IdValue); - } - - if (source.NestedNullable != null) - { - target.NestedNullableIntValue = DirectInt(source.NestedNullable.IntValue); - target.NestedNullable = MapToTestObjectNestedDto(source.NestedNullable); - } - - if (source.NestedNullableTargetNotNullable != null) - { - target.NestedNullableTargetNotNullable = MapToTestObjectNestedDto(source.NestedNullableTargetNotNullable); - } - - if (source.StringNullableTargetNotNullable != null) - { - target.StringNullableTargetNotNullable = source.StringNullableTargetNotNullable; - } - - if (source.RecursiveObject != null) - { - target.RecursiveObject = MapToDtoExt(source.RecursiveObject); - } - - target.SourceTargetSameObjectType = source.SourceTargetSameObjectType; - if (source.NullableReadOnlyObjectCollection != null) - { - target.NullableReadOnlyObjectCollection = System.Linq.Enumerable.ToArray(System.Linq.Enumerable.Select(source.NullableReadOnlyObjectCollection, x => MapToTestObjectNestedDto(x))); - } - - target.EnumValue = (Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue)source.EnumValue; - target.EnumName = MapToEnumDtoByName(source.EnumName); - target.EnumRawValue = (byte)source.EnumRawValue; - target.EnumStringValue = MapToString(source.EnumStringValue); - target.EnumReverseStringValue = MapToTestEnumDtoByValue(source.EnumReverseStringValue); - if (source.SubObject != null) - { - target.SubObject = MapToInheritanceSubObjectDto(source.SubObject); - } - - target.IgnoredStringValue = source.IgnoredStringValue; - } - - private static partial int PrivateDirectInt(int value) - { - return value; - } } } \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/Descriptors/MethodNameBuilderTest.cs b/test/Riok.Mapperly.Tests/Descriptors/MethodNameBuilderTest.cs index 939df367ce..d840e847b3 100644 --- a/test/Riok.Mapperly.Tests/Descriptors/MethodNameBuilderTest.cs +++ b/test/Riok.Mapperly.Tests/Descriptors/MethodNameBuilderTest.cs @@ -42,7 +42,7 @@ public MockedMethodMapping(ITypeSymbol t) : base(t, t) { } - public override IEnumerable BuildBody(ExpressionSyntax source) + public override IEnumerable BuildBody(TypeMappingBuildContext ctx) => Enumerable.Empty(); } } diff --git a/test/Riok.Mapperly.Tests/DiagnosticMatcher.cs b/test/Riok.Mapperly.Tests/DiagnosticMatcher.cs index 06faf30650..b6319260e3 100644 --- a/test/Riok.Mapperly.Tests/DiagnosticMatcher.cs +++ b/test/Riok.Mapperly.Tests/DiagnosticMatcher.cs @@ -7,15 +7,14 @@ public record DiagnosticMatcher( string? Message = null) { public bool Matches(Diagnostic diagnostic) - { - if (!diagnostic.Descriptor.Equals(Descriptor)) - return false; + => Descriptor.Equals(diagnostic.Descriptor); - if (Message != null) - { - diagnostic.GetMessage().Should().Be(Message); - } + public void EnsureMatches(Diagnostic diagnostic) + { + diagnostic.Descriptor.Id + .Should() + .Be(Descriptor.Id); - return true; + Message?.Should().Be(diagnostic.GetMessage(), $"Message for descriptor id {Descriptor.Id} does not match"); } } diff --git a/test/Riok.Mapperly.Tests/GeneratedMethod.cs b/test/Riok.Mapperly.Tests/GeneratedMethod.cs new file mode 100644 index 0000000000..0cad1796a1 --- /dev/null +++ b/test/Riok.Mapperly.Tests/GeneratedMethod.cs @@ -0,0 +1,31 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Riok.Mapperly.Tests; + +public class GeneratedMethod +{ + + public GeneratedMethod(MethodDeclarationSyntax declarationSyntax) + { + Name = declarationSyntax.Identifier.ToString(); + Signature = $"{declarationSyntax.ReturnType.ToString()} {Name}{declarationSyntax.ParameterList.ToString().Trim()}"; + Body = ExtractBody(declarationSyntax); + } + + public string Name { get; } + + public string Signature { get; } + + public string Body { get; } + + private static string ExtractBody(MethodDeclarationSyntax declarationSyntax) + { + return declarationSyntax + .Body + ?.NormalizeWhitespace() + .ToFullString() + .Trim('{', '}', ' ', '\r', '\n') // simplify string to make assertions simpler + .ReplaceLineEndings() ?? string.Empty; + } +} diff --git a/test/Riok.Mapperly.Tests/Helpers/EnumerableExtensionsTest.cs b/test/Riok.Mapperly.Tests/Helpers/EnumerableExtensionsTest.cs index d64ca1453c..cfd0917c54 100644 --- a/test/Riok.Mapperly.Tests/Helpers/EnumerableExtensionsTest.cs +++ b/test/Riok.Mapperly.Tests/Helpers/EnumerableExtensionsTest.cs @@ -4,6 +4,15 @@ namespace Riok.Mapperly.Tests.Helpers; public class EnumerableExtensionsTest { + [Fact] + public void WhereValueTypeNotNullShouldFilterNulls() + { + new int?[] { 1, null, null, 2, 3, 4, null } + .WhereNotNull() + .Should() + .BeEquivalentTo(new[] { 1, 2, 3, 4 }, o => o.WithStrictOrdering()); + } + [Fact] public void WhereNotNullShouldFilterNulls() { diff --git a/test/Riok.Mapperly.Tests/MapperGenerationResult.cs b/test/Riok.Mapperly.Tests/MapperGenerationResult.cs index 3f592af597..3a2efd8f39 100644 --- a/test/Riok.Mapperly.Tests/MapperGenerationResult.cs +++ b/test/Riok.Mapperly.Tests/MapperGenerationResult.cs @@ -2,7 +2,9 @@ namespace Riok.Mapperly.Tests; -public record MapperGenerationResult(IReadOnlyCollection Diagnostics, IReadOnlyDictionary MethodBodies) +public record MapperGenerationResult( + IReadOnlyCollection Diagnostics, + IReadOnlyDictionary Methods) { public MapperGenerationResultAssertions Should() => new(this); diff --git a/test/Riok.Mapperly.Tests/MapperGenerationResultAssertions.cs b/test/Riok.Mapperly.Tests/MapperGenerationResultAssertions.cs index bb03ba621d..dad38fafe6 100644 --- a/test/Riok.Mapperly.Tests/MapperGenerationResultAssertions.cs +++ b/test/Riok.Mapperly.Tests/MapperGenerationResultAssertions.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis; namespace Riok.Mapperly.Tests; @@ -22,16 +23,17 @@ public MapperGenerationResultAssertions NotHaveDiagnostics(IReadOnlySet HaveMethodBody(TestSourceBuilder.DefaultMapMethodName, mapperMethodBody); } diff --git a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyInitPropertyTest.cs b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyInitPropertyTest.cs index 167526a620..ca1afa7d62 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyInitPropertyTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyInitPropertyTest.cs @@ -1,3 +1,5 @@ +using Riok.Mapperly.Diagnostics; + namespace Riok.Mapperly.Tests.Mapping; [UsesVerify] @@ -119,18 +121,20 @@ public void InitOnlyPropertyWithAutoFlattened() } [Fact] - public Task InitOnlyPropertyShouldDiagnosticOnVoidMethod() + public void InitOnlyPropertyShouldDiagnosticOnVoidMethod() { var source = TestSourceBuilder.MapperWithBodyAndTypes( "partial void Map(A source, B target);", "class A { public string StringValue { get; } }", "class B { public string StringValue { get; init; } }"); - - return TestHelper.VerifyGenerator(source); + TestHelper.GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic(new(DiagnosticDescriptors.CannotMapToInitOnlyPropertyPath, "Cannot map from property A.StringValue of type string to init only property path B.StringValue of type string")) + .HaveDiagnostic(new(DiagnosticDescriptors.SourcePropertyNotMapped, "The property StringValue on the mapping source type A is not mapped to any property on the mapping target type B")); } [Fact] - public Task InitOnlyPropertySourceNotFoundShouldDiagnostic() + public void InitOnlyPropertySourceNotFoundShouldDiagnostic() { var source = TestSourceBuilder.Mapping( "A", @@ -138,7 +142,10 @@ public Task InitOnlyPropertySourceNotFoundShouldDiagnostic() "class A { public string StringValue2 { get; init; } public int IntValue { get; set; } }", "class B { public string StringValue { get; init; } public int IntValue { get; set; } }"); - return TestHelper.VerifyGenerator(source); + TestHelper.GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic(new(DiagnosticDescriptors.MappingSourcePropertyNotFound, "Property StringValue on source type A was not found")) + .HaveDiagnostic(new(DiagnosticDescriptors.SourcePropertyNotMapped, "The property StringValue2 on the mapping source type A is not mapped to any property on the mapping target type B")); } [Fact] diff --git a/test/Riok.Mapperly.Tests/Mapping/ReferenceHandlingTest.cs b/test/Riok.Mapperly.Tests/Mapping/ReferenceHandlingTest.cs new file mode 100644 index 0000000000..eec5d55fb3 --- /dev/null +++ b/test/Riok.Mapperly.Tests/Mapping/ReferenceHandlingTest.cs @@ -0,0 +1,231 @@ +using Riok.Mapperly.Diagnostics; + +namespace Riok.Mapperly.Tests.Mapping; + +[UsesVerify] +public class ReferenceHandlingTest +{ + [Fact] + public Task ShouldWork() + { + var source = TestSourceBuilder.Mapping( + "A", + "B", + 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; } }", + "class C { public string StringValue { get; set; } }", + "class D { public string StringValue { get; set; } }"); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task ManuallyMappedPropertiesShouldWork() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapProperty(\"Value\", \"MyValue\")] partial B MapToB(A source);" + + "[MapProperty(\"Value\", \"MyValue2\")] partial B MapToB1(A source);", + TestSourceBuilderOptions.WithReferenceHandling, + "class A { public A Parent { get; set; } public C Value { get; set; } }", + "class B { public B Parent { get; set; } public D MyValue { get; set; } public D MyValue2 { get; set; } }", + "class C { public string StringValue { get; set; } }", + "class D { public string StringValue { get; set; } }"); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task EnumerableShouldWork() + { + var source = TestSourceBuilder.Mapping( + "A", + "B", + TestSourceBuilderOptions.WithReferenceHandling, + "class A { public IEnumerable Parent { get; set; } public C Value { get; set; } }", + "class B { public IEnumerable Parent { get; set; } public D Value { get; set; } }", + "class C { public string StringValue { get; set; } }", + "class D { public string StringValue { get; set; } }"); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task ArrayShouldWork() + { + var source = TestSourceBuilder.Mapping( + "A", + "B", + 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; } }", + "class C { public string StringValue { get; set; } }", + "class D { public string StringValue { get; set; } }"); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task ExistingInstanceShouldWork() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "partial void Map(A source, B target);", + 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; } }", + "class C { public string StringValue { get; set; } }", + "class D { public string StringValue { get; set; } }"); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task ObjectFactoryShouldWork() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[ObjectFactory] B CreateB() => new B();" + + "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; } }", + "class C { public string StringValue { get; set; } }", + "class D { public string StringValue { get; set; } }"); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task CustomHandlerShouldWork() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "partial B Map(A source, [ReferenceHandler] IReferenceHandler refHandler);", + 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; } }", + "class C { public string StringValue { get; set; } }", + "class D { public string StringValue { get; set; } }"); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public void CustomHandlerWithWrongTypeShouldDiagnostic() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "partial B Map(A source, [ReferenceHandler] MyRefHandler refHandler);", + TestSourceBuilderOptions.WithReferenceHandling, + "class MyRefHandler : IReferenceHandler {}", + "class A { public A Parent { get; set; } public C Value { get; set; } }", + "class B { public B Parent { get; set; } public D Value { get; set; } }", + "class C { public string StringValue { get; set; } }", + "class D { public string StringValue { get; set; } }"); + + TestHelper.GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic(new DiagnosticMatcher( + DiagnosticDescriptors.ReferenceHandlerParameterWrongType, + "The reference handler parameter of Mapper.Map needs to be of type Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler but is MyRefHandler")); + } + + [Fact] + public void CustomHandlerWithDisabledReferenceHandlingShouldDiagnostic() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "partial B Map(A source, [ReferenceHandler] IReferenceHandler refHandler);", + "class A { public string Value { get; set; } }", + "class B { public string Value { get; set; } }"); + + TestHelper.GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic(new DiagnosticMatcher( + DiagnosticDescriptors.ReferenceHandlingNotEnabled, + "Mapper.Map uses reference handling, but it is not enabled on the mapper attribute, to enable reference handling set UseReferenceHandling to true")); + } + + [Fact] + public Task CustomHandlerWithObjectFactoryShouldWork() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[ObjectFactory] B CreateB() => new B();" + + "partial B Map(A a, [ReferenceHandler] IReferenceHandler refHandler);", + 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; } }", + "class C { public string StringValue { get; set; } }", + "class D { public string StringValue { get; set; } }"); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task CustomHandlerWithExistingInstanceShouldWork() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "partial void Map(A a, B b, [ReferenceHandler] IReferenceHandler refHandler);", + 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; } }", + "class C { public string StringValue { get; set; } }", + "class D { public string StringValue { get; set; } }"); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task CustomHandlerAtIndex0ShouldWork() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "partial B Map([ReferenceHandler] IReferenceHandler refHandler, 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; } }", + "class C { public string StringValue { get; set; } }", + "class D { public string StringValue { get; set; } }"); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task CustomHandlerAtIndex1WithExistingInstanceShouldWork() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "partial void Map([ReferenceHandler] IReferenceHandler refHandler, A a, B b);", + 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; } }", + "class C { public string StringValue { get; set; } }", + "class D { public string StringValue { get; set; } }"); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task UserImplementedWithoutReferenceHandlerShouldWork() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "partial B Map(A a);" + + "string ToStringMod(string s) => s + \"-modified\";", + 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; } }", + "class C { public string StringValue { get; set; } }", + "class D { public string StringValue { get; set; } }"); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task UserImplementedWithReferenceHandlerShouldWork() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "partial B Map(A a);" + + "string ToStringMod(string s, [ReferenceHandler] IReferenceHandler _) => s + \"-modified\";", + 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; } }", + "class C { public string StringValue { get; set; } }", + "class D { public string StringValue { get; set; } }"); + + return TestHelper.VerifyGenerator(source); + } +} diff --git a/test/Riok.Mapperly.Tests/Mapping/UserMethodTest.cs b/test/Riok.Mapperly.Tests/Mapping/UserMethodTest.cs index 2453dfcbe4..ab7871f061 100644 --- a/test/Riok.Mapperly.Tests/Mapping/UserMethodTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/UserMethodTest.cs @@ -1,3 +1,5 @@ +using Riok.Mapperly.Diagnostics; + namespace Riok.Mapperly.Tests.Mapping; [UsesVerify] @@ -99,7 +101,7 @@ public void WithMultipleUserDefinedMethodDifferentConfigShouldWork() var mapper = TestHelper.GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics); mapper.Should() - .HaveMethodCount(2) + .HaveOnlyMethods("Map", "Map2") .HaveMethodBody("Map", @"var target = new B(); target.StringValue = source.StringValue; return target;") @@ -119,25 +121,29 @@ public void WithSameNamesShouldGenerateUniqueMethodNames() TestHelper.GenerateMapper(source) .Should() - .HaveMethods("MapToB", "MapToB1"); + .HaveOnlyMethods("MapToB", "MapToB1"); } [Fact] - public Task WithInvalidSignatureShouldDiagnostic() + public void WithInvalidSignatureShouldDiagnostic() { var source = TestSourceBuilder.MapperWithBodyAndTypes( "partial string ToString(T source, string format);"); - return TestHelper.VerifyGenerator(source); + TestHelper.GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic(new DiagnosticMatcher(DiagnosticDescriptors.UnsupportedMappingMethodSignature, "ToString has an unsupported mapping method signature")); } [Fact] - public Task WithInvalidGenericSignatureShouldDiagnostic() + public void WithInvalidGenericSignatureShouldDiagnostic() { var source = TestSourceBuilder.MapperWithBodyAndTypes( "partial string ToString(T source);"); - return TestHelper.VerifyGenerator(source); + TestHelper.GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic(new DiagnosticMatcher(DiagnosticDescriptors.UnsupportedMappingMethodSignature, "ToString has an unsupported mapping method signature")); } [Fact] diff --git a/test/Riok.Mapperly.Tests/StringSyntax.cs b/test/Riok.Mapperly.Tests/StringSyntax.cs new file mode 100644 index 0000000000..c0db615b13 --- /dev/null +++ b/test/Riok.Mapperly.Tests/StringSyntax.cs @@ -0,0 +1,6 @@ +namespace Riok.Mapperly.Tests; + +public static class StringSyntax +{ + public const string CSharp = "csharp"; +} diff --git a/test/Riok.Mapperly.Tests/TestHelper.cs b/test/Riok.Mapperly.Tests/TestHelper.cs index 71299e7db4..4fecbb6f98 100644 --- a/test/Riok.Mapperly.Tests/TestHelper.cs +++ b/test/Riok.Mapperly.Tests/TestHelper.cs @@ -26,12 +26,13 @@ public static MapperGenerationResult GenerateMapper(string source, TestHelperOpt .ChildNodes() .OfType() .Single(); - var methodBodies = mapperClassImpl + var methods = mapperClassImpl .ChildNodes() .OfType() - .ToDictionary(m => m.Identifier.ToString(), ExtractBody); + .Select(x => new GeneratedMethod(x)) + .ToDictionary(x => x.Name); - var mapperResult = new MapperGenerationResult(result.Diagnostics, methodBodies); + var mapperResult = new MapperGenerationResult(result.Diagnostics, methods); if (options.AllowedDiagnostics != null) { mapperResult.Should().NotHaveDiagnostics(options.AllowedDiagnostics); @@ -40,16 +41,6 @@ public static MapperGenerationResult GenerateMapper(string source, TestHelperOpt return mapperResult; } - private static string ExtractBody(MethodDeclarationSyntax methodImpl) - { - return methodImpl - .Body - ?.NormalizeWhitespace() - .ToFullString() - .Trim('{', '}', ' ', '\r', '\n') // simplify string to make assertions simpler - .ReplaceLineEndings() ?? string.Empty; - } - private static GeneratorDriver Generate( string source, TestHelperOptions? options) diff --git a/test/Riok.Mapperly.Tests/TestSourceBuilder.cs b/test/Riok.Mapperly.Tests/TestSourceBuilder.cs index 9ac04a13de..dd6f5c9b43 100644 --- a/test/Riok.Mapperly.Tests/TestSourceBuilder.cs +++ b/test/Riok.Mapperly.Tests/TestSourceBuilder.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace Riok.Mapperly.Tests; @@ -6,10 +7,10 @@ public static class TestSourceBuilder { internal const string DefaultMapMethodName = "Map"; - public static string Mapping(string fromTypeName, string toTypeName, params string[] types) + public static string Mapping(string fromTypeName, string toTypeName, [StringSyntax(StringSyntax.CSharp)] params string[] types) => Mapping(fromTypeName, toTypeName, null, types); - public static string Mapping(string fromTypeName, string toTypeName, TestSourceBuilderOptions? options, params string[] types) + public static string Mapping(string fromTypeName, string toTypeName, TestSourceBuilderOptions? options, [StringSyntax(StringSyntax.CSharp)] params string[] types) { return MapperWithBodyAndTypes( $"partial {toTypeName} {DefaultMapMethodName}({fromTypeName} source);", @@ -17,7 +18,7 @@ public static string Mapping(string fromTypeName, string toTypeName, TestSourceB types); } - public static string MapperWithBody(string body, TestSourceBuilderOptions? options = null) + public static string MapperWithBody([StringSyntax(StringSyntax.CSharp)] string body, TestSourceBuilderOptions? options = null) { options ??= TestSourceBuilderOptions.Default; @@ -25,6 +26,7 @@ public static string MapperWithBody(string body, TestSourceBuilderOptions? optio using System; using System.Collections.Generic; using Riok.Mapperly.Abstractions; +using Riok.Mapperly.Abstractions.ReferenceHandling; {(options.Namespace != null ? $"namespace {options.Namespace};" : string.Empty)} @@ -36,11 +38,23 @@ public partial class Mapper "; } + public static string MapperWithBodyAndTypes([StringSyntax(StringSyntax.CSharp)] string body, [StringSyntax(StringSyntax.CSharp)] params string[] types) + => MapperWithBodyAndTypes(body, null, types); + + public static string MapperWithBodyAndTypes([StringSyntax(StringSyntax.CSharp)] string body, TestSourceBuilderOptions? options, [StringSyntax(StringSyntax.CSharp)] params string[] types) + { + var sep = Environment.NewLine + Environment.NewLine; + return MapperWithBody(body, options) + + sep + + string.Join(sep, types); + } + private static string BuildAttribute(TestSourceBuilderOptions options) { var attrs = new[] { Attribute(options.UseDeepCloning), + Attribute(options.UseReferenceHandling), Attribute(options.ThrowOnMappingNullMismatch), Attribute(options.ThrowOnPropertyMappingNullMismatch), Attribute(options.EnabledConversions), @@ -64,15 +78,4 @@ private static string Attribute(string value, [CallerArgumentExpression("value") return $"{expression.Split(".").Last()} = {value}"; } - - public static string MapperWithBodyAndTypes(string body, params string[] types) - => MapperWithBodyAndTypes(body, null, types); - - public static string MapperWithBodyAndTypes(string body, TestSourceBuilderOptions? options, params string[] types) - { - var sep = Environment.NewLine + Environment.NewLine; - return MapperWithBody(body, options) - + sep - + string.Join(sep, types); - } } diff --git a/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs b/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs index e7158d24f4..2b3550c962 100644 --- a/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs +++ b/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs @@ -5,6 +5,7 @@ namespace Riok.Mapperly.Tests; public record TestSourceBuilderOptions( string? Namespace = null, bool UseDeepCloning = false, + bool UseReferenceHandling = false, bool ThrowOnMappingNullMismatch = true, bool ThrowOnPropertyMappingNullMismatch = false, PropertyNameMappingStrategy PropertyNameMappingStrategy = PropertyNameMappingStrategy.CaseSensitive, @@ -12,6 +13,8 @@ public record TestSourceBuilderOptions( { public static readonly TestSourceBuilderOptions Default = new(); public static readonly TestSourceBuilderOptions WithDeepCloning = new(UseDeepCloning: true); + public static readonly TestSourceBuilderOptions WithReferenceHandling = new(UseReferenceHandling: true); + public static TestSourceBuilderOptions WithDisabledMappingConversion(params MappingConversionType[] conversionTypes) { var enabled = MappingConversionType.All; @@ -21,6 +24,6 @@ public static TestSourceBuilderOptions WithDisabledMappingConversion(params Mapp enabled &= ~disabledConversionType; } - return Default with { EnabledConversions = enabled }; + return new(EnabledConversions: enabled); } } diff --git a/test/Riok.Mapperly.Tests/_snapshots/DictionaryTest.DictionaryToCustomDictionaryWithPrivateCtorShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/DictionaryTest.DictionaryToCustomDictionaryWithPrivateCtorShouldDiagnostic.verified.txt index 3bdc03409e..73add26510 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/DictionaryTest.DictionaryToCustomDictionaryWithPrivateCtorShouldDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/DictionaryTest.DictionaryToCustomDictionaryWithPrivateCtorShouldDiagnostic.verified.txt @@ -5,7 +5,7 @@ Title: No accessible parameterless constructor found, Severity: Error, WarningLevel: 0, - Location: : (10,4)-(10,51), + Location: : (11,4)-(11,51), Description: , HelpLink: , MessageFormat: {0} has no accessible parameterless constructor, @@ -17,7 +17,7 @@ Title: No accessible parameterless constructor found, Severity: Error, WarningLevel: 0, - Location: : (10,4)-(10,51), + Location: : (11,4)-(11,51), Description: , HelpLink: , MessageFormat: {0} has no accessible parameterless constructor, @@ -29,7 +29,7 @@ Title: Could not create mapping, Severity: Error, WarningLevel: 0, - Location: : (10,4)-(10,51), + Location: : (11,4)-(11,51), Description: , HelpLink: , MessageFormat: Could not create mapping from {0} to {1}. Consider implementing the mapping manually., diff --git a/test/Riok.Mapperly.Tests/_snapshots/EnumTest.EnumToOtherEnumByNameWithoutOverlap.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/EnumTest.EnumToOtherEnumByNameWithoutOverlap.verified.txt index 55fc3bd1f1..f08cfbdd0d 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/EnumTest.EnumToOtherEnumByNameWithoutOverlap.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/EnumTest.EnumToOtherEnumByNameWithoutOverlap.verified.txt @@ -5,7 +5,7 @@ Title: No overlapping enum members found, Severity: Warning, WarningLevel: 1, - Location: : (10,4)-(10,69), + Location: : (11,4)-(11,69), Description: , HelpLink: , MessageFormat: {0} and {1} don't have overlapping enum member names, mapping will therefore always result in an exception, @@ -13,4 +13,4 @@ Category: Mapper } ] -} \ No newline at end of file +} diff --git a/test/Riok.Mapperly.Tests/_snapshots/NullableTest.NullableToNonNullableWithNoThrowNoAccessibleCtorShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/NullableTest.NullableToNonNullableWithNoThrowNoAccessibleCtorShouldDiagnostic.verified.txt index 60ab859dc8..5986ce088b 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/NullableTest.NullableToNonNullableWithNoThrowNoAccessibleCtorShouldDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/NullableTest.NullableToNonNullableWithNoThrowNoAccessibleCtorShouldDiagnostic.verified.txt @@ -5,7 +5,7 @@ Title: No accessible parameterless constructor found, Severity: Error, WarningLevel: 0, - Location: : (10,4)-(10,34), + Location: : (11,4)-(11,34), Description: , HelpLink: , MessageFormat: {0} has no accessible parameterless constructor, @@ -13,4 +13,4 @@ Category: Mapper } ] -} \ No newline at end of file +} diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyConstructorResolverTest.ClassToClassPrivateCtorShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyConstructorResolverTest.ClassToClassPrivateCtorShouldDiagnostic.verified.txt index f4db388c9b..ffb2bcd45e 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyConstructorResolverTest.ClassToClassPrivateCtorShouldDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyConstructorResolverTest.ClassToClassPrivateCtorShouldDiagnostic.verified.txt @@ -5,7 +5,7 @@ Title: Could not create mapping, Severity: Error, WarningLevel: 0, - Location: : (10,4)-(10,28), + Location: : (11,4)-(11,28), Description: , HelpLink: , MessageFormat: Could not create mapping from {0} to {1}. Consider implementing the mapping manually., diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyConstructorResolverTest.ClassToClassUnmatchedAttributedCtorShouldDiagnosticAndUseAlternative.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyConstructorResolverTest.ClassToClassUnmatchedAttributedCtorShouldDiagnosticAndUseAlternative.verified.txt index 6f8d7cd85a..a998b279b7 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyConstructorResolverTest.ClassToClassUnmatchedAttributedCtorShouldDiagnosticAndUseAlternative.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyConstructorResolverTest.ClassToClassUnmatchedAttributedCtorShouldDiagnosticAndUseAlternative.verified.txt @@ -5,7 +5,7 @@ Title: Cannot map to the configured constructor to be used by Mapperly, Severity: Warning, WarningLevel: 1, - Location: : (10,4)-(10,28), + Location: : (11,4)-(11,28), Description: , HelpLink: , MessageFormat: Cannot map from {0} to the configured constructor {1}, diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyConstructorResolverTest.ClassToClassUnmatchedCtorShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyConstructorResolverTest.ClassToClassUnmatchedCtorShouldDiagnostic.verified.txt index 4bfa733eb3..1d2c5bcae6 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyConstructorResolverTest.ClassToClassUnmatchedCtorShouldDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyConstructorResolverTest.ClassToClassUnmatchedCtorShouldDiagnostic.verified.txt @@ -5,7 +5,7 @@ Title: No accessible constructor with mappable arguments found, Severity: Error, WarningLevel: 0, - Location: : (10,4)-(10,28), + Location: : (11,4)-(11,28), Description: , HelpLink: , MessageFormat: {0} has no accessible constructor with mappable arguments, @@ -17,7 +17,7 @@ Title: Source property is not mapped to any target property, Severity: Info, WarningLevel: 1, - Location: : (10,4)-(10,28), + Location: : (11,4)-(11,28), Description: , HelpLink: , MessageFormat: The property {0} on the mapping source type {1} is not mapped to any property on the mapping target type {2}, diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyConstructorResolverTest.ClassToClassWithOneMatchingCtorAndUnmatchedSourcePropertyShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyConstructorResolverTest.ClassToClassWithOneMatchingCtorAndUnmatchedSourcePropertyShouldDiagnostic.verified.txt index bbe902f731..e66218254f 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyConstructorResolverTest.ClassToClassWithOneMatchingCtorAndUnmatchedSourcePropertyShouldDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyConstructorResolverTest.ClassToClassWithOneMatchingCtorAndUnmatchedSourcePropertyShouldDiagnostic.verified.txt @@ -5,7 +5,7 @@ Title: Source property is not mapped to any target property, Severity: Info, WarningLevel: 1, - Location: : (10,4)-(10,28), + Location: : (11,4)-(11,28), Description: , HelpLink: , MessageFormat: The property {0} on the mapping source type {1} is not mapped to any property on the mapping target type {2}, diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyFlatteningTest.ManualUnflattenedPropertyNullablePathNoParameterlessCtorShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyFlatteningTest.ManualUnflattenedPropertyNullablePathNoParameterlessCtorShouldDiagnostic.verified.txt index 07284ded82..a5bfd25688 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyFlatteningTest.ManualUnflattenedPropertyNullablePathNoParameterlessCtorShouldDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyFlatteningTest.ManualUnflattenedPropertyNullablePathNoParameterlessCtorShouldDiagnostic.verified.txt @@ -5,7 +5,7 @@ Title: No accessible parameterless constructor found, Severity: Error, WarningLevel: 0, - Location: : (10,4)-(10,68), + Location: : (11,4)-(11,68), Description: , HelpLink: , MessageFormat: {0} has no accessible parameterless constructor, diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyFlatteningTest.ManualUnflattenedPropertySourcePropertyNotFoundShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyFlatteningTest.ManualUnflattenedPropertySourcePropertyNotFoundShouldDiagnostic.verified.txt index 1b51e0f5c4..fa21d0497f 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyFlatteningTest.ManualUnflattenedPropertySourcePropertyNotFoundShouldDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyFlatteningTest.ManualUnflattenedPropertySourcePropertyNotFoundShouldDiagnostic.verified.txt @@ -5,7 +5,7 @@ Title: Mapping source property not found, Severity: Error, WarningLevel: 0, - Location: : (10,4)-(10,71), + Location: : (11,4)-(11,71), Description: , HelpLink: , MessageFormat: Specified property {0} on source type {1} was not found, @@ -17,7 +17,7 @@ Title: Source property is not mapped to any target property, Severity: Info, WarningLevel: 1, - Location: : (10,4)-(10,71), + Location: : (11,4)-(11,71), Description: , HelpLink: , MessageFormat: The property {0} on the mapping source type {1} is not mapped to any property on the mapping target type {2}, diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyFlatteningTest.ManualUnflattenedPropertyTargetPropertyNotFoundShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyFlatteningTest.ManualUnflattenedPropertyTargetPropertyNotFoundShouldDiagnostic.verified.txt index 18a070ebef..70d6e17a9b 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyFlatteningTest.ManualUnflattenedPropertyTargetPropertyNotFoundShouldDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyFlatteningTest.ManualUnflattenedPropertyTargetPropertyNotFoundShouldDiagnostic.verified.txt @@ -5,7 +5,7 @@ Title: Mapping target property not found, Severity: Error, WarningLevel: 0, - Location: : (10,4)-(10,71), + Location: : (11,4)-(11,71), Description: , HelpLink: , MessageFormat: Specified property {0} on mapping target type {1} was not found, @@ -17,7 +17,7 @@ Title: Source property is not mapped to any target property, Severity: Info, WarningLevel: 1, - Location: : (10,4)-(10,71), + Location: : (11,4)-(11,71), Description: , HelpLink: , MessageFormat: The property {0} on the mapping source type {1} is not mapped to any property on the mapping target type {2}, diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyFlatteningTest.ManualUnflattenedPropertyTargetPropertyPathWriteOnlyShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyFlatteningTest.ManualUnflattenedPropertyTargetPropertyPathWriteOnlyShouldDiagnostic.verified.txt index 2b9598c9e3..c2ce6a5ebb 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyFlatteningTest.ManualUnflattenedPropertyTargetPropertyPathWriteOnlyShouldDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyFlatteningTest.ManualUnflattenedPropertyTargetPropertyPathWriteOnlyShouldDiagnostic.verified.txt @@ -5,7 +5,7 @@ Title: Cannot map to write only property path, Severity: Info, WarningLevel: 1, - Location: : (10,4)-(10,68), + Location: : (11,4)-(11,68), Description: , HelpLink: , MessageFormat: Cannot map from property {0}.{1} of type {2} to write only property path {3}.{4} of type {5}, @@ -17,7 +17,7 @@ Title: Source property is not mapped to any target property, Severity: Info, WarningLevel: 1, - Location: : (10,4)-(10,68), + Location: : (11,4)-(11,68), Description: , HelpLink: , MessageFormat: The property {0} on the mapping source type {1} is not mapped to any property on the mapping target type {2}, diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertyShouldDiagnosticOnVoidMethod#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertyShouldDiagnosticOnVoidMethod#Mapper.g.verified.cs deleted file mode 100644 index dca540741a..0000000000 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertyShouldDiagnosticOnVoidMethod#Mapper.g.verified.cs +++ /dev/null @@ -1,8 +0,0 @@ -//HintName: Mapper.g.cs -#nullable enable -public partial class Mapper -{ - private partial void Map(A source, B target) - { - } -} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertyShouldDiagnosticOnVoidMethod.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertyShouldDiagnosticOnVoidMethod.verified.txt deleted file mode 100644 index c5cc4c9251..0000000000 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertyShouldDiagnosticOnVoidMethod.verified.txt +++ /dev/null @@ -1,28 +0,0 @@ -{ - Diagnostics: [ - { - Id: RMG015, - Title: Cannot map to init only property path, - Severity: Info, - WarningLevel: 1, - Location: : (10,4)-(10,41), - Description: , - HelpLink: , - MessageFormat: Cannot map from property {0}.{1} of type {2} to init only property path {3}.{4} of type {5}, - Message: Cannot map from property A.StringValue of type string to init only property path B.StringValue of type string, - Category: Mapper - }, - { - Id: RMG020, - Title: Source property is not mapped to any target property, - Severity: Info, - WarningLevel: 1, - Location: : (10,4)-(10,41), - Description: , - HelpLink: , - MessageFormat: The property {0} on the mapping source type {1} is not mapped to any property on the mapping target type {2}, - Message: The property StringValue on the mapping source type A is not mapped to any property on the mapping target type B, - Category: Mapper - } - ] -} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertySourceNotFoundShouldDiagnostic#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertySourceNotFoundShouldDiagnostic#Mapper.g.verified.cs deleted file mode 100644 index 4bc06314fe..0000000000 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertySourceNotFoundShouldDiagnostic#Mapper.g.verified.cs +++ /dev/null @@ -1,11 +0,0 @@ -//HintName: Mapper.g.cs -#nullable enable -public partial class Mapper -{ - private partial B Map(A source) - { - var target = new B(); - target.IntValue = source.IntValue; - return target; - } -} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertySourceNotFoundShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertySourceNotFoundShouldDiagnostic.verified.txt deleted file mode 100644 index 5f8ca88242..0000000000 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertySourceNotFoundShouldDiagnostic.verified.txt +++ /dev/null @@ -1,28 +0,0 @@ -{ - Diagnostics: [ - { - Id: RMG012, - Title: Mapping source property not found, - Severity: Info, - WarningLevel: 1, - Location: : (10,4)-(10,28), - Description: , - HelpLink: , - MessageFormat: Property {0} on source type {1} was not found, - Message: Property StringValue on source type A was not found, - Category: Mapper - }, - { - Id: RMG020, - Title: Source property is not mapped to any target property, - Severity: Info, - WarningLevel: 1, - Location: : (10,4)-(10,28), - Description: , - HelpLink: , - MessageFormat: The property {0} on the mapping source type {1} is not mapped to any property on the mapping target type {2}, - Message: The property StringValue2 on the mapping source type A is not mapped to any property on the mapping target type B, - Category: Mapper - } - ] -} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertyWithConfigurationNotFoundSourcePropertyShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertyWithConfigurationNotFoundSourcePropertyShouldDiagnostic.verified.txt index a70594b706..4042735569 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertyWithConfigurationNotFoundSourcePropertyShouldDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertyWithConfigurationNotFoundSourcePropertyShouldDiagnostic.verified.txt @@ -5,7 +5,7 @@ Title: Mapping source property not found, Severity: Info, WarningLevel: 1, - Location: : (10,4)-(10,74), + Location: : (11,4)-(11,74), Description: , HelpLink: , MessageFormat: Property {0} on source type {1} was not found, @@ -17,7 +17,7 @@ Title: Source property is not mapped to any target property, Severity: Info, WarningLevel: 1, - Location: : (10,4)-(10,74), + Location: : (11,4)-(11,74), Description: , HelpLink: , MessageFormat: The property {0} on the mapping source type {1} is not mapped to any property on the mapping target type {2}, diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertyWithMultipleConfigurationsShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertyWithMultipleConfigurationsShouldDiagnostic.verified.txt index bed44bed39..b5e5fd3f78 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertyWithMultipleConfigurationsShouldDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertyWithMultipleConfigurationsShouldDiagnostic.verified.txt @@ -5,7 +5,7 @@ Title: An init only property can have one configuration at max, Severity: Warning, WarningLevel: 1, - Location: : (10,4)-(10,120), + Location: : (11,4)-(11,120), Description: , HelpLink: , MessageFormat: The init only property {0}.{1} can have one configuration at max, @@ -17,7 +17,7 @@ Title: Source property is not mapped to any target property, Severity: Info, WarningLevel: 1, - Location: : (10,4)-(10,120), + Location: : (11,4)-(11,120), Description: , HelpLink: , MessageFormat: The property {0} on the mapping source type {1} is not mapped to any property on the mapping target type {2}, diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertyWithPathConfigurationsShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertyWithPathConfigurationsShouldDiagnostic.verified.txt index 3df3f644a1..cd3a507a8c 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertyWithPathConfigurationsShouldDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertyWithPathConfigurationsShouldDiagnostic.verified.txt @@ -5,7 +5,7 @@ Title: Init only property cannot handle target paths, Severity: Error, WarningLevel: 0, - Location: : (10,4)-(10,74), + Location: : (11,4)-(11,74), Description: , HelpLink: , MessageFormat: Cannot map to init only property path {0}.{1}, @@ -17,7 +17,7 @@ Title: Source property is not mapped to any target property, Severity: Info, WarningLevel: 1, - Location: : (10,4)-(10,74), + Location: : (11,4)-(11,74), Description: , HelpLink: , MessageFormat: The property {0} on the mapping source type {1} is not mapped to any property on the mapping target type {2}, diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.RequiredPropertySourceNotFoundShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.RequiredPropertySourceNotFoundShouldDiagnostic.verified.txt index f7cb78bb2e..f4953b77ff 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.RequiredPropertySourceNotFoundShouldDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.RequiredPropertySourceNotFoundShouldDiagnostic.verified.txt @@ -5,7 +5,7 @@ Title: Source property was not found for required target property, Severity: Error, WarningLevel: 0, - Location: : (10,4)-(10,28), + Location: : (11,4)-(11,28), Description: , HelpLink: , MessageFormat: Required property {0} on mapping target type was not found on the mapping source type {1}, @@ -17,7 +17,7 @@ Title: Source property is not mapped to any target property, Severity: Info, WarningLevel: 1, - Location: : (10,4)-(10,28), + Location: : (11,4)-(11,28), Description: , HelpLink: , MessageFormat: The property {0} on the mapping source type {1} is not mapped to any property on the mapping target type {2}, diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ShouldIgnoreReadOnlyPropertyOnTargetWithDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ShouldIgnoreReadOnlyPropertyOnTargetWithDiagnostic.verified.txt index f28bac0249..a286b722a2 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ShouldIgnoreReadOnlyPropertyOnTargetWithDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ShouldIgnoreReadOnlyPropertyOnTargetWithDiagnostic.verified.txt @@ -5,7 +5,7 @@ Title: Cannot map to read only property, Severity: Info, WarningLevel: 1, - Location: : (10,4)-(10,28), + Location: : (11,4)-(11,28), Description: , HelpLink: , MessageFormat: Cannot map property {0}.{1} of type {2} to read only property {3}.{4} of type {5}, @@ -17,7 +17,7 @@ Title: Source property is not mapped to any target property, Severity: Info, WarningLevel: 1, - Location: : (10,4)-(10,28), + Location: : (11,4)-(11,28), Description: , HelpLink: , MessageFormat: The property {0} on the mapping source type {1} is not mapped to any property on the mapping target type {2}, diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ShouldIgnoreWriteOnlyPropertyOnSourceWithDiagnostics.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ShouldIgnoreWriteOnlyPropertyOnSourceWithDiagnostics.verified.txt index de5d059383..61e8a9b867 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ShouldIgnoreWriteOnlyPropertyOnSourceWithDiagnostics.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ShouldIgnoreWriteOnlyPropertyOnSourceWithDiagnostics.verified.txt @@ -5,7 +5,7 @@ Title: Cannot map from write only property, Severity: Info, WarningLevel: 1, - Location: : (10,4)-(10,28), + Location: : (11,4)-(11,28), Description: , HelpLink: , MessageFormat: Cannot map from write only property {0}.{1} of type {2} to property {3}.{4} of type {5}, @@ -17,7 +17,7 @@ Title: Source property is not mapped to any target property, Severity: Info, WarningLevel: 1, - Location: : (10,4)-(10,28), + Location: : (11,4)-(11,28), Description: , HelpLink: , MessageFormat: The property {0} on the mapping source type {1} is not mapped to any property on the mapping target type {2}, diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithIgnoredSourcePropertyShouldIgnoreAndGenerateDiagnostics.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithIgnoredSourcePropertyShouldIgnoreAndGenerateDiagnostics.verified.txt index 269dc80a64..0c71804518 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithIgnoredSourcePropertyShouldIgnoreAndGenerateDiagnostics.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithIgnoredSourcePropertyShouldIgnoreAndGenerateDiagnostics.verified.txt @@ -5,7 +5,7 @@ Title: Mapping source property not found, Severity: Info, WarningLevel: 1, - Location: : (10,4)-(10,69), + Location: : (11,4)-(11,69), Description: , HelpLink: , MessageFormat: Property {0} on source type {1} was not found, diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithIgnoredTargetPropertyShouldIgnoreAndGenerateDiagnostics.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithIgnoredTargetPropertyShouldIgnoreAndGenerateDiagnostics.verified.txt index 91abc68533..d6566a6d9b 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithIgnoredTargetPropertyShouldIgnoreAndGenerateDiagnostics.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithIgnoredTargetPropertyShouldIgnoreAndGenerateDiagnostics.verified.txt @@ -5,7 +5,7 @@ Title: Source property is not mapped to any target property, Severity: Info, WarningLevel: 1, - Location: : (10,4)-(10,69), + Location: : (11,4)-(11,69), Description: , HelpLink: , MessageFormat: The property {0} on the mapping source type {1} is not mapped to any property on the mapping target type {2}, diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualMappedNotFoundSourcePropertyShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualMappedNotFoundSourcePropertyShouldDiagnostic.verified.txt index f712bdf2ce..abff9ed13f 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualMappedNotFoundSourcePropertyShouldDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualMappedNotFoundSourcePropertyShouldDiagnostic.verified.txt @@ -5,7 +5,7 @@ Title: Mapping source property not found, Severity: Error, WarningLevel: 0, - Location: : (10,4)-(10,89), + Location: : (11,4)-(11,89), Description: , HelpLink: , MessageFormat: Specified property {0} on source type {1} was not found, @@ -17,7 +17,7 @@ Title: Source property is not mapped to any target property, Severity: Info, WarningLevel: 1, - Location: : (10,4)-(10,89), + Location: : (11,4)-(11,89), Description: , HelpLink: , MessageFormat: The property {0} on the mapping source type {1} is not mapped to any property on the mapping target type {2}, diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualMappedNotFoundTargetPropertyShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualMappedNotFoundTargetPropertyShouldDiagnostic.verified.txt index 935918ccfa..b172e4ae5e 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualMappedNotFoundTargetPropertyShouldDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualMappedNotFoundTargetPropertyShouldDiagnostic.verified.txt @@ -5,7 +5,7 @@ Title: Mapping source property not found, Severity: Info, WarningLevel: 1, - Location: : (10,4)-(10,88), + Location: : (11,4)-(11,88), Description: , HelpLink: , MessageFormat: Property {0} on source type {1} was not found, @@ -17,7 +17,7 @@ Title: Mapping target property not found, Severity: Error, WarningLevel: 0, - Location: : (10,4)-(10,88), + Location: : (11,4)-(11,88), Description: , HelpLink: , MessageFormat: Specified property {0} on mapping target type {1} was not found, @@ -29,7 +29,7 @@ Title: Source property is not mapped to any target property, Severity: Info, WarningLevel: 1, - Location: : (10,4)-(10,88), + Location: : (11,4)-(11,88), Description: , HelpLink: , MessageFormat: The property {0} on the mapping source type {1} is not mapped to any property on the mapping target type {2}, diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualNotFoundSourcePropertyShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualNotFoundSourcePropertyShouldDiagnostic.verified.txt index 847e5c1082..5039135c8d 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualNotFoundSourcePropertyShouldDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualNotFoundSourcePropertyShouldDiagnostic.verified.txt @@ -5,7 +5,7 @@ Title: Mapping source property not found, Severity: Error, WarningLevel: 0, - Location: : (10,4)-(10,79), + Location: : (11,4)-(11,79), Description: , HelpLink: , MessageFormat: Specified property {0} on source type {1} was not found, @@ -17,7 +17,7 @@ Title: Source property is not mapped to any target property, Severity: Info, WarningLevel: 1, - Location: : (10,4)-(10,79), + Location: : (11,4)-(11,79), Description: , HelpLink: , MessageFormat: The property {0} on the mapping source type {1} is not mapped to any property on the mapping target type {2}, diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithNotFoundIgnoredObsoleteTargetAttributePropertyShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithNotFoundIgnoredObsoleteTargetAttributePropertyShouldDiagnostic.verified.txt index 476262418e..d50706a438 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithNotFoundIgnoredObsoleteTargetAttributePropertyShouldDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithNotFoundIgnoredObsoleteTargetAttributePropertyShouldDiagnostic.verified.txt @@ -5,7 +5,7 @@ Title: Ignored target property not found, Severity: Warning, WarningLevel: 1, - Location: : (10,4)-(10,56), + Location: : (11,4)-(11,56), Description: , HelpLink: , MessageFormat: Ignored target property {0} on {1} was not found, diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithNotFoundIgnoredSourcePropertyShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithNotFoundIgnoredSourcePropertyShouldDiagnostic.verified.txt index a08cc7e563..f45d9a07a9 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithNotFoundIgnoredSourcePropertyShouldDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithNotFoundIgnoredSourcePropertyShouldDiagnostic.verified.txt @@ -5,7 +5,7 @@ Title: Ignored source property not found, Severity: Warning, WarningLevel: 1, - Location: : (10,4)-(10,62), + Location: : (11,4)-(11,62), Description: , HelpLink: , MessageFormat: Ignored source property {0} on {1} was not found, diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithNotFoundIgnoredTargetPropertyShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithNotFoundIgnoredTargetPropertyShouldDiagnostic.verified.txt index 313de895e7..09d485eb0e 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithNotFoundIgnoredTargetPropertyShouldDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithNotFoundIgnoredTargetPropertyShouldDiagnostic.verified.txt @@ -5,7 +5,7 @@ Title: Ignored target property not found, Severity: Warning, WarningLevel: 1, - Location: : (10,4)-(10,62), + Location: : (11,4)-(11,62), Description: , HelpLink: , MessageFormat: Ignored target property {0} on {1} was not found, diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithObsoleteIgnoredTargetPropertyAttributeShouldIgnoreAndGenerateDiagnostics.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithObsoleteIgnoredTargetPropertyAttributeShouldIgnoreAndGenerateDiagnostics.verified.txt index 28ee72ca54..8365ef4616 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithObsoleteIgnoredTargetPropertyAttributeShouldIgnoreAndGenerateDiagnostics.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithObsoleteIgnoredTargetPropertyAttributeShouldIgnoreAndGenerateDiagnostics.verified.txt @@ -5,7 +5,7 @@ Title: Source property is not mapped to any target property, Severity: Info, WarningLevel: 1, - Location: : (10,4)-(10,63), + Location: : (11,4)-(11,63), Description: , HelpLink: , MessageFormat: The property {0} on the mapping source type {1} is not mapped to any property on the mapping target type {2}, diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPrivateSourceGetterShouldIgnoreAndDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPrivateSourceGetterShouldIgnoreAndDiagnostic.verified.txt index 6f23062705..42f4b4e5d1 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPrivateSourceGetterShouldIgnoreAndDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPrivateSourceGetterShouldIgnoreAndDiagnostic.verified.txt @@ -5,7 +5,7 @@ Title: Cannot map from write only property, Severity: Info, WarningLevel: 1, - Location: : (10,4)-(10,28), + Location: : (11,4)-(11,28), Description: , HelpLink: , MessageFormat: Cannot map from write only property {0}.{1} of type {2} to property {3}.{4} of type {5}, @@ -17,7 +17,7 @@ Title: Source property is not mapped to any target property, Severity: Info, WarningLevel: 1, - Location: : (10,4)-(10,28), + Location: : (11,4)-(11,28), Description: , HelpLink: , MessageFormat: The property {0} on the mapping source type {1} is not mapped to any property on the mapping target type {2}, diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPrivateSourcePathGetterShouldIgnoreAndDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPrivateSourcePathGetterShouldIgnoreAndDiagnostic.verified.txt index f74eacd42d..cdc9247871 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPrivateSourcePathGetterShouldIgnoreAndDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPrivateSourcePathGetterShouldIgnoreAndDiagnostic.verified.txt @@ -5,7 +5,7 @@ Title: Cannot map from write only property, Severity: Info, WarningLevel: 1, - Location: : (10,4)-(10,28), + Location: : (11,4)-(11,28), Description: , HelpLink: , MessageFormat: Cannot map from write only property {0}.{1} of type {2} to property {3}.{4} of type {5}, @@ -17,7 +17,7 @@ Title: Source property is not mapped to any target property, Severity: Info, WarningLevel: 1, - Location: : (10,4)-(10,28), + Location: : (11,4)-(11,28), Description: , HelpLink: , MessageFormat: The property {0} on the mapping source type {1} is not mapped to any property on the mapping target type {2}, diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPrivateTargetPathGetterShouldIgnoreAndDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPrivateTargetPathGetterShouldIgnoreAndDiagnostic.verified.txt index 19d9e5c3a2..c3258b3e79 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPrivateTargetPathGetterShouldIgnoreAndDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPrivateTargetPathGetterShouldIgnoreAndDiagnostic.verified.txt @@ -5,7 +5,7 @@ Title: Cannot map to read only property, Severity: Info, WarningLevel: 1, - Location: : (10,4)-(10,28), + Location: : (11,4)-(11,28), Description: , HelpLink: , MessageFormat: Cannot map property {0}.{1} of type {2} to read only property {3}.{4} of type {5}, @@ -17,7 +17,7 @@ Title: Source property is not mapped to any target property, Severity: Info, WarningLevel: 1, - Location: : (10,4)-(10,28), + Location: : (11,4)-(11,28), Description: , HelpLink: , MessageFormat: The property {0} on the mapping source type {1} is not mapped to any property on the mapping target type {2}, diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPrivateTargetSetterShouldIgnoreAndDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPrivateTargetSetterShouldIgnoreAndDiagnostic.verified.txt index c311d86ddb..cc80527f23 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPrivateTargetSetterShouldIgnoreAndDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPrivateTargetSetterShouldIgnoreAndDiagnostic.verified.txt @@ -5,7 +5,7 @@ Title: Cannot map to read only property, Severity: Info, WarningLevel: 1, - Location: : (10,4)-(10,28), + Location: : (11,4)-(11,28), Description: , HelpLink: , MessageFormat: Cannot map property {0}.{1} of type {2} to read only property {3}.{4} of type {5}, @@ -17,7 +17,7 @@ Title: Source property is not mapped to any target property, Severity: Info, WarningLevel: 1, - Location: : (10,4)-(10,28), + Location: : (11,4)-(11,28), Description: , HelpLink: , MessageFormat: The property {0} on the mapping source type {1} is not mapped to any property on the mapping target type {2}, diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPropertyNameMappingStrategyCaseSensitive.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPropertyNameMappingStrategyCaseSensitive.verified.txt index af75c35ae4..0af0985854 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPropertyNameMappingStrategyCaseSensitive.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPropertyNameMappingStrategyCaseSensitive.verified.txt @@ -5,7 +5,7 @@ Title: Mapping source property not found, Severity: Info, WarningLevel: 1, - Location: : (10,4)-(10,28), + Location: : (11,4)-(11,28), Description: , HelpLink: , MessageFormat: Property {0} on source type {1} was not found, @@ -17,7 +17,7 @@ Title: Source property is not mapped to any target property, Severity: Info, WarningLevel: 1, - Location: : (10,4)-(10,28), + Location: : (11,4)-(11,28), Description: , HelpLink: , MessageFormat: The property {0} on the mapping source type {1} is not mapped to any property on the mapping target type {2}, diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithUnmappablePropertyShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithUnmappablePropertyShouldDiagnostic.verified.txt index ff9d8ed2c0..d3395f86a7 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithUnmappablePropertyShouldDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithUnmappablePropertyShouldDiagnostic.verified.txt @@ -5,7 +5,7 @@ Title: Could not map property, Severity: Error, WarningLevel: 0, - Location: : (10,4)-(10,28), + Location: : (11,4)-(11,28), Description: , HelpLink: , MessageFormat: Could not map property {0}.{1} of type {2} to {3}.{4} of type {5}, @@ -17,7 +17,7 @@ Title: Source property is not mapped to any target property, Severity: Info, WarningLevel: 1, - Location: : (10,4)-(10,28), + Location: : (11,4)-(11,28), Description: , HelpLink: , MessageFormat: The property {0} on the mapping source type {1} is not mapped to any property on the mapping target type {2}, diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithUnmatchedPropertyShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithUnmatchedPropertyShouldDiagnostic.verified.txt index f1b3950704..d20a833f37 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithUnmatchedPropertyShouldDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithUnmatchedPropertyShouldDiagnostic.verified.txt @@ -5,7 +5,7 @@ Title: Mapping source property not found, Severity: Info, WarningLevel: 1, - Location: : (10,4)-(10,28), + Location: : (11,4)-(11,28), Description: , HelpLink: , MessageFormat: Property {0} on source type {1} was not found, @@ -17,7 +17,7 @@ Title: Source property is not mapped to any target property, Severity: Info, WarningLevel: 1, - Location: : (10,4)-(10,28), + Location: : (11,4)-(11,28), Description: , HelpLink: , MessageFormat: The property {0} on the mapping source type {1} is not mapped to any property on the mapping target type {2}, diff --git a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ArrayShouldWork#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ArrayShouldWork#Mapper.g.verified.cs new file mode 100644 index 0000000000..eccf07a7ab --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ArrayShouldWork#Mapper.g.verified.cs @@ -0,0 +1,41 @@ +//HintName: Mapper.g.cs +#nullable enable +public partial class Mapper +{ + private partial B Map(A source) + { + return MapToB(source, new Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler()); + } + + private B MapToB(A source, Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + { + if (refHandler.TryGetReference(source, out var existingTargetReference)) + return existingTargetReference; + var target = new B(); + refHandler.SetReference(source, target); + target.Parent = MapToBArray(source.Parent, refHandler); + target.Value = MapToD(source.Value, refHandler); + return target; + } + + private B[] MapToBArray(A[] source, Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + { + var target = new B[source.Length]; + for (var i = 0; i < source.Length; i++) + { + target[i] = MapToB(source[i], refHandler); + } + + return target; + } + + private D MapToD(C source, Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + { + if (refHandler.TryGetReference(source, out var existingTargetReference)) + return existingTargetReference; + var target = new D(); + refHandler.SetReference(source, target); + target.StringValue = source.StringValue; + return target; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.CustomHandlerAtIndex0ShouldWork#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.CustomHandlerAtIndex0ShouldWork#Mapper.g.verified.cs new file mode 100644 index 0000000000..9c18bc1d75 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.CustomHandlerAtIndex0ShouldWork#Mapper.g.verified.cs @@ -0,0 +1,25 @@ +//HintName: Mapper.g.cs +#nullable enable +public partial class Mapper +{ + private partial B Map(Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler, A a) + { + if (refHandler.TryGetReference(a, out var existingTargetReference)) + return existingTargetReference; + var target = new B(); + refHandler.SetReference(a, target); + target.Parent = Map(refHandler, a.Parent); + target.Value = MapToD(a.Value, refHandler); + return target; + } + + private D MapToD(C source, Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + { + if (refHandler.TryGetReference(source, out var existingTargetReference)) + return existingTargetReference; + var target = new D(); + refHandler.SetReference(source, target); + target.StringValue = source.StringValue; + return target; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.CustomHandlerAtIndex1WithExistingInstanceShouldWork#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.CustomHandlerAtIndex1WithExistingInstanceShouldWork#Mapper.g.verified.cs new file mode 100644 index 0000000000..fe893b7efa --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.CustomHandlerAtIndex1WithExistingInstanceShouldWork#Mapper.g.verified.cs @@ -0,0 +1,31 @@ +//HintName: Mapper.g.cs +#nullable enable +public partial class Mapper +{ + private partial void Map(Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler, A a, B b) + { + b.Parent = MapToB(a.Parent, refHandler); + b.Value = MapToD(a.Value, refHandler); + } + + private B MapToB(A source, Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + { + if (refHandler.TryGetReference(source, out var existingTargetReference)) + return existingTargetReference; + var target = new B(); + refHandler.SetReference(source, target); + target.Parent = MapToB(source.Parent, refHandler); + target.Value = MapToD(source.Value, refHandler); + return target; + } + + private D MapToD(C source, Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + { + if (refHandler.TryGetReference(source, out var existingTargetReference)) + return existingTargetReference; + var target = new D(); + refHandler.SetReference(source, target); + target.StringValue = source.StringValue; + return target; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.CustomHandlerShouldWork#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.CustomHandlerShouldWork#Mapper.g.verified.cs new file mode 100644 index 0000000000..9c25824afe --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.CustomHandlerShouldWork#Mapper.g.verified.cs @@ -0,0 +1,25 @@ +//HintName: Mapper.g.cs +#nullable enable +public partial class Mapper +{ + private partial B Map(A source, Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + { + if (refHandler.TryGetReference(source, out var existingTargetReference)) + return existingTargetReference; + var target = new B(); + refHandler.SetReference(source, target); + target.Parent = Map(source.Parent, refHandler); + target.Value = MapToD(source.Value, refHandler); + return target; + } + + private D MapToD(C source, Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + { + if (refHandler.TryGetReference(source, out var existingTargetReference)) + return existingTargetReference; + var target = new D(); + refHandler.SetReference(source, target); + target.StringValue = source.StringValue; + return target; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.CustomHandlerWithExistingInstanceShouldWork#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.CustomHandlerWithExistingInstanceShouldWork#Mapper.g.verified.cs new file mode 100644 index 0000000000..082e176825 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.CustomHandlerWithExistingInstanceShouldWork#Mapper.g.verified.cs @@ -0,0 +1,31 @@ +//HintName: Mapper.g.cs +#nullable enable +public partial class Mapper +{ + private partial void Map(A a, B b, Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + { + b.Parent = MapToB(a.Parent, refHandler); + b.Value = MapToD(a.Value, refHandler); + } + + private B MapToB(A source, Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + { + if (refHandler.TryGetReference(source, out var existingTargetReference)) + return existingTargetReference; + var target = new B(); + refHandler.SetReference(source, target); + target.Parent = MapToB(source.Parent, refHandler); + target.Value = MapToD(source.Value, refHandler); + return target; + } + + private D MapToD(C source, Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + { + if (refHandler.TryGetReference(source, out var existingTargetReference)) + return existingTargetReference; + var target = new D(); + refHandler.SetReference(source, target); + target.StringValue = source.StringValue; + return target; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.CustomHandlerWithObjectFactoryShouldWork#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.CustomHandlerWithObjectFactoryShouldWork#Mapper.g.verified.cs new file mode 100644 index 0000000000..8f95703fd3 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.CustomHandlerWithObjectFactoryShouldWork#Mapper.g.verified.cs @@ -0,0 +1,25 @@ +//HintName: Mapper.g.cs +#nullable enable +public partial class Mapper +{ + private partial B Map(A a, Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + { + if (refHandler.TryGetReference(a, out var existingTargetReference)) + return existingTargetReference; + var target = CreateB(); + refHandler.SetReference(a, target); + target.Parent = Map(a.Parent, refHandler); + target.Value = MapToD(a.Value, refHandler); + return target; + } + + private D MapToD(C source, Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + { + if (refHandler.TryGetReference(source, out var existingTargetReference)) + return existingTargetReference; + var target = new D(); + refHandler.SetReference(source, target); + target.StringValue = source.StringValue; + return target; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.EnumerableShouldWork#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.EnumerableShouldWork#Mapper.g.verified.cs new file mode 100644 index 0000000000..9cce6aa34e --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.EnumerableShouldWork#Mapper.g.verified.cs @@ -0,0 +1,30 @@ +//HintName: Mapper.g.cs +#nullable enable +public partial class Mapper +{ + private partial B Map(A source) + { + return MapToB(source, new Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler()); + } + + private B MapToB(A source, Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + { + if (refHandler.TryGetReference(source, out var existingTargetReference)) + return existingTargetReference; + var target = new B(); + refHandler.SetReference(source, target); + target.Parent = System.Linq.Enumerable.Select(source.Parent, x => MapToB(x, refHandler)); + target.Value = MapToD(source.Value, refHandler); + return target; + } + + private D MapToD(C source, Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + { + if (refHandler.TryGetReference(source, out var existingTargetReference)) + return existingTargetReference; + var target = new D(); + refHandler.SetReference(source, target); + target.StringValue = source.StringValue; + return target; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ExistingInstanceShouldWork#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ExistingInstanceShouldWork#Mapper.g.verified.cs new file mode 100644 index 0000000000..59b5cc8196 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ExistingInstanceShouldWork#Mapper.g.verified.cs @@ -0,0 +1,32 @@ +//HintName: Mapper.g.cs +#nullable enable +public partial class Mapper +{ + private partial void Map(A source, B target) + { + var refHandler = new Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler(); + target.Parent = MapToB(source.Parent, refHandler); + target.Value = MapToD(source.Value, refHandler); + } + + private B MapToB(A source, Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + { + if (refHandler.TryGetReference(source, out var existingTargetReference)) + return existingTargetReference; + var target = new B(); + refHandler.SetReference(source, target); + target.Parent = MapToB(source.Parent, refHandler); + target.Value = MapToD(source.Value, refHandler); + return target; + } + + private D MapToD(C source, Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + { + if (refHandler.TryGetReference(source, out var existingTargetReference)) + return existingTargetReference; + var target = new D(); + refHandler.SetReference(source, target); + target.StringValue = source.StringValue; + return target; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ManuallyMappedPropertiesShouldWork#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ManuallyMappedPropertiesShouldWork#Mapper.g.verified.cs new file mode 100644 index 0000000000..2c8e260c98 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ManuallyMappedPropertiesShouldWork#Mapper.g.verified.cs @@ -0,0 +1,46 @@ +//HintName: Mapper.g.cs +#nullable enable +public partial class Mapper +{ + private partial B MapToB(A source) + { + return MapToB2(source, new Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler()); + } + + private partial B MapToB1(A source) + { + return MapToB3(source, new Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler()); + } + + private B MapToB2(A source, Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + { + if (refHandler.TryGetReference(source, out var existingTargetReference)) + return existingTargetReference; + var target = new B(); + refHandler.SetReference(source, target); + target.Parent = MapToB2(source.Parent, refHandler); + target.MyValue = MapToD(source.Value, refHandler); + return target; + } + + private B MapToB3(A source, Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + { + if (refHandler.TryGetReference(source, out var existingTargetReference)) + return existingTargetReference; + var target = new B(); + refHandler.SetReference(source, target); + target.Parent = MapToB2(source.Parent, refHandler); + target.MyValue2 = MapToD(source.Value, refHandler); + return target; + } + + private D MapToD(C source, Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + { + if (refHandler.TryGetReference(source, out var existingTargetReference)) + return existingTargetReference; + var target = new D(); + refHandler.SetReference(source, target); + target.StringValue = source.StringValue; + return target; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ManuallyMappedPropertiesShouldWork.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ManuallyMappedPropertiesShouldWork.verified.txt new file mode 100644 index 0000000000..06b54b0b82 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ManuallyMappedPropertiesShouldWork.verified.txt @@ -0,0 +1,28 @@ +{ + Diagnostics: [ + { + Id: RMG012, + Title: Mapping source property not found, + Severity: Info, + WarningLevel: 1, + Location: : (11,4)-(11,65), + Description: , + HelpLink: , + MessageFormat: Property {0} on source type {1} was not found, + Message: Property MyValue2 on source type A was not found, + Category: Mapper + }, + { + Id: RMG012, + Title: Mapping source property not found, + Severity: Info, + WarningLevel: 1, + Location: : (11,65)-(11,128), + Description: , + HelpLink: , + MessageFormat: Property {0} on source type {1} was not found, + Message: Property MyValue on source type A was not found, + Category: Mapper + } + ] +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ObjectFactoryShouldWork#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ObjectFactoryShouldWork#Mapper.g.verified.cs new file mode 100644 index 0000000000..59ab762329 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ObjectFactoryShouldWork#Mapper.g.verified.cs @@ -0,0 +1,30 @@ +//HintName: Mapper.g.cs +#nullable enable +public partial class Mapper +{ + private partial B Map(A a) + { + return MapToB(a, new Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler()); + } + + private B MapToB(A source, Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + { + if (refHandler.TryGetReference(source, out var existingTargetReference)) + return existingTargetReference; + var target = CreateB(); + refHandler.SetReference(source, target); + target.Parent = MapToB(source.Parent, refHandler); + target.Value = MapToD(source.Value, refHandler); + return target; + } + + private D MapToD(C source, Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + { + if (refHandler.TryGetReference(source, out var existingTargetReference)) + return existingTargetReference; + var target = new D(); + refHandler.SetReference(source, target); + target.StringValue = source.StringValue; + return target; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ShouldWork#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ShouldWork#Mapper.g.verified.cs new file mode 100644 index 0000000000..17893e6b90 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ShouldWork#Mapper.g.verified.cs @@ -0,0 +1,30 @@ +//HintName: Mapper.g.cs +#nullable enable +public partial class Mapper +{ + private partial B Map(A source) + { + return MapToB(source, new Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler()); + } + + private B MapToB(A source, Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + { + if (refHandler.TryGetReference(source, out var existingTargetReference)) + return existingTargetReference; + var target = new B(); + refHandler.SetReference(source, target); + target.Parent = MapToB(source.Parent, refHandler); + target.Value = MapToD(source.Value, refHandler); + return target; + } + + private D MapToD(C source, Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + { + if (refHandler.TryGetReference(source, out var existingTargetReference)) + return existingTargetReference; + var target = new D(); + refHandler.SetReference(source, target); + target.StringValue = source.StringValue; + return target; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.UserImplementedWithReferenceHandlerShouldWork#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.UserImplementedWithReferenceHandlerShouldWork#Mapper.g.verified.cs new file mode 100644 index 0000000000..91e1f70e8a --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.UserImplementedWithReferenceHandlerShouldWork#Mapper.g.verified.cs @@ -0,0 +1,30 @@ +//HintName: Mapper.g.cs +#nullable enable +public partial class Mapper +{ + private partial B Map(A a) + { + return MapToB(a, new Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler()); + } + + private B MapToB(A source, Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + { + if (refHandler.TryGetReference(source, out var existingTargetReference)) + return existingTargetReference; + var target = new B(); + refHandler.SetReference(source, target); + target.Parent = MapToB(source.Parent, refHandler); + target.Value = MapToD(source.Value, refHandler); + return target; + } + + private D MapToD(C source, Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + { + if (refHandler.TryGetReference(source, out var existingTargetReference)) + return existingTargetReference; + var target = new D(); + refHandler.SetReference(source, target); + target.StringValue = ToStringMod(source.StringValue, refHandler); + return target; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.UserImplementedWithoutReferenceHandlerButNestedRequiresReferenceHandlerTODO#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.UserImplementedWithoutReferenceHandlerButNestedRequiresReferenceHandlerTODO#Mapper.g.verified.cs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.UserImplementedWithoutReferenceHandlerShouldWork#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.UserImplementedWithoutReferenceHandlerShouldWork#Mapper.g.verified.cs new file mode 100644 index 0000000000..8c7105aa50 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.UserImplementedWithoutReferenceHandlerShouldWork#Mapper.g.verified.cs @@ -0,0 +1,30 @@ +//HintName: Mapper.g.cs +#nullable enable +public partial class Mapper +{ + private partial B Map(A a) + { + return MapToB(a, new Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler()); + } + + private B MapToB(A source, Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + { + if (refHandler.TryGetReference(source, out var existingTargetReference)) + return existingTargetReference; + var target = new B(); + refHandler.SetReference(source, target); + target.Parent = MapToB(source.Parent, refHandler); + target.Value = MapToD(source.Value, refHandler); + return target; + } + + private D MapToD(C source, Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + { + if (refHandler.TryGetReference(source, out var existingTargetReference)) + return existingTargetReference; + var target = new D(); + refHandler.SetReference(source, target); + target.StringValue = ToStringMod(source.StringValue); + return target; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/UserMethodTest.WithInvalidGenericSignatureShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/UserMethodTest.WithInvalidGenericSignatureShouldDiagnostic.verified.txt deleted file mode 100644 index ec08ac968a..0000000000 --- a/test/Riok.Mapperly.Tests/_snapshots/UserMethodTest.WithInvalidGenericSignatureShouldDiagnostic.verified.txt +++ /dev/null @@ -1,16 +0,0 @@ -{ - Diagnostics: [ - { - Id: RMG001, - Title: Has an unsupported mapping method signature, - Severity: Error, - WarningLevel: 0, - Location: : (10,4)-(10,41), - Description: , - HelpLink: , - MessageFormat: {0} has an unsupported mapping method signature, - Message: ToString has an unsupported mapping method signature, - Category: Mapper - } - ] -} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/UserMethodTest.WithInvalidSignatureShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/UserMethodTest.WithInvalidSignatureShouldDiagnostic.verified.txt deleted file mode 100644 index 6f37da27b2..0000000000 --- a/test/Riok.Mapperly.Tests/_snapshots/UserMethodTest.WithInvalidSignatureShouldDiagnostic.verified.txt +++ /dev/null @@ -1,16 +0,0 @@ -{ - Diagnostics: [ - { - Id: RMG001, - Title: Has an unsupported mapping method signature, - Severity: Error, - WarningLevel: 0, - Location: : (10,4)-(10,53), - Description: , - HelpLink: , - MessageFormat: {0} has an unsupported mapping method signature, - Message: ToString has an unsupported mapping method signature, - Category: Mapper - } - ] -} \ No newline at end of file