Skip to content

Commit

Permalink
feat(enumerables): optimize array mapping by using Array.Clone() in c…
Browse files Browse the repository at this point in the history
…ertain cases (#7)

Array.Clone is used if the element mapping is a direct assignment and source type is an array and target an array or an IReadOnlyCollection
  • Loading branch information
latonz committed Feb 15, 2022
1 parent 93d8eec commit 89f96c4
Show file tree
Hide file tree
Showing 9 changed files with 124 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,32 @@ public static class EnumerableMappingBuilder

// true if there is no need to convert elements (eg. the source instances can directly be reused)
// this is the case if the types are equal and the mapping is a direct assignment.
var isDirectMapping = elementMapping is DirectAssignmentMapping;
var isDirectElementMapping = elementMapping is DirectAssignmentMapping;

// if element mapping is a direct assignment
// and target is an IEnumerable, there is no mapping needed at all.
if (isDirectMapping && IsType(ctx, _enumerableIntfName, ctx.Target.OriginalDefinition))
if (isDirectElementMapping && IsType(ctx, _enumerableIntfName, ctx.Target.OriginalDefinition))
return new CastMapping(ctx.Source, ctx.Target);

// if element mapping is a direct assignment
// and source type is an array
// and the target type is an array or an IReadOnlyCollection,
// a single Array.Clone call should be sufficient and fast.
if (isDirectElementMapping
&& ctx.Source.IsArrayType()
&& (ctx.Target.IsArrayType() || IsType(ctx, _readOnlyCollectionIntfName, ctx.Target.OriginalDefinition))
&& SymbolEqualityComparer.IncludeNullability.Equals(ctx.Source, ctx.Target))
{
return new ArrayCloneMapping(ctx.Source, ctx.Target);
}

// 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 BuildCustomTypeMapping(ctx, elementMapping);

return BuildLinqMapping(ctx, isDirectMapping, elementMapping, collectMethodName);
return BuildLinqMapping(ctx, isDirectElementMapping, elementMapping, collectMethodName);
}

private static LinqEnumerableMapping BuildLinqMapping(
Expand Down
28 changes: 28 additions & 0 deletions src/Riok.Mapperly/Descriptors/TypeMappings/ArrayCloneMapping.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Riok.Mapperly.Emit.SyntaxFactoryHelper;

namespace Riok.Mapperly.Descriptors.TypeMappings;

/// <summary>
/// Represents a mapping from an array to an array of the same type by using Array.Clone.
/// </summary>
public class ArrayCloneMapping : TypeMapping
{
private const string CloneMethodName = nameof(Array.Clone);

public ArrayCloneMapping(
ITypeSymbol sourceType,
ITypeSymbol targetType)
: base(sourceType, targetType)
{
}

public override ExpressionSyntax Build(ExpressionSyntax source)
{
return CastExpression(
IdentifierName(TargetType.ToDisplayString()),
InvocationExpression(MemberAccess(source, CloneMethodName)));
}
}
20 changes: 17 additions & 3 deletions src/Riok.Mapperly/Emit/SyntaxFactoryHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ public static LiteralExpressionSyntax NullLiteral()
public static LiteralExpressionSyntax StringLiteral(string content) =>
LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(content));

public static LiteralExpressionSyntax NumericLiteral(int v)
=> LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(0));

