diff --git a/docs/docs/configuration/mapper.mdx b/docs/docs/configuration/mapper.mdx index fc947dd218..28e123fa11 100644 --- a/docs/docs/configuration/mapper.mdx +++ b/docs/docs/configuration/mapper.mdx @@ -185,3 +185,24 @@ dotnet_diagnostic.RMG020.severity = error # Unmapped source member ### Strict enum mappings To enforce strict enum mappings set `RMG037` and `RMG038` to error, see [strict enum mappings](./enum.mdx). + +### Static methods in instantiable class + +When you need to generate static methods inside nonstatic/instantiable class you can use `GenerateStaticMethods` option. + +```csharp +public interface IMyInterface +{ + static abstract CarDto ToDto(Car car); +} + +// highlight-start +[Mapper(GenerateStaticMethods = true)] +// highlight-end +public partial class CarMapper : IMyInterface +{ + [MapperIgnoreTarget(nameof(CarDto.MakeId))] + [MapperIgnoreSource(nameof(Car.Id))] + public static partial CarDto ToDto(Car car); +} +``` diff --git a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs index 2f73dcd219..a08d804937 100644 --- a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs +++ b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs @@ -86,4 +86,9 @@ public sealed class MapperAttribute : Attribute /// Defaults to . /// public IgnoreObsoleteMembersStrategy IgnoreObsoleteMembersStrategy { get; set; } = IgnoreObsoleteMembersStrategy.None; + + /// + /// Enables static methods in instantiable mappers (e.g. public partial class A { public static partial string MapToString(int value); }) + /// + public bool GenerateStaticMethods { get; set; } } diff --git a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt index 94add9f456..d41396e9db 100644 --- a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt +++ b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt @@ -24,6 +24,8 @@ Riok.Mapperly.Abstractions.MapperAttribute.EnumMappingIgnoreCase.get -> bool Riok.Mapperly.Abstractions.MapperAttribute.EnumMappingIgnoreCase.set -> void Riok.Mapperly.Abstractions.MapperAttribute.EnumMappingStrategy.get -> Riok.Mapperly.Abstractions.EnumMappingStrategy Riok.Mapperly.Abstractions.MapperAttribute.EnumMappingStrategy.set -> void +Riok.Mapperly.Abstractions.MapperAttribute.GenerateStaticMethods.get -> bool +Riok.Mapperly.Abstractions.MapperAttribute.GenerateStaticMethods.set -> void Riok.Mapperly.Abstractions.MapperAttribute.IgnoreObsoleteMembersStrategy.get -> Riok.Mapperly.Abstractions.IgnoreObsoleteMembersStrategy Riok.Mapperly.Abstractions.MapperAttribute.IgnoreObsoleteMembersStrategy.set -> void Riok.Mapperly.Abstractions.MapperAttribute.MapperAttribute() -> void diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs index 5bf8920cf6..942c64f701 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs @@ -25,6 +25,7 @@ public abstract class MethodMapping : NewInstanceMapping private readonly ITypeSymbol _returnType; private string? _methodName; + private bool? _methodIsStatic; protected MethodMapping(ITypeSymbol sourceType, ITypeSymbol targetType) : base(sourceType, targetType) @@ -47,6 +48,7 @@ ITypeSymbol targetType ReferenceHandlerParameter = referenceHandlerParameter; _accessibility = method.DeclaredAccessibility; _methodName = method.Name; + _methodIsStatic = method.IsStatic; _returnType = method.ReturnType.UpgradeNullable(); } @@ -77,7 +79,7 @@ public virtual MethodDeclarationSyntax BuildMethod(SourceEmitterContext ctx) ReserveParameterNames(typeMappingBuildContext.NameBuilder, parameters); return MethodDeclaration(returnType, Identifier(MethodName)) - .WithModifiers(TokenList(BuildModifiers(ctx.IsStatic))) + .WithModifiers(TokenList(BuildModifiers(ctx.IsStatic || (_methodIsStatic ?? false)))) .WithParameterList(parameters) .WithBody(Block(BuildBody(typeMappingBuildContext))); } diff --git a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs index ae858eec44..a256e24a04 100644 --- a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs +++ b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs @@ -16,9 +16,11 @@ internal static IEnumerable ExtractUserMappings(SimpleMappingBuild // extract user implemented and user defined mappings from mapper foreach (var methodSymbol in ExtractMethods(mapperSymbol)) { + var isStatic = mapperSymbol.IsStatic || ctx.MapperConfiguration.GenerateStaticMethods; + var mapping = - BuilderUserDefinedMapping(ctx, methodSymbol, mapperSymbol.IsStatic) - ?? BuildUserImplementedMapping(ctx, methodSymbol, null, false, mapperSymbol.IsStatic); + BuilderUserDefinedMapping(ctx, methodSymbol, isStatic) + ?? BuildUserImplementedMapping(ctx, methodSymbol, null, false, isStatic); if (mapping != null) yield return mapping; } diff --git a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs index e2b6f36d24..90cf11dee4 100644 --- a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs +++ b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs @@ -162,7 +162,7 @@ public static class DiagnosticDescriptors public static readonly DiagnosticDescriptor PartialStaticMethodInInstanceMapper = new DiagnosticDescriptor( "RMG018", "Partial static mapping method in an instance mapper", - "{0} is a partial static mapping method in an instance mapper. Static mapping methods are only supported in static mappers.", + $"{{0}} is a partial static mapping method in an instance mapper. Static mapping methods are supported in static mappers or using following mapper configuration: Mapper({nameof(MapperAttribute.GenerateStaticMethods)}=true).", DiagnosticCategories.Mapper, DiagnosticSeverity.Error, true diff --git a/test/Riok.Mapperly.IntegrationTests/Dto/ITestStaticInterface.cs b/test/Riok.Mapperly.IntegrationTests/Dto/ITestStaticInterface.cs new file mode 100644 index 0000000000..107cacc890 --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/Dto/ITestStaticInterface.cs @@ -0,0 +1,9 @@ +namespace Riok.Mapperly.IntegrationTests.Dto +{ + public interface ITestStaticInterface + { +#if NET7_0_OR_GREATER + static abstract int StaticAbstractDirectInt(int value); +#endif + } +} diff --git a/test/Riok.Mapperly.IntegrationTests/Mapper/TestMapperWithStaticMethods.cs b/test/Riok.Mapperly.IntegrationTests/Mapper/TestMapperWithStaticMethods.cs new file mode 100644 index 0000000000..efd69b22b5 --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/Mapper/TestMapperWithStaticMethods.cs @@ -0,0 +1,13 @@ +using Riok.Mapperly.Abstractions; +using Riok.Mapperly.IntegrationTests.Dto; + +namespace Riok.Mapperly.IntegrationTests.Mapper +{ + [Mapper(GenerateStaticMethods = true)] + public partial class TestMapperWithStaticMethods : ITestStaticInterface + { + public static partial double DirectDouble(double value); + + public static partial int StaticAbstractDirectInt(int value); + } +} diff --git a/test/Riok.Mapperly.IntegrationTests/TestMapperWithStaticMethodsTest.cs b/test/Riok.Mapperly.IntegrationTests/TestMapperWithStaticMethodsTest.cs new file mode 100644 index 0000000000..10f09db04d --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/TestMapperWithStaticMethodsTest.cs @@ -0,0 +1,44 @@ +using System.Threading.Tasks; +using Riok.Mapperly.IntegrationTests.Dto; +using Riok.Mapperly.IntegrationTests.Helpers; +using Riok.Mapperly.IntegrationTests.Mapper; +using VerifyXunit; +using Xunit; + +namespace Riok.Mapperly.IntegrationTests +{ + [UsesVerify] + public class TestMapperWithStaticMethodsTest : BaseMapperTest + { + [Fact] + [VersionedSnapshot(Versions.NET6_0 | Versions.NET7_0)] + public Task SnapshotGeneratedSource() + { + var path = GetGeneratedMapperFilePath(nameof(TestMapperWithStaticMethods)); + return Verifier.VerifyFile(path); + } + + [Theory +#if !NET7_0_OR_GREATER + (Skip = "Requires language version '11.0' or greater") +#endif + ] + [InlineData(8)] + [InlineData(12)] + public void CallStaticInterfaceMemberShouldWork(int value) + { + Assert.Equal(value, GenericMethod(value)); + + static int GenericMethod(int value) + where T : ITestStaticInterface + { +#if NET7_0_OR_GREATER + return T.StaticAbstractDirectInt(value); +#else + // It's intentional "wrong" value, it's here only to return something + return 0; +#endif + } + } + } +} diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/TestMapperWithStaticMethodsTest.SnapshotGeneratedSource_NET6_0.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/TestMapperWithStaticMethodsTest.SnapshotGeneratedSource_NET6_0.verified.cs new file mode 100644 index 0000000000..c682f61f0c --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/TestMapperWithStaticMethodsTest.SnapshotGeneratedSource_NET6_0.verified.cs @@ -0,0 +1,17 @@ +// +#nullable enable +namespace Riok.Mapperly.IntegrationTests.Mapper +{ + public partial class TestMapperWithStaticMethods + { + public static partial double DirectDouble(double value) + { + return value; + } + + public static partial int StaticAbstractDirectInt(int value) + { + return value; + } + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/TestMapperWithStaticMethodsTest.SnapshotGeneratedSource_NET7_0.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/TestMapperWithStaticMethodsTest.SnapshotGeneratedSource_NET7_0.verified.cs new file mode 100644 index 0000000000..c682f61f0c --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/TestMapperWithStaticMethodsTest.SnapshotGeneratedSource_NET7_0.verified.cs @@ -0,0 +1,17 @@ +// +#nullable enable +namespace Riok.Mapperly.IntegrationTests.Mapper +{ + public partial class TestMapperWithStaticMethods + { + public static partial double DirectDouble(double value) + { + return value; + } + + public static partial int StaticAbstractDirectInt(int value) + { + return value; + } + } +} \ No newline at end of file