Skip to content

Commit

Permalink
feat: add RMG072 and RMG073 diagnostics for named existing target map…
Browse files Browse the repository at this point in the history
…pings
  • Loading branch information
clegoz committed Feb 20, 2025
1 parent cd6fdfe commit 44c277f
Show file tree
Hide file tree
Showing 7 changed files with 338 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Descriptors.Mappings.ExistingTarget;
Expand Down Expand Up @@ -187,6 +188,9 @@ MemberMappingInfo memberMappingInfo
// even if a mapping validation fails
ctx.ConsumeMemberConfigs(memberMappingInfo);

if (TryAddNamedExistingTargetMapping(ctx, memberMappingInfo))
return;

if (TryAddExistingTargetMapping(ctx, memberMappingInfo))
return;

Expand All @@ -206,40 +210,150 @@ MemberMappingInfo memberMappingInfo
}
}

private static bool TryAddExistingTargetMapping(
private static bool TryAddNamedExistingTargetMapping(
IMembersContainerBuilderContext<IMemberAssignmentTypeMapping> ctx,
MemberMappingInfo memberMappingInfo
)
{
// can only map with an existing target from a source member
if (memberMappingInfo.SourceMember == null)
if (memberMappingInfo.SourceMember is null)
return false;

var sourceMemberPath = memberMappingInfo.SourceMember;
if (memberMappingInfo.Configuration?.Use is null)
return false;

var use = memberMappingInfo.Configuration.Use;
var existingTargetMapping = ctx.BuilderContext.FindExistingTargetNamedMapping(use);
if (existingTargetMapping is null)
return false;

var sourceMemberPath = memberMappingInfo.SourceMember.MemberPath;
var targetMemberPath = memberMappingInfo.TargetMember;

IExistingTargetMapping? existingTargetMapping;
if (memberMappingInfo.Configuration?.Use is not null)
var differentSourceType = !SymbolEqualityComparer.IncludeNullability.Equals(
sourceMemberPath.MemberType,
existingTargetMapping.SourceType
);
var differentTargetType = !SymbolEqualityComparer.IncludeNullability.Equals(
targetMemberPath.MemberType,
existingTargetMapping.TargetType
);

if (!differentSourceType && !differentTargetType)
{
existingTargetMapping = ctx.BuilderContext.FindExistingTargetNamedMapping(memberMappingInfo.Configuration.Use);
var sourceMemberGetter = sourceMemberPath.BuildGetter(ctx.BuilderContext);
var targetMemberGetter = targetMemberPath.BuildGetter(ctx.BuilderContext);

var memberMapping = new MemberExistingTargetMapping(
existingTargetMapping,
sourceMemberGetter,
targetMemberGetter,
memberMappingInfo
);
ctx.AddMemberAssignmentMapping(memberMapping);
return true;
}
else

if (differentSourceType)
{
ctx.BuilderContext.ReportDiagnostic(
DiagnosticDescriptors.ReferencedMappingSourceTypeMismatch,
use,
existingTargetMapping.SourceType.ToDisplayString(),
sourceMemberPath.MemberType.ToDisplayString()
);
}

if (differentTargetType)
{
ctx.BuilderContext.ReportDiagnostic(
DiagnosticDescriptors.ReferencedMappingTargetTypeMismatch,
use,
existingTargetMapping.TargetType.ToDisplayString(),
targetMemberPath.MemberType.ToDisplayString()
);
}

return TryAddCompositeExistingTargetMapping(ctx, existingTargetMapping, memberMappingInfo);
}

private static bool TryAddCompositeExistingTargetMapping(
IMembersContainerBuilderContext<IMemberAssignmentTypeMapping> ctx,
IExistingTargetMapping existingTargetMapping,
MemberMappingInfo memberMappingInfo
)
{
if (memberMappingInfo.SourceMember is null)
return false;

var sourceMemberPath = memberMappingInfo.SourceMember.MemberPath;
var targetMemberPath = memberMappingInfo.TargetMember;

var sourceMemberGetter = sourceMemberPath.BuildGetter(ctx.BuilderContext);
var targetMemberGetter = targetMemberPath.BuildGetter(ctx.BuilderContext);

var sourceMapping = ctx.BuilderContext.FindOrBuildMapping(sourceMemberPath.MemberType, existingTargetMapping.SourceType);
if (sourceMapping == null)
{
ctx.BuilderContext.ReportDiagnostic(
DiagnosticDescriptors.CouldNotCreateMapping,
sourceMemberPath.MemberType,
existingTargetMapping.SourceType
);

return true;
}

var targetMapping = ctx.BuilderContext.FindOrBuildMapping(targetMemberPath.MemberType, existingTargetMapping.TargetType);
if (targetMapping == null)
{
// if the member is readonly
// and the target and source path is readable,
// we try to create an existing target mapping
if (
targetMemberPath.Member is { CanSet: true, IsInitOnly: false }
|| !targetMemberPath.Path.All(op => op.CanGet)
|| !sourceMemberPath.MemberPath.Path.All(op => op.CanGet)
ctx.BuilderContext.ReportDiagnostic(
DiagnosticDescriptors.CouldNotCreateMapping,
targetMemberPath.MemberType.ToDisplayString(),
existingTargetMapping.TargetType.ToDisplayString()
);

return true;
}

ctx.AddMemberAssignmentMapping(
new CompositeMemberExistingTargetMapping(
existingTargetMapping,
sourceMapping,
sourceMemberGetter,
targetMapping,
targetMemberGetter,
memberMappingInfo
)
{
return false;
}
);
return true;
}

private static bool TryAddExistingTargetMapping(
IMembersContainerBuilderContext<IMemberAssignmentTypeMapping> ctx,
MemberMappingInfo memberMappingInfo
)
{
// can only map with an existing target from a source member
if (memberMappingInfo.SourceMember == null)
return false;

var sourceMemberPath = memberMappingInfo.SourceMember;
var targetMemberPath = memberMappingInfo.TargetMember;

existingTargetMapping = ctx.BuilderContext.FindOrBuildExistingTargetMapping(memberMappingInfo.ToTypeMappingKey());
// if the member is readonly
// and the target and source path is readable,
// we try to create an existing target mapping
if (
targetMemberPath.Member is { CanSet: true, IsInitOnly: false }
|| !targetMemberPath.Path.All(op => op.CanGet)
|| !sourceMemberPath.MemberPath.Path.All(op => op.CanGet)
)
{
return false;
}

var existingTargetMapping = ctx.BuilderContext.FindOrBuildExistingTargetMapping(memberMappingInfo.ToTypeMappingKey());
if (existingTargetMapping == null)
return false;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Riok.Mapperly.Descriptors.Mappings.ExistingTarget;
using Riok.Mapperly.Symbols.Members;

namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings;

/// <summary>
/// Creates a composite mapping for existing target
/// <code>
/// OutputMapping(SourceMapping(source), TargetMapping(target));
/// </code>
/// </summary>
public class CompositeMemberExistingTargetMapping(
IExistingTargetMapping delegateMapping,
INewInstanceMapping sourceMapping,
MemberPathGetter sourcePath,
INewInstanceMapping targetMapping,
MemberPathGetter targetPath,
MemberMappingInfo memberInfo
) : IMemberAssignmentMapping
{
public MemberMappingInfo MemberInfo { get; } = memberInfo;

public IEnumerable<StatementSyntax> Build(TypeMappingBuildContext ctx, ExpressionSyntax targetAccess)
{
var sourcePathAccess = sourcePath.BuildAccess(ctx.Source);
var source = sourceMapping.Build(ctx.WithSource(sourcePathAccess));

var targetPathAccess = targetPath.BuildAccess(targetAccess);
var target = targetMapping.Build(ctx.WithSource(targetPathAccess));

return delegateMapping.Build(ctx.WithSource(source), target);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,88 @@ public class B
return TestHelper.VerifyGenerator(source);
}

[Fact]
public Task UserImplementedExistingTargetMappingWithDifferentSourceType()
{
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"""
[MapProperty(nameof(A.Value), nameof(B.Value), Use = "MapValues")]
public partial void Map(A source, B target);
private void MapValues(F source, D target) {}
""",
"""
public class A
{
public C Value { get; set; }
}
""",
"""
public class C
{
public List<string> Values { get; set; }
}
""",
"""
public class B
{
public D Value { get; set; }
}
public class F
{
public List<string> Values { get; set; }
}
""",
"""
public class D
{
public List<string> Values { get; set; }
}
"""
);
return TestHelper.VerifyGenerator(source);
}

[Fact]
public Task UserImplementedExistingTargetMappingWithDifferentTargetType()
{
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"""
[MapProperty(nameof(A.Value), nameof(B.Value), Use = "MapValues")]
public partial void Map(A source, B target);
private void MapValues(C source, F target) {}
""",
"""
public class A
{
public C Value { get; set; }
}
""",
"""
public class C
{
public List<string> Values { get; set; }
}
""",
"""
public class B
{
public D Value { get; set; }
}
public class F
{
public List<string> Values { get; set; }
}
""",
"""
public class D
{
public List<string> Values { get; set; }
}
"""
);
return TestHelper.VerifyGenerator(source);
}

[Fact]
public Task ShouldUseReferencedMappingOnSelectedPropertiesWithExistingInstance()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//HintName: Mapper.g.cs
// <auto-generated />
#nullable enable
public partial class Mapper
{
[global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
public partial void Map(global::A source, global::B target)
{
MapValues(MapToF(source.Value), target.Value);
}

[global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
private global::F MapToF(global::C source)
{
var target = new global::F();
target.Values = source.Values;
return target;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
Diagnostics: [
{
Location: /*
{
[MapProperty(nameof(A.Value), nameof(B.Value), Use = "MapValues")]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
public partial void Map(A source, B target);
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
private void MapValues(F source, D target) {}
*/
: (11,4)-(12,44),
Message: The source type F of the referenced mapping MapValues does not match the expected type C,
Severity: Warning,
WarningLevel: 1,
Descriptor: {
Id: RMG072,
Title: The source type of the referenced mapping does not match,
MessageFormat: The source type {1} of the referenced mapping {0} does not match the expected type {2},
Category: Mapper,
DefaultSeverity: Warning,
IsEnabledByDefault: true
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//HintName: Mapper.g.cs
// <auto-generated />
#nullable enable
public partial class Mapper
{
[global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
public partial void Map(global::A source, global::B target)
{
MapValues(source.Value, MapToF(target.Value));
}

[global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
private global::F MapToF(global::D source)
{
var target = new global::F();
target.Values = source.Values;
return target;
}
}
Loading

0 comments on commit 44c277f

Please sign in to comment.