diff --git a/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs b/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs index 678d39f16b..a03d42e112 100644 --- a/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs +++ b/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs @@ -136,7 +136,9 @@ private IEnumerable FindMemberPath(ITypeSymbol type, IEnumerabl { foreach (var name in path) { - if (GetMappableMembers(type, name, comparer).FirstOrDefault() is not { } member) + // get T if type is Nullable, prevents Value being treated as a member + var actualType = type.NonNullableValueType() ?? type; + if (GetMappableMembers(actualType, name, comparer).FirstOrDefault() is not { } member) break; type = member.Type; diff --git a/src/Riok.Mapperly/Helpers/NullableSymbolExtensions.cs b/src/Riok.Mapperly/Helpers/NullableSymbolExtensions.cs index ba051e46d3..c6fdd3b638 100644 --- a/src/Riok.Mapperly/Helpers/NullableSymbolExtensions.cs +++ b/src/Riok.Mapperly/Helpers/NullableSymbolExtensions.cs @@ -83,6 +83,13 @@ internal static bool IsNullable(this ITypeSymbol symbol) => internal static bool IsNullableValueType(this ITypeSymbol symbol) => symbol.NonNullableValueType() != null; + internal static ITypeSymbol? NonNullableValueType(this ITypeSymbol symbol) + { + if (symbol.IsValueType && symbol is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } namedType) + return namedType.TypeArguments[0]; + return null; + } + /// /// Whether or not the is nullable. /// @@ -104,11 +111,4 @@ internal static bool IsNullable(this ITypeParameterSymbol typeParameter, Nullabl } private static bool IsNullable(this NullableAnnotation nullable) => nullable is NullableAnnotation.Annotated or NullableAnnotation.None; - - private static ITypeSymbol? NonNullableValueType(this ITypeSymbol symbol) - { - if (symbol.IsValueType && symbol is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } namedType) - return namedType.TypeArguments[0]; - return null; - } } diff --git a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyFlatteningTest.cs b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyFlatteningTest.cs index 4422fed05c..20ef901a93 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyFlatteningTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyFlatteningTest.cs @@ -1,3 +1,5 @@ +using Riok.Mapperly.Diagnostics; + namespace Riok.Mapperly.Tests.Mapping; [UsesVerify] @@ -126,6 +128,79 @@ public void AutoFlattenedMultiplePropertiesNullablePath() ); } + [Fact] + public void AutoFlattenedPropertyNullableValueTypePath() + { + var source = TestSourceBuilder.Mapping( + "A", + "B", + "public class A { public C Id { get; set; } }", + "public class B { public int IdValue { get; set; } }", + "public class C { public int? Value { get; set; } }" + ); + TestHelper + .GenerateMapper(source) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::B(); + if (source.Id.Value != null) + { + target.IdValue = source.Id.Value.Value; + } + + return target; + """ + ); + } + + [Fact] + public void AutoFlattenedPropertyNullableValueTypePathShouldResolve() + { + var source = TestSourceBuilder.Mapping( + "A", + "B", + "public class A { public C? Prop { get; set; } }", + "public class B { public string PropInteger { get; set; } }", + "public struct C { public int Integer { get; set; } }" + ); + TestHelper + .GenerateMapper(source) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::B(); + if (source.Prop != null) + { + target.PropInteger = source.Prop.Value.Integer.ToString(); + } + + return target; + """ + ); + } + + [Fact] + public void AutoFlattenedPropertyNullableValueTypePathShouldDiagnostic() + { + var source = TestSourceBuilder.Mapping( + "A", + "B", + "public record A(int? Id);", + "public record B { public int IdValue { get; set; } }" + ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotFound) + .HaveSingleMethodBody( + """ + var target = new global::B(); + return target; + """ + ); + } + [Fact] public void AutoFlattenedMultiplePropertiesPathDisabledNullable() {