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

handle nullable value types with MapValueAttribute and methods correct #1538

Merged
merged 1 commit into from
Oct 15, 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
Original file line number Diff line number Diff line change
Expand Up @@ -181,10 +181,11 @@ private static bool ValidateValueProviderMethod(IMembersBuilderContext<IMapping>
return false;
}

// use non-nullable target type to allow non-null value type assignments
// use non-nullable to allow non-null value type assignments
// to nullable value types
// nullable is checked with nullable annotation
var methodCandidates = namedMethodCandidates.Where(x =>
SymbolEqualityComparer.Default.Equals(x.ReturnType, memberMappingInfo.TargetMember.MemberType.NonNullable())
SymbolEqualityComparer.Default.Equals(x.ReturnType.NonNullable(), memberMappingInfo.TargetMember.MemberType.NonNullable())
);

if (!memberMappingInfo.TargetMember.Member.IsNullable)
Expand Down
59 changes: 57 additions & 2 deletions test/Riok.Mapperly.Tests/Mapping/ObjectPropertyValueMethodTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ public void MethodReturnTypeMismatchShouldDiagnostic()
}

[Fact]
public void MethodReturnTypeNullMismatchShouldDiagnostic()
public void MethodReturnTypeNullableToNonNullableShouldDiagnostic()
{
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"""
Expand Down Expand Up @@ -187,6 +187,31 @@ public void MethodReturnTypeNonNullableToNullable()
);
}

[Fact]
public void MethodReturnValueTypeNullableToNullable()
{
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"""
[MapValue("Value", Use = nameof(BuildC))] partial B Map(A source);
C? BuildC() => C.C1;
""",
"class A;",
"class B { public C? Value { get; set; } }",
"enum C { C1 }"
);

TestHelper
.GenerateMapper(source)
.Should()
.HaveSingleMethodBody(
"""
var target = new global::B();
target.Value = BuildC();
return target;
"""
);
}

[Fact]
public void MethodReturnValueTypeNonNullableToNullable()
{
Expand All @@ -197,7 +222,7 @@ public void MethodReturnValueTypeNonNullableToNullable()
""",
"class A;",
"class B { public C? Value { get; set; } }",
"enum C { C1 };"
"enum C { C1 }"
);

TestHelper
Expand All @@ -212,6 +237,36 @@ public void MethodReturnValueTypeNonNullableToNullable()
);
}

[Fact]
public void MethodReturnValueTypeNullableToNonNullableShouldDiagnostic()
{
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"""
[MapValue("Value", Use = nameof(BuildC))] partial B Map(A source);
System.Nullable<C> BuildC() => C.C1;
""",
"class A;",
"class B { public C Value { get; set; } }",
"enum C { C1 }"
);

TestHelper
.GenerateMapper(source, TestHelperOptions.AllowAndIncludeAllDiagnostics)
.Should()
.HaveDiagnostic(
DiagnosticDescriptors.MapValueMethodTypeMismatch,
"Cannot assign method return type C? of BuildC() to B.Value of type C"
)
.HaveDiagnostic(DiagnosticDescriptors.NoMemberMappings, "No members are mapped in the object mapping from A to B")
.HaveAssertedAllDiagnostics()
.HaveSingleMethodBody(
"""
var target = new global::B();
return target;
"""
);
}

[Fact]
public void MethodReturnTypeInDisabledNullableContext()
{
Expand Down
157 changes: 136 additions & 21 deletions test/Riok.Mapperly.Tests/Mapping/ObjectPropertyValueTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -595,13 +595,13 @@ public void EnumToProperty()
}

[Fact]
public void NamespacedEnumToProperty()
public void EnumToNullableProperty()
{
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"""[MapValue("EnumValue", MyNamespace.E1.Value2)] partial B Map(A source);""",
"""[MapValue("EnumValue", E1.Value2)] partial B Map(A source);""",
"class A;",
"namespace MyNamespace { enum E1 { Value1, Value2 } }",
"class B { public MyNamespace.E1 EnumValue { get; set; } }"
"enum E1 { Value1, Value2 }",
"class B { public E1? EnumValue { get; set; } }"
);

TestHelper
Expand All @@ -610,63 +610,148 @@ public void NamespacedEnumToProperty()
.HaveSingleMethodBody(
"""
var target = new global::B();
target.EnumValue = global::MyNamespace.E1.Value2;
target.EnumValue = global::E1.Value2;
return target;
"""
);
}

[Fact]
public void EnumTypeMismatchShouldDiagnostic()
public void DefaultToEnumProperty()
{
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"""[MapValue("EnumValue", 1)] partial B Map(A source);""",
"""[MapValue("EnumValue", default)] partial B Map(A source);""",
"class A;",
"enum E1 { Value1, Value2 }",
"class B { public E1 EnumValue { get; set; } }"
);