public static AttributeListSyntax ReturnNotNullIfNotNullAttribute(string paramName)
{
var attribute = Attribute(IdentifierName(NotNullIfNotNullAttributeName))
Expand All @@ -92,6 +95,12 @@ public static MemberAccessExpressionSyntax MemberAccess(ExpressionSyntax idExpre
public static InvocationExpressionSyntax NameOf(ExpressionSyntax expression)
=> Invocation(IdentifierName("nameof"), expression);

public static ElementAccessExpressionSyntax ArrayElementAccess(ExpressionSyntax array, ExpressionSyntax index)
{
return ElementAccessExpression(array)
.WithArgumentList(BracketedArgumentList(SingletonSeparatedList(Argument(index))));
}

public static ThrowExpressionSyntax ThrowArgumentOutOfRangeException(ExpressionSyntax arg)
{
return ThrowExpression(ObjectCreationExpression(IdentifierName(ArgumentOutOfRangeExceptionClassName))
Expand Down Expand Up @@ -141,17 +150,22 @@ public static InvocationExpressionSyntax StaticInvocation(IMethodSymbol method,
return FieldDeclaration(variableDeclaration).WithModifiers(modifierTokenList);
}

public static LocalDeclarationStatementSyntax DeclareVariable(string variableName, ExpressionSyntax initializationValue)
public static VariableDeclarationSyntax DeclareVariable(string variableName, ExpressionSyntax initializationValue)
{
var initializer = EqualsValueClause(initializationValue);
var declarator = VariableDeclarator(Identifier(variableName)).WithInitializer(initializer);
var variableDeclaration = VariableDeclaration(VarIdentifier).WithVariables(SingletonSeparatedList(declarator));
return VariableDeclaration(VarIdentifier).WithVariables(SingletonSeparatedList(declarator));
}

public static LocalDeclarationStatementSyntax DeclareLocalVariable(string variableName, ExpressionSyntax initializationValue)
{
var variableDeclaration = DeclareVariable(variableName, initializationValue);
return LocalDeclarationStatement(variableDeclaration);
}

public static StatementSyntax CreateInstance(string variableName, ITypeSymbol typeSymbol, params ExpressionSyntax[] args)
{
return DeclareVariable(variableName, CreateInstance(typeSymbol, args));
return DeclareLocalVariable(variableName, CreateInstance(typeSymbol, args));
}

public static ObjectCreationExpressionSyntax CreateInstance(ITypeSymbol typeSymbol, params ExpressionSyntax[] args)
Expand Down
25 changes: 24 additions & 1 deletion test/Riok.Mapperly.Tests/Mapping/EnumerableTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,30 @@ public void ArrayToArrayOfPrimitiveTypes()
"int[]");
TestHelper.GenerateSingleMapperMethodBody(source)
.Should()
.Be("return System.Linq.Enumerable.ToArray(source);");
.Be("return (int[])source.Clone();");
}

[Fact]
public void ArrayCustomClassToArrayCustomClass()
{
var source = TestSourceBuilder.Mapping(
"B[]",
"B[]",
"class B { public int Value {get; set; }}");
TestHelper.GenerateMapperMethodBody(source)
.Should()
.Be("return System.Linq.Enumerable.ToArray(System.Linq.Enumerable.Select(source, x => MapToB(x)));");
}

[Fact]
public void ArrayToArrayOfString()
{
var source = TestSourceBuilder.Mapping(
"string[]",
"string[]");
TestHelper.GenerateSingleMapperMethodBody(source)
.Should()
.Be("return (string[])source.Clone();");
}

[Fact]
Expand Down
9 changes: 9 additions & 0 deletions test/Riok.Mapperly.Tests/Mapping/UserMethodTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,15 @@ public void WithMultipleUserDefinedMethodDifferentConfigShouldWork()

[Fact]
public Task WithInvalidSignatureShouldDiagnostic()
{
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"string ToString(T source, string format);");

return TestHelper.VerifyGenerator(source);
}

[Fact]
public Task WithInvalidGenericSignatureShouldDiagnostic()
{
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"string ToString<T>(T source);");
Expand Down
7 changes: 7 additions & 0 deletions test/Riok.Mapperly.Tests/TestHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ public static string GenerateSingleMapperMethodBody(string source, bool allowDia
.Body;
}

public static string GenerateMapperMethodBody(string source, string methodName = TestSourceBuilder.DefaultMapMethodName, bool allowDiagnostics = false)
{
return GenerateMapperMethodBodies(source, allowDiagnostics)
.Single(x => x.Name == methodName)
.Body;
}

public static IEnumerable<(string Name, string Body)> GenerateMapperMethodBodies(string source, bool allowDiagnostics = false)
{
var result = Generate(source).GetRunResult();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
Diagnostics: [
{
Id: RMG001,
Title: Has an unsupported mapping method signature,
Severity: Error,
WarningLevel: 0,
Location: : (10,4)-(10,33),
Description: ,
HelpLink: ,
MessageFormat: {0} has an unsupported mapping method signature,
Message: ToString has an unsupported mapping method signature,
Category: Mapper,
CustomTags: []
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
//HintName: Mapper.g.cs
#nullable enable
public sealed class Mapper : IMapper
{
public static readonly IMapper Instance = new Mapper();
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
Title: Has an unsupported mapping method signature,
Severity: Error,
WarningLevel: 0,
Location: : (10,4)-(10,33),
Location: : (10,4)-(10,45),
Description: ,
HelpLink: ,
MessageFormat: {0} has an unsupported mapping method signature,
Expand Down

0 comments on commit 89f96c4

Please sign in to comment.