diff --git a/src/Riok.Mapperly/AnalyzerReleases.Shipped.md b/src/Riok.Mapperly/AnalyzerReleases.Shipped.md index 965710dd25..745881de50 100644 --- a/src/Riok.Mapperly/AnalyzerReleases.Shipped.md +++ b/src/Riok.Mapperly/AnalyzerReleases.Shipped.md @@ -105,3 +105,4 @@ RMG043 | Mapper | Warning | Enum fallback values are only supported for the RMG044 | Mapper | Warning | An ignored enum member can not be found on the source enum RMG045 | Mapper | Warning | An ignored enum member can not be found on the target enum RMG046 | Mapper | Error | The used C# language version is not supported by Mapperly, Mapperly requires at least C# 9.0 +RMG047 | Mapper | Error | Cannot map to member path due to modifying a temporary value, see CS1612 diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/ObjectMemberMappingBodyBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/ObjectMemberMappingBodyBuilder.cs index 58a4aa7528..7a902f25bb 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/ObjectMemberMappingBodyBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/ObjectMemberMappingBodyBuilder.cs @@ -132,6 +132,36 @@ public static bool ValidateMappingSpecification( return false; } + // cannot assign to intermediate value type, error CS1612 + // invalid mapping a value type has a property set + if (targetMemberPath.Path.Count > 1) + { + // iterate backwards, if a reference type property is found then path is valid + // if a value type property is found then invalid, a temporary struct is being modified + for (var i = targetMemberPath.Path.Count - 2; i >= 0; i--) + { + var member = targetMemberPath.Path[i]; + if (member is PropertyMember { Type: { IsValueType: true, IsRefLikeType: false } }) + { + ctx.BuilderContext.ReportDiagnostic( + DiagnosticDescriptors.CannotMapToTemporarySourceMember, + ctx.Mapping.SourceType, + sourceMemberPath.FullName, + sourceMemberPath.Member.Type, + ctx.Mapping.TargetType, + targetMemberPath.FullName, + targetMemberPath.Member.Type, + member.Name, + member.Type + ); + return false; + } + + if (member is PropertyMember { Type.IsReferenceType: true }) + break; + } + } + // a target member path part is init only var noInitOnlyPath = allowInitOnlyMember ? targetMemberPath.ObjectPath : targetMemberPath.Path; if (noInitOnlyPath.Any(p => p.IsInitOnly)) diff --git a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs index d28c7ef23e..7712221e9c 100644 --- a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs +++ b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs @@ -410,4 +410,13 @@ internal static class DiagnosticDescriptors DiagnosticSeverity.Error, true ); + + public static readonly DiagnosticDescriptor CannotMapToTemporarySourceMember = new DiagnosticDescriptor( + "RMG047", + "Cannot map to member path due to modifying a temporary value, see CS1612", + "Cannot map from member {0}.{1} of type {2} to member path {3}.{4} of type {5} because {6}.{7} is a value type, returning a temporary value, see CS1612", + DiagnosticCategories.Mapper, + DiagnosticSeverity.Error, + true + ); } diff --git a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyTest.cs b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyTest.cs index fbc7c3c235..e631a5e74f 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyTest.cs @@ -443,4 +443,59 @@ public void ShouldIgnoreStaticConstructorAndDiagnostic() .HaveDiagnostic(DiagnosticDescriptors.CouldNotCreateMapping) .HaveAssertedAllDiagnostics(); } + + [Fact] + public void ModifyingTemporaryStructShouldDiagnostic() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapProperty("StringValue", "NestedValue.StringValue")] + partial B Map(A src); + """, + "class A { public string StringValue { get; set; } }", + "class B { public C NestedValue { get; set; } }", + "struct C { public string StringValue { get; set; } }" + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowAllDiagnostics) + .Should() + .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotMapped) + .HaveDiagnostic( + DiagnosticDescriptors.CannotMapToTemporarySourceMember, + "Cannot map from member A.StringValue of type string to member path B.NestedValue.StringValue of type string because NestedValue.C is a value type, returning a temporary value" + ) + .HaveAssertedAllDiagnostics() + .HaveSingleMethodBody( + """ + var target = new global::B(); + return target; + """ + ); + } + + [Fact] + public void ModifyingPathIfClassPrecedesShouldNotDiagnostic() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapProperty("StringValue", "NestedValue.StringValue")] + partial B Map(A src); + """, + "class A { public string StringValue { get; set; } }", + "struct B { public C NestedValue { get; set; } }", + "class C { public string StringValue { get; set; } }" + ); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.NestedValue.StringValue = src.StringValue; + return target; + """ + ); + } }