Skip to content

Commit

Permalink
fix: allow reference handling for generic and runtime target type map…
Browse files Browse the repository at this point in the history
…ping methods
  • Loading branch information
latonz committed Jun 19, 2023
1 parent 81b53f2 commit 3c8b0a2
Show file tree
Hide file tree
Showing 23 changed files with 435 additions and 75 deletions.
Expand Up @@ -13,9 +13,8 @@ public static void BuildMappingBody(MappingBuilderContext ctx, UserDefinedNewIns
// source nulls are filtered out by the type switch arms,
// therefore set source type always to nun-nullable
// as non-nullables are also assignable to nullables.
var mappings = ctx.CallableUserMappings.Where(
x => mapping.TypeParameters.CanConsumeTypes(ctx.Compilation, x.SourceType.NonNullable(), x.TargetType)
);
var mappings = GetUserMappingCandidates(ctx)
.Where(x => mapping.TypeParameters.CanConsumeTypes(ctx.Compilation, x.SourceType.NonNullable(), x.TargetType));

BuildMappingBody(ctx, mapping, mappings);
}
Expand All @@ -25,15 +24,32 @@ public static void BuildMappingBody(MappingBuilderContext ctx, UserDefinedNewIns
// source nulls are filtered out by the type switch arms,
// therefore set source type always to nun-nullable
// as non-nullables are also assignable to nullables.
var mappings = ctx.CallableUserMappings.Where(
x =>
x.SourceType.NonNullable().IsAssignableTo(ctx.Compilation, mapping.SourceType)
&& x.TargetType.IsAssignableTo(ctx.Compilation, mapping.TargetType)
);
var mappings = GetUserMappingCandidates(ctx)
.Where(
x =>
x.SourceType.NonNullable().IsAssignableTo(ctx.Compilation, mapping.SourceType)
&& x.TargetType.IsAssignableTo(ctx.Compilation, mapping.TargetType)
);

BuildMappingBody(ctx, mapping, mappings);
}

private static IEnumerable<ITypeMapping> GetUserMappingCandidates(MappingBuilderContext ctx)
{
foreach (var userMapping in ctx.UserMappings)
{
// exclude runtime target type mappings
if (userMapping is UserDefinedNewInstanceRuntimeTargetTypeMapping)
continue;

if (userMapping.CallableByOtherMappings)
yield return userMapping;

if (userMapping is IDelegateUserMapping { DelegateMapping.CallableByOtherMappings: true } delegateUserMapping)
yield return delegateUserMapping.DelegateMapping;
}
}

