Skip to content

Commit b4548d2

Browse files
authored
fix: prefer coalesce operator over if-else to simplify generated source code (#1262)
1 parent 3d43a6f commit b4548d2

8 files changed

+69
-16
lines changed

src/Riok.Mapperly/Descriptors/Mappings/NullDelegateMapping.cs

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,29 +48,50 @@ public override ExpressionSyntax Build(TypeMappingBuildContext ctx)
4848
if (!SourceType.IsNullable())
4949
{
5050
// if the target type is a nullable value type, there needs to be an additional cast in some cases
51-
// (eg. in a linq expression, int => int?)
51+
// (e.g. in a linq expression, int => int?)
5252
return TargetType.IsNullableValueType()
5353
? CastExpression(FullyQualifiedIdentifier(TargetType), _delegateMapping.Build(ctx))
5454
: _delegateMapping.Build(ctx);
5555
}
5656

5757
// source is nullable and the mapping method cannot handle nulls,
5858
// call mapping only if source is not null.
59+
// with coalesce: Map(source ?? throw)
60+
// or with if-else:
5961
// source == null ? <null-substitute> : Map(source)
6062
// or for nullable value types:
6163
// source == null ? <null-substitute> : Map(source.Value)
62-
var sourceValue = SourceType.IsNullableValueType() ? MemberAccess(ctx.Source, NullableValueProperty) : ctx.Source;
64+
var nullSubstitute = NullSubstitute(TargetType, ctx.Source, _nullFallbackValue);
65+
66+
// if throw is used instead of a default value
67+
// and the delegate mapping is a synthetic or method mapping
68+
// the coalesce operator can be used
69+
// (the Map method isn't invoked in the null case since the exception is thrown,
70+
// and it is ensured no parentheses are needed since it is directly used or is passed as argument).
71+
// This simplifies the generated source code.
72+
if (
73+
_nullFallbackValue == NullFallbackValue.ThrowArgumentNullException
74+
&& (_delegateMapping.IsSynthetic || _delegateMapping is MethodMapping)
75+
)
76+
{
77+
ctx = ctx.WithSource(Coalesce(ctx.Source, nullSubstitute));
78+
return _delegateMapping.Build(ctx);
79+
}
80+
81+
var nonNullableSourceValue = ctx.Source;
6382

6483
// disable nullable waring if accessing array
65-
if (sourceValue is ElementAccessExpressionSyntax)
84+
if (nonNullableSourceValue is ElementAccessExpressionSyntax)
85+
{
86+
nonNullableSourceValue = PostfixUnaryExpression(SyntaxKind.SuppressNullableWarningExpression, nonNullableSourceValue);
87+
}
88+
89+
// if it is a value type, access the value property
90+
if (SourceType.IsNullableValueType())
6691
{
67-
sourceValue = PostfixUnaryExpression(SyntaxKind.SuppressNullableWarningExpression, sourceValue);
92+
nonNullableSourceValue = MemberAccess(nonNullableSourceValue, NullableValueProperty);
6893
}
6994

70-
return Conditional(
71-
IsNull(ctx.Source),
72-
NullSubstitute(TargetType, ctx.Source, _nullFallbackValue),
73-
_delegateMapping.Build(ctx.WithSource(sourceValue))
74-
);
95+
return Conditional(IsNull(ctx.Source), nullSubstitute, _delegateMapping.Build(ctx.WithSource(nonNullableSourceValue)));
7596
}
7697
}

test/Riok.Mapperly.Tests/Mapping/DictionaryTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public void DictionaryToDictionaryNullableToNonNullable()
5757
var target = new global::System.Collections.Generic.Dictionary<string, int>(source.Count);
5858
foreach (var item in source)
5959
{
60-
target[item.Key] = item.Value == null ? throw new System.ArgumentNullException(nameof(item.Value)) : item.Value.Value;
60+
target[item.Key] = item.Value ?? throw new System.ArgumentNullException(nameof(item.Value));
6161
}
6262
return target;
6363
"""

test/Riok.Mapperly.Tests/Mapping/EnumerableDeepCloningTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public void ArrayOfNullablePrimitiveTypesToNonNullableArrayDeepCloning()
4141
var target = new int[source.Length];
4242
for (var i = 0; i < source.Length; i++)
4343
{
44-
target[i] = source[i] == null ? throw new System.NullReferenceException($"Sequence {nameof(source)}, contained a null value at index {i}.") : source[i].Value;
44+
target[i] = source[i] ?? throw new System.NullReferenceException($"Sequence {nameof(source)}, contained a null value at index {i}.");
4545
}
4646
return target;
4747
"""

