Skip to content

Commit

Permalink
feat: support static mapping methods in non-static mappers (#681)
Browse files Browse the repository at this point in the history
  • Loading branch information
trejjam authored Nov 19, 2023
1 parent c5f0c54 commit 1724124
Show file tree
Hide file tree
Showing 23 changed files with 474 additions and 68 deletions.
21 changes: 21 additions & 0 deletions docs/docs/configuration/static-mappers.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,24 @@ public static partial class CarMapper
private static int TimeSpanToHours(TimeSpan t) => t.Hours;
}
```

## Static methods in instantiable class

Static methods are supported in non-static mapper classes. This supports the static interface use case. When a static mapping method is present, to simplify mapping method resolution and reduce confusion about which mapping method Mapperly uses, all methods must be static.

```csharp
public interface ICarMapper
{
static abstract CarDto ToDto(Car car);
}

[Mapper]
// highlight-start
public partial class CarMapper : ICarMapper
// highlight-end
{
// highlight-start
public static partial CarDto ToDto(Car car);
// highlight-end
}
```
4 changes: 2 additions & 2 deletions docs/docs/contributing/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ The `DescriptorBuilder` is responsible to build a `MapperDescriptor` which holds
The `DescriptorBuilder` does this by following this process:

1. Extracting the configuration from the attributes
2. Extracting user implemented object factories
3. Extracting user implemented and user defined mapping methods.
2. Extracting user implemented and user defined mapping methods.
It instantiates a `User*Mapping` (eg. `UserDefinedNewInstanceMethodMapping`) for each discovered mapping method and adds it to the queue of mappings to work on.
3. Extracting user implemented object factories
4. Extracting external mappings
5. For each mapping in the queue the `DescriptorBuilder` tries to build its implementation bodies.
This is done by a so called `*MappingBodyBuilder`.
Expand Down
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
52 changes: 43 additions & 9 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 All @@ -28,8 +29,6 @@ public class DescriptorBuilder
private readonly UnsafeAccessorContext _unsafeAccessorContext;
private readonly MapperConfigurationReader _configurationReader;

private ObjectFactoryCollection _objectFactories = ObjectFactoryCollection.Empty;

public DescriptorBuilder(
CompilationContext compilationContext,
MapperDeclaration mapperDeclaration,
Expand Down Expand Up @@ -63,8 +62,10 @@ MapperConfiguration defaultMapperConfiguration
{
ConfigureMemberVisibility();
ReserveMethodNames();
ExtractObjectFactories();
ExtractUserMappings();
// ExtractObjectFactories needs to be called after ExtractUserMappings due to configuring mapperDescriptor.Static
var objectFactories = ExtractObjectFactories();
EnqueueUserMappings(objectFactories);
ExtractExternalMappings();
_mappingBodyBuilder.BuildMappingBodies(cancellationToken);
BuildMappingMethodNames();
Expand Down Expand Up @@ -106,24 +107,57 @@ private void ReserveMethodNames()
}
}

private void ExtractObjectFactories()
private void ExtractUserMappings()
{
_mapperDescriptor.Static = _mapperDescriptor.Symbol.IsStatic;
IMethodSymbol? firstNonStaticUserMapping = null;

foreach (var userMapping in UserMethodMappingExtractor.ExtractUserMappings(_builderContext, _mapperDescriptor.Symbol))
{
// if a user defined mapping method is static, all of them need to be static to avoid confusion for mapping method resolution
// however, user implemented mapping methods are allowed to be static in a non-static context.
// Therefore we are only interested in partial method definitions here.
if (userMapping.Method is { IsStatic: true, IsPartialDefinition: true })
{
_mapperDescriptor.Static = true;
}
else if (firstNonStaticUserMapping == null && !userMapping.Method.IsStatic)
{
firstNonStaticUserMapping = userMapping.Method;
}

_mappings.Add(userMapping);
}

if (_mapperDescriptor.Static && firstNonStaticUserMapping is not null)
{
_diagnostics.Add(
Diagnostic.Create(
DiagnosticDescriptors.MixingStaticPartialWithInstanceMethod,
firstNonStaticUserMapping.Locations.FirstOrDefault(),
_mapperDescriptor.Symbol.ToDisplayString()
)
);
}
}

private ObjectFactoryCollection ExtractObjectFactories()
{
_objectFactories = ObjectFactoryBuilder.ExtractObjectFactories(_builderContext, _mapperDescriptor.Symbol);
return ObjectFactoryBuilder.ExtractObjectFactories(_builderContext, _mapperDescriptor.Symbol);
}

private void ExtractUserMappings()
private void EnqueueUserMappings(ObjectFactoryCollection objectFactories)
{
foreach (var userMapping in UserMethodMappingExtractor.ExtractUserMappings(_builderContext, _mapperDescriptor.Symbol))
foreach (var userMapping in _mappings.UserMappings)
{
var ctx = new MappingBuilderContext(
_builderContext,
_objectFactories,
objectFactories,
userMapping.Method,
userMapping.SourceType,
userMapping.TargetType
);

_mappings.Add(userMapping);
_mappings.EnqueueToBuildBody(userMapping, ctx);
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/Riok.Mapperly/Descriptors/MapperDescriptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public class MapperDescriptor
private readonly List<IUnsafeAccessor> _unsafeAccessors = new();
private readonly HashSet<TemplateReference> _requiredTemplates = new();

public bool Static { get; set; }

public MapperDescriptor(MapperDeclaration declaration, UniqueNameBuilder nameBuilder)
{
_declaration = declaration;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public static ObjectFactoryCollection ExtractObjectFactories(SimpleMappingBuilde
|| methodSymbol.IsPartialDefinition
|| methodSymbol.MethodKind != MethodKind.Ordinary
|| methodSymbol.ReturnsVoid
|| (!methodSymbol.IsStatic && ctx.Static)
)
{
ctx.ReportDiagnostic(DiagnosticDescriptors.InvalidObjectFactorySignature, methodSymbol, methodSymbol.Name);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ namespace Riok.Mapperly.Descriptors.ObjectFactories;

public class ObjectFactoryCollection
{
public static readonly ObjectFactoryCollection Empty = new(Array.Empty<ObjectFactory>());

private readonly IReadOnlyCollection<ObjectFactory> _objectFactories;
private readonly Dictionary<ITypeSymbol, ObjectFactory> _concreteObjectFactories = new(SymbolEqualityComparer.IncludeNullability);

Expand Down
2 changes: 2 additions & 0 deletions src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ protected SimpleMappingBuilderContext(SimpleMappingBuilderContext ctx)

public WellKnownTypes Types => _compilationContext.Types;

public bool Static => _descriptor.Static;

public SymbolAccessor SymbolAccessor { get; }

public AttributeDataAccessor AttributeAccessor { get; }
Expand Down
13 changes: 3 additions & 10 deletions src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ internal static IEnumerable<IUserMapping> ExtractUserMappings(SimpleMappingBuild
foreach (var methodSymbol in ExtractMethods(mapperSymbol))
{
var mapping =
BuilderUserDefinedMapping(ctx, methodSymbol, mapperSymbol.IsStatic)
?? BuildUserImplementedMapping(ctx, methodSymbol, null, false, mapperSymbol.IsStatic);
BuilderUserDefinedMapping(ctx, methodSymbol)
?? BuildUserImplementedMapping(ctx, methodSymbol, receiver: null, allowPartial: false, mapperSymbol.IsStatic);
if (mapping != null)
yield return mapping;
}
Expand Down Expand Up @@ -109,18 +109,11 @@ bool isStatic
: new UserImplementedMethodMapping(receiver, method, parameters.Source, parameters.ReferenceHandler);
}

private static IUserMapping? BuilderUserDefinedMapping(SimpleMappingBuilderContext ctx, IMethodSymbol methodSymbol, bool isStatic)
private static IUserMapping? BuilderUserDefinedMapping(SimpleMappingBuilderContext ctx, IMethodSymbol methodSymbol)
{
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
);
}
2 changes: 1 addition & 1 deletion src/Riok.Mapperly/Emit/SourceEmitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public static class SourceEmitter
public static CompilationUnitSyntax Build(MapperDescriptor descriptor, CancellationToken cancellationToken)
{
var ctx = new SourceEmitterContext(
descriptor.Symbol.IsStatic,
descriptor.Static,
descriptor.NameBuilder,
new SyntaxFactoryHelper(descriptor.Symbol.ContainingAssembly.Name)
);
Expand Down
2 changes: 1 addition & 1 deletion src/Riok.Mapperly/MapperGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Configuration;
Expand Down
18 changes: 15 additions & 3 deletions test/Riok.Mapperly.Tests/Mapping/DictionaryTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,11 @@ public void DictionaryToCustomDictionary()
public void DictionaryToCustomDictionaryWithObjectFactory()
{
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"[ObjectFactory] A CreateA() => new();" + "partial A Map(IDictionary<string, int> source);",
"""
[ObjectFactory]
A CreateA() => new();
partial A Map(IDictionary<string, int> source);
""",
"class A : Dictionary<string, int> {}"
);
TestHelper
Expand Down Expand Up @@ -354,7 +358,11 @@ string IDictionary<string, string>.this[string key]
public void DictionaryToExplicitDictionaryWithObjectFactoryShouldCast()
{
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"[ObjectFactory] A CreateA() => new();" + "partial A Map(Dictionary<string, string> source);",
"""
[ObjectFactory]
A CreateA() => new();
partial A Map(Dictionary<string, string> source);
""",
"""
public class A : IDictionary<string, string>
{
Expand Down Expand Up @@ -387,7 +395,11 @@ string IDictionary<string, string>.this[string key]
public void DictionaryToImplicitDictionaryWithObjectFactoryShouldNotCast()
{
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"[ObjectFactory] A CreateA() => new();" + "partial A Map(Dictionary<string, string> source);",
"""
[ObjectFactory]
A CreateA() => new();
partial A Map(Dictionary<string, string> source);
""",
"""
public class A : IDictionary<string, string>
{
Expand Down
Loading

0 comments on commit 1724124

Please sign in to comment.