private static void BuildMappingBody(
MappingBuilderContext ctx,
UserDefinedNewInstanceRuntimeTargetTypeMapping mapping,
Expand Down
6 changes: 3 additions & 3 deletions src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs
Expand Up @@ -11,7 +11,7 @@

namespace Riok.Mapperly.Descriptors;

[DebuggerDisplay("{GetType()}({Source.Name} => {Target.Name})")]
[DebuggerDisplay("{GetType().Name}({Source.Name} => {Target.Name})")]
public class MappingBuilderContext : SimpleMappingBuilderContext
{
private readonly IMethodSymbol? _userSymbol;
Expand Down Expand Up @@ -51,8 +51,8 @@ protected MappingBuilderContext(MappingBuilderContext ctx, IMethodSymbol? userSy

public ObjectFactoryCollection ObjectFactories { get; }

/// <inheritdoc cref="MappingBuilderContext.CallableUserMappings"/>
public IReadOnlyCollection<IUserMapping> CallableUserMappings => MappingBuilder.CallableUserMappings;
/// <inheritdoc cref="MappingBuilders.MappingBuilder.UserMappings"/>
public IReadOnlyCollection<IUserMapping> UserMappings => MappingBuilder.UserMappings;

/// <summary>
/// Tries to find an existing mapping for the provided types.
Expand Down
Expand Up @@ -37,8 +37,8 @@ public MappingBuilder(MappingCollection mappings)
_mappings = mappings;
}

/// <inheritdoc cref="MappingCollection.CallableUserMappings"/>
public IReadOnlyCollection<IUserMapping> CallableUserMappings => _mappings.CallableUserMappings;
/// <inheritdoc cref="MappingCollection.UserMappings"/>
public IReadOnlyCollection<IUserMapping> UserMappings => _mappings.UserMappings;

/// <inheritdoc cref="MappingBuilderContext.FindMapping"/>
public ITypeMapping? Find(ITypeSymbol sourceType, ITypeSymbol targetType) => _mappings.Find(sourceType, targetType);
Expand Down
17 changes: 9 additions & 8 deletions src/Riok.Mapperly/Descriptors/MappingCollection.cs
Expand Up @@ -20,14 +20,14 @@ public class MappingCollection
private readonly List<MethodMapping> _methodMappings = new();

/// <summary>
/// A list of all callable user mappings with <see cref="ITypeMapping.CallableByOtherMappings"/> <c>true</c>.
/// A list of all user mappings.
/// </summary>
private readonly List<IUserMapping> _callableUserMappings = new();
private readonly List<IUserMapping> _userMappings = new();

/// <summary>
/// Queue of mappings which don't have the body built yet
/// </summary>
private readonly Queue<(IMapping, MappingBuilderContext)> _mappingsToBuildBody = new();
private readonly PriorityQueue<(IMapping, MappingBuilderContext), MappingBodyBuildingPriority> _mappingsToBuildBody = new();

/// <summary>
/// All existing target mappings
Expand All @@ -36,8 +36,8 @@ public class MappingCollection

public IReadOnlyCollection<MethodMapping> MethodMappings => _methodMappings;

/// <inheritdoc cref="_callableUserMappings"/>
public IReadOnlyCollection<IUserMapping> CallableUserMappings => _callableUserMappings;
/// <inheritdoc cref="_userMappings"/>
public IReadOnlyCollection<IUserMapping> UserMappings => _userMappings;

public ITypeMapping? Find(ITypeSymbol sourceType, ITypeSymbol targetType)
{
Expand All @@ -51,13 +51,14 @@ public class MappingCollection
return mapping;
}

public void EnqueueToBuildBody(IMapping mapping, MappingBuilderContext ctx) => _mappingsToBuildBody.Enqueue((mapping, ctx));
public void EnqueueToBuildBody(IMapping mapping, MappingBuilderContext ctx) =>
_mappingsToBuildBody.Enqueue((mapping, ctx), mapping.BodyBuildingPriority);

public void Add(ITypeMapping mapping)
{
if (mapping is IUserMapping { CallableByOtherMappings: true } userMapping)
if (mapping is IUserMapping userMapping)
{
_callableUserMappings.Add(userMapping);
_userMappings.Add(userMapping);
}

if (mapping is MethodMapping methodMapping)
Expand Down
Expand Up @@ -18,5 +18,7 @@ protected ExistingTargetMapping(ITypeSymbol sourceType, ITypeSymbol targetType)

public ITypeSymbol TargetType { get; }

public MappingBodyBuildingPriority BodyBuildingPriority => MappingBodyBuildingPriority.Default;

public abstract IEnumerable<StatementSyntax> Build(TypeMappingBuildContext ctx, ExpressionSyntax target);
}
Expand Up @@ -18,4 +18,6 @@ public ObjectMemberExistingTargetMapping(ITypeSymbol sourceType, ITypeSymbol tar
public ITypeSymbol SourceType { get; }

public ITypeSymbol TargetType { get; }

public MappingBodyBuildingPriority BodyBuildingPriority => MappingBodyBuildingPriority.Default;
}
2 changes: 2 additions & 0 deletions src/Riok.Mapperly/Descriptors/Mappings/IMapping.cs
Expand Up @@ -10,4 +10,6 @@ public interface IMapping
ITypeSymbol SourceType { get; }

ITypeSymbol TargetType { get; }

MappingBodyBuildingPriority BodyBuildingPriority { get; }
}
6 changes: 6 additions & 0 deletions src/Riok.Mapperly/Descriptors/Mappings/ITypeMapping.cs
Expand Up @@ -4,6 +4,7 @@ namespace Riok.Mapperly.Descriptors.Mappings;

/// <summary>
/// Represents a mapping from one type to another.
/// The target is usually a new instance.
/// </summary>
public interface ITypeMapping : IMapping
{
Expand All @@ -18,5 +19,10 @@ public interface ITypeMapping : IMapping
/// </summary>
bool IsSynthetic { get; }

/// <summary>
/// Serializes the mapping as c# syntax.
/// </summary>
/// <param name="ctx">The build context.</param>
/// <returns>The built syntax.</returns>
ExpressionSyntax Build(TypeMappingBuildContext ctx);
}
@@ -0,0 +1,12 @@
namespace Riok.Mapperly.Descriptors.Mappings;

/// <summary>
/// When to build the body of this mapping.
/// The body of mappings with a higher priority
/// is built before the bodies of mappings with a lower priority.
/// </summary>
public enum MappingBodyBuildingPriority
{
AfterUserMappings,
Default,
}
4 changes: 3 additions & 1 deletion src/Riok.Mapperly/Descriptors/Mappings/TypeMapping.cs
Expand Up @@ -5,7 +5,7 @@
namespace Riok.Mapperly.Descriptors.Mappings;

/// <inheritdoc cref="ITypeMapping"/>
[DebuggerDisplay("{GetType()}({SourceType} => {TargetType})")]
[DebuggerDisplay("{GetType().Name}({SourceType} => {TargetType})")]
public abstract class TypeMapping : ITypeMapping
{
protected TypeMapping(ITypeSymbol sourceType, ITypeSymbol targetType)
Expand All @@ -18,6 +18,8 @@ protected TypeMapping(ITypeSymbol sourceType, ITypeSymbol targetType)

public ITypeSymbol TargetType { get; }

public virtual MappingBodyBuildingPriority BodyBuildingPriority => MappingBodyBuildingPriority.Default;

/// <inheritdoc cref="ITypeMapping.CallableByOtherMappings"/>
public virtual bool CallableByOtherMappings => true;

Expand Down
@@ -0,0 +1,18 @@
namespace Riok.Mapperly.Descriptors.Mappings.UserMappings;

/// <summary>
/// A delegated user mapping.
/// </summary>
public interface IDelegateUserMapping : IUserMapping
{
/// <summary>
/// Gets the delegate mapping or <c>null</c> if none is set (yet).
/// </summary>
ITypeMapping? DelegateMapping { get; }

/// <summary>
/// Sets the delegate mapping.
/// </summary>
/// <param name="mapping">The mapping.</param>
void SetDelegateMapping(ITypeMapping mapping);
}
Expand Up @@ -10,15 +10,13 @@ namespace Riok.Mapperly.Descriptors.Mappings.UserMappings;
/// <summary>
/// Represents a mapping method declared but not implemented by the user which results in a new target object instance.
/// </summary>
public class UserDefinedNewInstanceMethodMapping : MethodMapping, IUserMapping
public class UserDefinedNewInstanceMethodMapping : MethodMapping, IDelegateUserMapping
{
private const string NoMappingComment = "// Could not generate mapping";

private readonly bool _enableReferenceHandling;
private readonly INamedTypeSymbol _referenceHandlerType;

private ITypeMapping? _delegateMapping;

public UserDefinedNewInstanceMethodMapping(
IMethodSymbol method,
MethodParameter sourceParameter,
Expand All @@ -35,11 +33,13 @@ INamedTypeSymbol referenceHandlerType

public IMethodSymbol Method { get; }

public void SetDelegateMapping(ITypeMapping delegateMapping) => _delegateMapping = delegateMapping;
public ITypeMapping? DelegateMapping { get; private set; }

public void SetDelegateMapping(ITypeMapping mapping) => DelegateMapping = mapping;

public override IEnumerable<StatementSyntax> BuildBody(TypeMappingBuildContext ctx)
{
if (_delegateMapping == null)
if (DelegateMapping == null)
{
return new[] { ExpressionStatement(ThrowNotImplementedException()).WithLeadingTrivia(TriviaList(Comment(NoMappingComment))), };
}
Expand All @@ -52,13 +52,13 @@ public override IEnumerable<StatementSyntax> BuildBody(TypeMappingBuildContext c
// new RefHandler();
var createRefHandler = CreateInstance(_referenceHandlerType);
ctx = ctx.WithRefHandler(createRefHandler);
return new[] { ReturnStatement(_delegateMapping.Build(ctx)) };
return new[] { ReturnStatement(DelegateMapping.Build(ctx)) };
}

if (_delegateMapping is MethodMapping delegateMethodMapping)
if (DelegateMapping is MethodMapping delegateMethodMapping)
return delegateMethodMapping.BuildBody(ctx);

return new[] { ReturnStatement(_delegateMapping.Build(ctx)) };
return new[] { ReturnStatement(DelegateMapping.Build(ctx)) };
}

/// <summary>
Expand All @@ -71,7 +71,7 @@ internal override void EnableReferenceHandling(INamedTypeSymbol iReferenceHandle
{
// the parameters of user defined methods should not be manipulated
// if the user did not define a parameter a new reference handler is initialized
if (_delegateMapping is MethodMapping methodMapping)
if (DelegateMapping is MethodMapping methodMapping)
{
methodMapping.EnableReferenceHandling(iReferenceHandlerType);
}
Expand Down
Expand Up @@ -40,6 +40,8 @@ ITypeSymbol objectType
_objectType = objectType;
}

public override MappingBodyBuildingPriority BodyBuildingPriority => MappingBodyBuildingPriority.AfterUserMappings;

public IMethodSymbol Method { get; }

public override bool CallableByOtherMappings => false;
Expand Down
50 changes: 50 additions & 0 deletions src/Riok.Mapperly/Helpers/PriorityQueue.cs
@@ -0,0 +1,50 @@
namespace Riok.Mapperly.Helpers;

/// <summary>
/// A simple implementation of a priority queue.
/// </summary>
/// <typeparam name="TElement">The type of the elements.</typeparam>
/// <typeparam name="TPriority">The priority of an element.</typeparam>
public class PriorityQueue<TElement, TPriority>
{
private readonly Dictionary<TPriority, Queue<TElement>> _nodes = new();
private readonly SortedSet<TPriority> _knownPriorities = new();

public void Enqueue(TElement element, TPriority priority)
{
if (_nodes.TryGetValue(priority, out var node))
{
node.Enqueue(element);
return;
}

var queue = new Queue<TElement>();
queue.Enqueue(element);
_nodes[priority] = queue;
_knownPriorities.Add(priority);
}

/// <summary>
/// Dequeues all nodes.
/// The nodes with the highest priority are returned first ordered in a FIFO fashion.
/// Items added while this operation is in progress are also considered.
/// </summary>
/// <returns>An enumerable with all items.</returns>
public IEnumerable<TElement> DequeueAll()
{
while (_knownPriorities.Count > 0)
{
var priority = _knownPriorities.Max;
var queue = _nodes[priority];
var item = queue.Dequeue();

if (queue.Count == 0)
{
_nodes.Remove(priority);
_knownPriorities.Remove(priority);
}

yield return item;
}
}
}
12 changes: 0 additions & 12 deletions src/Riok.Mapperly/Helpers/QueueExtensions.cs

This file was deleted.

32 changes: 32 additions & 0 deletions test/Riok.Mapperly.Tests/Helpers/PriorityQueueTest.cs
@@ -0,0 +1,32 @@
namespace Riok.Mapperly.Tests.Helpers;

public class PriorityQueueTest
{
[Fact]
public void EnqueueAndDequeueAllShouldWork()
{
var queue = new Mapperly.Helpers.PriorityQueue<char, int>();
queue.Enqueue('C', 1);
queue.Enqueue('A', 3);
queue.Enqueue('B', 2);

var index = 0;
foreach (var item in queue.DequeueAll())
{
var expectedValue = (char)('A' + index);
item.Should().Be(expectedValue);

// enqueue during dequeue
if (index == 2)
{
queue.Enqueue('E', 0);
queue.Enqueue('D', 1);
queue.Enqueue('F', 0);
}

index++;
}

index.Should().Be(6);
}
}

0 comments on commit 3c8b0a2

Please sign in to comment.