test/Riok.Mapperly.Tests/Mapping/EnumerableTest.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public void NullableArrayToNonNullableArray()
1919
TestHelper
2020
.GenerateMapper(source)
2121
.Should()
22-
.HaveSingleMethodBody("return source == null ? throw new System.ArgumentNullException(nameof(source)) : source;");
22+
.HaveSingleMethodBody("return source ?? throw new System.ArgumentNullException(nameof(source));");
2323
}
2424

2525
[Fact]
@@ -34,7 +34,7 @@ public void ArrayOfNullablePrimitiveTypesToNonNullableArray()
3434
var target = new int[source.Length];
3535
for (var i = 0; i < source.Length; i++)
3636
{
37-
target[i] = source[i] == null ? throw new System.NullReferenceException($"Sequence {nameof(source)}, contained a null value at index {i}.") : source[i].Value;
37+
target[i] = source[i] ?? throw new System.NullReferenceException($"Sequence {nameof(source)}, contained a null value at index {i}.");
3838
}
3939
return target;
4040
"""
@@ -79,7 +79,7 @@ public void ArrayCustomClassNullableToArrayCustomClassNonNullable()
7979
var target = new global::B[source.Length];
8080
for (var i = 0; i < source.Length; i++)
8181
{
82-
target[i] = source[i] == null ? throw new System.NullReferenceException($"Sequence {nameof(source)}, contained a null value at index {i}.") : source[i]!;
82+
target[i] = source[i] ?? throw new System.NullReferenceException($"Sequence {nameof(source)}, contained a null value at index {i}.");
8383
}
8484
return target;
8585
"""

test/Riok.Mapperly.Tests/Mapping/ObjectPropertyUseNamedMappingTest.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,25 @@ class B
363363
return TestHelper.VerifyGenerator(source);
364364
}
365365

366+
[Fact]
367+
public Task UserMethodReturnsNullableShouldThrow()
368+
{
369+
var source = TestSourceBuilder.MapperWithBodyAndTypes(
370+
"""
371+
[MapProperty("Name", "Value", Use = nameof(ToC))]
372+
public partial B Map(A source);
373+
374+
[UserMapping(Default = false)]
375+
public C? ToC(string name) => new C(name);
376+
""",
377+
"class A { public string? Name { get; set; } }",
378+
"class B { public C Value { get; set; } }",
379+
"record C(string Name);"
380+
);
381+
382+
return TestHelper.VerifyGenerator(source);
383+
}
384+
366385
[Fact]
367386
public Task ShouldPassNullValueToNullableUserMappingMethod()
368387
{

test/Riok.Mapperly.Tests/TestHelper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public static Task<VerifyResult> VerifyGenerator(string source, TestHelperOption
1616
var driver = Generate(source, options);
1717
var verify = Verify(driver);
1818

19-
if (args.Any())
19+
if (args.Length != 0)
2020
{
2121
verify.UseParameters(args);
2222
}

test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyNullableTest.NullableToNullablePropertyWithAnotherNullableToNonNullableMappingShouldDirectAssign#Mapper.g.verified.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public partial class Mapper
2525
var target = new global::System.Collections.Generic.List<int>(source.Count);
2626
foreach (var item in source)
2727
{
28-
target.Add(item == null ? throw new System.ArgumentNullException(nameof(item)) : item.Value);
28+
target.Add(item ?? throw new System.ArgumentNullException(nameof(item)));
2929
}
3030
return target;
3131
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//HintName: Mapper.g.cs
2+
// <auto-generated />
3+
#nullable enable
4+
public partial class Mapper
5+
{
6+
[global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
7+
public partial global::B Map(global::A source)
8+
{
9+
var target = new global::B();
10+
target.Value = ToC(source.Name ?? throw new System.ArgumentNullException(nameof(source.Name))) ?? throw new System.NullReferenceException("ToC returned null");
11+
return target;
12+
}
13+
}

0 commit comments

Comments
 (0)