Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: inline methods consisting of a single return statement or local variable declaration expression #1422

Merged
merged 2 commits into from
Aug 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/docs/configuration/queryable-projections.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,15 @@ public static class InlineExpressionMappingBuilder
}

var methodSyntax = methodSyntaxRef.GetSyntax();
if (methodSyntax is not MethodDeclarationSyntax { ExpressionBody: { } body, ParameterList.Parameters: [var sourceParameter] })

if (methodSyntax is not MethodDeclarationSyntax { ParameterList.Parameters: [var sourceParameter] } methodDeclaration)
{
ctx.ReportDiagnostic(DiagnosticDescriptors.QueryableProjectionMappingCannotInline, mapping.Method);
return null;
}

var bodyExpression = TryGetBodyExpression(methodDeclaration);
if (bodyExpression == null)
{
ctx.ReportDiagnostic(DiagnosticDescriptors.QueryableProjectionMappingCannotInline, mapping.Method);
return null;
Expand All @@ -38,7 +46,7 @@ public static class InlineExpressionMappingBuilder
}

var inlineRewriter = new InlineExpressionRewriter(semanticModel, ctx.FindNewInstanceMapping);
var bodyExpression = (ExpressionSyntax?)body.Expression.Accept(inlineRewriter);
bodyExpression = (ExpressionSyntax?)bodyExpression.Accept(inlineRewriter);
if (bodyExpression == null || !inlineRewriter.CanBeInlined)
{
ctx.ReportDiagnostic(DiagnosticDescriptors.QueryableProjectionMappingCannotInline, mapping.Method);
Expand All @@ -47,4 +55,30 @@ public static class InlineExpressionMappingBuilder

return new UserImplementedInlinedExpressionMapping(mapping, sourceParameter, inlineRewriter.MappingInvocations, bodyExpression);
}

private static ExpressionSyntax? TryGetBodyExpression(MethodDeclarationSyntax methodDeclaration)
{
return methodDeclaration switch
{
// => expression
{ ExpressionBody: { } body } => body.Expression,

// { 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
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public Task ClassToClassInlinedExpression()
}

[Fact]
public Task ClassToClassNonInlinedMethod()
public Task ClassToClassInlinedSingleStatement()
{
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"""
Expand All @@ -41,6 +41,51 @@ private D MapToD(C v)
return TestHelper.VerifyGenerator(source);
}

[Fact]
public Task ClassToClassInlinedSingleDeclaration()
{
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"""
private partial System.Linq.IQueryable<B> Map(System.Linq.IQueryable<A> source);

private D MapToD(C v)
{
var dest = new D { 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 ClassToClassNonInlinedMethod()
{
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"""
private partial System.Linq.IQueryable<B> Map(System.Linq.IQueryable<A> 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()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//HintName: Mapper.g.cs
// <auto-generated />
#nullable enable
public partial class Mapper
{
[global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
private partial global::System.Linq.IQueryable<global::B> Map(global::System.Linq.IQueryable<global::A> 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
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//HintName: Mapper.g.cs
// <auto-generated />
#nullable enable
public partial class Mapper
{
[global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
private partial global::System.Linq.IQueryable<global::B> Map(global::System.Linq.IQueryable<global::A> 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
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//HintName: Mapper.g.cs
//HintName: Mapper.g.cs
// <auto-generated />
#nullable enable
public partial class Mapper
Expand Down