TestHelper
.GenerateMapper(source, TestHelperOptions.AllowDiagnostics)
.GenerateMapper(source)
.Should()
.HaveSingleMethodBody(
"""
var target = new global::B();
target.EnumValue = default;
return target;
"""
);
}

[Fact]
public void NullToEnumPropertyShouldDiagnostic()
{
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"""[MapValue("EnumValue", null)] partial B Map(A source);""",
"class A;",
"enum E1 { Value1, Value2 }",
"class B { public E1 EnumValue { get; set; } }"
);

TestHelper
.GenerateMapper(source, TestHelperOptions.AllowAndIncludeAllDiagnostics)
.Should()
.HaveDiagnostic(
DiagnosticDescriptors.MapValueTypeMismatch,
"Cannot assign constant value 1 of type int to B.EnumValue of type E1"
DiagnosticDescriptors.CannotMapValueNullToNonNullable,
"Cannot assign null to non-nullable member B.EnumValue of type E1"
)
.HaveAssertedAllDiagnostics()
.HaveSingleMethodBody(
"""
var target = new global::B();
target.EnumValue = default;
return target;
"""
);
}

[Fact]
public void MapValueDuplicateForSameTargetMemberShouldDiagnostic()
public void NullToEnumNullableProperty()
{
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"""
[MapValue("Value", 10)]
[MapValue("Value", 20)]
partial B Map(A source);
""",
"""[MapValue("EnumValue", null)] partial B Map(A source);""",
"class A;",
"class B { public int Value { get; set; } }"
"enum E1 { Value1, Value2 }",
"class B { public E1? EnumValue { get; set; } }"
);

TestHelper
.GenerateMapper(source)
.Should()
.HaveSingleMethodBody(
"""
var target = new global::B();
target.EnumValue = null;
return target;
"""
);
}

[Fact]
public void DefaultToEnumNullableProperty()
{
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"""[MapValue("EnumValue", default)] partial B Map(A source);""",
"class A;",
"enum E1 { Value1, Value2 }",
"class B { public E1? EnumValue { get; set; } }"
);

TestHelper
.GenerateMapper(source)
.Should()
.HaveSingleMethodBody(
"""
var target = new global::B();
target.EnumValue = default;
return target;
"""
);
}

[Fact]
public void NamespacedEnumToProperty()
{
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"""[MapValue("EnumValue", MyNamespace.E1.Value2)] partial B Map(A source);""",
"class A;",
"namespace MyNamespace { enum E1 { Value1, Value2 } }",
"class B { public MyNamespace.E1 EnumValue { get; set; } }"
);

TestHelper
.GenerateMapper(source)
.Should()
.HaveSingleMethodBody(
"""
var target = new global::B();
target.EnumValue = global::MyNamespace.E1.Value2;
return target;
"""
);
}

[Fact]
public void EnumTypeMismatchShouldDiagnostic()
{
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"""[MapValue("EnumValue", 1)] partial B Map(A source);""",
"class A;",
"enum E1 { Value1, Value2 }",
"class B { public E1 EnumValue { get; set; } }"
);

TestHelper
.GenerateMapper(source, TestHelperOptions.AllowDiagnostics)
.Should()
.HaveDiagnostic(
DiagnosticDescriptors.MultipleConfigurationsForTargetMember,
"Multiple mappings are configured for the same target member B.Value"
DiagnosticDescriptors.MapValueTypeMismatch,
"Cannot assign constant value 1 of type int to B.EnumValue of type E1"
)
.HaveAssertedAllDiagnostics()
.HaveMapMethodBody(
.HaveSingleMethodBody(
"""
var target = new global::B();
target.Value = 10;
return target;
"""
);
Expand Down Expand Up @@ -696,6 +781,36 @@ public void EnumToNestedNullableProperty()
);
}

[Fact]
public void MapValueDuplicateForSameTargetMemberShouldDiagnostic()
{
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"""
[MapValue("Value", 10)]
[MapValue("Value", 20)]
partial B Map(A source);
""",
"class A;",
"class B { public int Value { get; set; } }"
);

TestHelper
.GenerateMapper(source, TestHelperOptions.AllowDiagnostics)
.Should()
.HaveDiagnostic(
DiagnosticDescriptors.MultipleConfigurationsForTargetMember,
"Multiple mappings are configured for the same target member B.Value"
)
.HaveAssertedAllDiagnostics()
.HaveMapMethodBody(
"""
var target = new global::B();
target.Value = 10;
return target;
"""
);
}

[Fact]
public void MapValueAndPropertyAttributeForSameTargetShouldDiagnostic()
{
Expand Down