Skip to content

Commit

Permalink
fix: clone array when deepcloning is enabled and target is an IEnumer…
Browse files Browse the repository at this point in the history
…able (#431)

If the source type is an array, the target type is an IEnumerable,
the source should be cloned instead of casted
This also extracts deep cloning enumerable tests into a separate test class.
  • Loading branch information
latonz committed May 16, 2023
1 parent 84011e4 commit 082d877
Show file tree
Hide file tree
Showing 3 changed files with 199 additions and 179 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,14 @@ public static class EnumerableMappingBuilder
if (BuildElementMapping(ctx) is not { } elementMapping)
return null;

// if element mapping is synthetic
// and target is an IEnumerable, there is no mapping needed at all.
if (elementMapping.IsSynthetic && SymbolEqualityComparer.Default.Equals(ctx.Target.OriginalDefinition, ctx.Types.IEnumerableT))
return new CastMapping(ctx.Source, ctx.Target);

// if source is an array and target is an array or IReadOnlyCollection faster mappings can be applied
// if source is an array and target is an array, IEnumerable, IReadOnlyCollection faster mappings can be applied
if (
!ctx.IsExpression
&& ctx.Source.IsArrayType()
&& (
ctx.Target.IsArrayType()
|| SymbolEqualityComparer.Default.Equals(ctx.Target.OriginalDefinition, ctx.Types.IReadOnlyCollectionT)
|| SymbolEqualityComparer.Default.Equals(ctx.Target.OriginalDefinition, ctx.Types.IEnumerableT)
)
)
{
Expand Down
197 changes: 197 additions & 0 deletions test/Riok.Mapperly.Tests/Mapping/EnumerableDeepCloningTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
namespace Riok.Mapperly.Tests.Mapping;

public class EnumerableDeepCloningTest
{
[Fact]
public void ArrayOfPrimitivesToReadOnlyCollectionDeepCloning()
{
var source = TestSourceBuilder.Mapping("int[]", "IReadOnlyCollection<int>", TestSourceBuilderOptions.WithDeepCloning);
TestHelper
.GenerateMapper(source)
.Should()
.HaveSingleMethodBody("return (global::System.Collections.Generic.IReadOnlyCollection<int>)source.Clone();");
}

[Fact]
public void ArrayOfPrimitivesToEnumerableDeepCloning()
{
var source = TestSourceBuilder.Mapping("int[]", "IEnumerable<int>", TestSourceBuilderOptions.WithDeepCloning);
TestHelper
.GenerateMapper(source)
.Should()
.HaveSingleMethodBody("return (global::System.Collections.Generic.IEnumerable<int>)source.Clone();");
}

[Fact]
public void ArrayToArrayOfPrimitiveTypesDeepCloning()
{
var source = TestSourceBuilder.Mapping("int[]", "int[]", TestSourceBuilderOptions.WithDeepCloning);
TestHelper.GenerateMapper(source).Should().HaveSingleMethodBody("return (int[])source.Clone();");
}

[Fact]
public void ArrayOfNullablePrimitiveTypesToNonNullableArrayDeepCloning()
{
var source = TestSourceBuilder.Mapping("int?[]", "int[]", TestSourceBuilderOptions.WithDeepCloning);
TestHelper
.GenerateMapper(source)
.Should()
.HaveSingleMethodBody(
"""
var target = new int[source.Length];
for (var i = 0; i < source.Length; i++)
{
target[i] = source[i] == null ? throw new System.NullReferenceException($"Sequence {nameof(source)}, contained a null value at index {i}.") : source[i].Value;
}

return target;
"""
);
}

[Fact]
public void ArrayOfPrimitiveTypesToNullablePrimitiveTypesArrayDeepCloning()
{
var source = TestSourceBuilder.Mapping("int[]", "int?[]", TestSourceBuilderOptions.WithDeepCloning);
TestHelper
.GenerateMapper(source)
.Should()
.HaveSingleMethodBody(
"""
var target = new int? [source.Length];
for (var i = 0; i < source.Length; i++)
{
target[i] = (int? )source[i];
}

return target;
"""
);
}

[Fact]
public void ArrayCustomClassToArrayCustomClassDeepCloning()
{
var source = TestSourceBuilder.Mapping(
"B[]",
"B[]",
TestSourceBuilderOptions.WithDeepCloning,
"class B { public int Value { get; set; }}"
);
TestHelper
.GenerateMapper(source)
.Should()
.HaveMapMethodBody(
"""
var target = new global::B[source.Length];
for (var i = 0; i < source.Length; i++)
{
target[i] = MapToB(source[i]);
}

return target;
"""
);
}

[Fact]
public void ArrayToArrayOfStringDeepCloning()
{
var source = TestSourceBuilder.Mapping("string[]", "string[]", TestSourceBuilderOptions.WithDeepCloning);
TestHelper.GenerateMapper(source).Should().HaveSingleMethodBody("return (string[])source.Clone();");
}

[Fact]
public void ArrayToArrayOfNullableStringDeepCloning()
{
var source = TestSourceBuilder.Mapping("string[]", "string?[]", TestSourceBuilderOptions.WithDeepCloning);
TestHelper.GenerateMapper(source).Should().HaveSingleMethodBody("return (string? [])source.Clone();");
}

[Fact]
public void ArrayToArrayOfReadOnlyStructDeepCloning()
{
var source = TestSourceBuilder.Mapping("A[]", "A[]", TestSourceBuilderOptions.WithDeepCloning, "readonly struct A{}");
TestHelper.GenerateMapper(source).Should().HaveSingleMethodBody("return (global::A[])source.Clone();");
}

[Fact]
public void ArrayToArrayOfMutableStructDeepCloning()
{
var source = TestSourceBuilder.Mapping(
"A[]",
"A[]",
TestSourceBuilderOptions.WithDeepCloning,
"struct A{ public string Value { get; set; } }"
);
TestHelper
.GenerateMapper(source)
.Should()
.HaveMapMethodBody(
"""
var target = new global::A[source.Length];
for (var i = 0; i < source.Length; i++)
{
target[i] = MapToA(source[i]);
}

return target;
"""
);
}

[Fact]
public void ArrayToArrayOfUnmanagedStructDeepCloning()
{
var source = TestSourceBuilder.Mapping("A[]", "A[]", TestSourceBuilderOptions.WithDeepCloning, "struct A{}");
TestHelper.GenerateMapper(source).Should().HaveMapMethodBody("return (global::A[])source.Clone();");
}

[Fact]
public void ArrayToArrayOfMutableStructDeepCloningLoopNameTaken()
{
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"partial A[] Map(A[] i);",
TestSourceBuilderOptions.WithDeepCloning,
"struct A{ public string Value { get; set; } }"
);
TestHelper
.GenerateMapper(source)
.Should()
.HaveMapMethodBody(
"""
var target = new global::A[i.Length];
for (var i1 = 0; i1 < i.Length; i1++)
{
target[i1] = MapToA(i[i1]);
}

return target;
"""
);
}

[Fact]
public void ArrayToArrayOfMutableStructDeepCloningTargetNameTaken()
{
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"partial A[] Map(A[] target);",
TestSourceBuilderOptions.WithDeepCloning,
"struct A{ public string Value { get; set; } }"
);
TestHelper
.GenerateMapper(source)
.Should()
.HaveMapMethodBody(
"""
var target1 = new global::A[target.Length];
for (var i = 0; i < target.Length; i++)
{
target1[i] = MapToA(target[i]);
}

return target1;
"""
);
}
}
Loading

0 comments on commit 082d877

Please sign in to comment.