Skip to content

Commit

Permalink
feat: Add option to use other mappers
Browse files Browse the repository at this point in the history
  • Loading branch information
latonz committed Aug 18, 2023
1 parent a5f6aef commit b27c8c0
Show file tree
Hide file tree
Showing 13 changed files with 235 additions and 46 deletions.
6 changes: 6 additions & 0 deletions src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,9 @@ Riok.Mapperly.Abstractions.MapperIgnoreSourceValueAttribute.SourceValue.get -> S
Riok.Mapperly.Abstractions.MapperIgnoreTargetValueAttribute.TargetValue.get -> System.Enum?
Riok.Mapperly.Abstractions.MapperAttribute.AllowNullPropertyAssignment.get -> bool
Riok.Mapperly.Abstractions.MapperAttribute.AllowNullPropertyAssignment.set -> void
Riok.Mapperly.Abstractions.UseMapperAttribute
Riok.Mapperly.Abstractions.UseMapperAttribute.UseMapperAttribute() -> void
Riok.Mapperly.Abstractions.UseStaticMapperAttribute
Riok.Mapperly.Abstractions.UseStaticMapperAttribute.UseStaticMapperAttribute(System.Type! mapperType) -> void
Riok.Mapperly.Abstractions.UseStaticMapperAttribute<T>
Riok.Mapperly.Abstractions.UseStaticMapperAttribute<T>.UseStaticMapperAttribute() -> void
7 changes: 7 additions & 0 deletions src/Riok.Mapperly.Abstractions/UseMapperAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Riok.Mapperly.Abstractions;

/// <summary>
/// Uses mapping methods provided by the type of this member to map types.
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public sealed class UseMapperAttribute : Attribute { }
21 changes: 21 additions & 0 deletions src/Riok.Mapperly.Abstractions/UseStaticMapperAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace Riok.Mapperly.Abstractions;

/// <summary>
/// Uses static mapping methods provided by the provided type to map types.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class UseStaticMapperAttribute : Attribute
{
/// <summary>
/// Uses static mapping methods provided by <paramref name="mapperType"/> to map types.
/// </summary>
/// <param name="mapperType">The type of which mapping methods will be included.</param>
public UseStaticMapperAttribute(Type mapperType) { }
}

/// <summary>
/// Uses static mapping methods provided by the provided type to map types.
/// </summary>
/// <typeparam name="T">The type of which mapping methods will be included.</typeparam>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class UseStaticMapperAttribute<T> : Attribute { }
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using Microsoft.CodeAnalysis;

namespace Riok.Mapperly.Configuration;

public record UseStaticMapperConfiguration(ITypeSymbol MapperType);
10 changes: 10 additions & 0 deletions src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Riok.Mapperly.Abstractions.ReferenceHandling;
using Riok.Mapperly.Configuration;
using Riok.Mapperly.Descriptors.ExternalMappings;
using Riok.Mapperly.Descriptors.MappingBodyBuilders;
using Riok.Mapperly.Descriptors.MappingBuilders;
using Riok.Mapperly.Descriptors.ObjectFactories;
Expand Down Expand Up @@ -49,6 +50,7 @@ SymbolAccessor symbolAccessor
{
ReserveMethodNames();
ExtractObjectFactories();
ExtractExternalMappings();
ExtractUserMappings();
_mappingBodyBuilder.BuildMappingBodies();
BuildMappingMethodNames();
Expand Down Expand Up @@ -79,6 +81,14 @@ private void ExtractUserMappings()
}
}

private void ExtractExternalMappings()
{
foreach (var externalMapping in ExternalMappingsExtractor.ExtractExternalMappings(_builderContext, _mapperDescriptor.Symbol))
{
_mappings.Add(externalMapping);
}
}

