Skip to content

Commit

Permalink
feat: Support partial static methods in non-static classes
Browse files Browse the repository at this point in the history
  • Loading branch information
trejjam committed Nov 4, 2023
1 parent a8acc04 commit 57008f3
Show file tree
Hide file tree
Showing 14 changed files with 311 additions and 24 deletions.
23 changes: 23 additions & 0 deletions docs/docs/configuration/mapper.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
```
6 changes: 6 additions & 0 deletions src/Riok.Mapperly/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
55 changes: 52 additions & 3 deletions src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -57,10 +58,11 @@ MapperConfiguration defaultMapperConfiguration

public (MapperDescriptor descriptor, IReadOnlyCollection<Diagnostic> diagnostics) Build(CancellationToken cancellationToken)
{
var instanceWithStaticPartialMethod = IsInstanceWithStaticPartialMethod();
ConfigureMemberVisibility();
ReserveMethodNames();
ExtractObjectFactories();
ExtractUserMappings();
ExtractUserMappings(instanceWithStaticPartialMethod);
ExtractExternalMappings();
_mappingBodyBuilder.BuildMappingBodies(cancellationToken);
BuildMappingMethodNames();
Expand Down Expand Up @@ -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))
Expand All @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -54,6 +55,7 @@ ITypeSymbol targetType
ReferenceHandlerParameter = referenceHandlerParameter;
_partialMethodDefinition = method;
_methodName = method.Name;
_methodIsStatic = method.IsStatic;
_returnType = method.ReturnType.UpgradeNullable();
}

Expand Down Expand Up @@ -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)));
}
Expand Down
21 changes: 10 additions & 11 deletions src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,20 @@ namespace Riok.Mapperly.Descriptors;

public static class UserMethodMappingExtractor
{
internal static IEnumerable<IUserMapping> ExtractUserMappings(SimpleMappingBuilderContext ctx, ITypeSymbol mapperSymbol)
internal static IEnumerable<IUserMapping> 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;
}
Expand Down Expand Up @@ -51,7 +57,7 @@ bool isStatic
return BuildUserImplementedMappings(ctx, methods, receiver, isStatic);
}

private static IEnumerable<IMethodSymbol> ExtractMethods(ITypeSymbol mapperSymbol) => mapperSymbol.GetMembers().OfType<IMethodSymbol>();
public static IEnumerable<IMethodSymbol> ExtractMethods(ITypeSymbol mapperSymbol) => mapperSymbol.GetMembers().OfType<IMethodSymbol>();

private static IEnumerable<IUserMapping> BuildUserImplementedMappings(
SimpleMappingBuilderContext ctx,
Expand Down Expand Up @@ -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);
Expand Down
18 changes: 9 additions & 9 deletions src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Riok.Mapperly.IntegrationTests.Dto
{
public interface ITestStaticInterface
{
#if NET7_0_OR_GREATER
static abstract int StaticAbstractDirectInt(int value);
#endif
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<TestMapperWithStaticMethods>(value));

static int GenericMethod<T>(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
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// <auto-generated />
#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;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// <auto-generated />
#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;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// <auto-generated />
#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;
}
}
}
Loading

0 comments on commit 57008f3

Please sign in to comment.