Skip to content

Commit

Permalink
fix: add modifying nested struct diagnostic
Browse files Browse the repository at this point in the history
  • Loading branch information
TimothyMakkison committed Aug 8, 2023
1 parent 3ee2e96 commit c15c130
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/Riok.Mapperly/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Configuration;
using Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext;
Expand Down Expand Up @@ -95,6 +96,7 @@ PropertyMappingConfiguration config
BuildMemberAssignmentMapping(ctx, sourceMemberPath, targetMemberPath);
}

[SuppressMessage(" Meziantou.Analyzer", "MA0051:MethodIsTooLong")]
public static bool ValidateMappingSpecification(
IMembersBuilderContext<IMapping> ctx,
MemberPath sourceMemberPath,
Expand Down Expand Up @@ -132,6 +134,11 @@ public static bool ValidateMappingSpecification(
return false;
}

// cannot assign to intermediate value type, error CS1612
// invalid mapping a value type has a property set
if (!ValidateStructModification(ctx, sourceMemberPath, targetMemberPath))
return false;

// a target member path part is init only
var noInitOnlyPath = allowInitOnlyMember ? targetMemberPath.ObjectPath : targetMemberPath.Path;
if (noInitOnlyPath.Any(p => p.IsInitOnly))
Expand Down Expand Up @@ -179,6 +186,43 @@ public static bool ValidateMappingSpecification(
return true;
}

private static bool ValidateStructModification(
IMembersBuilderContext<IMapping> ctx,
MemberPath sourceMemberPath,
MemberPath targetMemberPath
)
{
if (targetMemberPath.Path.Count <= 1)
return true;

// 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;
}

return true;
}

private static void BuildMemberAssignmentMapping(
IMembersContainerBuilderContext<IMemberAssignmentTypeMapping> ctx,
MemberPath sourceMemberPath,
Expand Down
9 changes: 9 additions & 0 deletions src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}
55 changes: 55 additions & 0 deletions test/Riok.Mapperly.Tests/Mapping/ObjectPropertyTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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, see CS1612"
)
.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;
"""
);
}
}

0 comments on commit c15c130

Please sign in to comment.