Skip to content

Commit

Permalink
fix: generate enumerable mapping methods with interface parameters in…
Browse files Browse the repository at this point in the history
…stead of concrete types to reduce generated methods (#1088)
  • Loading branch information
latonz committed Jan 26, 2024
1 parent 09f409b commit 66752cf
Show file tree
Hide file tree
Showing 23 changed files with 586 additions and 91 deletions.
Expand Up @@ -64,6 +64,10 @@ public static class CollectionInfoBuilder
new CollectionTypeInfo(CollectionType.ReadOnlyMemory, typeof(ReadOnlyMemory<>)),
};

private static readonly IReadOnlyDictionary<CollectionType, Type> _collectionClrTypeByType = _collectionTypeInfos
.Where(x => x.ReflectionType != null)
.ToDictionary(x => x.CollectionType, x => x.ReflectionType!);

public static CollectionInfos? Build(
WellKnownTypes wellKnownTypes,
SymbolAccessor symbolAccessor,
Expand Down Expand Up @@ -380,4 +384,8 @@ static CollectionType IterateImplementedTypes(ITypeSymbol type, WellKnownTypes t
return implementedCollectionTypes;
}
}

public static Type GetGenericClrCollectionType(CollectionType type) =>
_collectionClrTypeByType.GetValueOrDefault(type)
?? throw new InvalidOperationException("Could not get clr collection type for " + type);
}
41 changes: 41 additions & 0 deletions src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs
Expand Up @@ -72,6 +72,15 @@ bool clearDerivedTypes
/// <inheritdoc cref="MappingBuilders.MappingBuilder.UserMappings"/>
public IReadOnlyCollection<IUserMapping> UserMappings => MappingBuilder.UserMappings;

/// <summary>
/// Tries to find an existing mapping for the provided key.
/// If none is found, <c>null</c> is returned.
/// </summary>
/// <param name="source">The source type</param>
/// <param name="target">The target type</param>
/// <returns>The found mapping, or <c>null</c> if none is found.</returns>
public INewInstanceMapping? FindMapping(ITypeSymbol source, ITypeSymbol target) => FindMapping(new TypeMappingKey(source, target));

/// <summary>
/// Tries to find an existing mapping for the provided key.
/// If none is found, <c>null</c> is returned.
Expand Down Expand Up @@ -228,6 +237,38 @@ bool clearDerivedTypes
return ExistingTargetMappingBuilder.Build(ctx, options.HasFlag(MappingBuildingOptions.MarkAsReusable));
}

/// <summary>
/// Tries to build a mapping which delegates the actual mapping to an existing mapping.
/// Returns <c>null</c> if for the given types no mapping exists
/// or both types (<paramref name="source"/> and <paramref name="target"/>
/// equal the types of the context (<see cref="Source"/> and <see cref="Target"/>)
/// (since this does not make sense (delegating to yourself) and would lead to mapping recursion).
/// </summary>
/// <remarks>
/// This can be used to reuse mappings of more generalized types.
/// E.g. a mapping from <c>List&lt;A&gt;</c> to <c>IReadOnlyCollection&lt;B&gt;</c>
/// and a mapping from <c>IReadOnlyCollection&lt;A&gt;</c> to <c>IReadOnlyList&lt;B&gt;</c>
/// can both use the same mapping method with a signature of <c>List&lt;B&gt; Map(IReadOnlyCollection&lt;A&gt; source)</c>.
/// </remarks>
/// <param name="source">The source type. <see cref="Source"/> needs to be assignable to this type.</param>
/// <param name="target">The target type. Needs to be assignable to <see cref="Target"/>.</param>
/// <returns>The built <see cref="DelegateMapping"/> or <c>null</c>.</returns>
public DelegateMapping? BuildDelegatedMapping(ITypeSymbol source, ITypeSymbol target)
{
// don't create a delegate mapping for the same types as the current context types
// since this would lead to a mapping recursion.
if (
SymbolEqualityComparer.IncludeNullability.Equals(Source, source)
&& SymbolEqualityComparer.IncludeNullability.Equals(Target, target)
)
{
return null;
}

var existingMapping = FindMapping(source, target);
return existingMapping == null ? null : new DelegateMapping(Source, Target, existingMapping);
}

public void ReportDiagnostic(DiagnosticDescriptor descriptor, params object[] messageArgs) =>
base.ReportDiagnostic(descriptor, null, messageArgs);

Expand Down
Expand Up @@ -37,33 +37,27 @@ or CollectionType.IDictionary
or CollectionType.IReadOnlyDictionary
)
{
if (TryGetFromEnumerable(ctx, keyMapping, valueMapping) is { } toDictionary)
return toDictionary;

var targetDictionarySymbol = ctx.Types.Get(typeof(Dictionary<,>)).Construct(keyMapping.TargetType, valueMapping.TargetType);
ctx.ObjectFactories.TryFindObjectFactory(ctx.Source, ctx.Target, out var dictionaryObjectFactory);
return new ForEachSetDictionaryMapping(
ctx.Source,
ctx.Target,
keyMapping,
valueMapping,
ctx.CollectionInfos.Source.CountIsKnown,
targetDictionarySymbol,
dictionaryObjectFactory
);
return BuildDictionaryMapping(ctx, keyMapping, valueMapping);
}

// if target is an immutable dictionary then use LinqDictionaryMapper
var immutableLinqMapping = ResolveImmutableCollectMethod(ctx, keyMapping, valueMapping);
var immutableLinqMapping = BuildImmutableMapping(ctx, keyMapping, valueMapping);
if (immutableLinqMapping != null)
return immutableLinqMapping;