private void ReserveMethodNames()
{
foreach (var methodSymbol in _symbolAccessor.GetAllMembers(_mapperDescriptor.Symbol))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Configuration;
using Riok.Mapperly.Descriptors.Mappings.UserMappings;
using Riok.Mapperly.Emit;

namespace Riok.Mapperly.Descriptors.ExternalMappings;

internal static class ExternalMappingsExtractor
{
// TODO move to user extractor

Check warning on line 11 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

TODO move to user extractor

Check warning on line 11 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / package

TODO move to user extractor

Check warning on line 11 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / package

TODO move to user extractor

Check warning on line 11 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / package

TODO move to user extractor

Check warning on line 11 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / lint-dotnet

TODO move to user extractor

Check warning on line 11 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / build

TODO move to user extractor

Check warning on line 11 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / build

TODO move to user extractor

Check warning on line 11 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / MappingBenchmarks

TODO move to user extractor

Check warning on line 11 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / SourceGeneratorBenchmarks

TODO move to user extractor

Check warning on line 11 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / sample

TODO move to user extractor

Check failure on line 11 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / test

TODO move to user extractor

Check failure on line 11 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / test

TODO move to user extractor
public static IEnumerable<IUserMapping> ExtractExternalMappings(SimpleMappingBuilderContext ctx, INamedTypeSymbol mapperSymbol)
{
// TODO extract attribute data accessor

Check warning on line 14 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

TODO extract attribute data accessor

Check warning on line 14 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / package

TODO extract attribute data accessor

Check warning on line 14 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / package

TODO extract attribute data accessor

Check warning on line 14 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / package

TODO extract attribute data accessor

Check warning on line 14 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / lint-dotnet

TODO extract attribute data accessor

Check warning on line 14 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / build

TODO extract attribute data accessor

Check warning on line 14 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / build

TODO extract attribute data accessor

Check warning on line 14 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / MappingBenchmarks

TODO extract attribute data accessor

Check warning on line 14 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / SourceGeneratorBenchmarks

TODO extract attribute data accessor

Check warning on line 14 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / sample

TODO extract attribute data accessor

Check failure on line 14 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / test

TODO extract attribute data accessor

Check failure on line 14 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / test

TODO extract attribute data accessor
var accessor = new AttributeDataAccessor(ctx.SymbolAccessor);

// TODO should not access syntax factory helper here

Check warning on line 17 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

TODO should not access syntax factory helper here

Check warning on line 17 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / package

TODO should not access syntax factory helper here

Check warning on line 17 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / package

TODO should not access syntax factory helper here

Check warning on line 17 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / package

TODO should not access syntax factory helper here

Check warning on line 17 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / lint-dotnet

TODO should not access syntax factory helper here

Check warning on line 17 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / build

TODO should not access syntax factory helper here

Check warning on line 17 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / build

TODO should not access syntax factory helper here

Check warning on line 17 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / MappingBenchmarks

TODO should not access syntax factory helper here

Check warning on line 17 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / SourceGeneratorBenchmarks

TODO should not access syntax factory helper here

Check warning on line 17 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / sample

TODO should not access syntax factory helper here

Check failure on line 17 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / test

TODO should not access syntax factory helper here

Check failure on line 17 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / test

TODO should not access syntax factory helper here
var staticExternalMappers = accessor
.Access<UseStaticMapperAttribute, UseStaticMapperConfiguration>(mapperSymbol)
.Concat(accessor.Access<UseStaticMapperAttribute<object>, UseStaticMapperConfiguration>(mapperSymbol))
.SelectMany(
x =>
UserMethodMappingExtractor.ExtractUserImplementedMappings(
ctx,
x.MapperType,
SyntaxFactoryHelper.FullyQualifiedIdentifierName(x.MapperType),
true
)
);

var externalInstanceMappers = ctx.SymbolAccessor
.GetAllMembers(mapperSymbol)
.Select(x => new { x.Name, Type = (x as IFieldSymbol)?.Type ?? (x as IPropertySymbol)?.Type })
.Where(x => x.Type != null)
.SelectMany(x => UserMethodMappingExtractor.ExtractUserImplementedMappings(ctx, x.Type!, x.Name, false));

return staticExternalMappers.Concat(externalInstanceMappers);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Riok.Mapperly.Descriptors.Mappings.ExistingTarget;
using Riok.Mapperly.Emit;
using Riok.Mapperly.Helpers;
using Riok.Mapperly.Symbols;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Riok.Mapperly.Emit.SyntaxFactoryHelper;

namespace Riok.Mapperly.Descriptors.Mappings.UserMappings;

Expand All @@ -16,8 +16,10 @@ public class UserImplementedExistingTargetMethodMapping : ExistingTargetMapping,
private readonly MethodParameter _sourceParameter;
private readonly MethodParameter _targetParameter;
private readonly MethodParameter? _referenceHandlerParameter;
private readonly string? _receiver;

public UserImplementedExistingTargetMethodMapping(
string? receiver,
IMethodSymbol method,
MethodParameter sourceParameter,
MethodParameter targetParameter,
Expand All @@ -29,24 +31,20 @@ public UserImplementedExistingTargetMethodMapping(
_sourceParameter = sourceParameter;
_targetParameter = targetParameter;
_referenceHandlerParameter = referenceHandlerParameter;
_receiver = receiver;
}

public IMethodSymbol Method { get; }

public ExpressionSyntax Build(TypeMappingBuildContext ctx) =>
throw new InvalidOperationException(
$"{nameof(UserImplementedExistingTargetMethodMapping)} {ctx.Source}, {ctx.ReferenceHandler} does not support {nameof(Build)}"
);

public override IEnumerable<StatementSyntax> Build(TypeMappingBuildContext ctx, ExpressionSyntax target)
{
// if the user implemented method is on an interface,
// we explicitly cast to be able to use the default interface implementation or explicit implementations
if (Method.ReceiverType?.TypeKind != TypeKind.Interface)
{
yield return SyntaxFactory.ExpressionStatement(
SyntaxFactoryHelper.Invocation(
Method.Name,
yield return ExpressionStatement(
Invocation(
_receiver == null ? IdentifierName(Method.Name) : MemberAccess(_receiver, Method.Name),
_sourceParameter.WithArgument(ctx.Source),
_targetParameter.WithArgument(target),
_referenceHandlerParameter?.WithArgument(ctx.ReferenceHandler)
Expand All @@ -55,13 +53,13 @@ public override IEnumerable<StatementSyntax> Build(TypeMappingBuildContext ctx,
yield break;
}

var castedThis = SyntaxFactory.CastExpression(
SyntaxFactoryHelper.FullyQualifiedIdentifier(Method.ReceiverType!),
SyntaxFactory.ThisExpression()
var castedThis = CastExpression(
FullyQualifiedIdentifier(Method.ReceiverType!),
_receiver != null ? IdentifierName(_receiver) : ThisExpression()
);
var method = SyntaxFactoryHelper.MemberAccess(SyntaxFactory.ParenthesizedExpression(castedThis), Method.Name);
yield return SyntaxFactory.ExpressionStatement(
SyntaxFactoryHelper.Invocation(
var method = MemberAccess(ParenthesizedExpression(castedThis), Method.Name);
yield return ExpressionStatement(
Invocation(
method,
_sourceParameter.WithArgument(ctx.Source),
_targetParameter.WithArgument(target),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,20 @@ namespace Riok.Mapperly.Descriptors.Mappings.UserMappings;
/// </summary>
public class UserImplementedMethodMapping : NewInstanceMapping, IUserMapping
{
private readonly string? _receiver;
private readonly MethodParameter _sourceParameter;
private readonly MethodParameter? _referenceHandlerParameter;

public UserImplementedMethodMapping(IMethodSymbol method, MethodParameter sourceParameter, MethodParameter? referenceHandlerParameter)
public UserImplementedMethodMapping(
string? receiver,
IMethodSymbol method,
MethodParameter sourceParameter,
MethodParameter? referenceHandlerParameter
)
: base(method.Parameters[0].Type.UpgradeNullable(), method.ReturnType.UpgradeNullable())
{
Method = method;
_receiver = receiver;
_sourceParameter = sourceParameter;
_referenceHandlerParameter = referenceHandlerParameter;
}
Expand All @@ -31,13 +38,16 @@ public override ExpressionSyntax Build(TypeMappingBuildContext ctx)
// we explicitly cast to be able to use the default interface implementation or explicit implementations
if (Method.ReceiverType?.TypeKind != TypeKind.Interface)
return Invocation(
Method.Name,
_receiver == null ? IdentifierName(Method.Name) : MemberAccess(_receiver, Method.Name),
_sourceParameter.WithArgument(ctx.Source),
_referenceHandlerParameter?.WithArgument(ctx.ReferenceHandler)
);

var castedThis = CastExpression(FullyQualifiedIdentifier(Method.ReceiverType!), ThisExpression());
var method = MemberAccess(ParenthesizedExpression(castedThis), Method.Name);
var castedReceiver = CastExpression(
FullyQualifiedIdentifier(Method.ReceiverType!),
_receiver == null ? ThisExpression() : IdentifierName(_receiver)
);
var method = MemberAccess(ParenthesizedExpression(castedReceiver), Method.Name);
return Invocation(
method,
_sourceParameter.WithArgument(ctx.Source),
Expand Down
66 changes: 46 additions & 20 deletions src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public static IEnumerable<IUserMapping> ExtractUserMappings(SimpleMappingBuilder
{
var mapping =
BuilderUserDefinedMapping(ctx, methodSymbol, mapperSymbol.IsStatic)
?? BuildUserImplementedMapping(ctx, methodSymbol, false, mapperSymbol.IsStatic);
?? BuildUserImplementedMapping(ctx, methodSymbol, null, false, mapperSymbol.IsStatic);
if (mapping != null)
yield return mapping;
}
Expand All @@ -28,40 +28,65 @@ public static IEnumerable<IUserMapping> ExtractUserMappings(SimpleMappingBuilder
yield break;

// extract user implemented mappings from base methods
foreach (var method in ExtractBaseMethods(ctx, mapperSymbol))
var methods = mapperSymbol.AllInterfaces.SelectMany(ctx.SymbolAccessor.GetAllMethods);
if (mapperSymbol.BaseType is { } mapperBaseSymbol)
{
methods = methods.Concat(ctx.SymbolAccessor.GetAllMethods(mapperBaseSymbol));
}

foreach (var mapping in BuildUserImplementedMappings(ctx, methods, null, false))
{
yield return mapping;
}
}

public static IEnumerable<IUserMapping> ExtractUserImplementedMappings(
SimpleMappingBuilderContext ctx,
ITypeSymbol type,
string? receiver,
bool isStatic
)
{
var methods = ctx.SymbolAccessor.GetAllMethods(type).Concat(type.AllInterfaces.SelectMany(ctx.SymbolAccessor.GetAllMethods));
return BuildUserImplementedMappings(ctx, methods, receiver, isStatic);
}

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

private static IEnumerable<IUserMapping> BuildUserImplementedMappings(
SimpleMappingBuilderContext ctx,
IEnumerable<IMethodSymbol> methods,
string? receiver,
bool isStatic
)
{
foreach (var method in methods)
{
if (!IsMappingMethodCandidate(ctx, method))
continue;

// Partial method declarations are allowed for base classes,
// but still treated as user implemented methods,
// since the user should provide an implementation elsewhere.
// This is the case if a partial mapper class is extended.
var mapping = BuildUserImplementedMapping(ctx, method, true, mapperSymbol.IsStatic);
var mapping = BuildUserImplementedMapping(ctx, method, receiver, true, isStatic);
if (mapping != null)
yield return mapping;
}
}

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

private static IEnumerable<IMethodSymbol> ExtractBaseMethods(SimpleMappingBuilderContext ctx, ITypeSymbol mapperSymbol)
private static bool IsMappingMethodCandidate(SimpleMappingBuilderContext ctx, IMethodSymbol method)
{
var baseMethods =
mapperSymbol.BaseType != null ? ctx.SymbolAccessor.GetAllMethods(mapperSymbol.BaseType!) : Enumerable.Empty<ISymbol>();
var intfMethods = mapperSymbol.AllInterfaces.SelectMany(ctx.SymbolAccessor.GetAllMethods);
return baseMethods
.Concat(intfMethods)
.OfType<IMethodSymbol>()
// ignore all non ordinary methods (eg. ctor, operators, etc.) and methods declared on the object type (eg. ToString)
.Where(
x =>
x.MethodKind == MethodKind.Ordinary
&& ctx.SymbolAccessor.IsAccessible(x)
&& !SymbolEqualityComparer.Default.Equals(x.ReceiverType, ctx.Compilation.ObjectType)
);
// ignore all non ordinary methods (eg. ctor, operators, etc.) and methods declared on the object type (eg. ToString)
return method.MethodKind == MethodKind.Ordinary
&& ctx.SymbolAccessor.IsAccessible(method)
&& !SymbolEqualityComparer.Default.Equals(method.ReceiverType, ctx.Compilation.ObjectType);
}

private static IUserMapping? BuildUserImplementedMapping(
SimpleMappingBuilderContext ctx,
IMethodSymbol method,
string? receiver,
bool allowPartial,
bool isStatic
)
Expand All @@ -75,12 +100,13 @@ bool isStatic

return method.ReturnsVoid
? new UserImplementedExistingTargetMethodMapping(
receiver,
method,
parameters.Source,
parameters.Target!.Value,
parameters.ReferenceHandler
)
: new UserImplementedMethodMapping(method, parameters.Source, parameters.ReferenceHandler);
: new UserImplementedMethodMapping(receiver, method, parameters.Source, parameters.ReferenceHandler);
}

private static IUserMapping? BuilderUserDefinedMapping(SimpleMappingBuilderContext ctx, IMethodSymbol methodSymbol, bool isStatic)
Expand Down
Loading

0 comments on commit b27c8c0

Please sign in to comment.