From 57008f37d4c58e154b69a6d521d1a7afc4c4d4f9 Mon Sep 17 00:00:00 2001 From: Jan Trejbal Date: Thu, 24 Aug 2023 15:21:49 +0200 Subject: [PATCH] feat: Support partial static methods in non-static classes --- docs/docs/configuration/mapper.mdx | 23 ++++++ src/Riok.Mapperly/AnalyzerReleases.Shipped.md | 6 ++ .../Descriptors/DescriptorBuilder.cs | 55 ++++++++++++- .../Descriptors/Mappings/MethodMapping.cs | 4 +- .../Descriptors/UserMethodMappingExtractor.cs | 21 +++-- .../Diagnostics/DiagnosticDescriptors.cs | 18 ++--- .../Dto/ITestStaticInterface.cs | 9 +++ .../Mapper/TestMapperWithStaticMethods.cs | 13 +++ .../TestMapperWithStaticMethodsTest.cs | 44 ++++++++++ ...dsTest.SnapshotGeneratedSource.verified.cs | 17 ++++ ...SnapshotGeneratedSource_NET6_0.verified.cs | 17 ++++ ...SnapshotGeneratedSource_NET7_0.verified.cs | 17 ++++ ...InstantiableMapperWithStaticMethodsTest.cs | 80 +++++++++++++++++++ ...t.StaticPartialMethod#Mapper.g.verified.cs | 11 +++ 14 files changed, 311 insertions(+), 24 deletions(-) create mode 100644 test/Riok.Mapperly.IntegrationTests/Dto/ITestStaticInterface.cs create mode 100644 test/Riok.Mapperly.IntegrationTests/Mapper/TestMapperWithStaticMethods.cs create mode 100644 test/Riok.Mapperly.IntegrationTests/TestMapperWithStaticMethodsTest.cs create mode 100644 test/Riok.Mapperly.IntegrationTests/_snapshots/TestMapperWithStaticMethodsTest.SnapshotGeneratedSource.verified.cs create mode 100644 test/Riok.Mapperly.IntegrationTests/_snapshots/TestMapperWithStaticMethodsTest.SnapshotGeneratedSource_NET6_0.verified.cs create mode 100644 test/Riok.Mapperly.IntegrationTests/_snapshots/TestMapperWithStaticMethodsTest.SnapshotGeneratedSource_NET7_0.verified.cs create mode 100644 test/Riok.Mapperly.Tests/Mapping/InstantiableMapperWithStaticMethodsTest.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/InstantiableMapperWithStaticMethodsTest.StaticPartialMethod#Mapper.g.verified.cs diff --git a/docs/docs/configuration/mapper.mdx b/docs/docs/configuration/mapper.mdx index 2e6d85ed83..2ef8bb4639 100644 --- a/docs/docs/configuration/mapper.mdx +++ b/docs/docs/configuration/mapper.mdx @@ -236,3 +236,26 @@ The `MapperDefaultsAttribute` allows to set default configurations applied to al ```csharp [assembly: MapperDefaults(EnumMappingIgnoreCase = true)] ``` + +### Static methods in instantiable class + +Are supported, with limitation. Only static methods may be present inside mapper class. + +```csharp +public interface IMyInterface +{ + static abstract CarDto ToDto(Car car); +} + +// highlight-start +[Mapper] +// highlight-end +public partial class CarMapper : IMyInterface +{ + [MapperIgnoreTarget(nameof(CarDto.MakeId))] + [MapperIgnoreSource(nameof(Car.Id))] +// highlight-start + public static partial CarDto ToDto(Car car); +// highlight-end +} +``` diff --git a/src/Riok.Mapperly/AnalyzerReleases.Shipped.md b/src/Riok.Mapperly/AnalyzerReleases.Shipped.md index d8f8df9e79..02a59da661 100644 --- a/src/Riok.Mapperly/AnalyzerReleases.Shipped.md +++ b/src/Riok.Mapperly/AnalyzerReleases.Shipped.md @@ -126,3 +126,9 @@ Rule ID | Category | Severity | Notes RMG051 | Mapper | Warning | Invalid ignore source member found, nested ignores are not supported RMG052 | Mapper | Warning | Invalid ignore target member found, nested ignores are not supported RMG053 | Mapper | Error | The flag MemberVisibility.Accessible cannot be disabled, this feature requires .NET 8.0 or greater +RMG054 | Mapper | Error | Mapper class containing 'static partial' method must not have any instance methods + +### Removed Rules +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +RMG018 | Mapper | Disabled | Partial static mapping method in an instance mapper diff --git a/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs b/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs index e6bf94e264..54f5704978 100644 --- a/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs @@ -6,6 +6,7 @@ using Riok.Mapperly.Descriptors.MappingBodyBuilders; using Riok.Mapperly.Descriptors.MappingBuilders; using Riok.Mapperly.Descriptors.ObjectFactories; +using Riok.Mapperly.Diagnostics; using Riok.Mapperly.Helpers; using Riok.Mapperly.Symbols; using Riok.Mapperly.Templates; @@ -57,10 +58,11 @@ MapperConfiguration defaultMapperConfiguration public (MapperDescriptor descriptor, IReadOnlyCollection diagnostics) Build(CancellationToken cancellationToken) { + var instanceWithStaticPartialMethod = IsInstanceWithStaticPartialMethod(); ConfigureMemberVisibility(); ReserveMethodNames(); ExtractObjectFactories(); - ExtractUserMappings(); + ExtractUserMappings(instanceWithStaticPartialMethod); ExtractExternalMappings(); _mappingBodyBuilder.BuildMappingBodies(cancellationToken); BuildMappingMethodNames(); @@ -94,6 +96,47 @@ private void ConfigureMemberVisibility() #endif } + private bool IsInstanceWithStaticPartialMethod() + { + if (_mapperDescriptor.Symbol.IsStatic) + { + return false; + } + + var instanceWithStaticPartialMethod = false; + + // extract user implemented and user defined mappings from mapper + foreach (var methodSymbol in UserMethodMappingExtractor.ExtractMethods(_mapperDescriptor.Symbol)) + { + if (methodSymbol.IsPartialDefinition && methodSymbol.IsStatic) + { + instanceWithStaticPartialMethod = true; + break; + } + } + + if (instanceWithStaticPartialMethod) + { + foreach (var methodSymbol in UserMethodMappingExtractor.ExtractMethods(_mapperDescriptor.Symbol)) + { + if (methodSymbol is { IsStatic: false, MethodKind: not MethodKind.Constructor }) + { + _diagnostics.Add( + Diagnostic.Create( + DiagnosticDescriptors.MixingStaticPartialWithInstanceMethod, + _mapperDescriptor.Symbol.Locations.FirstOrDefault(), + _mapperDescriptor.Symbol.ToDisplayString() + ) + ); + + break; + } + } + } + + return instanceWithStaticPartialMethod; + } + private void ReserveMethodNames() { foreach (var methodSymbol in _symbolAccessor.GetAllMembers(_mapperDescriptor.Symbol)) @@ -107,9 +150,15 @@ private void ExtractObjectFactories() _objectFactories = ObjectFactoryBuilder.ExtractObjectFactories(_builderContext, _mapperDescriptor.Symbol); } - private void ExtractUserMappings() + private void ExtractUserMappings(bool instanceWithStaticPartialMethod) { - foreach (var userMapping in UserMethodMappingExtractor.ExtractUserMappings(_builderContext, _mapperDescriptor.Symbol)) + foreach ( + var userMapping in UserMethodMappingExtractor.ExtractUserMappings( + _builderContext, + _mapperDescriptor.Symbol, + instanceWithStaticPartialMethod + ) + ) { var ctx = new MappingBuilderContext( _builderContext, diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs index 424bf55aa1..c645eab97e 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs @@ -33,6 +33,7 @@ public abstract class MethodMapping : NewInstanceMapping private readonly IMethodSymbol? _partialMethodDefinition; private string? _methodName; + private bool? _methodIsStatic; protected MethodMapping(ITypeSymbol sourceType, ITypeSymbol targetType) : base(sourceType, targetType) @@ -54,6 +55,7 @@ ITypeSymbol targetType ReferenceHandlerParameter = referenceHandlerParameter; _partialMethodDefinition = method; _methodName = method.Name; + _methodIsStatic = method.IsStatic; _returnType = method.ReturnType.UpgradeNullable(); } @@ -82,7 +84,7 @@ public virtual MethodDeclarationSyntax BuildMethod(SourceEmitterContext ctx) var returnType = FullyQualifiedIdentifier(_returnType); return MethodDeclaration(returnType.AddTrailingSpace(), Identifier(MethodName)) - .WithModifiers(TokenList(BuildModifiers(ctx.IsStatic))) + .WithModifiers(TokenList(BuildModifiers(ctx.IsStatic || (_methodIsStatic ?? false)))) .WithParameterList(parameters) .WithBody(ctx.SyntaxFactory.Block(BuildBody(typeMappingBuildContext))); } diff --git a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs index 273a8fbf92..35cd853df0 100644 --- a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs +++ b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs @@ -11,14 +11,20 @@ namespace Riok.Mapperly.Descriptors; public static class UserMethodMappingExtractor { - internal static IEnumerable ExtractUserMappings(SimpleMappingBuilderContext ctx, ITypeSymbol mapperSymbol) + internal static IEnumerable ExtractUserMappings( + SimpleMappingBuilderContext ctx, + ITypeSymbol mapperSymbol, + bool instanceWithStaticPartialMethod + ) { + var isStatic = mapperSymbol.IsStatic || instanceWithStaticPartialMethod; + // extract user implemented and user defined mappings from mapper foreach (var methodSymbol in ExtractMethods(mapperSymbol)) { 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; } @@ -51,7 +57,7 @@ bool isStatic return BuildUserImplementedMappings(ctx, methods, receiver, isStatic); } - private static IEnumerable ExtractMethods(ITypeSymbol mapperSymbol) => mapperSymbol.GetMembers().OfType(); + public static IEnumerable ExtractMethods(ITypeSymbol mapperSymbol) => mapperSymbol.GetMembers().OfType(); private static IEnumerable BuildUserImplementedMappings( SimpleMappingBuilderContext ctx, @@ -114,13 +120,6 @@ bool isStatic if (!methodSymbol.IsPartialDefinition) return null; - if (!isStatic && methodSymbol.IsStatic) - { - ctx.ReportDiagnostic(DiagnosticDescriptors.PartialStaticMethodInInstanceMapper, methodSymbol, methodSymbol.Name); - - return null; - } - if (methodSymbol.IsAsync) { ctx.ReportDiagnostic(DiagnosticDescriptors.UnsupportedMappingMethodSignature, methodSymbol, methodSymbol.Name); diff --git a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs index 36b45233ae..4dc7399376 100644 --- a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs +++ b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs @@ -159,15 +159,6 @@ public static class DiagnosticDescriptors true ); - 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.", - DiagnosticCategories.Mapper, - DiagnosticSeverity.Error, - true - ); - public static readonly DiagnosticDescriptor SourceMemberNotMapped = new DiagnosticDescriptor( "RMG020", "Source member is not mapped to any target member", @@ -473,4 +464,13 @@ public static class DiagnosticDescriptors DiagnosticSeverity.Error, true ); + + public static readonly DiagnosticDescriptor MixingStaticPartialWithInstanceMethod = new DiagnosticDescriptor( + "RMG054", + "Mapper class containing 'static partial' method must not have any instance methods", + "Mapper class {0} contains 'static partial' method. Use only instance method or only static methods.", + 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..a20a479ef6 --- /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] + 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.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/TestMapperWithStaticMethodsTest.SnapshotGeneratedSource.verified.cs new file mode 100644 index 0000000000..c682f61f0c --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/TestMapperWithStaticMethodsTest.SnapshotGeneratedSource.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_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 diff --git a/test/Riok.Mapperly.Tests/Mapping/InstantiableMapperWithStaticMethodsTest.cs b/test/Riok.Mapperly.Tests/Mapping/InstantiableMapperWithStaticMethodsTest.cs new file mode 100644 index 0000000000..e9834194ba --- /dev/null +++ b/test/Riok.Mapperly.Tests/Mapping/InstantiableMapperWithStaticMethodsTest.cs @@ -0,0 +1,80 @@ +using Riok.Mapperly.Diagnostics; + +namespace Riok.Mapperly.Tests.Mapping; + +[UsesVerify] +public class InstantiableMapperWithStaticMethodsTest +{ + [Fact] + public Task StaticPartialMethod() + { + var source = TestSourceBuilder.CSharp( + """ + using Riok.Mapperly.Abstractions; + + record A(int Value); + record B(int Value); + + [Mapper] + public partial class Mapper + { + static partial B Map(A source); + } + """ + ); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public void MixedStaticMethodWithPartialInstanceMethod() + { + var source = TestSourceBuilder.CSharp( + """ + using Riok.Mapperly.Abstractions; + + record A(int Value); + record B(int Value); + + [Mapper] + public partial class Mapper + { + static partial B Map(A source); + + partial B Map2(A source); + } + """ + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowAllDiagnostics) + .Should() + .HaveDiagnostic(DiagnosticDescriptors.MixingStaticPartialWithInstanceMethod); + } + + [Fact] + public void MixedStaticMethodWithInstanceMethod() + { + var source = TestSourceBuilder.CSharp( + """ + using Riok.Mapperly.Abstractions; + + record A(int Value); + record B(int Value); + + [Mapper] + public partial class Mapper + { + static partial B Map(A source); + + private B Map2(A source); + } + """ + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowAllDiagnostics) + .Should() + .HaveDiagnostic(DiagnosticDescriptors.MixingStaticPartialWithInstanceMethod); + } +} diff --git a/test/Riok.Mapperly.Tests/_snapshots/InstantiableMapperWithStaticMethodsTest.StaticPartialMethod#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/InstantiableMapperWithStaticMethodsTest.StaticPartialMethod#Mapper.g.verified.cs new file mode 100644 index 0000000000..5ff6ef5d2b --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/InstantiableMapperWithStaticMethodsTest.StaticPartialMethod#Mapper.g.verified.cs @@ -0,0 +1,11 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + static partial global::B Map(global::A source) + { + var target = new global::B(source.Value); + return target; + } +} \ No newline at end of file