return BuildCustomTypeMapping(ctx, keyMapping, valueMapping);
}

private static INewInstanceMapping? BuildCustomTypeMapping(
MappingBuilderContext ctx,
INewInstanceMapping keyMapping,
INewInstanceMapping valueMapping
)
{
// the target is not a well known dictionary type
// it should have a an object factory or a parameterless public ctor
if (
!ctx.ObjectFactories.TryFindObjectFactory(ctx.Source, ctx.Target, out var objectFactory)
&& !ctx.SymbolAccessor.HasDirectlyAccessibleParameterlessConstructor(ctx.Target)
)
var hasObjectFactory = ctx.ObjectFactories.TryFindObjectFactory(ctx.Source, ctx.Target, out var objectFactory);
if (!hasObjectFactory && !ctx.SymbolAccessor.HasDirectlyAccessibleParameterlessConstructor(ctx.Target))
{
ctx.ReportDiagnostic(DiagnosticDescriptors.NoParameterlessConstructorFound, ctx.Target);
return null;
Expand All @@ -72,9 +66,25 @@ or CollectionType.IReadOnlyDictionary
if (!ctx.CollectionInfos!.Target.ImplementedTypes.HasFlag(CollectionType.IDictionary))
return null;

var sourceType = ctx.Source;
if (!hasObjectFactory)
{
sourceType = BuildCollectionTypeForIDictionary(
ctx,
ctx.CollectionInfos!.Source,
keyMapping.SourceType,
valueMapping.SourceType
);
ctx.ObjectFactories.TryFindObjectFactory(ctx.Source, ctx.Target, out objectFactory);

var existingMapping = ctx.BuildDelegatedMapping(sourceType, ctx.Target);
if (existingMapping != null)
return existingMapping;
}

var ensureCapacityStatement = EnsureCapacityBuilder.TryBuildEnsureCapacity(ctx);
return new ForEachSetDictionaryMapping(
ctx.Source,
sourceType,
ctx.Target,
keyMapping,
valueMapping,
Expand All @@ -85,6 +95,55 @@ or CollectionType.IReadOnlyDictionary
);
}

/// <summary>
/// Builds a for each set mapping for a dictionary.
/// Target type needs to be assignable from <see cref="Dictionary{TKey,TValue}"/>.
/// </summary>
private static INewInstanceMapping BuildDictionaryMapping(
MappingBuilderContext ctx,
INewInstanceMapping keyMapping,
INewInstanceMapping valueMapping
)
{
if (TryGetFromEnumerable(ctx, keyMapping, valueMapping) is { } toDictionary)
return toDictionary;

// there might be an object factory for the exact types
var hasObjectFactory = ctx.ObjectFactories.TryFindObjectFactory(ctx.Source, ctx.Target, out var objectFactory);

// use generalized types to reuse generated mappings
var sourceType = ctx.Source;
var targetType = ctx.Target;
if (!hasObjectFactory)
{
sourceType = BuildCollectionTypeForIDictionary(
ctx,
ctx.CollectionInfos!.Source,
keyMapping.SourceType,
valueMapping.SourceType
);

targetType = ctx.Types.Get(typeof(Dictionary<,>))
.Construct(keyMapping.TargetType, valueMapping.TargetType)
.WithNullableAnnotation(NullableAnnotation.NotAnnotated);

ctx.ObjectFactories.TryFindObjectFactory(sourceType, targetType, out objectFactory);

var delegateMapping = ctx.BuildDelegatedMapping(sourceType, targetType);
if (delegateMapping != null)
return delegateMapping;
}

return new ForEachSetDictionaryMapping(
sourceType,
targetType,
keyMapping,
valueMapping,
ctx.CollectionInfos!.Source.CountIsKnown,
objectFactory
);
}

public static IExistingTargetMapping? TryBuildExistingTargetMapping(MappingBuilderContext ctx)
{
if (!ctx.IsConversionEnabled(MappingConversionType.Dictionary))
Expand Down Expand Up @@ -182,7 +241,7 @@ INewInstanceMapping valueMapping
return typedInter;
}

private static LinqDictionaryMapping? ResolveImmutableCollectMethod(
private static LinqDictionaryMapping? BuildImmutableMapping(
MappingBuilderContext ctx,
INewInstanceMapping keyMapping,
INewInstanceMapping valueMapping
Expand All @@ -199,4 +258,30 @@ or CollectionType.IImmutableDictionary
_ => null,
};
}

private static ITypeSymbol BuildCollectionTypeForIDictionary(
MappingBuilderContext ctx,
CollectionInfo info,
ITypeSymbol key,
ITypeSymbol value
)
{
return info.ImplementedTypes.HasFlag(CollectionType.IReadOnlyDictionary)
? BuildDictionaryType(ctx, CollectionType.IReadOnlyDictionary, key, value)
: info.ImplementedTypes.HasFlag(CollectionType.IDictionary)
? BuildDictionaryType(ctx, CollectionType.IDictionary, key, value)
: info.Type;
}

private static INamedTypeSymbol BuildDictionaryType(
MappingBuilderContext ctx,
CollectionType type,
ITypeSymbol keyType,
ITypeSymbol valueType
)
{
var genericType = CollectionInfoBuilder.GetGenericClrCollectionType(type);
return (INamedTypeSymbol)
ctx.Types.Get(genericType).Construct(keyType, valueType).WithNullableAnnotation(NullableAnnotation.NotAnnotated);
}
}

0 comments on commit 66752cf

Please sign in to comment.