From ed87c503c16bb4843e79c3dced31d1752c369633 Mon Sep 17 00:00:00 2001 From: Abhijith T V Date: Fri, 1 May 2026 12:51:28 +0530 Subject: [PATCH 01/13] fix: Ignore MaybeNull property of target member --- .../BuilderContext/MembersContainerBuilderContext.cs | 5 +++-- .../Descriptors/MappingBodyBuilders/MemberMappingBuilder.cs | 4 ++-- .../Descriptors/MappingBodyBuilders/SourceValueBuilder.cs | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersContainerBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersContainerBuilderContext.cs index 04a5b87736..c9a7ce1f6d 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersContainerBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersContainerBuilderContext.cs @@ -35,7 +35,8 @@ public void AddNullDelegateMemberAssignmentMapping(IMemberAssignmentMapping memb // set target member to null if null assignments are allowed // and the source is null var setMemberToNull = - BuilderContext.Configuration.Mapper.AllowNullPropertyAssignment && memberMapping.MemberInfo.TargetMember.Member.IsNullable; + BuilderContext.Configuration.Mapper.AllowNullPropertyAssignment + && memberMapping.MemberInfo.TargetMember.Member.Type.IsNullable(); // if the member is explicitly set to null, // make sure the parent members are initialized/non-null, @@ -76,7 +77,7 @@ private void AddMemberAssignmentMapping(IMemberAssignmentMappingContainer contai // if the source value is a non-nullable value, // the target should be non-null after this assignment and can be set as initialized. - if (!mapping.MemberInfo.IsSourceNullable && mapping.MemberInfo.TargetMember.MemberType.IsNullable()) + if (!mapping.MemberInfo.IsSourceNullable && mapping.MemberInfo.TargetMember.Member.Type.IsNullable()) { _initializedNullableTargetPaths.Add((container, mapping.MemberInfo.TargetMember)); } diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MemberMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MemberMappingBuilder.cs index bb7ab04242..c16c5c6df7 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MemberMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MemberMappingBuilder.cs @@ -85,7 +85,7 @@ public static bool TryBuild( return false; } - var memberTargetNullable = memberMappingInfo.TargetMember.MemberType.IsNullable(); + var memberTargetNullable = memberMappingInfo.TargetMember.Member.Type.IsNullable(); var delegateTargetNullable = delegateMapping.TargetType.IsNullable(); var memberSourceNullable = memberMappingInfo.IsSourceNullable; var delegateSourceNullable = delegateMapping.SourceType.IsNullable(); @@ -209,7 +209,7 @@ NonEmptyMemberPath targetMember // access the source in a null save matter (via ?.) but no other special handling required. if ( ctx.BuilderContext.Configuration.Mapper.AllowNullPropertyAssignment - && (delegateMapping.SourceType.IsNullable() || delegateMapping.IsSynthetic && targetMember.Member.IsNullable) + && (delegateMapping.SourceType.IsNullable() || delegateMapping.IsSynthetic && targetMember.Member.Type.IsNullable()) ) { return new MappedMemberSourceValue(delegateMapping, sourceGetter, true, false); diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/SourceValueBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/SourceValueBuilder.cs index a370e16e35..95a49c164e 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/SourceValueBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/SourceValueBuilder.cs @@ -78,7 +78,7 @@ private static bool TryBuildConstantSourceValue( if ( value.ConstantValue.IsNull && memberMappingInfo.TargetMember.MemberType.IsReferenceType - && !memberMappingInfo.TargetMember.Member.IsNullable + && !memberMappingInfo.TargetMember.Member.Type.IsNullable() ) { ctx.BuilderContext.ReportDiagnostic( @@ -189,7 +189,7 @@ private static bool TryBuildMethodProvidedSourceValue( SymbolEqualityComparer.Default.Equals(x.ReturnType.NonNullable(), memberMappingInfo.TargetMember.MemberType.NonNullable()) ); - if (!memberMappingInfo.TargetMember.Member.IsNullable) + if (!memberMappingInfo.TargetMember.Member.Type.IsNullable()) { // Filter out methods that may return null when the target is non-nullable. methodCandidates = methodCandidates.Where(m => !ctx.BuilderContext.SymbolAccessor.MayReturnNull(m, false)); From f09ac6f931f4b97762a574610df64e833fd96ce3 Mon Sep 17 00:00:00 2001 From: Abhijith T V Date: Fri, 1 May 2026 14:04:04 +0530 Subject: [PATCH 02/13] refactor: Refactor the code --- .../MappingBodyBuilders/MemberMappingBuilder.cs | 3 ++- .../MappingBodyBuilders/SourceValueBuilder.cs | 2 +- .../Mapping/ObjectPropertyNullableTest.cs | 9 ++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MemberMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MemberMappingBuilder.cs index c16c5c6df7..85343bc9dd 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MemberMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MemberMappingBuilder.cs @@ -89,11 +89,12 @@ public static bool TryBuild( var delegateTargetNullable = delegateMapping.TargetType.IsNullable(); var memberSourceNullable = memberMappingInfo.IsSourceNullable; var delegateSourceNullable = delegateMapping.SourceType.IsNullable(); + var memberTargetAcceptsNull = memberMappingInfo.TargetMember.MemberType.IsNullable(); if ( memberMappingInfo.Configuration?.SuppressNullMismatchDiagnostic != true && memberSourceNullable - && !memberTargetNullable + && !memberTargetAcceptsNull && !(delegateSourceNullable && !delegateTargetNullable) ) { diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/SourceValueBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/SourceValueBuilder.cs index 95a49c164e..9d38c0e333 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/SourceValueBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/SourceValueBuilder.cs @@ -189,7 +189,7 @@ private static bool TryBuildMethodProvidedSourceValue( SymbolEqualityComparer.Default.Equals(x.ReturnType.NonNullable(), memberMappingInfo.TargetMember.MemberType.NonNullable()) ); - if (!memberMappingInfo.TargetMember.Member.Type.IsNullable()) + if (!memberMappingInfo.TargetMember.Member.IsNullable) { // Filter out methods that may return null when the target is non-nullable. methodCandidates = methodCandidates.Where(m => !ctx.BuilderContext.SymbolAccessor.MayReturnNull(m, false)); diff --git a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyNullableTest.cs b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyNullableTest.cs index ce8b5a99d3..6efb3a87d1 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyNullableTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyNullableTest.cs @@ -1076,7 +1076,10 @@ class B .HaveSingleMethodBody( """ var target = new global::B(); - target.Name = source.Name; + if (source.Name != null) + { + target.Name = source.Name; + } return target; """ ); @@ -1180,10 +1183,6 @@ class B { target.Value = MapToD(source.Value); } - else - { - target.Value = null; - } return target; """ ); From a8265f53cd2871f40fa1c262d20c8af5a67af793 Mon Sep 17 00:00:00 2001 From: Abhijith T V Date: Sat, 2 May 2026 01:17:33 +0530 Subject: [PATCH 03/13] refactor: Add write and read null check todo - add test for allow Null --- .../MembersContainerBuilderContext.cs | 11 ++++---- .../MemberMappingBuilder.cs | 8 +++--- .../MappingBodyBuilders/SourceValueBuilder.cs | 2 +- .../MemberMappings/MemberMappingInfo.cs | 2 +- .../SourceValue/MappedMemberSourceValue.cs | 2 +- .../NullMappedMemberSourceValue.cs | 4 +-- .../Descriptors/SymbolAccessor.cs | 24 ++++++++++++++++ .../Members/ConstructorParameterMember.cs | 2 ++ .../Symbols/Members/FieldMember.cs | 2 ++ .../Symbols/Members/IMappableMember.cs | 4 +++ .../Symbols/Members/MemberPath.cs | 10 ++++--- .../Symbols/Members/MemberPathGetter.cs | 12 ++++---- .../Symbols/Members/NonEmptyMemberPath.cs | 2 +- .../Symbols/Members/ParameterSourceMember.cs | 2 ++ .../Symbols/Members/PropertyMember.cs | 4 +++ .../Mapping/ObjectPropertyValueMethodTest.cs | 28 ++++++++----------- 16 files changed, 77 insertions(+), 42 deletions(-) diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersContainerBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersContainerBuilderContext.cs index c9a7ce1f6d..0bde6daea2 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersContainerBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersContainerBuilderContext.cs @@ -35,8 +35,7 @@ public void AddNullDelegateMemberAssignmentMapping(IMemberAssignmentMapping memb // set target member to null if null assignments are allowed // and the source is null var setMemberToNull = - BuilderContext.Configuration.Mapper.AllowNullPropertyAssignment - && memberMapping.MemberInfo.TargetMember.Member.Type.IsNullable(); + BuilderContext.Configuration.Mapper.AllowNullPropertyAssignment && memberMapping.MemberInfo.TargetMember.Member.IsWriteNullable; // if the member is explicitly set to null, // make sure the parent members are initialized/non-null, @@ -51,7 +50,7 @@ public void AddNullDelegateMemberAssignmentMapping(IMemberAssignmentMapping memb var nullConditionSourcePath = new NonEmptyMemberPath( memberMapping.MemberInfo.SourceMember.MemberPath.RootType, - memberMapping.MemberInfo.SourceMember.MemberPath.PathWithoutTrailingNonNullable().ToList() + memberMapping.MemberInfo.SourceMember.MemberPath.ReadPathWithoutTrailingNonNullable().ToList() ); var container = GetOrCreateNullDelegateMappingForPath(nullConditionSourcePath); AddMemberAssignmentMapping(container, memberMapping); @@ -77,7 +76,7 @@ private void AddMemberAssignmentMapping(IMemberAssignmentMappingContainer contai // if the source value is a non-nullable value, // the target should be non-null after this assignment and can be set as initialized. - if (!mapping.MemberInfo.IsSourceNullable && mapping.MemberInfo.TargetMember.Member.Type.IsNullable()) + if (!mapping.MemberInfo.IsSourceNullable && mapping.MemberInfo.TargetMember.Member.IsWriteNullable) { _initializedNullableTargetPaths.Add((container, mapping.MemberInfo.TargetMember)); } @@ -85,7 +84,7 @@ private void AddMemberAssignmentMapping(IMemberAssignmentMappingContainer contai private void AddNullMemberInitializers(IMemberAssignmentMappingContainer container, MemberPath path) { - foreach (var nullablePathList in path.ObjectPathNullableSubPaths()) + foreach (var nullablePathList in path.ObjectReadPathNullableSubPaths()) { var nullablePath = new NonEmptyMemberPath(path.RootType, nullablePathList); var type = nullablePath.Member.Type.NonNullable(); @@ -138,7 +137,7 @@ private IMemberAssignmentMappingContainer FindParentNonNullContainer(MemberPath // try to reuse parent path mappings and wrap inside them // if the parentMapping is the first nullable path, no need to access the path in the condition in a null-safe way. needsNullSafeAccess = false; - foreach (var nullablePathList in nullConditionSourcePath.ObjectPathNullableSubPaths().Reverse()) + foreach (var nullablePathList in nullConditionSourcePath.ObjectReadPathNullableSubPaths().Reverse()) { var nullablePath = new NonEmptyMemberPath(nullConditionSourcePath.RootType, nullablePathList); if (_nullDelegateMappings.TryGetValue(nullablePath, out var parentMappingContainer)) diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MemberMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MemberMappingBuilder.cs index 85343bc9dd..fa09f6db82 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MemberMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MemberMappingBuilder.cs @@ -85,7 +85,7 @@ public static bool TryBuild( return false; } - var memberTargetNullable = memberMappingInfo.TargetMember.Member.Type.IsNullable(); + var memberTargetNullable = memberMappingInfo.TargetMember.Member.IsWriteNullable; var delegateTargetNullable = delegateMapping.TargetType.IsNullable(); var memberSourceNullable = memberMappingInfo.IsSourceNullable; var delegateSourceNullable = delegateMapping.SourceType.IsNullable(); @@ -175,7 +175,7 @@ ITypeSymbol targetMemberType ) { var nullFallback = NullFallbackValue.Default; - if (!delegateMapping.SourceType.IsNullable() && sourcePath.IsAnyNullable()) + if (!delegateMapping.SourceType.IsNullable() && sourcePath.IsAnyReadNullable()) { nullFallback = ctx.BuilderContext.GetNullFallbackValue(targetMemberType); } @@ -199,7 +199,7 @@ NonEmptyMemberPath targetMember var sourceGetter = sourceMember.BuildGetter(ctx.BuilderContext); // no member of the source path is nullable, no null handling needed - if (!sourceMember.IsAnyNullable()) + if (!sourceMember.IsAnyReadNullable()) { return new MappedMemberSourceValue(delegateMapping, sourceGetter, false, true); } @@ -210,7 +210,7 @@ NonEmptyMemberPath targetMember // access the source in a null save matter (via ?.) but no other special handling required. if ( ctx.BuilderContext.Configuration.Mapper.AllowNullPropertyAssignment - && (delegateMapping.SourceType.IsNullable() || delegateMapping.IsSynthetic && targetMember.Member.Type.IsNullable()) + && (delegateMapping.SourceType.IsNullable() || delegateMapping.IsSynthetic && targetMember.Member.IsWriteNullable) ) { return new MappedMemberSourceValue(delegateMapping, sourceGetter, true, false); diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/SourceValueBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/SourceValueBuilder.cs index 9d38c0e333..943fd45298 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/SourceValueBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/SourceValueBuilder.cs @@ -78,7 +78,7 @@ private static bool TryBuildConstantSourceValue( if ( value.ConstantValue.IsNull && memberMappingInfo.TargetMember.MemberType.IsReferenceType - && !memberMappingInfo.TargetMember.Member.Type.IsNullable() + && !memberMappingInfo.TargetMember.Member.IsWriteNullable ) { ctx.BuilderContext.ReportDiagnostic( diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberMappingInfo.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberMappingInfo.cs index c9465ad397..1be4fb5a17 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberMappingInfo.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberMappingInfo.cs @@ -18,7 +18,7 @@ public MemberMappingInfo(SourceMemberPath? sourceMember, NonEmptyMemberPath targ public MemberMappingInfo(NonEmptyMemberPath targetMember, MemberValueMappingConfiguration configuration) : this(null, targetMember, configuration) { } - public bool IsSourceNullable => SourceMember?.MemberPath.IsAnyNullable() ?? ValueConfiguration?.Value?.ConstantValue.IsNull ?? true; + public bool IsSourceNullable => SourceMember?.MemberPath.IsAnyReadNullable() ?? ValueConfiguration?.Value?.ConstantValue.IsNull ?? true; private string DebuggerDisplay => $"{SourceMember?.MemberPath.FullName ?? ValueConfiguration?.DescribeValue()} => {TargetMember.FullName}"; diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/MappedMemberSourceValue.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/MappedMemberSourceValue.cs index dacde868fe..02439ca6f6 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/MappedMemberSourceValue.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/MappedMemberSourceValue.cs @@ -19,7 +19,7 @@ bool addValuePropertyOnNullable private readonly MemberPathGetter _sourceMember = sourceMember; private readonly INewInstanceMapping _delegateMapping = delegateMapping; - public bool RequiresSourceNullCheck => !nullConditionalAccess && _sourceMember.MemberPath.IsAnyNullable(); + public bool RequiresSourceNullCheck => !nullConditionalAccess && _sourceMember.MemberPath.IsAnyReadNullable(); public ExpressionSyntax Build(TypeMappingBuildContext ctx) { diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/NullMappedMemberSourceValue.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/NullMappedMemberSourceValue.cs index c5c7cef44f..da4dbe9e8e 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/NullMappedMemberSourceValue.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/NullMappedMemberSourceValue.cs @@ -27,7 +27,7 @@ bool useNullConditionalAccess public ExpressionSyntax Build(TypeMappingBuildContext ctx) { // if the source is not nullable, return it directly. - if (!_sourceGetter.MemberPath.IsAnyNullable()) + if (!_sourceGetter.MemberPath.IsAnyReadNullable()) { ctx = ctx.WithSource(_sourceGetter.BuildAccess(ctx.Source)); return _delegateMapping.Build(ctx); @@ -49,7 +49,7 @@ public ExpressionSyntax Build(TypeMappingBuildContext ctx) // source.A?.B == null ? : Map(source.A.B.Value) // use simplified coalesce expression for synthetic mappings: // source.A?.B ?? - if (_delegateMapping.IsSynthetic && (useNullConditionalAccess || !_sourceGetter.MemberPath.IsAnyObjectPathNullable())) + if (_delegateMapping.IsSynthetic && (useNullConditionalAccess || !_sourceGetter.MemberPath.IsAnyObjectReadPathNullable())) { var nullConditionalSourceAccess = _sourceGetter.BuildAccess(ctx.Source, nullConditional: true); var nameofSourceAccess = _sourceGetter.BuildAccess(ctx.Source, nullConditional: false); diff --git a/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs b/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs index 99995c0f1c..b2b19f894d 100644 --- a/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs +++ b/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs @@ -92,6 +92,18 @@ private bool IsAccessible(ISymbol symbol, MemberVisibility visibility) } public bool IsNullable(ISymbol symbol) + { + return symbol switch + { + ITypeSymbol t => t.IsNullable(), + IPropertySymbol p => p.Type.IsNullable(), + IFieldSymbol f => f.Type.IsNullable(), + IParameterSymbol p => p.Type.IsNullable(), + _ => false, + }; + } + + public bool IsReadNullable(ISymbol symbol) { return symbol switch { @@ -103,6 +115,18 @@ public bool IsNullable(ISymbol symbol) }; } + public bool IsWriteNullable(ISymbol symbol) + { + return symbol switch + { + ITypeSymbol t => t.IsNullable(), + IPropertySymbol p => p.Type.IsNullable() || TryHasAttribute(p), + IFieldSymbol f => f.Type.IsNullable() || TryHasAttribute(f), + IParameterSymbol p => p.Type.IsNullable() || TryHasAttribute(p), + _ => false, + }; + } + public bool MayReturnNull(IMethodSymbol symbol, bool treatUnannotatedAsNullable = true) { // only treat annotated as nullable diff --git a/src/Riok.Mapperly/Symbols/Members/ConstructorParameterMember.cs b/src/Riok.Mapperly/Symbols/Members/ConstructorParameterMember.cs index 39a1926b27..89493a2f90 100644 --- a/src/Riok.Mapperly/Symbols/Members/ConstructorParameterMember.cs +++ b/src/Riok.Mapperly/Symbols/Members/ConstructorParameterMember.cs @@ -21,6 +21,8 @@ public class ConstructorParameterMember(IParameterSymbol symbol, SymbolAccessor public ITypeSymbol Type { get; } = accessor.UpgradeNullable(symbol.Type); public INamedTypeSymbol ContainingType { get; } = symbol.ContainingType; public bool IsNullable => accessor.IsNullable(Symbol); + public bool IsReadNullable => accessor.IsReadNullable(Symbol); + public bool IsWriteNullable => accessor.IsWriteNullable(Symbol); public bool CanGet => false; public bool CanGetDirectly => false; public bool CanSet => false; diff --git a/src/Riok.Mapperly/Symbols/Members/FieldMember.cs b/src/Riok.Mapperly/Symbols/Members/FieldMember.cs index 7275a26963..1ec02a39ee 100644 --- a/src/Riok.Mapperly/Symbols/Members/FieldMember.cs +++ b/src/Riok.Mapperly/Symbols/Members/FieldMember.cs @@ -18,6 +18,8 @@ public class FieldMember(IFieldSymbol symbol, SymbolAccessor symbolAccessor) public ITypeSymbol Type { get; } = symbolAccessor.UpgradeNullable(symbol.Type); public INamedTypeSymbol ContainingType { get; } = symbol.ContainingType; public bool IsNullable => symbolAccessor.IsNullable(Symbol); + public bool IsReadNullable => symbolAccessor.IsReadNullable(Symbol); + public bool IsWriteNullable => symbolAccessor.IsWriteNullable(Symbol); public bool CanGet => true; public bool CanGetDirectly => symbolAccessor.IsDirectlyAccessible(Symbol); public bool CanSet => !Symbol.IsReadOnly; diff --git a/src/Riok.Mapperly/Symbols/Members/IMappableMember.cs b/src/Riok.Mapperly/Symbols/Members/IMappableMember.cs index 02e1833f2a..599e974f4e 100644 --- a/src/Riok.Mapperly/Symbols/Members/IMappableMember.cs +++ b/src/Riok.Mapperly/Symbols/Members/IMappableMember.cs @@ -18,6 +18,10 @@ public interface IMappableMember bool IsNullable { get; } + bool IsReadNullable { get; } + + bool IsWriteNullable { get; } + /// /// Whether the member can be read using direct access or an unsafe accessor method. /// diff --git a/src/Riok.Mapperly/Symbols/Members/MemberPath.cs b/src/Riok.Mapperly/Symbols/Members/MemberPath.cs index 5fa5cec385..e8fad6ce1a 100644 --- a/src/Riok.Mapperly/Symbols/Members/MemberPath.cs +++ b/src/Riok.Mapperly/Symbols/Members/MemberPath.cs @@ -44,29 +44,31 @@ public abstract class MemberPath(ITypeSymbol rootType, IReadOnlyList /// The built path. - public IEnumerable PathWithoutTrailingNonNullable() => Path.Reverse().SkipWhile(x => !x.IsNullable).Reverse(); + public IEnumerable ReadPathWithoutTrailingNonNullable() => Path.Reverse().SkipWhile(x => !x.IsReadNullable).Reverse(); /// /// Returns an element for each nullable sub-path of the . /// If the is nullable, the entire is not returned. /// /// All nullable sub-paths of the . - public IEnumerable> ObjectPathNullableSubPaths() + public IEnumerable> ObjectReadPathNullableSubPaths() { var pathParts = new List(Path.Count); foreach (var pathPart in ObjectPath) { pathParts.Add(pathPart); - if (!pathPart.IsNullable) + if (!pathPart.IsReadNullable) continue; yield return pathParts.ToArray(); } } + public bool IsAnyReadNullable() => Path.Any(x => x.IsReadNullable); + public bool IsAnyNullable() => Path.Any(p => p.IsNullable); - public bool IsAnyObjectPathNullable() => ObjectPath.Any(p => p.IsNullable); + public bool IsAnyObjectReadPathNullable() => ObjectPath.Any(p => p.IsReadNullable); public MemberPathGetter BuildGetter(SimpleMappingBuilderContext ctx) => MemberPathGetter.Build(ctx, this); diff --git a/src/Riok.Mapperly/Symbols/Members/MemberPathGetter.cs b/src/Riok.Mapperly/Symbols/Members/MemberPathGetter.cs index 153cc061f2..2a44bc7938 100644 --- a/src/Riok.Mapperly/Symbols/Members/MemberPathGetter.cs +++ b/src/Riok.Mapperly/Symbols/Members/MemberPathGetter.cs @@ -40,7 +40,7 @@ public static MemberPathGetter Build(SimpleMappingBuilderContext ctx, MemberPath bool skipTrailingNonNullable = false ) { - var path = skipTrailingNonNullable ? PathWithoutTrailingNonNullable() : _path; + var path = skipTrailingNonNullable ? ReadPathWithoutTrailingNonNullable() : _path; return BuildAccess(baseAccess, path, addValuePropertyOnNullable, nullConditional); } @@ -56,7 +56,7 @@ public static MemberPathGetter Build(SimpleMappingBuilderContext ctx, MemberPath { return path.AggregateWithPrevious( baseAccess, - (expr, prevProp, prop) => prop.Getter.BuildAccess(expr, prop.Member.ContainingType, prevProp.Member?.IsNullable == true) + (expr, prevProp, prop) => prop.Getter.BuildAccess(expr, prop.Member.ContainingType, prevProp.Member?.IsReadNullable == true) ); } @@ -94,14 +94,14 @@ private BinaryExpressionSyntax BuildNonNullCondition(ExpressionSyntax baseAccess private ExpressionSyntax? BuildNonNullConditionWithoutConditionalAccess(ExpressionSyntax baseAccess) { - var nullablePath = PathWithoutTrailingNonNullable(); + var nullablePath = ReadPathWithoutTrailingNonNullable(); var access = baseAccess; var conditions = new List(); foreach (var pathPart in nullablePath) { access = pathPart.Getter.BuildAccess(access, pathPart.Member.ContainingType); - if (!pathPart.Member.IsNullable) + if (!pathPart.Member.IsReadNullable) continue; conditions.Add(IsNotNull(access)); @@ -110,8 +110,8 @@ private BinaryExpressionSyntax BuildNonNullCondition(ExpressionSyntax baseAccess return conditions.Count == 0 ? null : And(conditions); } - private IEnumerable PathWithoutTrailingNonNullable() => - _path.Reverse().SkipWhile(x => !x.Member.IsNullable).Reverse(); + private IEnumerable ReadPathWithoutTrailingNonNullable() => + _path.Reverse().SkipWhile(x => !x.Member.IsReadNullable).Reverse(); public override bool Equals(object? obj) { diff --git a/src/Riok.Mapperly/Symbols/Members/NonEmptyMemberPath.cs b/src/Riok.Mapperly/Symbols/Members/NonEmptyMemberPath.cs index c88b98419a..d18e0cdbf6 100644 --- a/src/Riok.Mapperly/Symbols/Members/NonEmptyMemberPath.cs +++ b/src/Riok.Mapperly/Symbols/Members/NonEmptyMemberPath.cs @@ -26,7 +26,7 @@ public NonEmptyMemberPath(ITypeSymbol rootType, IReadOnlyList p /// Gets the type of the . If any part of the path is nullable, this type will be nullable too. /// public override ITypeSymbol MemberType => - IsAnyNullable() ? Member.Type.WithNullableAnnotation(NullableAnnotation.Annotated) : Member.Type; + IsAnyReadNullable() ? Member.Type.WithNullableAnnotation(NullableAnnotation.Annotated) : Member.Type; public MemberPathSetter BuildSetter(SimpleMappingBuilderContext ctx) => MemberPathSetter.Build(ctx, this); diff --git a/src/Riok.Mapperly/Symbols/Members/ParameterSourceMember.cs b/src/Riok.Mapperly/Symbols/Members/ParameterSourceMember.cs index af8fef90ba..6f46f1b598 100644 --- a/src/Riok.Mapperly/Symbols/Members/ParameterSourceMember.cs +++ b/src/Riok.Mapperly/Symbols/Members/ParameterSourceMember.cs @@ -20,6 +20,8 @@ public class ParameterSourceMember(MethodParameter parameter) : IMappableMember, public ITypeSymbol Type => parameter.Type; public INamedTypeSymbol? ContainingType => null; public bool IsNullable => parameter.Type.IsNullable(); + public bool IsReadNullable => parameter.Type.IsNullable(); + public bool IsWriteNullable => parameter.Type.IsNullable(); public bool CanGet => true; public bool CanGetDirectly => true; public bool CanSet => false; diff --git a/src/Riok.Mapperly/Symbols/Members/PropertyMember.cs b/src/Riok.Mapperly/Symbols/Members/PropertyMember.cs index b747e4c21f..b261606507 100644 --- a/src/Riok.Mapperly/Symbols/Members/PropertyMember.cs +++ b/src/Riok.Mapperly/Symbols/Members/PropertyMember.cs @@ -21,6 +21,10 @@ public class PropertyMember(IPropertySymbol symbol, SymbolAccessor symbolAccesso public bool IsNullable => symbolAccessor.IsNullable(Symbol); + public bool IsReadNullable => symbolAccessor.IsReadNullable(Symbol); + + public bool IsWriteNullable => symbolAccessor.IsWriteNullable(Symbol); + public bool CanGet => !Symbol.IsWriteOnly && (Symbol.GetMethod == null || symbolAccessor.IsMemberAccessible(Symbol.GetMethod)); public bool CanGetDirectly => diff --git a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyValueMethodTest.cs b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyValueMethodTest.cs index 7d09fd9fc6..9514627b75 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyValueMethodTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyValueMethodTest.cs @@ -629,15 +629,13 @@ class B ); TestHelper - .GenerateMapper(source) + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) .Should() - .HaveSingleMethodBody( - """ - var target = new global::B(); - target.Value = BuildValue(); - return target; - """ - ); + .HaveDiagnostic( + DiagnosticDescriptors.MapValueMethodTypeMismatch, + "Cannot assign method return type string? of BuildValue() to B.Value of type string" + ) + .HaveAssertedAllDiagnostics(); } [Fact] @@ -713,14 +711,12 @@ class B ); TestHelper - .GenerateMapper(source) + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) .Should() - .HaveSingleMethodBody( - """ - var target = new global::B(); - target.Value = BuildValue(); - return target; - """ - ); + .HaveDiagnostic( + DiagnosticDescriptors.MapValueMethodTypeMismatch, + "Cannot assign method return type string of BuildValue() to B.Value of type string" + ) + .HaveAssertedAllDiagnostics(); } } From 467117e4c733580ac852aef68a99d53674ebd9ea Mon Sep 17 00:00:00 2001 From: Abhijith T V Date: Sun, 3 May 2026 14:48:52 +0530 Subject: [PATCH 04/13] fix: Rework delegate mapping and failed test cases --- .../BuilderContext/NestedMappingsContext.cs | 2 +- .../MemberMappingBuilder.cs | 5 +- .../MappingBodyBuilders/SourceValueBuilder.cs | 12 ++-- .../MemberMappings/MemberMappingInfo.cs | 6 +- .../Symbols/Members/EmptyMemberPath.cs | 6 +- .../Symbols/Members/MemberPath.cs | 14 +++++ .../Symbols/Members/NonEmptyMemberPath.cs | 8 +++ .../Mapping/ObjectPropertyFlatteningTest.cs | 15 ++++- .../Mapping/ObjectPropertyNestedTest.cs | 7 ++- .../Mapping/ObjectPropertyNullableTest.cs | 55 ++++++++++++++++++- 10 files changed, 114 insertions(+), 16 deletions(-) diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NestedMappingsContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NestedMappingsContext.cs index 16c48b20a8..3cfd332dc4 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NestedMappingsContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NestedMappingsContext.cs @@ -70,7 +70,7 @@ private bool TryFindNestedSourcePath( { if ( _context.SymbolAccessor.TryFindMemberPath( - nestedMemberPath.MemberType, + nestedMemberPath.MemberReadType, pathCandidates, // Use empty ignore list to support ignoring a property for normal search while flattening its properties Array.Empty(), diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MemberMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MemberMappingBuilder.cs index fa09f6db82..9277b68c2b 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MemberMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MemberMappingBuilder.cs @@ -85,11 +85,12 @@ public static bool TryBuild( return false; } + //var memberTargetNullable = memberMappingInfo.TargetMember.Member.Type.IsNullable(); var memberTargetNullable = memberMappingInfo.TargetMember.Member.IsWriteNullable; var delegateTargetNullable = delegateMapping.TargetType.IsNullable(); var memberSourceNullable = memberMappingInfo.IsSourceNullable; var delegateSourceNullable = delegateMapping.SourceType.IsNullable(); - var memberTargetAcceptsNull = memberMappingInfo.TargetMember.MemberType.IsNullable(); + var memberTargetAcceptsNull = memberMappingInfo.TargetMember.Member.IsWriteNullable; if ( memberMappingInfo.Configuration?.SuppressNullMismatchDiagnostic != true @@ -133,7 +134,7 @@ public static bool TryBuild( return false; } - sourceValue = BuildInlineNullHandlingMapping(ctx, delegateMapping, sourceMember.MemberPath, targetMember.MemberType); + sourceValue = BuildInlineNullHandlingMapping(ctx, delegateMapping, sourceMember.MemberPath, targetMember.MemberWriteType); return true; } diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/SourceValueBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/SourceValueBuilder.cs index 943fd45298..0ef89c65f4 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/SourceValueBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/SourceValueBuilder.cs @@ -77,7 +77,7 @@ private static bool TryBuildConstantSourceValue( // but the provided value is null or default (for default IsNullable is also true) if ( value.ConstantValue.IsNull - && memberMappingInfo.TargetMember.MemberType.IsReferenceType + && memberMappingInfo.TargetMember.MemberWriteType.IsReferenceType && !memberMappingInfo.TargetMember.Member.IsWriteNullable ) { @@ -92,8 +92,8 @@ private static bool TryBuildConstantSourceValue( // target is value type but value is null if ( value.ConstantValue.IsNull - && memberMappingInfo.TargetMember.MemberType.IsValueType - && !memberMappingInfo.TargetMember.MemberType.IsNullableValueType() + && memberMappingInfo.TargetMember.MemberWriteType.IsValueType + && !memberMappingInfo.TargetMember.MemberWriteType.IsNullableValueType() && value.Expression.IsKind(SyntaxKind.NullLiteralExpression) ) { @@ -116,7 +116,7 @@ private static bool TryBuildConstantSourceValue( // 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())) + if (!SymbolEqualityComparer.Default.Equals(value.ConstantValue.Type, memberMappingInfo.TargetMember.MemberWriteType.NonNullable())) { ctx.BuilderContext.ReportDiagnostic( DiagnosticDescriptors.MapValueTypeMismatch, @@ -137,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.NonNullable()); + var enumTypeFullName = FullyQualifiedIdentifier(memberMappingInfo.TargetMember.MemberWriteType.NonNullable()); sourceValue = new ConstantSourceValue(MemberAccess(enumTypeFullName, enumMemberName)); return true; case TypedConstantKind.Type: @@ -186,7 +186,7 @@ private static bool TryBuildMethodProvidedSourceValue( // to nullable value types // nullable is checked with nullable annotation var methodCandidates = namedMethodCandidates.Where(x => - SymbolEqualityComparer.Default.Equals(x.ReturnType.NonNullable(), memberMappingInfo.TargetMember.MemberType.NonNullable()) + SymbolEqualityComparer.Default.Equals(x.ReturnType.NonNullable(), memberMappingInfo.TargetMember.MemberWriteType.NonNullable()) ); if (!memberMappingInfo.TargetMember.Member.IsNullable) diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberMappingInfo.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberMappingInfo.cs index 1be4fb5a17..e0766a0155 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberMappingInfo.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberMappingInfo.cs @@ -34,7 +34,11 @@ public TypeMappingKey ToTypeMappingKey() if (SourceMember == null) throw new InvalidOperationException($"{SourceMember} and {TargetMember} need to be set to create a {nameof(TypeMappingKey)}"); - return new TypeMappingKey(SourceMember.MemberPath.MemberType, TargetMember.MemberType, Configuration?.ToTypeMappingConfiguration()); + return new TypeMappingKey( + SourceMember.MemberPath.MemberReadType, + TargetMember.MemberWriteType, + Configuration?.ToTypeMappingConfiguration() + ); } public string DescribeSource() diff --git a/src/Riok.Mapperly/Symbols/Members/EmptyMemberPath.cs b/src/Riok.Mapperly/Symbols/Members/EmptyMemberPath.cs index 34cd5feee3..ae60fb44d4 100644 --- a/src/Riok.Mapperly/Symbols/Members/EmptyMemberPath.cs +++ b/src/Riok.Mapperly/Symbols/Members/EmptyMemberPath.cs @@ -8,7 +8,11 @@ public class EmptyMemberPath(ITypeSymbol rootType) : MemberPath(rootType, []) { public override IMappableMember? Member => null; - public override ITypeSymbol MemberType => RootType; + public override ITypeSymbol MemberType => RootType; //TODO - was the MayBeNull check not required here? + + public override ITypeSymbol MemberReadType => RootType; //after confirmation, add read null + + public override ITypeSymbol MemberWriteType => RootType; //after confirmation, add write null public override string ToDisplayString(bool includeRootType = true, bool includeMemberType = true) => includeRootType ? RootType.ToDisplayString() : string.Empty; diff --git a/src/Riok.Mapperly/Symbols/Members/MemberPath.cs b/src/Riok.Mapperly/Symbols/Members/MemberPath.cs index e8fad6ce1a..07bb508178 100644 --- a/src/Riok.Mapperly/Symbols/Members/MemberPath.cs +++ b/src/Riok.Mapperly/Symbols/Members/MemberPath.cs @@ -35,6 +35,18 @@ public abstract class MemberPath(ITypeSymbol rootType, IReadOnlyList public abstract ITypeSymbol MemberType { get; } + /// + /// Gets the type of the total path in the context of read, e.g. that of the if it exists, or the otherwise. + /// If any part of the path is nullable, this type will be nullable too. + /// + public abstract ITypeSymbol MemberReadType { get; } + + /// + /// Gets the type of the last part of path in the context of write, e.g. that of the if it exists, or the otherwise. + /// If last part of the path is nullable, this type will be nullable too. + /// + public abstract ITypeSymbol MemberWriteType { get; } + /// /// Gets the full name of the path (e.g. A.B.C). /// @@ -66,6 +78,8 @@ public IEnumerable> ObjectReadPathNullableSubPath public bool IsAnyReadNullable() => Path.Any(x => x.IsReadNullable); + public bool IsWriteNullable() => Path[^1].IsWriteNullable; + public bool IsAnyNullable() => Path.Any(p => p.IsNullable); public bool IsAnyObjectReadPathNullable() => ObjectPath.Any(p => p.IsReadNullable); diff --git a/src/Riok.Mapperly/Symbols/Members/NonEmptyMemberPath.cs b/src/Riok.Mapperly/Symbols/Members/NonEmptyMemberPath.cs index d18e0cdbf6..63e6eaab53 100644 --- a/src/Riok.Mapperly/Symbols/Members/NonEmptyMemberPath.cs +++ b/src/Riok.Mapperly/Symbols/Members/NonEmptyMemberPath.cs @@ -28,6 +28,14 @@ public NonEmptyMemberPath(ITypeSymbol rootType, IReadOnlyList p public override ITypeSymbol MemberType => IsAnyReadNullable() ? Member.Type.WithNullableAnnotation(NullableAnnotation.Annotated) : Member.Type; + //todo - summary + public override ITypeSymbol MemberReadType => + IsAnyReadNullable() ? Member.Type.WithNullableAnnotation(NullableAnnotation.Annotated) : Member.Type; + + //todo - summary + public override ITypeSymbol MemberWriteType => + IsWriteNullable() ? Member.Type.WithNullableAnnotation(NullableAnnotation.Annotated) : Member.Type; + public MemberPathSetter BuildSetter(SimpleMappingBuilderContext ctx) => MemberPathSetter.Build(ctx, this); public override string ToDisplayString(bool includeRootType = true, bool includeMemberType = true) diff --git a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyFlatteningTest.cs b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyFlatteningTest.cs index e42ccdb909..bffe8ee592 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyFlatteningTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyFlatteningTest.cs @@ -790,8 +790,21 @@ public void ManualNestedPropertyNullablePath() ); TestHelper - .GenerateMapper(source) + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) .Should() + .HaveDiagnostic( + DiagnosticDescriptors.NullableSourceValueToNonNullableTargetValue, + "Mapping the nullable source property Value1.Id100 of A to the target property Value2.Id200 of B which is not nullable" + ) + .HaveDiagnostic( + DiagnosticDescriptors.NullableSourceValueToNonNullableTargetValue, + "Mapping the nullable source property Value1.Value1.Id1 of A to the target property Value2.Value2.Id2 of B which is not nullable" + ) + .HaveDiagnostic( + DiagnosticDescriptors.NullableSourceValueToNonNullableTargetValue, + "Mapping the nullable source property Value1.Value1.Id10 of A to the target property Value2.Value2.Id20 of B which is not nullable" + ) + .HaveAssertedAllDiagnostics() .HaveSingleMethodBody( """ var target = new global::B(); diff --git a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyNestedTest.cs b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyNestedTest.cs index 825f324e4f..3b50e192c8 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyNestedTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyNestedTest.cs @@ -45,8 +45,13 @@ public void ManualNullableNestedToNullableNestedProperty() ); TestHelper - .GenerateMapper(source) + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) .Should() + .HaveDiagnostic( + DiagnosticDescriptors.NullableSourceValueToNonNullableTargetValue, + "Mapping the nullable source property Value.IntValue of A to the target property Value.StringValue of B which is not nullable" + ) + .HaveAssertedAllDiagnostics() .HaveSingleMethodBody( """ var target = new global::B(); diff --git a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyNullableTest.cs b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyNullableTest.cs index 6efb3a87d1..0d23b22e51 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyNullableTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyNullableTest.cs @@ -1071,8 +1071,13 @@ class B ); TestHelper - .GenerateMapper(source) + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) .Should() + .HaveDiagnostic( + DiagnosticDescriptors.NullableSourceValueToNonNullableTargetValue, + "Mapping the nullable source property Name of A to the target property Name of B which is not nullable" + ) + .HaveAssertedAllDiagnostics() .HaveSingleMethodBody( """ var target = new global::B(); @@ -1174,8 +1179,13 @@ class B ); TestHelper - .GenerateMapper(source) + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) .Should() + .HaveDiagnostic( + DiagnosticDescriptors.NullableSourceValueToNonNullableTargetValue, + "Mapping the nullable source property Value of A to the target property Value of B which is not nullable" + ) + .HaveAssertedAllDiagnostics() .HaveMapMethodBody( """ var target = new global::B(); @@ -1217,8 +1227,13 @@ class B ); TestHelper - .GenerateMapper(source) + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) .Should() + .HaveDiagnostic( + DiagnosticDescriptors.NullableSourceValueToNonNullableTargetValue, + "Mapping the nullable source property Value of A to the target property Value of B which is not nullable" + ) + .HaveAssertedAllDiagnostics() .HaveMapMethodBody( """ var target = new global::B(); @@ -1230,4 +1245,38 @@ class B """ ); } + + [Fact] + public void MaybeNullSourceToAllowNullTargetProperty() + { + var source = TestSourceBuilder.Mapping( + "A", + "B", + """ + class A + { + [System.Diagnostics.CodeAnalysis.MaybeNull] + public string Name { get; set; } = default!; + } + """, + """ + class B + { + [System.Diagnostics.CodeAnalysis.AllowNull] + public string Name { get; set; } = default!; + } + """ + ); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.Name = source.Name; + return target; + """ + ); + } } From f9c3e0bd00a1889206c615829688f3c98508e084 Mon Sep 17 00:00:00 2001 From: Abhijith T V Date: Sun, 3 May 2026 16:44:59 +0530 Subject: [PATCH 05/13] fix: Include MaybeNull to decide the return type of method symbol --- .../MappingBodyBuilders/SourceValueBuilder.cs | 17 +++++++++++++++-- .../Mapping/ObjectPropertyValueMethodTest.cs | 4 ++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/SourceValueBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/SourceValueBuilder.cs index 0ef89c65f4..ee4e625b17 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/SourceValueBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/SourceValueBuilder.cs @@ -189,7 +189,7 @@ private static bool TryBuildMethodProvidedSourceValue( SymbolEqualityComparer.Default.Equals(x.ReturnType.NonNullable(), memberMappingInfo.TargetMember.MemberWriteType.NonNullable()) ); - if (!memberMappingInfo.TargetMember.Member.IsNullable) + if (!memberMappingInfo.TargetMember.Member.IsWriteNullable) { // Filter out methods that may return null when the target is non-nullable. methodCandidates = methodCandidates.Where(m => !ctx.BuilderContext.SymbolAccessor.MayReturnNull(m, false)); @@ -201,7 +201,7 @@ private static bool TryBuildMethodProvidedSourceValue( ctx.BuilderContext.ReportDiagnostic( DiagnosticDescriptors.MapValueMethodTypeMismatch, methodReferenceConfiguration.Name, - namedMethodCandidates[0].ReturnType.ToDisplayString(), + FormattedReturnType(namedMethodCandidates[0]), memberMappingInfo.TargetMember.ToDisplayString() ); sourceValue = null; @@ -219,4 +219,17 @@ private static bool TryBuildMethodProvidedSourceValue( ); return true; } + + private static string FormattedReturnType(IMethodSymbol methodSymbol) + { + bool hasNullableAttribute = methodSymbol + .GetReturnTypeAttributes() + .Any(a => string.Equals(a.AttributeClass?.Name, nameof(MaybeNullAttribute))); + + var effectiveType = hasNullableAttribute + ? methodSymbol.ReturnType.WithNullableAnnotation(NullableAnnotation.Annotated) + : methodSymbol.ReturnType; + + return effectiveType.ToDisplayString(); + } } diff --git a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyValueMethodTest.cs b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyValueMethodTest.cs index 9514627b75..757e740cb4 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyValueMethodTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyValueMethodTest.cs @@ -686,7 +686,7 @@ public void ReturnMaybeNullMethodToNonNullableTargetShouldDiagnostic() .Should() .HaveDiagnostic( DiagnosticDescriptors.MapValueMethodTypeMismatch, - "Cannot assign method return type string of BuildValue() to B.Value of type string" + "Cannot assign method return type string? of BuildValue() to B.Value of type string" ) .HaveAssertedAllDiagnostics(); } @@ -715,7 +715,7 @@ class B .Should() .HaveDiagnostic( DiagnosticDescriptors.MapValueMethodTypeMismatch, - "Cannot assign method return type string of BuildValue() to B.Value of type string" + "Cannot assign method return type string? of BuildValue() to B.Value of type string" ) .HaveAssertedAllDiagnostics(); } From cbd86fc7ca57255145539c7c09f964115a51bb92 Mon Sep 17 00:00:00 2001 From: Abhijith T V Date: Sun, 3 May 2026 17:01:11 +0530 Subject: [PATCH 06/13] refactor: Remove redundant IsNullable and MemeberType --- .../MappingBodyBuilders/MemberMappingBuilder.cs | 8 +++----- .../Symbols/Members/ConstructorParameterMember.cs | 1 - src/Riok.Mapperly/Symbols/Members/EmptyMemberPath.cs | 3 --- src/Riok.Mapperly/Symbols/Members/FieldMember.cs | 1 - src/Riok.Mapperly/Symbols/Members/IMappableMember.cs | 2 -- src/Riok.Mapperly/Symbols/Members/MemberPath.cs | 8 -------- .../Symbols/Members/NonEmptyMemberPath.cs | 10 ++++------ .../Symbols/Members/ParameterSourceMember.cs | 1 - src/Riok.Mapperly/Symbols/Members/PropertyMember.cs | 2 -- 9 files changed, 7 insertions(+), 29 deletions(-) diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MemberMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MemberMappingBuilder.cs index 9277b68c2b..1f6f950fe2 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MemberMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MemberMappingBuilder.cs @@ -85,12 +85,10 @@ public static bool TryBuild( return false; } - //var memberTargetNullable = memberMappingInfo.TargetMember.Member.Type.IsNullable(); - var memberTargetNullable = memberMappingInfo.TargetMember.Member.IsWriteNullable; + var memberTargetAcceptsNull = memberMappingInfo.TargetMember.Member.IsWriteNullable; var delegateTargetNullable = delegateMapping.TargetType.IsNullable(); var memberSourceNullable = memberMappingInfo.IsSourceNullable; var delegateSourceNullable = delegateMapping.SourceType.IsNullable(); - var memberTargetAcceptsNull = memberMappingInfo.TargetMember.Member.IsWriteNullable; if ( memberMappingInfo.Configuration?.SuppressNullMismatchDiagnostic != true @@ -109,8 +107,8 @@ public static bool TryBuild( } if ( - (memberSourceNullable == delegateSourceNullable && memberTargetNullable == delegateTargetNullable) - || (memberSourceNullable && !memberTargetNullable && delegateSourceNullable && !delegateTargetNullable) + (memberSourceNullable == delegateSourceNullable && memberTargetAcceptsNull == delegateTargetNullable) + || (memberSourceNullable && !memberTargetAcceptsNull && delegateSourceNullable && !delegateTargetNullable) ) { sourceValue = new MappedMemberSourceValue( diff --git a/src/Riok.Mapperly/Symbols/Members/ConstructorParameterMember.cs b/src/Riok.Mapperly/Symbols/Members/ConstructorParameterMember.cs index 89493a2f90..148004d096 100644 --- a/src/Riok.Mapperly/Symbols/Members/ConstructorParameterMember.cs +++ b/src/Riok.Mapperly/Symbols/Members/ConstructorParameterMember.cs @@ -20,7 +20,6 @@ public class ConstructorParameterMember(IParameterSymbol symbol, SymbolAccessor { public ITypeSymbol Type { get; } = accessor.UpgradeNullable(symbol.Type); public INamedTypeSymbol ContainingType { get; } = symbol.ContainingType; - public bool IsNullable => accessor.IsNullable(Symbol); public bool IsReadNullable => accessor.IsReadNullable(Symbol); public bool IsWriteNullable => accessor.IsWriteNullable(Symbol); public bool CanGet => false; diff --git a/src/Riok.Mapperly/Symbols/Members/EmptyMemberPath.cs b/src/Riok.Mapperly/Symbols/Members/EmptyMemberPath.cs index ae60fb44d4..b027053fe5 100644 --- a/src/Riok.Mapperly/Symbols/Members/EmptyMemberPath.cs +++ b/src/Riok.Mapperly/Symbols/Members/EmptyMemberPath.cs @@ -7,9 +7,6 @@ namespace Riok.Mapperly.Symbols.Members; public class EmptyMemberPath(ITypeSymbol rootType) : MemberPath(rootType, []) { public override IMappableMember? Member => null; - - public override ITypeSymbol MemberType => RootType; //TODO - was the MayBeNull check not required here? - public override ITypeSymbol MemberReadType => RootType; //after confirmation, add read null public override ITypeSymbol MemberWriteType => RootType; //after confirmation, add write null diff --git a/src/Riok.Mapperly/Symbols/Members/FieldMember.cs b/src/Riok.Mapperly/Symbols/Members/FieldMember.cs index 1ec02a39ee..3918c0f2d2 100644 --- a/src/Riok.Mapperly/Symbols/Members/FieldMember.cs +++ b/src/Riok.Mapperly/Symbols/Members/FieldMember.cs @@ -17,7 +17,6 @@ public class FieldMember(IFieldSymbol symbol, SymbolAccessor symbolAccessor) { public ITypeSymbol Type { get; } = symbolAccessor.UpgradeNullable(symbol.Type); public INamedTypeSymbol ContainingType { get; } = symbol.ContainingType; - public bool IsNullable => symbolAccessor.IsNullable(Symbol); public bool IsReadNullable => symbolAccessor.IsReadNullable(Symbol); public bool IsWriteNullable => symbolAccessor.IsWriteNullable(Symbol); public bool CanGet => true; diff --git a/src/Riok.Mapperly/Symbols/Members/IMappableMember.cs b/src/Riok.Mapperly/Symbols/Members/IMappableMember.cs index 599e974f4e..05417e4fbd 100644 --- a/src/Riok.Mapperly/Symbols/Members/IMappableMember.cs +++ b/src/Riok.Mapperly/Symbols/Members/IMappableMember.cs @@ -16,8 +16,6 @@ public interface IMappableMember INamedTypeSymbol? ContainingType { get; } - bool IsNullable { get; } - bool IsReadNullable { get; } bool IsWriteNullable { get; } diff --git a/src/Riok.Mapperly/Symbols/Members/MemberPath.cs b/src/Riok.Mapperly/Symbols/Members/MemberPath.cs index 07bb508178..6fa28bd07b 100644 --- a/src/Riok.Mapperly/Symbols/Members/MemberPath.cs +++ b/src/Riok.Mapperly/Symbols/Members/MemberPath.cs @@ -29,12 +29,6 @@ public abstract class MemberPath(ITypeSymbol rootType, IReadOnlyList public abstract IMappableMember? Member { get; } - /// - /// Gets the type of the total path, e.g. that of the if it exists, or the otherwise. - /// If any part of the path is nullable, this type will be nullable too. - /// - public abstract ITypeSymbol MemberType { get; } - /// /// Gets the type of the total path in the context of read, e.g. that of the if it exists, or the otherwise. /// If any part of the path is nullable, this type will be nullable too. @@ -80,8 +74,6 @@ public IEnumerable> ObjectReadPathNullableSubPath public bool IsWriteNullable() => Path[^1].IsWriteNullable; - public bool IsAnyNullable() => Path.Any(p => p.IsNullable); - public bool IsAnyObjectReadPathNullable() => ObjectPath.Any(p => p.IsReadNullable); public MemberPathGetter BuildGetter(SimpleMappingBuilderContext ctx) => MemberPathGetter.Build(ctx, this); diff --git a/src/Riok.Mapperly/Symbols/Members/NonEmptyMemberPath.cs b/src/Riok.Mapperly/Symbols/Members/NonEmptyMemberPath.cs index 63e6eaab53..0f45247de9 100644 --- a/src/Riok.Mapperly/Symbols/Members/NonEmptyMemberPath.cs +++ b/src/Riok.Mapperly/Symbols/Members/NonEmptyMemberPath.cs @@ -23,16 +23,14 @@ public NonEmptyMemberPath(ITypeSymbol rootType, IReadOnlyList p public string RootName => Path[0].Name; /// - /// Gets the type of the . If any part of the path is nullable, this type will be nullable too. + /// Gets the type of the in the context of read. If any part of the path is nullable, this type will be nullable too. /// - public override ITypeSymbol MemberType => - IsAnyReadNullable() ? Member.Type.WithNullableAnnotation(NullableAnnotation.Annotated) : Member.Type; - - //todo - summary public override ITypeSymbol MemberReadType => IsAnyReadNullable() ? Member.Type.WithNullableAnnotation(NullableAnnotation.Annotated) : Member.Type; - //todo - summary + /// + /// Gets the type of the in the context of write. If any part of the path is nullable, this type will be nullable too. + /// public override ITypeSymbol MemberWriteType => IsWriteNullable() ? Member.Type.WithNullableAnnotation(NullableAnnotation.Annotated) : Member.Type; diff --git a/src/Riok.Mapperly/Symbols/Members/ParameterSourceMember.cs b/src/Riok.Mapperly/Symbols/Members/ParameterSourceMember.cs index 6f46f1b598..c934a9fe0c 100644 --- a/src/Riok.Mapperly/Symbols/Members/ParameterSourceMember.cs +++ b/src/Riok.Mapperly/Symbols/Members/ParameterSourceMember.cs @@ -19,7 +19,6 @@ public class ParameterSourceMember(MethodParameter parameter) : IMappableMember, public string Name => parameter.Name; public ITypeSymbol Type => parameter.Type; public INamedTypeSymbol? ContainingType => null; - public bool IsNullable => parameter.Type.IsNullable(); public bool IsReadNullable => parameter.Type.IsNullable(); public bool IsWriteNullable => parameter.Type.IsNullable(); public bool CanGet => true; diff --git a/src/Riok.Mapperly/Symbols/Members/PropertyMember.cs b/src/Riok.Mapperly/Symbols/Members/PropertyMember.cs index b261606507..b7a8682bca 100644 --- a/src/Riok.Mapperly/Symbols/Members/PropertyMember.cs +++ b/src/Riok.Mapperly/Symbols/Members/PropertyMember.cs @@ -19,8 +19,6 @@ public class PropertyMember(IPropertySymbol symbol, SymbolAccessor symbolAccesso public INamedTypeSymbol? ContainingType { get; } = symbol.ContainingType; - public bool IsNullable => symbolAccessor.IsNullable(Symbol); - public bool IsReadNullable => symbolAccessor.IsReadNullable(Symbol); public bool IsWriteNullable => symbolAccessor.IsWriteNullable(Symbol); From 9105d7523837a51bd9dc9bf53e5a21e010132ec3 Mon Sep 17 00:00:00 2001 From: Abhijith T V Date: Sun, 3 May 2026 21:13:57 +0530 Subject: [PATCH 07/13] feat:Add read and write null check for source parameter --- .../BuilderContext/MembersMappingState.cs | 5 +++-- .../BuilderContext/MembersMappingStateBuilder.cs | 3 ++- .../Descriptors/Mappings/MethodMapping.cs | 2 +- .../Symbols/Members/NonEmptyMemberPath.cs | 2 +- .../Symbols/Members/ParameterSourceMember.cs | 8 +++++--- src/Riok.Mapperly/Symbols/MethodParameter.cs | 11 +++++++++-- 6 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingState.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingState.cs index 6fc9f976e1..0ae9dc19a2 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingState.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingState.cs @@ -28,7 +28,8 @@ internal class MembersMappingState( Dictionary> memberConfigsByRootTargetName, Dictionary> configuredTargetMembersByRootName, HashSet ignoredSourceMemberNames, - ParameterScope parameterScope + ParameterScope parameterScope, + SymbolAccessor symbolAccessor ) { private readonly Dictionary _aliasedSourceMembers = new(StringComparer.OrdinalIgnoreCase); @@ -58,7 +59,7 @@ ParameterScope parameterScope public IReadOnlyDictionary AdditionalSourceMembers => field ??= parameterScope.Parameters.Values.ToDictionary( x => x.NormalizedName, - x => new ParameterSourceMember(x), + x => new ParameterSourceMember(x, symbolAccessor), StringComparer.OrdinalIgnoreCase ); diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingStateBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingStateBuilder.cs index f484aff7b9..393a989596 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingStateBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingStateBuilder.cs @@ -68,7 +68,8 @@ public static MembersMappingState Build(MappingBuilderContext ctx, IMapping mapp memberConfigsByRootTargetName, configuredTargetMembersByRootName.AsDictionary(), ignoredSourceMemberNames, - parameterScope + parameterScope, + ctx.SymbolAccessor ); } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs index 1f94a5c7e4..62e336d236 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs @@ -39,7 +39,7 @@ public abstract class MethodMapping : ITypeMapping, IParameterizedMapping protected MethodMapping(ITypeSymbol sourceType, ITypeSymbol targetType) { TargetType = targetType; - SourceParameter = new MethodParameter(SourceParameterIndex, DefaultSourceParameterName, sourceType); + SourceParameter = new MethodParameter(SourceParameterIndex, DefaultSourceParameterName, sourceType, null); _returnType = targetType; } diff --git a/src/Riok.Mapperly/Symbols/Members/NonEmptyMemberPath.cs b/src/Riok.Mapperly/Symbols/Members/NonEmptyMemberPath.cs index 0f45247de9..dc78558ba2 100644 --- a/src/Riok.Mapperly/Symbols/Members/NonEmptyMemberPath.cs +++ b/src/Riok.Mapperly/Symbols/Members/NonEmptyMemberPath.cs @@ -29,7 +29,7 @@ public NonEmptyMemberPath(ITypeSymbol rootType, IReadOnlyList p IsAnyReadNullable() ? Member.Type.WithNullableAnnotation(NullableAnnotation.Annotated) : Member.Type; /// - /// Gets the type of the in the context of write. If any part of the path is nullable, this type will be nullable too. + /// Gets the type of the in the context of write. If last part of the path is nullable, this type will be nullable too. /// public override ITypeSymbol MemberWriteType => IsWriteNullable() ? Member.Type.WithNullableAnnotation(NullableAnnotation.Annotated) : Member.Type; diff --git a/src/Riok.Mapperly/Symbols/Members/ParameterSourceMember.cs b/src/Riok.Mapperly/Symbols/Members/ParameterSourceMember.cs index c934a9fe0c..e0e39d8000 100644 --- a/src/Riok.Mapperly/Symbols/Members/ParameterSourceMember.cs +++ b/src/Riok.Mapperly/Symbols/Members/ParameterSourceMember.cs @@ -14,13 +14,15 @@ namespace Riok.Mapperly.Symbols.Members; /// and is therefore in terms of the mapping the same. /// [DebuggerDisplay("{Name}")] -public class ParameterSourceMember(MethodParameter parameter) : IMappableMember, IMemberGetter +public class ParameterSourceMember(MethodParameter parameter, SymbolAccessor symbolAccessor) : IMappableMember, IMemberGetter { public string Name => parameter.Name; public ITypeSymbol Type => parameter.Type; public INamedTypeSymbol? ContainingType => null; - public bool IsReadNullable => parameter.Type.IsNullable(); - public bool IsWriteNullable => parameter.Type.IsNullable(); + public bool IsReadNullable => + parameter.symbol is not null ? symbolAccessor.IsReadNullable(parameter.symbol) : parameter.Type.IsNullable(); + public bool IsWriteNullable => + parameter.symbol is not null ? symbolAccessor.IsWriteNullable(parameter.symbol) : parameter.Type.IsNullable(); public bool CanGet => true; public bool CanGetDirectly => true; public bool CanSet => false; diff --git a/src/Riok.Mapperly/Symbols/MethodParameter.cs b/src/Riok.Mapperly/Symbols/MethodParameter.cs index a669deae3a..5defef56c0 100644 --- a/src/Riok.Mapperly/Symbols/MethodParameter.cs +++ b/src/Riok.Mapperly/Symbols/MethodParameter.cs @@ -3,15 +3,22 @@ namespace Riok.Mapperly.Symbols; -public readonly record struct MethodParameter(int Ordinal, string Name, ITypeSymbol Type, RefKind RefKind = RefKind.None) +public readonly record struct MethodParameter( + int Ordinal, + string Name, + ITypeSymbol Type, + IParameterSymbol? symbol = null, + RefKind RefKind = RefKind.None +) { + //todo - has doubt private static readonly SymbolDisplayFormat _parameterNameFormat = new( parameterOptions: SymbolDisplayParameterOptions.IncludeName, miscellaneousOptions: SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers ); public MethodParameter(IParameterSymbol symbol, ITypeSymbol parameterType) - : this(symbol.Ordinal, symbol.ToDisplayString(_parameterNameFormat), parameterType, symbol.RefKind) { } + : this(symbol.Ordinal, symbol.ToDisplayString(_parameterNameFormat), parameterType, symbol, symbol.RefKind) { } /// /// The parameter name with the verbatim identifier prefix (@) removed. From 3cb9ace625a757252e097caf2d94293d035da2a6 Mon Sep 17 00:00:00 2001 From: Abhijith T V Date: Mon, 4 May 2026 01:57:23 +0530 Subject: [PATCH 08/13] refactor: Test name based on context --- src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs | 2 +- src/Riok.Mapperly/Symbols/MethodParameter.cs | 1 - .../Mapping/ObjectPropertyFlatteningTest.cs | 2 +- .../Riok.Mapperly.Tests/Mapping/ObjectPropertyNestedTest.cs | 2 +- .../Mapping/ObjectPropertyNullableTest.cs | 6 +++--- .../Mapping/ObjectPropertyValueMethodTest.cs | 4 ++-- 6 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs index 62e336d236..1f94a5c7e4 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs @@ -39,7 +39,7 @@ public abstract class MethodMapping : ITypeMapping, IParameterizedMapping protected MethodMapping(ITypeSymbol sourceType, ITypeSymbol targetType) { TargetType = targetType; - SourceParameter = new MethodParameter(SourceParameterIndex, DefaultSourceParameterName, sourceType, null); + SourceParameter = new MethodParameter(SourceParameterIndex, DefaultSourceParameterName, sourceType); _returnType = targetType; } diff --git a/src/Riok.Mapperly/Symbols/MethodParameter.cs b/src/Riok.Mapperly/Symbols/MethodParameter.cs index 5defef56c0..48caa65e19 100644 --- a/src/Riok.Mapperly/Symbols/MethodParameter.cs +++ b/src/Riok.Mapperly/Symbols/MethodParameter.cs @@ -11,7 +11,6 @@ public readonly record struct MethodParameter( RefKind RefKind = RefKind.None ) { - //todo - has doubt private static readonly SymbolDisplayFormat _parameterNameFormat = new( parameterOptions: SymbolDisplayParameterOptions.IncludeName, miscellaneousOptions: SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers diff --git a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyFlatteningTest.cs b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyFlatteningTest.cs index bffe8ee592..93f40a8f11 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyFlatteningTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyFlatteningTest.cs @@ -774,7 +774,7 @@ public Task ManualUnflattenedPropertyTargetPropertyNotFoundShouldDiagnostic() } [Fact] - public void ManualNestedPropertyNullablePath() + public void ManualNestedPropertyNullablePathShouldDiagnoseNullableProperty() { var source = TestSourceBuilder.MapperWithBodyAndTypes( "[MapProperty(\"Value1.Value1.Id1\", \"Value2.Value2.Id2\")]" diff --git a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyNestedTest.cs b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyNestedTest.cs index 3b50e192c8..c514bf8c71 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyNestedTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyNestedTest.cs @@ -31,7 +31,7 @@ public void ManualNestedToNestedProperty() } [Fact] - public void ManualNullableNestedToNullableNestedProperty() + public void ManualNullableNestedToNullableNestedPropertyShouldDiagnoseNullableSourceProperty() { var source = TestSourceBuilder.MapperWithBodyAndTypes( """ diff --git a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyNullableTest.cs b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyNullableTest.cs index 0d23b22e51..5ea2725dc5 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyNullableTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyNullableTest.cs @@ -1049,7 +1049,7 @@ public record A: ABase; } [Fact] - public void MaybeNullSourceToMaybeNullTargetProperty() + public void MaybeNullSourceToMaybeNullTargetPropertyShouldDiagnoseNullableSource() { var source = TestSourceBuilder.Mapping( "A", @@ -1155,7 +1155,7 @@ class B } [Fact] - public void MaybeNullSourceClassToMaybeNullTargetClassPropertyShouldSetNull() + public void MaybeNullSourceClassToMaybeNullTargetClassPropertyShouldDiagnoseNullableSourceClass() { var source = TestSourceBuilder.Mapping( "A", @@ -1199,7 +1199,7 @@ class B } [Fact] - public void MaybeNullSourceClassToMaybeNullTargetClassPropertyWithNoNullAssignment() + public void MaybeNullSourceClassToMaybeNullTargetClassPropertyWithNoNullAssignmentShouldDiagnoseNullableSourceClass() { var source = TestSourceBuilder.Mapping( "A", diff --git a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyValueMethodTest.cs b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyValueMethodTest.cs index 757e740cb4..6fc6eead19 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyValueMethodTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyValueMethodTest.cs @@ -611,7 +611,7 @@ public Task ShouldReportNonExistentStaticClassExternalMethodWithString() } [Fact] - public void MethodToMaybeNullTargetPropertyShouldAllowNullableReturnType() + public void ReturnNullableMethodToMaybeNullTargetPropertyShouldDiagnoseNullableReturnType() { var source = TestSourceBuilder.MapperWithBodyAndTypes( """ @@ -692,7 +692,7 @@ public void ReturnMaybeNullMethodToNonNullableTargetShouldDiagnostic() } [Fact] - public void ReturnMaybeNullMethodToMaybeNullTargetProperty() + public void ReturnMaybeNullMethodToMaybeNullTargetPropertyShouldDiagnostic() { var source = TestSourceBuilder.MapperWithBodyAndTypes( """ From 15b3c81985ccc12494802680b1fa1c0a2e497ca6 Mon Sep 17 00:00:00 2001 From: Abhijith T V Date: Mon, 4 May 2026 02:40:47 +0530 Subject: [PATCH 09/13] refactor: Refactor the code --- src/Riok.Mapperly/Descriptors/SymbolAccessor.cs | 12 ------------ src/Riok.Mapperly/Symbols/Members/EmptyMemberPath.cs | 5 +++-- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs b/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs index b2b19f894d..fa80c7ea6a 100644 --- a/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs +++ b/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs @@ -91,18 +91,6 @@ private bool IsAccessible(ISymbol symbol, MemberVisibility visibility) }; } - public bool IsNullable(ISymbol symbol) - { - return symbol switch - { - ITypeSymbol t => t.IsNullable(), - IPropertySymbol p => p.Type.IsNullable(), - IFieldSymbol f => f.Type.IsNullable(), - IParameterSymbol p => p.Type.IsNullable(), - _ => false, - }; - } - public bool IsReadNullable(ISymbol symbol) { return symbol switch diff --git a/src/Riok.Mapperly/Symbols/Members/EmptyMemberPath.cs b/src/Riok.Mapperly/Symbols/Members/EmptyMemberPath.cs index b027053fe5..7ffe5c04be 100644 --- a/src/Riok.Mapperly/Symbols/Members/EmptyMemberPath.cs +++ b/src/Riok.Mapperly/Symbols/Members/EmptyMemberPath.cs @@ -7,9 +7,10 @@ namespace Riok.Mapperly.Symbols.Members; public class EmptyMemberPath(ITypeSymbol rootType) : MemberPath(rootType, []) { public override IMappableMember? Member => null; - public override ITypeSymbol MemberReadType => RootType; //after confirmation, add read null - public override ITypeSymbol MemberWriteType => RootType; //after confirmation, add write null + public override ITypeSymbol MemberReadType => RootType; + + public override ITypeSymbol MemberWriteType => RootType; public override string ToDisplayString(bool includeRootType = true, bool includeMemberType = true) => includeRootType ? RootType.ToDisplayString() : string.Empty; From 4c6ff074db4f282e1c9359587011e98608f4f74b Mon Sep 17 00:00:00 2001 From: Abhijith T V Date: Tue, 5 May 2026 00:42:29 +0530 Subject: [PATCH 10/13] refactor: Tried fix method with nullable method --- .../Descriptors/UserMethodMappingExtractor.cs | 6 +++ .../Mapping/ObjectPropertyFromSourceTest.cs | 38 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs index c2fce437bb..8061efbc9d 100644 --- a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs +++ b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs @@ -268,6 +268,12 @@ string sourceParameterName ) { var targetType = ctx.SymbolAccessor.UpgradeNullable(method.ReturnType); + + if (ctx.SymbolAccessor.TryHasAttribute(method.GetReturnTypeAttributes())) + { + targetType = targetType.WithNullableAnnotation(NullableAnnotation.Annotated); + } + if (!targetType.IsNullable() || ctx.SymbolAccessor.TryHasAttribute(method.GetReturnTypeAttributes())) { return (targetType, UserImplementedMethodMapping.TargetNullability.NeverNull); diff --git a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyFromSourceTest.cs b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyFromSourceTest.cs index 9a94d5ad10..b6b34bed6c 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyFromSourceTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyFromSourceTest.cs @@ -461,6 +461,44 @@ class B """ ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic( + DiagnosticDescriptors.NullableSourceTypeToNonNullableTargetType, + "Mapping the nullable source of type string? to target of type string which is not nullable" + ) + .HaveAssertedAllDiagnostics() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.Value = BuildValue(source) ?? throw new global::System.NullReferenceException("BuildValue returned null"); + return target; + """ + ); + } + + [Fact] + public void ReturnMaybeNullMethodToMaybeNullTargetPropertyv2() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapPropertyFromSource(nameof(B.Value), Use = nameof(BuildValue))] + partial B Map(A source); + + [return: System.Diagnostics.CodeAnalysis.MaybeNull] + string BuildValue(A a) => a.Name; + """, + "class A { public string Name { get; set; } }", + """ + class B + { + [System.Diagnostics.CodeAnalysis.AllowNull] + public string Value { get; set; } = default!; + } + """ + ); + TestHelper .GenerateMapper(source) .Should() From 7368b77e97da22850999efa4674370edf398053c Mon Sep 17 00:00:00 2001 From: Abhijith T V Date: Tue, 5 May 2026 01:21:25 +0530 Subject: [PATCH 11/13] feat: Add tests --- .../Mapping/ObjectPropertyFromSourceTest.cs | 4 +- .../Mapping/ObjectPropertyNullableTest.cs | 114 ++++++++++++++++++ .../Mapping/ObjectPropertyValueMethodTest.cs | 91 ++++++++++++++ 3 files changed, 207 insertions(+), 2 deletions(-) diff --git a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyFromSourceTest.cs b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyFromSourceTest.cs index b6b34bed6c..26d12353c4 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyFromSourceTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyFromSourceTest.cs @@ -441,7 +441,7 @@ public Task ShouldReportNonExistentExternalMappingsOnString() } [Fact] - public void ReturnMaybeNullMethodToMaybeNullTargetProperty() + public void ReturnMaybeNullMethodToMaybeNullTargetPropertyShouldDiagnoseNullableSource() { var source = TestSourceBuilder.MapperWithBodyAndTypes( """ @@ -479,7 +479,7 @@ class B } [Fact] - public void ReturnMaybeNullMethodToMaybeNullTargetPropertyv2() + public void ReturnMaybeNullMethodToAllowNullTargetProperty() { var source = TestSourceBuilder.MapperWithBodyAndTypes( """ diff --git a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyNullableTest.cs b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyNullableTest.cs index 5ea2725dc5..2bd1d3cedb 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyNullableTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyNullableTest.cs @@ -1279,4 +1279,118 @@ class B """ ); } + + [Fact] + public void NonNullableSourceToAllowNullTargetProperty() + { + var source = TestSourceBuilder.Mapping( + "A", + "B", + "class A { public string Name { get; set; } }", + """ + class B + { + [System.Diagnostics.CodeAnalysis.AllowNull] + public string Name { get; set; } = default!; + } + """ + ); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.Name = source.Name; + return target; + """ + ); + } + + [Fact] + public void MaybeNullSourceClassToAllowNullTargetClassPropertyShouldSetNull() + { + var source = TestSourceBuilder.Mapping( + "A", + "B", + """ + class A + { + [System.Diagnostics.CodeAnalysis.MaybeNull] + public C Value { get; set; } = default!; + } + """, + """ + class B + { + [System.Diagnostics.CodeAnalysis.AllowNull] + public D Value { get; set; } = default!; + } + """, + "class C { public string V { get; set; } }", + "class D { public string V { get; set; } }" + ); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveMapMethodBody( + """ + var target = new global::B(); + if (source.Value != null) + { + target.Value = MapToD(source.Value); + } + else + { + target.Value = null; + } + return target; + """ + ); + } + + [Fact] + public void MaybeNullSourceClassToAllowNullTargetClassPropertyWithNoNullAssignment() + { + var source = TestSourceBuilder.Mapping( + "A", + "B", + TestSourceBuilderOptions.Default with + { + AllowNullPropertyAssignment = false, + }, + """ + class A + { + [System.Diagnostics.CodeAnalysis.MaybeNull] + public C Value { get; set; } = default!; + } + """, + """ + class B + { + [System.Diagnostics.CodeAnalysis.AllowNull] + public D Value { get; set; } = default!; + } + """, + "class C { public string V { get; set; } }", + "class D { public string V { get; set; } }" + ); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveMapMethodBody( + """ + var target = new global::B(); + if (source.Value != null) + { + target.Value = MapToD(source.Value); + } + return target; + """ + ); + } } diff --git a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyValueMethodTest.cs b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyValueMethodTest.cs index 6fc6eead19..f7b6a4e28e 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyValueMethodTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyValueMethodTest.cs @@ -719,4 +719,95 @@ class B ) .HaveAssertedAllDiagnostics(); } + + [Fact] + public void MethodToAllowNullTargetPropertyShouldAllowNullableReturnType() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapValue("Value", Use = nameof(BuildValue))] partial B Map(A source); + string? BuildValue() => null; + """, + "class A;", + """ + class B + { + [System.Diagnostics.CodeAnalysis.AllowNull] + public string Value { get; set; } = default!; + } + """ + ); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.Value = BuildValue(); + return target; + """ + ); + } + + [Fact] + public void MethodToAllowTargetPropertyShouldAllowNonNullableReturnType() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapValue("Value", Use = nameof(BuildValue))] partial B Map(A source); + string BuildValue() => "hello"; + """, + "class A;", + """ + class B + { + [System.Diagnostics.CodeAnalysis.AllowNull] + public string Value { get; set; } = default!; + } + """ + ); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.Value = BuildValue(); + return target; + """ + ); + } + + [Fact] + public void ReturnMaybeNullMethodToAllowNullTargetProperty() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapValue("Value", Use = nameof(BuildValue))] partial B Map(A source); + [return: System.Diagnostics.CodeAnalysis.MaybeNull] + string BuildValue() => null!; + """, + "class A;", + """ + class B + { + [System.Diagnostics.CodeAnalysis.AllowNull] + public string Value { get; set; } = default!; + } + """ + ); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.Value = BuildValue(); + return target; + """ + ); + } } From cc324ebaec7b8b8775293ae8d04c0d59833d7c57 Mon Sep 17 00:00:00 2001 From: Abhijith T V Date: Tue, 5 May 2026 01:26:44 +0530 Subject: [PATCH 12/13] chore: Update the breaking change doc --- docs/docs/breaking-changes/5-0.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/docs/breaking-changes/5-0.md b/docs/docs/breaking-changes/5-0.md index e6a26b7dfd..7bb6bb04c8 100644 --- a/docs/docs/breaking-changes/5-0.md +++ b/docs/docs/breaking-changes/5-0.md @@ -17,7 +17,8 @@ description: How to upgrade to Mapperly 5.0 and a list of all its breaking chang - `Stack` deep cloning now preserves the order of elements by default. - Inaccessible members from other assemblies are now included in the mapping process if `IncludedMembers` or `IncludedConstructors` options are configured to include them. - Constructor mappings with nullable source types no longer generate null guards when the constructor parameter accepts null, see below. -- `MaybeNull` attributes are now respected for fields, properties and constructor parameters. +- `MaybeNull` attributes are now respected for fields, properties and constructor parameters in read context. +- `AllowNull` attributes are now respected for fields, properties and constructor parameters in write context. - Copy constructors are no longer selected over constructors annotated with `[MapperConstructor]` or when `[MapProperty]` configurations are present. ## Inaccessible members from other assemblies included From 737c17d30d185c18fb084ec4967f7d59d798570a Mon Sep 17 00:00:00 2001 From: Abhijith T V Date: Wed, 6 May 2026 21:57:09 +0530 Subject: [PATCH 13/13] refactor: Refactor the code --- docs/docs/breaking-changes/5-0.md | 4 ++-- .../MappingBodyBuilders/SourceValueBuilder.cs | 15 +----------- .../Descriptors/SymbolAccessor.cs | 24 +++++++++++++++++++ .../Descriptors/UserMethodMappingExtractor.cs | 8 ++----- .../Symbols/Members/EmptyMemberPath.cs | 4 ++++ .../Symbols/Members/MemberPath.cs | 4 ++-- .../Symbols/Members/NonEmptyMemberPath.cs | 4 ++++ .../Symbols/Members/ParameterSourceMember.cs | 4 ++-- src/Riok.Mapperly/Symbols/MethodParameter.cs | 2 +- 9 files changed, 42 insertions(+), 27 deletions(-) diff --git a/docs/docs/breaking-changes/5-0.md b/docs/docs/breaking-changes/5-0.md index 7bb6bb04c8..be6df78517 100644 --- a/docs/docs/breaking-changes/5-0.md +++ b/docs/docs/breaking-changes/5-0.md @@ -17,8 +17,8 @@ description: How to upgrade to Mapperly 5.0 and a list of all its breaking chang - `Stack` deep cloning now preserves the order of elements by default. - Inaccessible members from other assemblies are now included in the mapping process if `IncludedMembers` or `IncludedConstructors` options are configured to include them. - Constructor mappings with nullable source types no longer generate null guards when the constructor parameter accepts null, see below. -- `MaybeNull` attributes are now respected for fields, properties and constructor parameters in read context. -- `AllowNull` attributes are now respected for fields, properties and constructor parameters in write context. +- `MaybeNull` attributes are now respected for fields, properties and constructor parameters when reading values. +- `AllowNull` attributes are now respected for fields, properties and constructor parameters when writing values. - Copy constructors are no longer selected over constructors annotated with `[MapperConstructor]` or when `[MapProperty]` configurations are present. ## Inaccessible members from other assemblies included diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/SourceValueBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/SourceValueBuilder.cs index ee4e625b17..c7cf9d3347 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/SourceValueBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/SourceValueBuilder.cs @@ -201,7 +201,7 @@ private static bool TryBuildMethodProvidedSourceValue( ctx.BuilderContext.ReportDiagnostic( DiagnosticDescriptors.MapValueMethodTypeMismatch, methodReferenceConfiguration.Name, - FormattedReturnType(namedMethodCandidates[0]), + ctx.BuilderContext.SymbolAccessor.UpgradeReturnNullable(namedMethodCandidates[0]).ToDisplayString(), memberMappingInfo.TargetMember.ToDisplayString() ); sourceValue = null; @@ -219,17 +219,4 @@ private static bool TryBuildMethodProvidedSourceValue( ); return true; } - - private static string FormattedReturnType(IMethodSymbol methodSymbol) - { - bool hasNullableAttribute = methodSymbol - .GetReturnTypeAttributes() - .Any(a => string.Equals(a.AttributeClass?.Name, nameof(MaybeNullAttribute))); - - var effectiveType = hasNullableAttribute - ? methodSymbol.ReturnType.WithNullableAnnotation(NullableAnnotation.Annotated) - : methodSymbol.ReturnType; - - return effectiveType.ToDisplayString(); - } } diff --git a/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs b/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs index fa80c7ea6a..8699b0f0f4 100644 --- a/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs +++ b/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs @@ -146,6 +146,30 @@ public bool CanAssign(ITypeSymbol sourceType, ITypeSymbol targetType) public MethodParameter WrapMethodParameter(IParameterSymbol symbol) => new(symbol, UpgradeNullable(symbol.Type)); + public ITypeSymbol UpgradeReturnNullable(IMethodSymbol methodSymbol) + { + TryUpgradeReturnNullable(methodSymbol, out var upgradedReturnSymbol); + return upgradedReturnSymbol ?? methodSymbol.ReturnType; + } + + private bool TryUpgradeReturnNullable(IMethodSymbol methodSymbol, [NotNullWhen(true)] out ITypeSymbol? upgradedReturnSymbol) + { + upgradedReturnSymbol = default; + + if (methodSymbol.ReturnsVoid || methodSymbol.ReturnType.IsValueType) + { + return false; + } + + if (!TryHasAttribute(methodSymbol.GetReturnTypeAttributes())) + { + return false; + } + + upgradedReturnSymbol = methodSymbol.ReturnType.WithNullableAnnotation(NullableAnnotation.Annotated); + return true; + } + /// /// Upgrade the nullability of a symbol from to . /// Value types are not upgraded. diff --git a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs index 8061efbc9d..34d3c24cd4 100644 --- a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs +++ b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs @@ -267,12 +267,8 @@ private static (ITypeSymbol, UserImplementedMethodMapping.TargetNullability) Bui string sourceParameterName ) { - var targetType = ctx.SymbolAccessor.UpgradeNullable(method.ReturnType); - - if (ctx.SymbolAccessor.TryHasAttribute(method.GetReturnTypeAttributes())) - { - targetType = targetType.WithNullableAnnotation(NullableAnnotation.Annotated); - } + var effectiveReturnType = ctx.SymbolAccessor.UpgradeReturnNullable(method); + var targetType = ctx.SymbolAccessor.UpgradeNullable(effectiveReturnType); if (!targetType.IsNullable() || ctx.SymbolAccessor.TryHasAttribute(method.GetReturnTypeAttributes())) { diff --git a/src/Riok.Mapperly/Symbols/Members/EmptyMemberPath.cs b/src/Riok.Mapperly/Symbols/Members/EmptyMemberPath.cs index 7ffe5c04be..f568d04c16 100644 --- a/src/Riok.Mapperly/Symbols/Members/EmptyMemberPath.cs +++ b/src/Riok.Mapperly/Symbols/Members/EmptyMemberPath.cs @@ -12,6 +12,10 @@ public class EmptyMemberPath(ITypeSymbol rootType) : MemberPath(rootType, []) public override ITypeSymbol MemberWriteType => RootType; + public override bool IsAnyReadNullable() => false; + + public override bool IsWriteNullable() => false; + public override string ToDisplayString(bool includeRootType = true, bool includeMemberType = true) => includeRootType ? RootType.ToDisplayString() : string.Empty; } diff --git a/src/Riok.Mapperly/Symbols/Members/MemberPath.cs b/src/Riok.Mapperly/Symbols/Members/MemberPath.cs index 6fa28bd07b..5318be90d1 100644 --- a/src/Riok.Mapperly/Symbols/Members/MemberPath.cs +++ b/src/Riok.Mapperly/Symbols/Members/MemberPath.cs @@ -70,9 +70,9 @@ public IEnumerable> ObjectReadPathNullableSubPath } } - public bool IsAnyReadNullable() => Path.Any(x => x.IsReadNullable); + public abstract bool IsAnyReadNullable(); - public bool IsWriteNullable() => Path[^1].IsWriteNullable; + public abstract bool IsWriteNullable(); public bool IsAnyObjectReadPathNullable() => ObjectPath.Any(p => p.IsReadNullable); diff --git a/src/Riok.Mapperly/Symbols/Members/NonEmptyMemberPath.cs b/src/Riok.Mapperly/Symbols/Members/NonEmptyMemberPath.cs index dc78558ba2..0dbedd83ff 100644 --- a/src/Riok.Mapperly/Symbols/Members/NonEmptyMemberPath.cs +++ b/src/Riok.Mapperly/Symbols/Members/NonEmptyMemberPath.cs @@ -36,6 +36,10 @@ public NonEmptyMemberPath(ITypeSymbol rootType, IReadOnlyList p public MemberPathSetter BuildSetter(SimpleMappingBuilderContext ctx) => MemberPathSetter.Build(ctx, this); + public override bool IsAnyReadNullable() => Path.Any(x => x.IsReadNullable); + + public override bool IsWriteNullable() => Path[^1].IsWriteNullable; + public override string ToDisplayString(bool includeRootType = true, bool includeMemberType = true) { var ofType = includeMemberType ? $" of type {Member.Type.ToDisplayString()}" : null; diff --git a/src/Riok.Mapperly/Symbols/Members/ParameterSourceMember.cs b/src/Riok.Mapperly/Symbols/Members/ParameterSourceMember.cs index e0e39d8000..086c0c5552 100644 --- a/src/Riok.Mapperly/Symbols/Members/ParameterSourceMember.cs +++ b/src/Riok.Mapperly/Symbols/Members/ParameterSourceMember.cs @@ -20,9 +20,9 @@ public class ParameterSourceMember(MethodParameter parameter, SymbolAccessor sym public ITypeSymbol Type => parameter.Type; public INamedTypeSymbol? ContainingType => null; public bool IsReadNullable => - parameter.symbol is not null ? symbolAccessor.IsReadNullable(parameter.symbol) : parameter.Type.IsNullable(); + parameter.Symbol is not null ? symbolAccessor.IsReadNullable(parameter.Symbol) : parameter.Type.IsNullable(); public bool IsWriteNullable => - parameter.symbol is not null ? symbolAccessor.IsWriteNullable(parameter.symbol) : parameter.Type.IsNullable(); + parameter.Symbol is not null ? symbolAccessor.IsWriteNullable(parameter.Symbol) : parameter.Type.IsNullable(); public bool CanGet => true; public bool CanGetDirectly => true; public bool CanSet => false; diff --git a/src/Riok.Mapperly/Symbols/MethodParameter.cs b/src/Riok.Mapperly/Symbols/MethodParameter.cs index 48caa65e19..1b35414a50 100644 --- a/src/Riok.Mapperly/Symbols/MethodParameter.cs +++ b/src/Riok.Mapperly/Symbols/MethodParameter.cs @@ -7,7 +7,7 @@ public readonly record struct MethodParameter( int Ordinal, string Name, ITypeSymbol Type, - IParameterSymbol? symbol = null, + IParameterSymbol? Symbol = null, RefKind RefKind = RefKind.None ) {