From e26778b9658cc5b43cc69da807ff94af231d93e8 Mon Sep 17 00:00:00 2001 From: Lars Date: Mon, 14 Oct 2024 19:44:53 +0200 Subject: [PATCH] allow assignments of MapValue constant value types to nullable value types (#1536) --- .../MappingBodyBuilders/SourceValueBuilder.cs | 10 ++-- .../Mapping/ObjectPropertyValueMethodTest.cs | 50 +++++++++++++++++++ .../Mapping/ObjectPropertyValueTest.cs | 45 +++++++++++++++++ 3 files changed, 102 insertions(+), 3 deletions(-) diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/SourceValueBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/SourceValueBuilder.cs index ddd4290b43..22b58996d9 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/SourceValueBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/SourceValueBuilder.cs @@ -114,7 +114,9 @@ private static bool TryBuildConstantSourceValue( return true; } - if (!SymbolEqualityComparer.Default.Equals(value.ConstantValue.Type, memberMappingInfo.TargetMember.MemberType)) + // use non-nullable target type to allow non-null value type assignments + // to nullable value types + if (!SymbolEqualityComparer.Default.Equals(value.ConstantValue.Type, memberMappingInfo.TargetMember.MemberType.NonNullable())) { ctx.BuilderContext.ReportDiagnostic( DiagnosticDescriptors.MapValueTypeMismatch, @@ -135,7 +137,7 @@ private static bool TryBuildConstantSourceValue( // expand enum member access to fully qualified identifier // use simple member name approach instead of slower visitor pattern on the expression var enumMemberName = ((MemberAccessExpressionSyntax)value.Expression).Name.Identifier.Text; - var enumTypeFullName = FullyQualifiedIdentifier(memberMappingInfo.TargetMember.MemberType); + var enumTypeFullName = FullyQualifiedIdentifier(memberMappingInfo.TargetMember.MemberType.NonNullable()); sourceValue = new ConstantSourceValue(MemberAccess(enumTypeFullName, enumMemberName)); return true; case TypedConstantKind.Type: @@ -179,8 +181,10 @@ private static bool ValidateValueProviderMethod(IMembersBuilderContext return false; } + // use non-nullable target type to allow non-null value type assignments + // to nullable value types var methodCandidates = namedMethodCandidates.Where(x => - SymbolEqualityComparer.Default.Equals(x.ReturnType, memberMappingInfo.TargetMember.MemberType) + SymbolEqualityComparer.Default.Equals(x.ReturnType, memberMappingInfo.TargetMember.MemberType.NonNullable()) ); if (!memberMappingInfo.TargetMember.Member.IsNullable) diff --git a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyValueMethodTest.cs b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyValueMethodTest.cs index 76969470f4..fe373091c1 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyValueMethodTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyValueMethodTest.cs @@ -162,6 +162,56 @@ public void MethodReturnTypeNullMismatchShouldDiagnostic() ); } + [Fact] + public void MethodReturnTypeNonNullableToNullable() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapValue("Value", Use = nameof(BuildC))] partial B Map(A source); + C BuildC() => new C(); + """, + "class A;", + "class B { public C? Value { get; set; } }", + "class C;" + ); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.Value = BuildC(); + return target; + """ + ); + } + + [Fact] + public void MethodReturnValueTypeNonNullableToNullable() + { + 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 MethodReturnTypeInDisabledNullableContext() { diff --git a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyValueTest.cs b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyValueTest.cs index f8d94cc84f..9cb7da5409 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyValueTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyValueTest.cs @@ -530,6 +530,27 @@ public void ExplicitNullToValuePropertyShouldDiagnostic() ); } + [Fact] + public void IntToNullableValueProperty() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """[MapValue("IntValue", 1)] partial B Map(A source);""", + "class A;", + "class B { public int? IntValue { get; set; } }" + ); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.IntValue = 1; + return target; + """ + ); + } + [Fact] public void ExplicitNullToNullableValueProperty() { @@ -651,6 +672,30 @@ public void MapValueDuplicateForSameTargetMemberShouldDiagnostic() ); } + [Fact] + public void EnumToNestedNullableProperty() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """[MapValue("Nested.Value", E.E1)] partial B Map(A source);""", + "class A;", + "class B { public C? Nested { get; set; } }", + "class C { public E? Value { get; set; } }", + "enum E { E1 }" + ); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.Nested ??= new global::C(); + target.Nested.Value = global::E.E1; + return target; + """ + ); + } + [Fact] public void MapValueAndPropertyAttributeForSameTargetShouldDiagnostic() {