Skip to content

Commit

Permalink
fix: do not build existing target mappings for immutable targets and …
Browse files Browse the repository at this point in the history
…do not report RMG009 for auto matched members
  • Loading branch information
latonz committed Jul 22, 2024
1 parent b754503 commit 2fc1d6b
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using System.Reflection;

Check failure on line 2 in src/Riok.Mapperly/Descriptors/MappingBodyBuilders/ObjectMemberMappingBodyBuilder.cs

View workflow job for this annotation

GitHub Actions / lint-dotnet

Using directive is unnecessary.
using Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Descriptors.Mappings.MemberMappings;
Expand All @@ -22,6 +23,10 @@ public static void BuildMappingBody(MappingBuilderContext ctx, IMemberAssignment
{
mappingCtx.SetTargetMemberMapped(initOnlyTargetMember);
}

// do not report "no member mapping" for existing target mappings
mappingCtx.MappingAdded();

mappingCtx.AddDiagnostics();
}

Expand Down Expand Up @@ -49,6 +54,17 @@ public static bool ValidateMappingSpecification(
// the target member path is readonly or not accessible
if (!targetMemberPath.Member.CanSet)
{
// If the mapping is matched automatically without any configuration
// mark both members as mapped,
// as the "CannotMapToReadOnlyMember" diagnostic should be informative.
// Also, an additional diagnostic may not even be expected here:
// for example, if the source and target contain the same computed read-only member.
if (memberInfo.IsAutoMatch && sourceMemberPath?.Type == SourceMemberType.Member)
{
ctx.SetMembersMapped(memberInfo);
return false;
}

ctx.BuilderContext.ReportDiagnostic(
DiagnosticDescriptors.CannotMapToReadOnlyMember,
memberInfo.DescribeSource(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ public static class NewInstanceObjectMemberMappingBuilder
if (ctx.Target.SpecialType != SpecialType.None || ctx.Source.SpecialType != SpecialType.None)
return null;

if (ctx.Target.IsImmutable())
return null;

if (ctx.Source.IsEnum() || ctx.Target.IsEnum())
return null;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ public MemberMappingInfo(NonEmptyMemberPath targetMember, MemberValueMappingConf
private string DebuggerDisplay =>
$"{SourceMember?.MemberPath.FullName ?? ValueConfiguration?.DescribeValue()} => {TargetMember.FullName}";

/// <summary>
/// Returns <c>true</c> if the <see cref="SourceMember"/> and <see cref="TargetMember"/>
/// were matched automatically by Mapperly without any additional user configuration.
/// </summary>
public bool IsAutoMatch => Configuration == null && ValueConfiguration == null;

public TypeMappingKey ToTypeMappingKey()
{
if (SourceMember == null)
Expand Down
171 changes: 171 additions & 0 deletions test/Riok.Mapperly.Tests/Mapping/ObjectPropertyExistingInstanceTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
namespace Riok.Mapperly.Tests.Mapping;

public class ObjectPropertyExistingInstanceTest
{
[Fact]
public void ReadOnlyPropertyShouldMap()
{
var source = TestSourceBuilder.Mapping(
"A",
"B",
"""
public class A
{
public int? IntValue { get; set; }
public C NestedValue { get; } = null!;
}
""",
"""
public class B
{
public int? IntValue { get; set; }
public D NestedValue { get; }= null!;
}
""",
"""
public class C
{
public int Value { get; set; }
public int? Value2 { get; set; }
public int? NullableValue { get; set; }
}
""",
"""
public class D
{
public string? Value { get; set; }
public string Value2 { get; set; }
public string? NullableValue { get; set; }
}
"""
);

TestHelper
.GenerateMapper(source, TestHelperOptions.AllowAndIncludeAllDiagnostics)
.Should()
.HaveAssertedAllDiagnostics()
.HaveMapMethodBody(
"""
var target = new global::B();
target.IntValue = source.IntValue;
target.NestedValue.Value = source.NestedValue.Value.ToString();
if (source.NestedValue.Value2 != null)
{
target.NestedValue.Value2 = source.NestedValue.Value2.Value.ToString();
}
if (source.NestedValue.NullableValue != null)
{
target.NestedValue.NullableValue = source.NestedValue.NullableValue.Value.ToString();
}
else
{
target.NestedValue.NullableValue = null;
}
return target;
"""
);
}

[Fact]
public void ReadOnlyNullablePropertyShouldMap()
{
var source = TestSourceBuilder.Mapping(
"A",
"B",
"""
public class A
{
public int? IntValue { get; set; }
public C? CValue { get; }
}
""",
"""
public class B
{
public int? IntValue { get; set; }
public C? CValue { get; }
}
""",
"""
public class C
{
public int IntValue { get; set; }
}
"""
);

TestHelper
.GenerateMapper(source, TestHelperOptions.AllowAndIncludeAllDiagnostics)
.Should()
.HaveAssertedAllDiagnostics()
.HaveMapMethodBody(
"""
var target = new global::B();
target.IntValue = source.IntValue;
if (source.CValue != null && target.CValue != null)
{
target.CValue.IntValue = source.CValue.IntValue;
}
return target;
"""
);
}

[Fact]
public void ObjectsWithoutPropertiesShouldNotDiagnostic()
{
var source = TestSourceBuilder.Mapping(
"A",
"B",
"public class A { public C? IntValue { get; } }",
"public class B { public C? IntValue { get; } }",
"public class C;"
);

TestHelper
.GenerateMapper(source, TestHelperOptions.AllowAndIncludeAllDiagnostics)
.Should()
.HaveAssertedAllDiagnostics()
.HaveMapMethodBody(
"""
var target = new global::B();
return target;
"""
);
}

[Fact]
public void SameReadOnlyPropertyShouldNotDiagnostic()
{
var source = TestSourceBuilder.Mapping(
"A",
"B",
"""
public class A
{
public int? IntValue { get; set; }
public bool? ComputedIntValue => IntValue == 42;
}
""",
"""
public class B
{
public int? IntValue { get; set; }
public bool? ComputedIntValue => IntValue == 42;
}
"""
);

TestHelper
.GenerateMapper(source, TestHelperOptions.AllowAndIncludeAllDiagnostics)
.Should()
.HaveAssertedAllDiagnostics()
.HaveMapMethodBody(
"""
var target = new global::B();
target.IntValue = source.IntValue;
return target;
"""
);
}
}

0 comments on commit 2fc1d6b

Please sign in to comment.