/
EnumerableMappingBuilder.cs
371 lines (315 loc) · 15.8 KB
/
EnumerableMappingBuilder.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Descriptors.Enumerables;
using Riok.Mapperly.Descriptors.Enumerables.EnsureCapacity;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Descriptors.Mappings.ExistingTarget;
using Riok.Mapperly.Diagnostics;
using Riok.Mapperly.Emit.Syntax;
using Riok.Mapperly.Helpers;
namespace Riok.Mapperly.Descriptors.MappingBuilders;
public static class EnumerableMappingBuilder
{
private const string SelectMethodName = "global::System.Linq.Enumerable.Select";
private const string ToArrayMethodName = "global::System.Linq.Enumerable.ToArray";
private const string ToListMethodName = "global::System.Linq.Enumerable.ToList";
private const string ToHashSetMethodName = "ToHashSet";
private const string AddMethodName = nameof(ICollection<object>.Add);
private const string ToImmutableArrayMethodName = "global::System.Collections.Immutable.ImmutableArray.ToImmutableArray";
private const string ToImmutableListMethodName = "global::System.Collections.Immutable.ImmutableList.ToImmutableList";
private const string ToImmutableHashSetMethodName = "global::System.Collections.Immutable.ImmutableHashSet.ToImmutableHashSet";
private const string CreateRangeQueueMethodName = "global::System.Collections.Immutable.ImmutableQueue.CreateRange";
private const string CreateRangeStackMethodName = "global::System.Collections.Immutable.ImmutableStack.CreateRange";
private const string ToImmutableSortedSetMethodName = "global::System.Collections.Immutable.ImmutableSortedSet.ToImmutableSortedSet";
public static NewInstanceMapping? TryBuildMapping(MappingBuilderContext ctx)
{
if (!ctx.IsConversionEnabled(MappingConversionType.Enumerable))
return null;
if (ctx.CollectionInfos == null)
return null;
if (!ctx.CollectionInfos.Source.ImplementsIEnumerable || !ctx.CollectionInfos.Target.ImplementsIEnumerable)
return null;
var elementMapping = ctx.FindOrBuildMapping(ctx.CollectionInfos.Source.EnumeratedType, ctx.CollectionInfos.Target.EnumeratedType);
if (elementMapping == null)
return null;
if (TryBuildCastMapping(ctx, elementMapping) is { } castMapping)
return castMapping;
if (TryBuildFastConversion(ctx, elementMapping) is { } fastLoopMapping)
return fastLoopMapping;
// try linq mapping: x.Select(Map).ToArray/ToList
// if that doesn't work do a foreach with add calls
var (canMapWithLinq, collectMethodName) = ResolveCollectMethodName(ctx);
if (canMapWithLinq)
return BuildLinqMapping(ctx, elementMapping, collectMethodName);
// try linq mapping: x.Select(Map).ToImmutableArray/ToImmutableList
// if that doesn't work do a foreach with add calls
var immutableLinqMapping = TryBuildImmutableLinqMapping(ctx, elementMapping);
if (immutableLinqMapping is not null)
return immutableLinqMapping;
// if target is a type that takes IEnumerable in its constructor
if (GetTypeConstructableFromEnumerable(ctx, elementMapping.TargetType) is { } constructableType)
return BuildLinqConstructorMapping(ctx, constructableType, elementMapping);
return ctx.IsExpression ? null : BuildCustomTypeMapping(ctx, elementMapping);
}
public static IExistingTargetMapping? TryBuildExistingTargetMapping(MappingBuilderContext ctx)
{
if (!ctx.IsConversionEnabled(MappingConversionType.Enumerable))
return null;
if (ctx.CollectionInfos == null)
return null;
if (!ctx.CollectionInfos.Source.ImplementsIEnumerable || !ctx.CollectionInfos.Target.ImplementsIEnumerable)
return null;
var elementMapping = ctx.FindOrBuildMapping(ctx.CollectionInfos.Source.EnumeratedType, ctx.CollectionInfos.Target.EnumeratedType);
if (elementMapping == null)
return null;
if (ctx.CollectionInfos.Target.CollectionType == CollectionType.Stack)
return CreateForEach(nameof(Stack<object>.Push));
if (ctx.CollectionInfos.Target.CollectionType == CollectionType.Queue)
return CreateForEach(nameof(Queue<object>.Enqueue));
// create a foreach loop with add calls if source is not an array
// and has an implicit .Add() method
// the implicit check is an easy way to exclude for example immutable types.
if (ctx.CollectionInfos.Target.CollectionType != CollectionType.Array && ctx.CollectionInfos.Target.HasImplicitCollectionAddMethod)
return CreateForEach(AddMethodName);
if (ctx.CollectionInfos.Target.IsImmutableCollectionType)
{
ctx.ReportDiagnostic(DiagnosticDescriptors.CannotMapToReadOnlyMember);
}
return null;
ForEachAddEnumerableExistingTargetMapping CreateForEach(string methodName)
{
var ensureCapacityStatement = EnsureCapacityBuilder.TryBuildEnsureCapacity(ctx);
return new ForEachAddEnumerableExistingTargetMapping(
ctx.Source,
ctx.Target,
elementMapping,
methodName,
ensureCapacityStatement
);
}
}
private static NewInstanceMapping? TryBuildCastMapping(MappingBuilderContext ctx, ITypeMapping elementMapping)
{
// cannot cast if the method mapping is synthetic, deep clone is enabled or target is an unknown collection
if (
!elementMapping.IsSynthetic
|| ctx.MapperConfiguration.UseDeepCloning
|| ctx.CollectionInfos!.Target.CollectionType == CollectionType.None
)
{
return null;
}
// manually check if source is an Array as it implements IList and ICollection at runtime, see https://stackoverflow.com/q/47361775/3302887
if (
ctx.CollectionInfos.Source.IsArray
&& ctx.CollectionInfos.Target.CollectionType is CollectionType.ICollection or CollectionType.IList
)
{
return null;
}
// if not an array check if source implements the target type
if (ctx.CollectionInfos.Source.ImplementedTypes.HasFlag(ctx.CollectionInfos.Target.CollectionType))
{
return new CastMapping(ctx.Source, ctx.Target);
}
return null;
}
private static NewInstanceMapping? TryBuildFastConversion(MappingBuilderContext ctx, INewInstanceMapping elementMapping)
{
if (
ctx.IsExpression
|| !ctx.CollectionInfos!.Source.CountIsKnown
|| ctx.CollectionInfos.Target.CollectionType == CollectionType.None
)
{
return null;
}
// if target is a list or type implemented by list
if (ctx.CollectionInfos.Target.CollectionType is CollectionType.List or CollectionType.ICollection or CollectionType.IList)
{
return BuildEnumerableToListMapping(ctx, elementMapping);
}
// if target is not an array or a type implemented by array return early
if (
ctx.CollectionInfos.Target.CollectionType
is not (
CollectionType.Array
or CollectionType.IReadOnlyCollection
or CollectionType.IReadOnlyList
or CollectionType.IEnumerable
)
)
{
return null;
}
// if source is not an array use a foreach mapping
// if source is an array and target is an array, IEnumerable, IReadOnlyCollection faster mappings can be applied
return ctx.CollectionInfos.Source.CollectionType != CollectionType.Array
? BuildEnumerableToArrayMapping(ctx, elementMapping)
: BuildArrayToArrayMapping(ctx, elementMapping);
}
private static NewInstanceMapping? BuildEnumerableToListMapping(MappingBuilderContext ctx, INewInstanceMapping elementMapping)
{
// if mapping is synthetic then ToList is probably faster
if (elementMapping.IsSynthetic)
return null;
var targetTypeToInstantiate = ctx.Types.Get(typeof(List<>))
.Construct(elementMapping.TargetType)
.WithNullableAnnotation(NullableAnnotation.NotAnnotated);
return new ForEachAddEnumerableMapping(
ctx.Source,
ctx.CollectionInfos!.Target.Type,
elementMapping,
AddMethodName,
targetTypeToInstantiate,
ctx.CollectionInfos.Source.CountPropertyName
);
}
private static NewInstanceMapping BuildArrayToArrayMapping(MappingBuilderContext ctx, INewInstanceMapping elementMapping)
{
// if element mapping is synthetic
// a single Array.Clone / cast mapping call should be sufficient and fast,
// use a for loop mapping otherwise.
if (!elementMapping.IsSynthetic)
{
return new ArrayForMapping(ctx.Source, ctx.Target, elementMapping, elementMapping.TargetType);
}
return ctx.MapperConfiguration.UseDeepCloning
? new ArrayCloneMapping(ctx.Source, ctx.Target)
: new CastMapping(ctx.Source, ctx.Target);
}
private static NewInstanceMapping? BuildEnumerableToArrayMapping(MappingBuilderContext ctx, INewInstanceMapping elementMapping)
{
// if mapping is synthetic then ToArray is probably faster
if (elementMapping.IsSynthetic)
return null;
return new ArrayForEachMapping(
ctx.Source,
ctx.Target,
elementMapping,
elementMapping.TargetType,
ctx.CollectionInfos!.Source.CountPropertyName!
);
}
private static LinqEnumerableMapping BuildLinqMapping(
MappingBuilderContext ctx,
INewInstanceMapping elementMapping,
string? collectMethod
)
{
var selectMethod = elementMapping.IsSynthetic ? null : SelectMethodName;
return new LinqEnumerableMapping(ctx.Source, ctx.Target, elementMapping, selectMethod, collectMethod);
}
private static INamedTypeSymbol? GetTypeConstructableFromEnumerable(MappingBuilderContext ctx, ITypeSymbol typeSymbol)
{
if (ctx.Target is not INamedTypeSymbol namedType)
return null;
var typedEnumerable = ctx.Types.Get(typeof(IEnumerable<>)).Construct(typeSymbol);
var hasCtor = namedType.Constructors.Any(
m => m.Parameters.Length == 1 && SymbolEqualityComparer.Default.Equals(m.Parameters[0].Type, typedEnumerable)
);
if (hasCtor)
return namedType;
if (ctx.CollectionInfos!.Target.CollectionType is CollectionType.ISet or CollectionType.IReadOnlySet)
return ctx.Types.Get(typeof(HashSet<>)).Construct(typeSymbol);
return null;
}
private static LinqConstructorMapping BuildLinqConstructorMapping(
MappingBuilderContext ctx,
INamedTypeSymbol targetTypeToConstruct,
INewInstanceMapping elementMapping
)
{
var selectMethod = elementMapping.IsSynthetic ? null : SelectMethodName;
return new LinqConstructorMapping(ctx.Source, ctx.Target, targetTypeToConstruct, elementMapping, selectMethod);
}
private static ExistingTargetMappingMethodWrapper? BuildCustomTypeMapping(MappingBuilderContext ctx, INewInstanceMapping elementMapping)
{
if (
!ctx.ObjectFactories.TryFindObjectFactory(ctx.Source, ctx.Target, out var objectFactory)
&& !ctx.SymbolAccessor.HasDirectlyAccessibleParameterlessConstructor(ctx.Target)
)
{
ctx.ReportDiagnostic(DiagnosticDescriptors.NoParameterlessConstructorFound, ctx.Target);
return null;
}
// create a foreach loop with add calls if source is not an array
// and has an implicit .Add() method
// the implicit check is an easy way to exclude for example immutable types.
if (
ctx.CollectionInfos!.Target.CollectionType == CollectionType.Array
|| !ctx.CollectionInfos.Target.HasImplicitCollectionAddMethod
)
return null;
var ensureCapacityStatement = EnsureCapacityBuilder.TryBuildEnsureCapacity(ctx);
return new ForEachAddEnumerableMapping(
ctx.Source,
ctx.Target,
elementMapping,
objectFactory,
AddMethodName,
ensureCapacityStatement
);
}
private static (bool CanMapWithLinq, string? CollectMethod) ResolveCollectMethodName(MappingBuilderContext ctx)
{
// if the target is an array we need to collect to array
if (ctx.Target.IsArrayType())
return (true, ToArrayMethodName);
// if the target is an IEnumerable<T> don't collect at all
// except deep cloning is enabled.
var targetIsIEnumerable = ctx.CollectionInfos!.Target.CollectionType == CollectionType.IEnumerable;
if (targetIsIEnumerable && !ctx.MapperConfiguration.UseDeepCloning)
return (true, null);
// if the target is IReadOnlyCollection<T> or IEnumerable<T>
// and the count of the source is known (array, IReadOnlyCollection<T>, ICollection<T>) we collect to array
// for performance/space reasons
var targetIsReadOnlyCollection = ctx.CollectionInfos.Target.CollectionType == CollectionType.IReadOnlyCollection;
if ((targetIsReadOnlyCollection || targetIsIEnumerable) && ctx.CollectionInfos.Source.CountIsKnown)
return (true, ToArrayMethodName);
// if target is Set
// and ToHashSet is supported (only supported for .NET5+)
// use ToHashSet
if (
ctx.CollectionInfos.Target.CollectionType is CollectionType.ISet or CollectionType.IReadOnlySet or CollectionType.HashSet
&& GetToHashSetLinqCollectMethod(ctx.Types) is { } toHashSetMethod
)
{
return (true, SyntaxFactoryHelper.StaticMethodString(toHashSetMethod));
}
// if target is a IReadOnlyCollection<T>, IEnumerable<T>, IList<T>, List<T> or ICollection<T> with ToList()
return
targetIsReadOnlyCollection
|| targetIsIEnumerable
|| ctx.CollectionInfos.Target.CollectionType
is CollectionType.IReadOnlyList
or CollectionType.IList
or CollectionType.List
or CollectionType.ICollection
? (true, ToListMethodName)
: (false, null);
}
private static LinqEnumerableMapping? TryBuildImmutableLinqMapping(MappingBuilderContext ctx, INewInstanceMapping elementMapping)
{
var collectMethod = ResolveImmutableCollectMethod(ctx);
if (collectMethod is null)
return null;
var selectMethod = elementMapping.IsSynthetic ? null : SelectMethodName;
return new LinqEnumerableMapping(ctx.Source, ctx.Target, elementMapping, selectMethod, collectMethod);
}
private static string? ResolveImmutableCollectMethod(MappingBuilderContext ctx)
{
return ctx.CollectionInfos!.Target.CollectionType switch
{
CollectionType.ImmutableArray => ToImmutableArrayMethodName,
CollectionType.ImmutableList or CollectionType.IImmutableList => ToImmutableListMethodName,
CollectionType.ImmutableHashSet or CollectionType.IImmutableSet => ToImmutableHashSetMethodName,
CollectionType.ImmutableQueue or CollectionType.IImmutableQueue => CreateRangeQueueMethodName,
CollectionType.ImmutableStack or CollectionType.IImmutableStack => CreateRangeStackMethodName,
CollectionType.ImmutableSortedSet => ToImmutableSortedSetMethodName,
_ => null
};
}
private static IMethodSymbol? GetToHashSetLinqCollectMethod(WellKnownTypes wellKnownTypes) =>
wellKnownTypes.Get(typeof(Enumerable)).GetStaticGenericMethod(ToHashSetMethodName);
}