diff --git a/docs/docs/configuration/queryable-projections.mdx b/docs/docs/configuration/queryable-projections.mdx index b24eef90e2..d433684777 100644 --- a/docs/docs/configuration/queryable-projections.mdx +++ b/docs/docs/configuration/queryable-projections.mdx @@ -80,7 +80,7 @@ public static partial class CarMapper Mapperly tries to inline user-implemented mapping methods. For this to work, user-implemented mapping methods need to satisfy certain limitations: -- Only expression-bodied methods can be inlined. +- Only expression-bodied methods or methods that consist of a single local variable declaration expression can be inlined. - The body needs to follow the [expression tree limitations](https://learn.microsoft.com/en-us/dotnet/csharp/advanced-topics/expression-trees/#limitations). - Nested MethodGroups cannot be inlined. diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/InlineExpressionMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/InlineExpressionMappingBuilder.cs index 3b40a06df0..51c0b5c84e 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/InlineExpressionMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/InlineExpressionMappingBuilder.cs @@ -26,12 +26,13 @@ public static class InlineExpressionMappingBuilder var methodSyntax = methodSyntaxRef.GetSyntax(); - var bodyExpression = methodSyntax switch + if (methodSyntax is not MethodDeclarationSyntax { ParameterList.Parameters: [var sourceParameter] } methodDeclaration) { - MethodDeclarationSyntax { ExpressionBody: { } body, ParameterList.Parameters: [var sourceParameter1] } => body.Expression, - MethodDeclarationSyntax { Body.Statements: [ReturnStatementSyntax singleStatement] } => singleStatement.Expression, - _ => null - }; + ctx.ReportDiagnostic(DiagnosticDescriptors.QueryableProjectionMappingCannotInline, mapping.Method); + return null; + } + + var bodyExpression = TryGetBodyExpression(methodDeclaration); if (bodyExpression == null) { ctx.ReportDiagnostic(DiagnosticDescriptors.QueryableProjectionMappingCannotInline, mapping.Method); @@ -52,12 +53,32 @@ public static class InlineExpressionMappingBuilder return null; } - if (methodSyntax is not MethodDeclarationSyntax { ParameterList.Parameters: [var sourceParameter] }) + return new UserImplementedInlinedExpressionMapping(mapping, sourceParameter, inlineRewriter.MappingInvocations, bodyExpression); + } + + private static ExpressionSyntax? TryGetBodyExpression(MethodDeclarationSyntax methodDeclaration) + { + return methodDeclaration switch { - ctx.ReportDiagnostic(DiagnosticDescriptors.QueryableProjectionMappingCannotInline, mapping.Method); - return null; - } + // => expression + { ExpressionBody: { } body } => body.Expression, - return new UserImplementedInlinedExpressionMapping(mapping, sourceParameter, inlineRewriter.MappingInvocations, bodyExpression); + // { return expression; } + { Body.Statements: [ReturnStatementSyntax singleStatement] } => singleStatement.Expression, + + // { var dest = expression; return dest; } + { + Body.Statements: [ + LocalDeclarationStatementSyntax + { + Declaration.Variables: [{ Initializer: { } variableInitializer } variableDeclarator] + }, + ReturnStatementSyntax { Expression: IdentifierNameSyntax identifierName } + ] + } when identifierName.Identifier.Value == variableDeclarator.Identifier.Value + => variableInitializer.Value, + + _ => null + }; } } diff --git a/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionUserImplementedTest.cs b/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionUserImplementedTest.cs index 57126800af..e8eb25a086 100644 --- a/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionUserImplementedTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionUserImplementedTest.cs @@ -42,7 +42,7 @@ private D MapToD(C v) } [Fact] - public Task ClassToClassNonInlinedMethod() + public Task ClassToClassInlinedSingleDeclaration() { var source = TestSourceBuilder.MapperWithBodyAndTypes( """ @@ -63,6 +63,29 @@ private D MapToD(C v) return TestHelper.VerifyGenerator(source); } + [Fact] + public Task ClassToClassNonInlinedMethod() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + private partial System.Linq.IQueryable Map(System.Linq.IQueryable source); + + private D MapToD(C v) + { + var dest = new D(); + dest.Value = v.Value + "-mapped"; + return dest; + } + """, + "class A { public string StringValue { get; set; } public C NestedValue { get; set; } }", + "class B { public string StringValue { get; set; } public D NestedValue { get; set; } }", + "class C { public string Value { get; set; } }", + "class D { public string Value { get; set; } }" + ); + + return TestHelper.VerifyGenerator(source); + } + [Fact] public Task ClassToClassUserImplementedOrdering() { diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUserImplementedTest.ClassToClassInlinedSingleDeclaration#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUserImplementedTest.ClassToClassInlinedSingleDeclaration#Mapper.g.verified.cs new file mode 100644 index 0000000000..43067ed7ee --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUserImplementedTest.ClassToClassInlinedSingleDeclaration#Mapper.g.verified.cs @@ -0,0 +1,17 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable source) + { +#nullable disable + return System.Linq.Queryable.Select(source, x => new global::B() + { + StringValue = x.StringValue, + NestedValue = new global::D { Value = x.NestedValue.Value + "-mapped" }, + }); +#nullable enable + } +} \ No newline at end of file