From 7ec8527862235cdc5fc383c89ac4209e7e29e430 Mon Sep 17 00:00:00 2001 From: latonz Date: Wed, 3 Jul 2024 19:24:10 -0500 Subject: [PATCH] Add MappingTarget attribute to set the mapping target as the first parameter --- docs/docs/configuration/existing-target.md | 76 -------- docs/docs/configuration/existing-target.mdx | 113 +++++++++++ docs/docs/configuration/static-mappers.md | 2 +- .../MappingTargetAttribute.cs | 10 + .../PublicAPI.Shipped.txt | 4 + .../PublicAPI.Unshipped.txt | 2 - .../Descriptors/SymbolAccessor.cs | 5 +- .../UserMappingMethodParameterExtractor.cs | 179 ++++++++++++++++++ .../Descriptors/UserMethodMappingExtractor.cs | 161 ++-------------- .../Symbols/MappingMethodParameters.cs | 2 +- .../Mapper/StaticTestMapper.cs | 4 + .../StaticMapperTest.cs | 16 ++ ...SnapshotGeneratedSource_NET6_0.verified.cs | 12 ++ .../Mapping/ExtensionMethodTest.cs | 14 +- .../Mapping/ReferenceHandlingTest.cs | 20 ++ .../Mapping/UserMethodTest.cs | 11 ++ ...sFirstParamShouldWork#Mapper.g.verified.cs | 11 ++ ...tingTargetShouldWork#Mapper.g.verified.cs} | 0 18 files changed, 409 insertions(+), 233 deletions(-) delete mode 100644 docs/docs/configuration/existing-target.md create mode 100644 docs/docs/configuration/existing-target.mdx create mode 100644 src/Riok.Mapperly.Abstractions/MappingTargetAttribute.cs create mode 100644 src/Riok.Mapperly/Descriptors/UserMappingMethodParameterExtractor.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/ExtensionMethodTest.ExtensionExistingTargetAsFirstParamShouldWork#Mapper.g.verified.cs rename test/Riok.Mapperly.Tests/_snapshots/{ExtensionMethodTest.ExtensionUpdateMethodShouldWork#Mapper.g.verified.cs => ExtensionMethodTest.ExtensionExistingTargetShouldWork#Mapper.g.verified.cs} (100%) diff --git a/docs/docs/configuration/existing-target.md b/docs/docs/configuration/existing-target.md deleted file mode 100644 index 59191526916..00000000000 --- a/docs/docs/configuration/existing-target.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -sidebar_position: 9 -description: Map to an existing target object ---- - -# Existing target object - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -If an existing object instance should be used as target, you can define the mapping method as void with the target as second parameter: - -```csharp title="Mapper declaration" -[Mapper] -public partial class CarMapper -{ - // highlight-start - public partial void CarToCarDto(Car car, CarDto dto); - // highlight-end -} -``` - -```csharp title="Mapper usage" -var mapper = new CarMapper(); -var car = new Car { NumberOfSeats = 10, ... }; -var dto = new CarDto(); - -mapper.CarToCarDto(car, dto); -dto.NumberOfSeats.Should().Be(10); -``` - -## Merge objects - -To merge two objects together, `AllowNullPropertyAssignment` can be set to `false`. -This ignores all properties on the source with a `null` value. - - - - -```csharp -// highlight-start -[Mapper(AllowNullPropertyAssignment = false)] -// highlight-end -static partial class FruitMapper -{ - public static partial void ApplyUpdate(FruitUpdate update, Fruit fruit); -} - -class Fruit { public required string Name { get; set; } public required string Color { get; set; } } -record FruitUpdate(string? Name, string? Color); -``` - - - - -```csharp -static partial class FruitMapper -{ - public static partial void Update(global::FruitUpdate update, global::Fruit fruit) - { - if (update.Name != null) - { - fruit.Name = update.Name; - } - if (update.Color != null) - { - fruit.Color = update.Color; - } - } -} -``` - - - - -See also [null value handling](./mapper.mdx#null-values). diff --git a/docs/docs/configuration/existing-target.mdx b/docs/docs/configuration/existing-target.mdx new file mode 100644 index 00000000000..3584ddbf578 --- /dev/null +++ b/docs/docs/configuration/existing-target.mdx @@ -0,0 +1,113 @@ +--- +sidebar_position: 9 +description: Map to an existing target object +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Existing target object + +If an existing object instance should be used as target, you can define the mapping method as void with the target as second parameter: + + + + ```csharp + [Mapper] + public partial class CarMapper + { + // highlight-start + public partial void UpdateCarDto(Car car, CarDto dto); + // highlight-end + } + ``` + + + ```csharp + var mapper = new CarMapper(); + var car = new Car { NumberOfSeats = 10, ... }; + var dto = new CarDto(); + + mapper.UpdateCarDto(car, dto); + dto.NumberOfSeats.Should().Be(10); + ``` + + + +## Merge objects + +To merge two objects together, `AllowNullPropertyAssignment` can be set to `false`. +This ignores all properties on the source with a `null` value. + + + + ```csharp + // highlight-start + [Mapper(AllowNullPropertyAssignment = false)] + // highlight-end + static partial class FruitMapper + { + // highlight-start + public static partial void ApplyUpdate(FruitUpdate update, Fruit fruit); + // highlight-end + } + + class Fruit { public required string Name { get; set; } public required string Color { get; set; } } + record FruitUpdate(string? Name, string? Color); + ``` + + + ```csharp + FruitMapper.ApplyUpdate(myUpdateRequest, myFruit); + ``` + + + ```csharp + static partial class FruitMapper + { + public static partial void Update(global::FruitUpdate update, global::Fruit fruit) + { + if (update.Name != null) + { + fruit.Name = update.Name; + } + if (update.Color != null) + { + fruit.Color = update.Color; + } + } + } + ``` + + + +See also [null value handling](./mapper.mdx#null-values). + +The `MappingTarget` attribute allows setting the first method parameter as mapping target: + + + + + ```csharp + // highlight-start + [Mapper(AllowNullPropertyAssignment = false)] + // highlight-end + static partial class FruitMapper + { + // highlight-start + public static partial void ApplyUpdate([MappingTarget] this Fruit fruit, FruitUpdate update); + // highlight-end + } + + class Fruit { public required string Name { get; set; } public required string Color { get; set; } } + record FruitUpdate(string? Name, string? Color); + ``` + + + ```csharp + myFruit.ApplyUpdate(myUpdateRequest); + ``` + + + +See also [extension methods](./static-mappers.md). diff --git a/docs/docs/configuration/static-mappers.md b/docs/docs/configuration/static-mappers.md index c49be0ba33f..fa7ce830cc4 100644 --- a/docs/docs/configuration/static-mappers.md +++ b/docs/docs/configuration/static-mappers.md @@ -11,7 +11,7 @@ Mapperly supports static mappers and extension methods: [Mapper] public static partial class CarMapper { - public static partial CarDto CarToCarDto(this Car car); + public static partial CarDto ToCarDto(this Car car); private static int TimeSpanToHours(TimeSpan t) => t.Hours; } diff --git a/src/Riok.Mapperly.Abstractions/MappingTargetAttribute.cs b/src/Riok.Mapperly.Abstractions/MappingTargetAttribute.cs new file mode 100644 index 00000000000..97986a2bf5e --- /dev/null +++ b/src/Riok.Mapperly.Abstractions/MappingTargetAttribute.cs @@ -0,0 +1,10 @@ +using System.Diagnostics; + +namespace Riok.Mapperly.Abstractions; + +/// +/// Marks a given parameter as the mapping target. +/// +[AttributeUsage(AttributeTargets.Parameter)] +[Conditional("MAPPERLY_ABSTRACTIONS_SCOPE_RUNTIME")] +public sealed class MappingTargetAttribute : Attribute; diff --git a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt index 467798939c0..3b12015a40a 100644 --- a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt +++ b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt @@ -182,3 +182,7 @@ Riok.Mapperly.Abstractions.MapValueAttribute.MapValueAttribute(string![]! target Riok.Mapperly.Abstractions.MapValueAttribute.Target.get -> System.Collections.Generic.IReadOnlyCollection! Riok.Mapperly.Abstractions.MapValueAttribute.TargetFullName.get -> string! Riok.Mapperly.Abstractions.MapValueAttribute.Value.get -> object? +Riok.Mapperly.Abstractions.MappingTargetAttribute +Riok.Mapperly.Abstractions.MappingTargetAttribute.MappingTargetAttribute() -> void +Riok.Mapperly.Abstractions.MapPropertyAttribute.MapPropertyAttribute(string! source, string![]! target) -> void +Riok.Mapperly.Abstractions.MapPropertyAttribute.MapPropertyAttribute(string![]! source, string! target) -> void diff --git a/src/Riok.Mapperly.Abstractions/PublicAPI.Unshipped.txt b/src/Riok.Mapperly.Abstractions/PublicAPI.Unshipped.txt index 41a6d8564b8..7dc5c58110b 100644 --- a/src/Riok.Mapperly.Abstractions/PublicAPI.Unshipped.txt +++ b/src/Riok.Mapperly.Abstractions/PublicAPI.Unshipped.txt @@ -1,3 +1 @@ #nullable enable -Riok.Mapperly.Abstractions.MapPropertyAttribute.MapPropertyAttribute(string! source, string![]! target) -> void -Riok.Mapperly.Abstractions.MapPropertyAttribute.MapPropertyAttribute(string![]! source, string! target) -> void diff --git a/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs b/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs index 6f3c5493226..07799c5a2a4 100644 --- a/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs +++ b/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs @@ -73,10 +73,7 @@ public bool CanAssign(ITypeSymbol sourceType, ITypeSymbol targetType) && (targetType.IsNullable() || !sourceType.IsNullable()); } - public MethodParameter? WrapOptionalMethodParameter(IParameterSymbol? symbol) - { - return symbol == null ? null : WrapMethodParameter(symbol); - } + public MethodParameter? WrapOptionalMethodParameter(IParameterSymbol? symbol) => symbol == null ? null : WrapMethodParameter(symbol); public MethodParameter WrapMethodParameter(IParameterSymbol symbol) => new(symbol, UpgradeNullable(symbol.Type)); diff --git a/src/Riok.Mapperly/Descriptors/UserMappingMethodParameterExtractor.cs b/src/Riok.Mapperly/Descriptors/UserMappingMethodParameterExtractor.cs new file mode 100644 index 00000000000..96d33d9ff56 --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/UserMappingMethodParameterExtractor.cs @@ -0,0 +1,179 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Riok.Mapperly.Abstractions; +using Riok.Mapperly.Abstractions.ReferenceHandling; +using Riok.Mapperly.Diagnostics; +using Riok.Mapperly.Symbols; + +namespace Riok.Mapperly.Descriptors; + +internal static class UserMappingMethodParameterExtractor +{ + public static bool BuildParameters( + SimpleMappingBuilderContext ctx, + IMethodSymbol method, + [NotNullWhen(true)] out MappingMethodParameters? parameters + ) + { + // the source param is always required + var expectedParameterCount = 1; + + var refHandlerParameter = FindReferenceHandlerParameter(ctx, method); + if (refHandlerParameter.HasValue) + { + expectedParameterCount++; + } + + var sourceParameter = FindSourceParameter(ctx, method, refHandlerParameter); + if (!sourceParameter.HasValue) + { + parameters = null; + return false; + } + + var targetParameter = FindTargetParameter(ctx, method, sourceParameter.Value, refHandlerParameter); + + // If the method returns void, a target parameter is required + // if the method doesn't return void, a target parameter is not allowed. + if (method.ReturnsVoid == !targetParameter.HasValue) + { + parameters = null; + return false; + } + + if (targetParameter.HasValue) + { + expectedParameterCount++; + } + + parameters = new MappingMethodParameters(sourceParameter.Value, targetParameter, refHandlerParameter); + return method.Parameters.Length == expectedParameterCount; + } + + public static bool BuildRuntimeTargetTypeMappingParameters( + SimpleMappingBuilderContext ctx, + IMethodSymbol method, + [NotNullWhen(true)] out RuntimeTargetTypeMappingMethodParameters? parameters + ) + { + // existing target instance runtime typed mappings are not supported + if (method.ReturnsVoid) + { + parameters = null; + return false; + } + + // the source and target type param is always required + var expectedParameterCount = 2; + + var refHandlerParameter = FindReferenceHandlerParameter(ctx, method); + if (refHandlerParameter.HasValue) + { + expectedParameterCount++; + } + + var sourceParameter = FindSourceParameter(ctx, method, refHandlerParameter); + if (!sourceParameter.HasValue) + { + parameters = null; + return false; + } + + // the target type param needs to exist + // and needs to be of type System.Type + var targetTypeParameter = FindTargetParameter(ctx, method, sourceParameter.Value, refHandlerParameter); + if (!targetTypeParameter.HasValue || !SymbolEqualityComparer.Default.Equals(targetTypeParameter.Value.Type, ctx.Types.Get())) + { + parameters = null; + return false; + } + + if (method.Parameters.Length != expectedParameterCount) + { + parameters = null; + return false; + } + + parameters = new RuntimeTargetTypeMappingMethodParameters(sourceParameter.Value, targetTypeParameter.Value, refHandlerParameter); + return true; + } + + private static MethodParameter? FindSourceParameter( + SimpleMappingBuilderContext ctx, + IMethodSymbol method, + MethodParameter? refHandlerParameter + ) + { + var refHandlerParameterOrdinal = refHandlerParameter?.Ordinal ?? -1; + + // source parameter is the first parameter not annotated as reference handler or mapping target + var sourceParameterSymbol = method.Parameters.FirstOrDefault(p => + p.Ordinal != refHandlerParameterOrdinal + && !ctx.SymbolAccessor.HasAttribute(p) + && !ctx.SymbolAccessor.HasAttribute(p) + ); + return ctx.SymbolAccessor.WrapOptionalMethodParameter(sourceParameterSymbol); + } + + private static MethodParameter? FindTargetParameter( + SimpleMappingBuilderContext ctx, + IMethodSymbol method, + MethodParameter sourceParameter, + MethodParameter? refHandlerParameter + ) + { + var refHandlerParameterOrdinal = refHandlerParameter?.Ordinal ?? -1; + + // The target parameter is the first parameter, + // which is not the source parameter, + // and is not annotated as reference handling parameter. + // It may be annotated as mapping target + // (for example, if it is the very first parameter, which is often the case in extension methods). + var targetParameterSymbol = method.Parameters.FirstOrDefault(p => + p.Ordinal != sourceParameter.Ordinal + && p.Ordinal != refHandlerParameterOrdinal + && !ctx.SymbolAccessor.HasAttribute(p) + ); + return ctx.SymbolAccessor.WrapOptionalMethodParameter(targetParameterSymbol); + } + + private static MethodParameter? FindReferenceHandlerParameter(SimpleMappingBuilderContext ctx, IMethodSymbol method) + { + var refHandlerParameterSymbol = method.Parameters.FirstOrDefault(p => + ctx.SymbolAccessor.HasAttribute(p) + ); + if (refHandlerParameterSymbol == null) + return null; + + // the reference handler parameter cannot also be the target parameter + if (ctx.SymbolAccessor.HasAttribute(refHandlerParameterSymbol)) + { + ctx.ReportDiagnostic(DiagnosticDescriptors.UnsupportedMappingMethodSignature, method, method.Name); + } + + var refHandlerParameter = ctx.SymbolAccessor.WrapMethodParameter(refHandlerParameterSymbol); + if (!SymbolEqualityComparer.Default.Equals(ctx.Types.Get(), refHandlerParameter.Type)) + { + ctx.ReportDiagnostic( + DiagnosticDescriptors.ReferenceHandlerParameterWrongType, + refHandlerParameterSymbol, + method.ContainingType.ToDisplayString(), + method.Name, + ctx.Types.Get().ToDisplayString(), + refHandlerParameterSymbol.Type.ToDisplayString() + ); + } + + if (!ctx.Configuration.Mapper.UseReferenceHandling) + { + ctx.ReportDiagnostic( + DiagnosticDescriptors.ReferenceHandlingNotEnabled, + refHandlerParameterSymbol, + method.ContainingType.ToDisplayString(), + method.Name + ); + } + + return refHandlerParameter; + } +} diff --git a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs index 2756eb230b0..1447216dc57 100644 --- a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs +++ b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs @@ -1,8 +1,6 @@ using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis; using Riok.Mapperly.Abstractions; -using Riok.Mapperly.Abstractions.ReferenceHandling; using Riok.Mapperly.Configuration; using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Descriptors.Mappings.UserMappings; @@ -21,7 +19,7 @@ internal static IEnumerable ExtractUserMappings(SimpleMappingBuild foreach (var method in methods) { var mapping = - BuilderUserDefinedMapping(ctx, method) + BuildUserDefinedMapping(ctx, method) ?? BuildUserImplementedMapping( ctx, method, @@ -109,7 +107,7 @@ private static bool IsMappingMethodCandidate(SimpleMappingBuilderContext ctx, IM { requireAttribute &= !ctx.Configuration.Mapper.AutoUserMappings; - // ignore all non ordinary methods (eg. ctor, operators, etc.) and methods declared on the object type (eg. ToString) + // ignore all non-ordinary methods (e.g. ctor, operators, etc.) and methods declared on the object type (e.g. ToString) return method.MethodKind == MethodKind.Ordinary && ctx.SymbolAccessor.IsDirectlyAccessible(method) && !SymbolEqualityComparer.Default.Equals(method.ReceiverType, ctx.Compilation.ObjectType) @@ -133,7 +131,7 @@ bool isExternal var userMappingConfig = GetUserMappingConfig(ctx, method, out var hasAttribute); var valid = !method.IsGenericMethod && (allowPartial || !method.IsPartialDefinition) && (!isStatic || method.IsStatic); - if (!valid || !BuildParameters(ctx, method, out var parameters)) + if (!valid || !UserMappingMethodParameterExtractor.BuildParameters(ctx, method, out var parameters)) { if (hasAttribute) { @@ -171,7 +169,7 @@ bool isExternal ); } - private static IUserMapping? BuilderUserDefinedMapping(SimpleMappingBuilderContext ctx, IMethodSymbol methodSymbol) + private static IUserMapping? BuildUserDefinedMapping(SimpleMappingBuilderContext ctx, IMethodSymbol methodSymbol) { if (!methodSymbol.IsPartialDefinition) return null; @@ -182,7 +180,14 @@ bool isExternal return null; } - if (!methodSymbol.IsGenericMethod && BuildRuntimeTargetTypeMappingParameters(ctx, methodSymbol, out var runtimeTargetTypeParams)) + if ( + !methodSymbol.IsGenericMethod + && UserMappingMethodParameterExtractor.BuildRuntimeTargetTypeMappingParameters( + ctx, + methodSymbol, + out var runtimeTargetTypeParams + ) + ) { return new UserDefinedNewInstanceRuntimeTargetTypeParameterMapping( methodSymbol, @@ -194,7 +199,7 @@ bool isExternal ); } - if (!BuildParameters(ctx, methodSymbol, out var parameters)) + if (!UserMappingMethodParameterExtractor.BuildParameters(ctx, methodSymbol, out var parameters)) { ctx.ReportDiagnostic(DiagnosticDescriptors.UnsupportedMappingMethodSignature, methodSymbol, methodSymbol.Name); return null; @@ -234,146 +239,6 @@ bool isExternal ); } - private static bool BuildRuntimeTargetTypeMappingParameters( - SimpleMappingBuilderContext ctx, - IMethodSymbol method, - [NotNullWhen(true)] out RuntimeTargetTypeMappingMethodParameters? parameters - ) - { - var expectedParametersCount = 0; - - // reference handler parameter is always annotated - var refHandlerParameter = BuildReferenceHandlerParameter(ctx, method); - var refHandlerParameterOrdinal = refHandlerParameter?.Ordinal ?? -1; - if (refHandlerParameter.HasValue) - { - expectedParametersCount++; - } - - // source parameter is the first parameter (except if the reference handler is the first parameter) - var sourceParameterSymbol = method.Parameters.FirstOrDefault(p => p.Ordinal != refHandlerParameterOrdinal); - if (sourceParameterSymbol == null) - { - parameters = null; - return false; - } - - var sourceParameter = ctx.SymbolAccessor.WrapMethodParameter(sourceParameterSymbol); - expectedParametersCount++; - - // target type parameter is the second parameter (except if the reference handler is the first or the second parameter) - var targetTypeParameterSymbol = method.Parameters.FirstOrDefault(p => - p.Ordinal != sourceParameter.Ordinal && p.Ordinal != refHandlerParameterOrdinal - ); - if ( - targetTypeParameterSymbol == null - || !SymbolEqualityComparer.Default.Equals(targetTypeParameterSymbol.Type, ctx.Types.Get()) - ) - { - parameters = null; - return false; - } - - var targetTypeParameter = ctx.SymbolAccessor.WrapMethodParameter(targetTypeParameterSymbol); - expectedParametersCount++; - - if (method.Parameters.Length != expectedParametersCount) - { - parameters = null; - return false; - } - - parameters = new RuntimeTargetTypeMappingMethodParameters(sourceParameter, targetTypeParameter, refHandlerParameter); - return true; - } - - private static bool BuildParameters( - SimpleMappingBuilderContext ctx, - IMethodSymbol method, - [NotNullWhen(true)] out MappingMethodParameters? parameters - ) - { - var expectedParameterCount = 1; - - // reference handler parameter is always annotated - var refHandlerParameter = BuildReferenceHandlerParameter(ctx, method); - var refHandlerParameterOrdinal = refHandlerParameter?.Ordinal ?? -1; - if (refHandlerParameter.HasValue) - { - expectedParameterCount++; - } - - // source parameter is the first parameter (except if the reference handler is the first parameter) - var sourceParameterSymbol = method.Parameters.FirstOrDefault(p => p.Ordinal != refHandlerParameterOrdinal); - if (sourceParameterSymbol == null) - { - parameters = null; - return false; - } - - var sourceParameter = ctx.SymbolAccessor.WrapMethodParameter(sourceParameterSymbol); - - // 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 = ctx.SymbolAccessor.WrapOptionalMethodParameter( - method.Parameters.FirstOrDefault(p => p.Ordinal != sourceParameter.Ordinal && p.Ordinal != refHandlerParameterOrdinal) - ); - if (method.ReturnsVoid == !targetParameter.HasValue) - { - parameters = null; - return false; - } - - if (targetParameter.HasValue) - { - expectedParameterCount++; - } - - if (method.Parameters.Length != expectedParameterCount) - { - parameters = null; - return false; - } - - parameters = new MappingMethodParameters(sourceParameter, targetParameter, refHandlerParameter); - return true; - } - - private static MethodParameter? BuildReferenceHandlerParameter(SimpleMappingBuilderContext ctx, IMethodSymbol method) - { - var refHandlerParameterSymbol = method.Parameters.FirstOrDefault(p => - ctx.SymbolAccessor.HasAttribute(p) - ); - if (refHandlerParameterSymbol == null) - return null; - - var refHandlerParameter = ctx.SymbolAccessor.WrapMethodParameter(refHandlerParameterSymbol); - if (!SymbolEqualityComparer.Default.Equals(ctx.Types.Get(), refHandlerParameter.Type)) - { - ctx.ReportDiagnostic( - DiagnosticDescriptors.ReferenceHandlerParameterWrongType, - refHandlerParameterSymbol, - method.ContainingType.ToDisplayString(), - method.Name, - ctx.Types.Get().ToDisplayString(), - refHandlerParameterSymbol.Type.ToDisplayString() - ); - } - - if (!ctx.Configuration.Mapper.UseReferenceHandling) - { - ctx.ReportDiagnostic( - DiagnosticDescriptors.ReferenceHandlingNotEnabled, - refHandlerParameterSymbol, - method.ContainingType.ToDisplayString(), - method.Name - ); - } - - return refHandlerParameter; - } - private static NullFallbackValue? GetTypeSwitchNullArm(IMethodSymbol method, MappingMethodParameters parameters) { // target is always the return type for runtime target mappings diff --git a/src/Riok.Mapperly/Symbols/MappingMethodParameters.cs b/src/Riok.Mapperly/Symbols/MappingMethodParameters.cs index 5fd2987ee0c..97f76dd3c61 100644 --- a/src/Riok.Mapperly/Symbols/MappingMethodParameters.cs +++ b/src/Riok.Mapperly/Symbols/MappingMethodParameters.cs @@ -1,6 +1,6 @@ namespace Riok.Mapperly.Symbols; /// -/// Well known mapping method parameters. +/// Well-known mapping method parameters. /// public record MappingMethodParameters(MethodParameter Source, MethodParameter? Target, MethodParameter? ReferenceHandler); diff --git a/test/Riok.Mapperly.IntegrationTests/Mapper/StaticTestMapper.cs b/test/Riok.Mapperly.IntegrationTests/Mapper/StaticTestMapper.cs index 50ab7de3926..bdcdc1e4c66 100644 --- a/test/Riok.Mapperly.IntegrationTests/Mapper/StaticTestMapper.cs +++ b/test/Riok.Mapperly.IntegrationTests/Mapper/StaticTestMapper.cs @@ -26,6 +26,10 @@ public static partial class StaticTestMapper public static partial DateTime DirectDateTime(DateTime dateTime); + public static partial void MapIdTargetExt([MappingTarget] this IdObject target, IdObjectDto source); + + public static partial void MapIdTargetFirst([MappingTarget] IdObject target, IdObjectDto source); + public static partial IEnumerable MapAllDtos(IEnumerable objects); [MapperIgnoreSource(nameof(TestObject.IgnoredIntValue))] diff --git a/test/Riok.Mapperly.IntegrationTests/StaticMapperTest.cs b/test/Riok.Mapperly.IntegrationTests/StaticMapperTest.cs index ff585b9ac32..aafa86b96e4 100644 --- a/test/Riok.Mapperly.IntegrationTests/StaticMapperTest.cs +++ b/test/Riok.Mapperly.IntegrationTests/StaticMapperTest.cs @@ -71,5 +71,21 @@ public Task ConstantValuesShouldWork() var dto = StaticTestMapper.MapConstantValues(obj); return Verifier.Verify(dto); } + + [Fact] + public void RunMappingIdTargetExtShouldWork() + { + var model = new IdObject { IdValue = 10 }; + model.MapIdTargetExt(new IdObjectDto { IdValue = 20 }); + model.IdValue.Should().Be(20); + } + + [Fact] + public void RunMappingIdTargetFirstShouldWork() + { + var model = new IdObject { IdValue = 10 }; + StaticTestMapper.MapIdTargetFirst(model, new IdObjectDto { IdValue = 20 }); + model.IdValue.Should().Be(20); + } } } diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs index acd46f8f658..5492effc04f 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs @@ -52,6 +52,18 @@ public static partial int ParseableInt(string value) return dateTime; } + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + public static partial void MapIdTargetExt(this global::Riok.Mapperly.IntegrationTests.Models.IdObject target, global::Riok.Mapperly.IntegrationTests.Dto.IdObjectDto source) + { + target.IdValue = DirectInt(source.IdValue); + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + public static partial void MapIdTargetFirst(global::Riok.Mapperly.IntegrationTests.Models.IdObject target, global::Riok.Mapperly.IntegrationTests.Dto.IdObjectDto source) + { + target.IdValue = DirectInt(source.IdValue); + } + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] public static partial global::System.Collections.Generic.IEnumerable MapAllDtos(global::System.Collections.Generic.IEnumerable objects) { diff --git a/test/Riok.Mapperly.Tests/Mapping/ExtensionMethodTest.cs b/test/Riok.Mapperly.Tests/Mapping/ExtensionMethodTest.cs index 7980e82a126..31478ee5ceb 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ExtensionMethodTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ExtensionMethodTest.cs @@ -15,7 +15,7 @@ public Task ExtensionMapMethodShouldWork() } [Fact] - public Task ExtensionUpdateMethodShouldWork() + public Task ExtensionExistingTargetShouldWork() { var source = TestSourceBuilder.MapperWithBodyAndTypes( "static partial void MapToB(this A source, B target);", @@ -25,4 +25,16 @@ public Task ExtensionUpdateMethodShouldWork() return TestHelper.VerifyGenerator(source); } + + [Fact] + public Task ExtensionExistingTargetAsFirstParamShouldWork() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "static partial void MapToB([MappingTarget] this B target, A source);", + "class A { public int Value { get; set; } }", + "class B { public int Value { get; set; } }" + ); + + return TestHelper.VerifyGenerator(source); + } } diff --git a/test/Riok.Mapperly.Tests/Mapping/ReferenceHandlingTest.cs b/test/Riok.Mapperly.Tests/Mapping/ReferenceHandlingTest.cs index a490ca1c55c..dceac3aa85c 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ReferenceHandlingTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ReferenceHandlingTest.cs @@ -301,4 +301,24 @@ public Task MultipleUserDefinedWithSpecifiedDefault() return TestHelper.VerifyGenerator(source); } + + [Fact] + public void ReferenceHandlerParameterIsAlsoMappingTargetParameterShouldDiagnostic() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "public partial B Map(A source, [MappingTarget, ReferenceHandler] IReferenceHandler refHandler);", + TestSourceBuilderOptions.WithReferenceHandling, + "record A;", + "record b;" + ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic(DiagnosticDescriptors.UnsupportedMappingMethodSignature, "Map has an unsupported mapping method signature") + .HaveDiagnostic( + DiagnosticDescriptors.CouldNotCreateMapping, + "Could not create mapping from A to B. Consider implementing the mapping manually." + ) + .HaveAssertedAllDiagnostics(); + } } diff --git a/test/Riok.Mapperly.Tests/Mapping/UserMethodTest.cs b/test/Riok.Mapperly.Tests/Mapping/UserMethodTest.cs index 07fb0bf326e..6b96416db74 100644 --- a/test/Riok.Mapperly.Tests/Mapping/UserMethodTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/UserMethodTest.cs @@ -804,4 +804,15 @@ public static D BaseMapping(C source) ); return TestHelper.VerifyGenerator(source); } + + [Fact] + public void MappingMethodWithSingleTargetParameterShouldDiagnostic() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes("public partial B Map([MappingTarget] A source);", "record A;", "record b;"); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic(DiagnosticDescriptors.UnsupportedMappingMethodSignature, "Map has an unsupported mapping method signature") + .HaveAssertedAllDiagnostics(); + } } diff --git a/test/Riok.Mapperly.Tests/_snapshots/ExtensionMethodTest.ExtensionExistingTargetAsFirstParamShouldWork#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ExtensionMethodTest.ExtensionExistingTargetAsFirstParamShouldWork#Mapper.g.verified.cs new file mode 100644 index 00000000000..ee490db89a7 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/ExtensionMethodTest.ExtensionExistingTargetAsFirstParamShouldWork#Mapper.g.verified.cs @@ -0,0 +1,11 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + static partial void MapToB(this global::B target, global::A source) + { + target.Value = source.Value; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/ExtensionMethodTest.ExtensionUpdateMethodShouldWork#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ExtensionMethodTest.ExtensionExistingTargetShouldWork#Mapper.g.verified.cs similarity index 100% rename from test/Riok.Mapperly.Tests/_snapshots/ExtensionMethodTest.ExtensionUpdateMethodShouldWork#Mapper.g.verified.cs rename to test/Riok.Mapperly.Tests/_snapshots/ExtensionMethodTest.ExtensionExistingTargetShouldWork#Mapper.g.verified.cs