From 833f8a43e2e6298265f776fc2698485439d1c774 Mon Sep 17 00:00:00 2001 From: Lars Date: Thu, 20 Jun 2024 13:30:30 -0500 Subject: [PATCH] fix!: replace RMG017 and RMG027 with a new RMG074 (#1334) The refactored member matching works as follows: * A member matching state and context is created * For each target member a matching source is looked up Now this happens unified for all types (constructor parameter, init property, ...) * A mapping is created with a MemberMappingInfo containing the information which members are mapped. * The mapping is added to the container. Based on the MemberMappingInfo the members are marked as mapped. * refactors and improves the readability of the member matching process * Introduces a new IgnoredMembersBuilder to build and validate ignored members * Introduces a new MemberMappingDiagnosticReporter to report all diagnostics after the member mapping build phase * Introduces a new NestedMappingsContext to handle nested mappings (MapPropertyFromSourceAttribute) * Introduces a new MemberMappingBuilder to build member (assignment) mappings for all types (objects, existing objects and tuples) * Introduces a new MembersMappingState holding the state during the member matching process (which members are not yet mapped, which are ignored, ...) * replace RMG017 and RMG027 with a new RMG074 when a target member path is used where it is not possible * Introduces a new MemberMappingInfo representing the mapped source and target members. This simplifies marking members as mapped * Introduces a new ConstructorParameterMember to simplify constructor parameter handling (can be treated as IMappableMember now) * Introduces a new ISourceValue interface which represents the right side of a member assignment mapping. For now only mapped source members and null mapped source members are supported. With #631 constant values and method provided values will also be ISourceValues BREAKING CHANGE: Replaces RMG017 and RMG027 with RMG074 --- src/Riok.Mapperly/AnalyzerReleases.Shipped.md | 16 + .../MembersMappingConfiguration.cs | 15 +- .../BuilderContext/IMembersBuilderContext.cs | 22 +- .../INewInstanceBuilderContext.cs | 16 +- .../INewValueTupleBuilderContext.cs | 4 + .../BuilderContext/IgnoredMembersBuilder.cs | 112 +++++ .../MemberMappingDiagnosticReporter.cs | 59 +++ .../MembersContainerBuilderContext.cs | 22 +- .../MembersMappingBuilderContext.cs | 392 +++++------------- .../BuilderContext/MembersMappingState.cs | 139 +++++++ .../MembersMappingStateBuilder.cs | 89 ++++ .../BuilderContext/NestedMappingsContext.cs | 101 +++++ .../NewInstanceBuilderContext.cs | 44 +- .../NewInstanceContainerBuilderContext.cs | 44 +- .../NewValueTupleConstructorBuilderContext.cs | 94 ++++- .../NewValueTupleExpressionBuilderContext.cs | 96 ++++- .../MemberMappingBuilder.cs | 189 +++++++++ ...wInstanceObjectMemberMappingBodyBuilder.cs | 238 ++--------- .../NewValueTupleMappingBodyBuilder.cs | 158 ++----- .../ObjectMemberMappingBodyBuilder.cs | 202 +++------ .../Descriptors/MappingBuilderContext.cs | 3 - .../MappingBuilders/MappingBuilder.cs | 4 - .../ConstructorParameterMapping.cs | 30 +- .../IMemberAssignmentMapping.cs | 5 +- .../Mappings/MemberMappings/IMemberMapping.cs | 15 - .../MemberMappings/MemberAssignmentMapping.cs | 34 +- .../MemberExistingTargetMapping.cs | 14 +- .../MemberMappings/MemberMappingInfo.cs | 21 + .../SourceValue/ISourceValue.cs | 12 + .../MappedMemberSourceValue.cs} | 14 +- .../NullMappedMemberSourceValue.cs} | 56 ++- .../ValueTupleConstructorParameterMapping.cs | 13 +- .../Diagnostics/DiagnosticDescriptors.cs | 40 +- .../Symbols/ConstructorParameterMember.cs | 37 ++ src/Riok.Mapperly/Symbols/EmptyMemberPath.cs | 3 +- .../Symbols/MappingSourceTarget.cs | 7 + src/Riok.Mapperly/Symbols/MemberPath.cs | 2 +- .../Symbols/NonEmptyMemberPath.cs | 5 +- .../ObjectPropertyConstructorResolverTest.cs | 16 + .../Mapping/ObjectPropertyFromSourceTest.cs | 5 +- .../Mapping/ObjectPropertyTest.cs | 41 +- .../Mapping/QueryableProjectionTest.cs | 18 + .../Mapping/UnsafeAccessorTest.cs | 5 + .../Mapping/ValueTupleTest.cs | 52 ++- ...WithPathShouldMapPath#Mapper.g.verified.cs | 13 + ...pertyNotFoundShouldDiagnostic.verified.txt | 10 + ...pertyNotFoundShouldDiagnostic.verified.txt | 10 + ...PathWriteOnlyShouldDiagnostic.verified.txt | 12 +- ...pertyShouldDiagnostic#Mapper.g.verified.cs | 5 +- ...ourcePropertyShouldDiagnostic.verified.txt | 33 +- ...onfigurationsShouldDiagnostic.verified.txt | 12 +- ...onfigurationsShouldDiagnostic.verified.txt | 12 +- ...ropertyOnTargetWithDiagnostic.verified.txt | 2 +- ...opertyOnSourceWithDiagnostics.verified.txt | 10 + ...ourcePropertyShouldDiagnostic.verified.txt | 10 + ...argetPropertyShouldDiagnostic.verified.txt | 20 +- ...ourcePropertyShouldDiagnostic.verified.txt | 10 + ...tterShouldIgnoreAndDiagnostic.verified.txt | 10 + ...tterShouldIgnoreAndDiagnostic.verified.txt | 10 + ...tterShouldIgnoreAndDiagnostic.verified.txt | 2 +- ...tterShouldIgnoreAndDiagnostic.verified.txt | 2 +- ...eMappingStrategyCaseSensitive.verified.txt | 16 +- ...pablePropertyShouldDiagnostic.verified.txt | 10 + ...tchedPropertyShouldDiagnostic.verified.txt | 16 +- ...ppingShouldDiagnostic#Mapper.g.verified.cs | 21 + ...thPathMappingShouldDiagnostic.verified.txt | 14 + ...est.ReferenceLoopInitProperty.verified.txt | 10 + 67 files changed, 1711 insertions(+), 1063 deletions(-) create mode 100644 src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/IgnoredMembersBuilder.cs create mode 100644 src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MemberMappingDiagnosticReporter.cs create mode 100644 src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingState.cs create mode 100644 src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingStateBuilder.cs create mode 100644 src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NestedMappingsContext.cs create mode 100644 src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MemberMappingBuilder.cs delete mode 100644 src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/IMemberMapping.cs create mode 100644 src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberMappingInfo.cs create mode 100644 src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/ISourceValue.cs rename src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/{MemberMapping.cs => SourceValue/MappedMemberSourceValue.cs} (50%) rename src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/{NullMemberMapping.cs => SourceValue/NullMappedMemberSourceValue.cs} (53%) create mode 100644 src/Riok.Mapperly/Symbols/ConstructorParameterMember.cs create mode 100644 src/Riok.Mapperly/Symbols/MappingSourceTarget.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyConstructorResolverTest.MapConstructorAndMapPropertyWithPathShouldMapPath#Mapper.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.CtorWithPathMappingShouldDiagnostic#Mapper.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.CtorWithPathMappingShouldDiagnostic.verified.txt diff --git a/src/Riok.Mapperly/AnalyzerReleases.Shipped.md b/src/Riok.Mapperly/AnalyzerReleases.Shipped.md index 675f48baeb..8e2f6a013b 100644 --- a/src/Riok.Mapperly/AnalyzerReleases.Shipped.md +++ b/src/Riok.Mapperly/AnalyzerReleases.Shipped.md @@ -165,3 +165,19 @@ Rule ID | Category | Severity | Notes RMG071 | Mapper | Warning | Nested properties mapping is not used RMG072 | Mapper | Warning | The source type of the referenced mapping does not match RMG073 | Mapper | Warning | The target type of the referenced mapping does not match + +## Release 4.0 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +RMG074 | Mapper | Error | Multiple mappings are configured for the same target member + +### Removed Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +RMG017 | Mapper | Warning | An init only member can have one configuration at max +RMG027 | Mapper | Warning | A constructor parameter can have one configuration at max +RMG028 | Mapper | Warning | Constructor parameter cannot handle target paths diff --git a/src/Riok.Mapperly/Configuration/MembersMappingConfiguration.cs b/src/Riok.Mapperly/Configuration/MembersMappingConfiguration.cs index e9b9fb9c62..01e4d3e84f 100644 --- a/src/Riok.Mapperly/Configuration/MembersMappingConfiguration.cs +++ b/src/Riok.Mapperly/Configuration/MembersMappingConfiguration.cs @@ -1,4 +1,5 @@ using Riok.Mapperly.Abstractions; +using Riok.Mapperly.Symbols; namespace Riok.Mapperly.Configuration; @@ -9,4 +10,16 @@ public record MembersMappingConfiguration( IReadOnlyCollection NestedMappings, IgnoreObsoleteMembersStrategy IgnoreObsoleteMembersStrategy, RequiredMappingStrategy RequiredMappingStrategy -); +) +{ + public IEnumerable GetMembersWithExplicitConfigurations(MappingSourceTarget sourceTarget) + { + var members = sourceTarget switch + { + MappingSourceTarget.Source => ExplicitMappings.Where(x => x.Source.Path.Count > 0).Select(x => x.Source.Path[0]), + MappingSourceTarget.Target => ExplicitMappings.Select(x => x.Target.Path[0]), + _ => throw new ArgumentOutOfRangeException(nameof(sourceTarget), sourceTarget, "Neither source or target"), + }; + return members.Distinct(); + } +} diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/IMembersBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/IMembersBuilderContext.cs index e3d7ce783a..00430a32a5 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/IMembersBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/IMembersBuilderContext.cs @@ -1,6 +1,3 @@ -using System.Diagnostics.CodeAnalysis; -using Microsoft.CodeAnalysis; -using Riok.Mapperly.Configuration; using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Descriptors.Mappings.MemberMappings; using Riok.Mapperly.Symbols; @@ -18,22 +15,13 @@ public interface IMembersBuilderContext MappingBuilderContext BuilderContext { get; } - IReadOnlyCollection IgnoredSourceMemberNames { get; } + void SetTargetMemberMapped(IMappableMember targetMember); - Dictionary TargetMembers { get; } + void ConsumeMemberConfig(MemberMappingInfo members); - Dictionary> MemberConfigsByRootTargetName { get; } + IEnumerable EnumerateUnmappedTargetMembers(); - void AddDiagnostics(); + IEnumerable EnumerateUnmappedOrConfiguredTargetMembers(); - /// - /// Tries to find a (possibly nested) MemberPath on the source type that can be mapped to . - /// - bool TryFindNestedSourceMembersPath( - string targetMemberName, - [NotNullWhen(true)] out MemberPath? sourceMemberPath, - bool? ignoreCase = null - ); - - NullMemberMapping BuildNullMemberMapping(MemberPath sourcePath, INewInstanceMapping delegateMapping, ITypeSymbol targetMemberType); + IEnumerable MatchMembers(IMappableMember targetMember); } diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/INewInstanceBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/INewInstanceBuilderContext.cs index 004267af2f..677e67176b 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/INewInstanceBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/INewInstanceBuilderContext.cs @@ -1,23 +1,23 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Descriptors.Mappings.MemberMappings; +using Riok.Mapperly.Symbols; namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; /// -/// A for mappings which create the target object via new(). +/// A for mappings which create the target object via new ...(). /// /// The mapping type. public interface INewInstanceBuilderContext : IMembersBuilderContext where T : IMapping { + bool TryMatchParameter(IParameterSymbol parameter, [NotNullWhen(true)] out MemberMappingInfo? memberInfo); + + bool TryMatchInitOnlyMember(IMappableMember targetMember, [NotNullWhen(true)] out MemberMappingInfo? memberInfo); + void AddConstructorParameterMapping(ConstructorParameterMapping mapping); void AddInitMemberMapping(MemberAssignmentMapping mapping); - - /// - /// Maps case insensitive target root member names to their real case sensitive names. - /// For example id => Id. The real name can then be used as key for . - /// This allows resolving case insensitive configuration member names (eg. when mapping to constructor parameters). - /// - IReadOnlyDictionary RootTargetNameCasingMapping { get; } } diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/INewValueTupleBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/INewValueTupleBuilderContext.cs index e92dca4548..69e2b735d6 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/INewValueTupleBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/INewValueTupleBuilderContext.cs @@ -1,3 +1,5 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Descriptors.Mappings.MemberMappings; @@ -11,5 +13,7 @@ namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; public interface INewValueTupleBuilderContext : IMembersBuilderContext where T : IMapping { + bool TryMatchTupleElement(IFieldSymbol member, [NotNullWhen(true)] out MemberMappingInfo? memberInfo); + void AddTupleConstructorParameterMapping(ValueTupleConstructorParameterMapping mapping); } diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/IgnoredMembersBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/IgnoredMembersBuilder.cs new file mode 100644 index 0000000000..0ad1fc9e4b --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/IgnoredMembersBuilder.cs @@ -0,0 +1,112 @@ +using Riok.Mapperly.Abstractions; +using Riok.Mapperly.Configuration; +using Riok.Mapperly.Diagnostics; +using Riok.Mapperly.Symbols; + +namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; + +/// +/// Builds a set of ignored source or target members +/// and reports diagnostics for any invalid configured members. +/// If a member is ignored and configured, +/// the configuration takes precedence and the member is not ignored. +/// +internal static class IgnoredMembersBuilder +{ + internal static HashSet BuildIgnoredMembers( + MappingBuilderContext ctx, + MappingSourceTarget sourceTarget, + IReadOnlyCollection allMembers + ) + { + HashSet ignoredMembers = + [ + .. sourceTarget == MappingSourceTarget.Source + ? ctx.Configuration.Members.IgnoredSources + : ctx.Configuration.Members.IgnoredTargets, + .. GetIgnoredAtMemberMembers(ctx, sourceTarget), + .. GetIgnoredObsoleteMembers(ctx, sourceTarget), + ]; + + RemoveAndReportConfiguredIgnoredMembers(ctx, sourceTarget, ignoredMembers); + ReportUnmatchedIgnoredMembers(ctx, sourceTarget, ignoredMembers, allMembers); + return ignoredMembers; + } + + private static void RemoveAndReportConfiguredIgnoredMembers( + MappingBuilderContext ctx, + MappingSourceTarget sourceTarget, + HashSet ignoredMembers + ) + { + var isSource = sourceTarget == MappingSourceTarget.Source; + var diagnostic = isSource + ? DiagnosticDescriptors.IgnoredSourceMemberExplicitlyMapped + : DiagnosticDescriptors.IgnoredTargetMemberExplicitlyMapped; + var type = isSource ? ctx.Source : ctx.Target; + var configuredMembers = ctx.Configuration.Members.GetMembersWithExplicitConfigurations(sourceTarget); + foreach (var configuredMember in configuredMembers) + { + if (ignoredMembers.Remove(configuredMember)) + { + ctx.ReportDiagnostic(diagnostic, configuredMember, type); + } + } + } + + private static void ReportUnmatchedIgnoredMembers( + MappingBuilderContext ctx, + MappingSourceTarget sourceTarget, + IEnumerable ignoredMembers, + IEnumerable allMembers + ) + { + var isSource = sourceTarget == MappingSourceTarget.Source; + var type = isSource ? ctx.Source : ctx.Target; + var nestedDiagnostic = isSource ? DiagnosticDescriptors.NestedIgnoredSourceMember : DiagnosticDescriptors.NestedIgnoredTargetMember; + var notFoundDiagnostic = isSource + ? DiagnosticDescriptors.IgnoredSourceMemberNotFound + : DiagnosticDescriptors.IgnoredTargetMemberNotFound; + + var unmatchedMembers = new HashSet(ignoredMembers); + unmatchedMembers.ExceptWith(allMembers); + + foreach (var member in unmatchedMembers) + { + if (member.Contains(StringMemberPath.MemberAccessSeparator, StringComparison.Ordinal)) + { + ctx.ReportDiagnostic(nestedDiagnostic, member, type); + continue; + } + + ctx.ReportDiagnostic(notFoundDiagnostic, member, type); + } + } + + private static IEnumerable GetIgnoredAtMemberMembers(MappingBuilderContext ctx, MappingSourceTarget sourceTarget) + { + var type = sourceTarget == MappingSourceTarget.Source ? ctx.Source : ctx.Target; + + return ctx + .SymbolAccessor.GetAllAccessibleMappableMembers(type) + .Where(x => ctx.SymbolAccessor.HasAttribute(x.MemberSymbol)) + .Select(x => x.Name); + } + + private static IEnumerable GetIgnoredObsoleteMembers(MappingBuilderContext ctx, MappingSourceTarget sourceTarget) + { + var obsoleteStrategy = ctx.Configuration.Members.IgnoreObsoleteMembersStrategy; + var strategy = + sourceTarget == MappingSourceTarget.Source ? IgnoreObsoleteMembersStrategy.Source : IgnoreObsoleteMembersStrategy.Target; + + if (!obsoleteStrategy.HasFlag(strategy)) + return Enumerable.Empty(); + + var type = sourceTarget == MappingSourceTarget.Source ? ctx.Source : ctx.Target; + + return ctx + .SymbolAccessor.GetAllAccessibleMappableMembers(type) + .Where(x => ctx.SymbolAccessor.HasAttribute(x.MemberSymbol)) + .Select(x => x.Name); + } +} diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MemberMappingDiagnosticReporter.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MemberMappingDiagnosticReporter.cs new file mode 100644 index 0000000000..b14b802a80 --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MemberMappingDiagnosticReporter.cs @@ -0,0 +1,59 @@ +using Riok.Mapperly.Abstractions; +using Riok.Mapperly.Diagnostics; + +namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; + +internal static class MemberMappingDiagnosticReporter +{ + public static void ReportDiagnostics(MappingBuilderContext ctx, MembersMappingState state) + { + AddUnusedTargetMembersDiagnostics(ctx, state); + AddUnmappedSourceMembersDiagnostics(ctx, state); + AddUnmappedTargetMembersDiagnostics(ctx, state); + AddNoMemberMappedDiagnostic(ctx, state); + } + + private static void AddUnusedTargetMembersDiagnostics(MappingBuilderContext ctx, MembersMappingState state) + { + foreach (var memberConfig in state.UnusedMemberConfigs) + { + ctx.ReportDiagnostic(DiagnosticDescriptors.ConfiguredMappingTargetMemberNotFound, memberConfig.Target.FullName, ctx.Target); + } + } + + private static void AddUnmappedSourceMembersDiagnostics(MappingBuilderContext ctx, MembersMappingState state) + { + if (!ctx.Configuration.Members.RequiredMappingStrategy.HasFlag(RequiredMappingStrategy.Source)) + return; + + foreach (var sourceMemberName in state.UnmappedSourceMemberNames) + { + ctx.ReportDiagnostic(DiagnosticDescriptors.SourceMemberNotMapped, sourceMemberName, ctx.Source, ctx.Target); + } + } + + private static void AddUnmappedTargetMembersDiagnostics(MappingBuilderContext ctx, MembersMappingState state) + { + foreach (var targetMember in state.UnmappedTargetMembers) + { + if (targetMember.IsRequired) + { + ctx.ReportDiagnostic(DiagnosticDescriptors.RequiredMemberNotMapped, targetMember.Name, ctx.Target, ctx.Source); + continue; + } + + if (targetMember.CanSet && ctx.Configuration.Members.RequiredMappingStrategy.HasFlag(RequiredMappingStrategy.Target)) + { + ctx.ReportDiagnostic(DiagnosticDescriptors.SourceMemberNotFound, targetMember.Name, ctx.Target, ctx.Source); + } + } + } + + private static void AddNoMemberMappedDiagnostic(MappingBuilderContext ctx, MembersMappingState state) + { + if (!state.HasMemberMapping) + { + ctx.ReportDiagnostic(DiagnosticDescriptors.NoMemberMappings, ctx.Source.ToDisplayString(), ctx.Target.ToDisplayString()); + } + } +} diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersContainerBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersContainerBuilderContext.cs index 0c870531aa..105409038e 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersContainerBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersContainerBuilderContext.cs @@ -18,20 +18,28 @@ public class MembersContainerBuilderContext(MappingBuilderContext builderCont public void AddMemberAssignmentMapping(IMemberAssignmentMapping memberMapping) => AddMemberAssignmentMapping(Mapping, memberMapping); + /// + /// Adds an if-else style block which only executes the + /// if the source member is not null. + /// + /// The member mapping to be applied if the source member is not null public void AddNullDelegateMemberAssignmentMapping(IMemberAssignmentMapping memberMapping) { - var nullConditionSourcePath = MemberPath.Create( - memberMapping.SourceGetter.MemberPath.RootType, - memberMapping.SourceGetter.MemberPath.PathWithoutTrailingNonNullable().ToList() + var nullConditionSourcePath = new NonEmptyMemberPath( + memberMapping.MemberInfo.SourceMember.RootType, + memberMapping.MemberInfo.SourceMember.PathWithoutTrailingNonNullable().ToList() ); var container = GetOrCreateNullDelegateMappingForPath(nullConditionSourcePath); AddMemberAssignmentMapping(container, memberMapping); // set target member to null if null assignments are allowed // and the source is null - if (BuilderContext.Configuration.Mapper.AllowNullPropertyAssignment && memberMapping.TargetPath.Member.Type.IsNullable()) + if ( + BuilderContext.Configuration.Mapper.AllowNullPropertyAssignment + && memberMapping.MemberInfo.TargetMember.Member.Type.IsNullable() + ) { - container.AddNullMemberAssignment(SetterMemberPath.Build(BuilderContext, memberMapping.TargetPath)); + container.AddNullMemberAssignment(SetterMemberPath.Build(BuilderContext, memberMapping.MemberInfo.TargetMember)); } else if (BuilderContext.Configuration.Mapper.ThrowOnPropertyMappingNullMismatch) { @@ -41,9 +49,9 @@ public void AddNullDelegateMemberAssignmentMapping(IMemberAssignmentMapping memb private void AddMemberAssignmentMapping(IMemberAssignmentMappingContainer container, IMemberAssignmentMapping mapping) { - SetSourceMemberMapped(mapping.SourceGetter.MemberPath); - AddNullMemberInitializers(container, mapping.TargetPath); + AddNullMemberInitializers(container, mapping.MemberInfo.TargetMember); container.AddMemberMapping(mapping); + MappingAdded(mapping.MemberInfo); } private void AddNullMemberInitializers(IMemberAssignmentMappingContainer container, MemberPath path) diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingBuilderContext.cs index 1be0465a49..0e98d6ab91 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingBuilderContext.cs @@ -1,5 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using Microsoft.CodeAnalysis; using Riok.Mapperly.Abstractions; using Riok.Mapperly.Configuration; using Riok.Mapperly.Descriptors.Mappings; @@ -14,348 +13,187 @@ namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; /// An abstract base implementation of . /// /// The type of the mapping. -public abstract class MembersMappingBuilderContext : IMembersBuilderContext +public abstract class MembersMappingBuilderContext(MappingBuilderContext builderContext, T mapping) : IMembersBuilderContext where T : IMapping { - private readonly HashSet _unmappedSourceMemberNames; - private readonly HashSet _unusedNestedMemberPaths; - private readonly HashSet _mappedAndIgnoredTargetMemberNames; - private readonly HashSet _mappedAndIgnoredSourceMemberNames; - private readonly IReadOnlyCollection _ignoredUnmatchedTargetMemberNames; - private readonly IReadOnlyCollection _ignoredUnmatchedSourceMemberNames; - private readonly IReadOnlyCollection _nestedMemberPaths; + private readonly MembersMappingState _state = MembersMappingStateBuilder.Build(builderContext); - private bool _hasMemberMapping; + private readonly NestedMappingsContext _nestedMappingsContext = NestedMappingsContext.Create(builderContext); - protected MembersMappingBuilderContext(MappingBuilderContext builderContext, T mapping) - { - BuilderContext = builderContext; - Mapping = mapping; - MemberConfigsByRootTargetName = GetMemberConfigurations(); - _nestedMemberPaths = GetNestedMemberPaths(); - _unusedNestedMemberPaths = _nestedMemberPaths.Select(c => c.FullName).ToHashSet(); - - _unmappedSourceMemberNames = GetSourceMemberNames(); - TargetMembers = GetTargetMembers(); - - IgnoredSourceMemberNames = builderContext - .Configuration.Members.IgnoredSources.Concat(GetIgnoredSourceMembers()) - .Concat(GetIgnoredObsoleteSourceMembers()) - .ToHashSet(); - var ignoredTargetMemberNames = builderContext - .Configuration.Members.IgnoredTargets.Concat(GetIgnoredTargetMembers()) - .Concat(GetIgnoredObsoleteTargetMembers()) - .ToHashSet(); - - _ignoredUnmatchedSourceMemberNames = InitIgnoredUnmatchedProperties(IgnoredSourceMemberNames, _unmappedSourceMemberNames); - _ignoredUnmatchedTargetMemberNames = InitIgnoredUnmatchedProperties( - builderContext.Configuration.Members.IgnoredTargets, - TargetMembers.Keys - ); - - _unmappedSourceMemberNames.ExceptWith(IgnoredSourceMemberNames); - - // source and target properties may have been ignored and mapped explicitly - _mappedAndIgnoredSourceMemberNames = MemberConfigsByRootTargetName - .Values.SelectMany(v => v.Where(s => s.Source.Path.Count > 0).Select(s => s.Source.Path.First())) - .ToHashSet(); - _mappedAndIgnoredSourceMemberNames.IntersectWith(IgnoredSourceMemberNames); + public MappingBuilderContext BuilderContext { get; } = builderContext; - _mappedAndIgnoredTargetMemberNames = new HashSet(ignoredTargetMemberNames); - _mappedAndIgnoredTargetMemberNames.IntersectWith(MemberConfigsByRootTargetName.Keys); + public T Mapping { get; } = mapping; - // remove explicitly mapped ignored targets from ignoredTargetMemberNames - // then remove all ignored targets from TargetMembers, leaving unignored and explicitly mapped ignored members - ignoredTargetMemberNames.ExceptWith(_mappedAndIgnoredTargetMemberNames); - - TargetMembers.RemoveRange(ignoredTargetMemberNames); + public void AddDiagnostics() + { + MemberMappingDiagnosticReporter.ReportDiagnostics(BuilderContext, _state); + _nestedMappingsContext.ReportDiagnostics(); } - public MappingBuilderContext BuilderContext { get; } + public IEnumerable EnumerateUnmappedTargetMembers() => _state.EnumerateUnmappedTargetMembers(); - public T Mapping { get; } + public IEnumerable EnumerateUnmappedOrConfiguredTargetMembers() => _state.EnumerateUnmappedOrConfiguredTargetMembers(); - public IReadOnlyCollection IgnoredSourceMemberNames { get; } + public void SetTargetMemberMapped(IMappableMember targetMember) => _state.SetTargetMemberMapped(targetMember); - public Dictionary TargetMembers { get; } + protected void SetTargetMemberMapped(string targetMemberName, bool ignoreCase = false) => + _state.SetTargetMemberMapped(targetMemberName, ignoreCase); - public Dictionary> MemberConfigsByRootTargetName { get; } + public void SetMembersMapped(MemberMappingInfo memberInfo) => _state.SetMembersMapped(memberInfo, false); - public NullMemberMapping BuildNullMemberMapping( - MemberPath sourcePath, - INewInstanceMapping delegateMapping, - ITypeSymbol targetMemberType - ) + public void ConsumeMemberConfig(MemberMappingInfo members) { - var getterSourcePath = GetterMemberPath.Build(BuilderContext, sourcePath); - - var nullFallback = NullFallbackValue.Default; - if (!delegateMapping.SourceType.IsNullable() && sourcePath.IsAnyNullable()) + if (members.Configuration != null) { - nullFallback = BuilderContext.GetNullFallbackValue(targetMemberType); + ConsumeMemberConfig(members.Configuration); } - - return new NullMemberMapping(delegateMapping, getterSourcePath, targetMemberType, nullFallback, !BuilderContext.IsExpression); } - public void AddDiagnostics() - { - AddUnmatchedIgnoredTargetMembersDiagnostics(); - AddUnmatchedIgnoredSourceMembersDiagnostics(); - AddUnmatchedTargetMembersDiagnostics(); - AddUnmatchedSourceMembersDiagnostics(); - AddUnusedNestedMembersDiagnostics(); - AddMappedAndIgnoredSourceMembersDiagnostics(); - AddMappedAndIgnoredTargetMembersDiagnostics(); - AddNoMemberMappedDiagnostic(); - } + protected void MappingAdded(MemberMappingInfo info, bool ignoreTargetCasing = false) => _state.MappingAdded(info, ignoreTargetCasing); - protected void SetSourceMemberMapped(MemberPath sourcePath) - { - _hasMemberMapping = true; + protected void ConsumeMemberConfig(MemberMappingConfiguration config) => _state.ConsumeMemberConfig(config); - if (sourcePath.Path.FirstOrDefault() is { } sourceMember) + public IEnumerable MatchMembers(IMappableMember targetMember) + { + if (TryGetMemberConfigs(targetMember.Name, false, out var memberConfigs)) { - _unmappedSourceMemberNames.Remove(sourceMember.Name); + // return configs with shorter target member paths first + // to prevent NRE's in the generated code + return ResolveMemberMappingInfo(memberConfigs.ToList()).OrderBy(x => x.TargetMember.Path.Count); } - else - // Assume all source members are used when the source object is used itself. + + // match directly + if (TryFindSourcePath(targetMember.Name, out var sourceMemberPath)) { - _unmappedSourceMemberNames.Clear(); + return [new MemberMappingInfo(sourceMemberPath, new NonEmptyMemberPath(Mapping.TargetType, [targetMember]))]; } + + return []; } - public bool TryFindNestedSourceMembersPath( - string targetMemberName, - [NotNullWhen(true)] out MemberPath? sourceMemberPath, - bool? ignoreCase = null - ) - { - ignoreCase ??= BuilderContext.Configuration.Mapper.PropertyNameMappingStrategy == PropertyNameMappingStrategy.CaseInsensitive; - var pathCandidates = MemberPathCandidateBuilder.BuildMemberPathCandidates(targetMemberName).Select(cs => cs.ToList()).ToList(); + protected bool TryMatchMember(IMappableMember targetMember, [NotNullWhen(true)] out MemberMappingInfo? memberInfo) => + TryMatchMember(targetMember, null, out memberInfo); - // First, try to find the property on (a sub-path of) the source type itself. (If this is undesired, an Ignore property can be used.) - if ( - BuilderContext.SymbolAccessor.TryFindMemberPath( - Mapping.SourceType, - pathCandidates, - IgnoredSourceMemberNames, - ignoreCase.Value, - out sourceMemberPath - ) - ) + protected bool TryMatchMember(IMappableMember targetMember, bool? ignoreCase, [NotNullWhen(true)] out MemberMappingInfo? memberInfo) + { + memberInfo = TryGetMemberConfigMappingInfo(targetMember, ignoreCase == true); + if (memberInfo != null) return true; - // Otherwise, search all nested members - foreach (var nestedMemberPath in _nestedMemberPaths) + // if no config was found, match the source path + if (TryFindSourcePath(targetMember.Name, out var sourceMemberPath, ignoreCase)) { - if ( - BuilderContext.SymbolAccessor.TryFindMemberPath( - nestedMemberPath.MemberType, - pathCandidates, - // Use empty ignore list to support ignoring a property for normal search while flattening its properties - [], - ignoreCase.Value, - out var nestedSourceMemberPath - ) - ) - { - sourceMemberPath = new NonEmptyMemberPath( - Mapping.SourceType, - nestedMemberPath.Path.Concat(nestedSourceMemberPath.Path).ToList() - ); - _unusedNestedMemberPaths.Remove(nestedMemberPath.FullName); - return true; - } + memberInfo = new MemberMappingInfo(sourceMemberPath, new NonEmptyMemberPath(BuilderContext.Target, [targetMember])); + return true; } + memberInfo = null; return false; } - private HashSet InitIgnoredUnmatchedProperties(IEnumerable allProperties, IEnumerable mappedProperties) + protected bool TryGetMemberConfigs( + string targetMemberName, + bool ignoreCase, + [NotNullWhen(true)] out IReadOnlyList? memberConfigs + ) => _state.TryGetMemberConfigs(targetMemberName, ignoreCase, out memberConfigs); + + protected virtual bool TryFindSourcePath( + IReadOnlyList> pathCandidates, + bool ignoreCase, + [NotNullWhen(true)] out MemberPath? sourceMemberPath + ) { - var unmatched = new HashSet(allProperties); - unmatched.ExceptWith(mappedProperties); - return unmatched; + return BuilderContext.SymbolAccessor.TryFindMemberPath( + Mapping.SourceType, + pathCandidates, + _state.IgnoredSourceMemberNames, + ignoreCase, + out sourceMemberPath + ); } - private IEnumerable GetIgnoredObsoleteTargetMembers() - { - var obsoleteStrategy = BuilderContext.Configuration.Members.IgnoreObsoleteMembersStrategy; - - if (!obsoleteStrategy.HasFlag(IgnoreObsoleteMembersStrategy.Target)) - return Enumerable.Empty(); + protected bool IsIgnoredSourceMember(string sourceMemberName) => _state.IgnoredSourceMemberNames.Contains(sourceMemberName); - return BuilderContext - .SymbolAccessor.GetAllAccessibleMappableMembers(Mapping.TargetType) - .Where(x => BuilderContext.SymbolAccessor.HasAttribute(x.MemberSymbol)) - .Select(x => x.Name); - } + private IReadOnlyList ResolveMemberMappingInfo(IEnumerable configs) => + configs.Select(ResolveMemberMappingInfo).WhereNotNull().ToList(); - private IEnumerable GetIgnoredObsoleteSourceMembers() + private MemberMappingInfo? ResolveMemberMappingInfo(MemberMappingConfiguration config) { - var obsoleteStrategy = BuilderContext.Configuration.Members.IgnoreObsoleteMembersStrategy; - - if (!obsoleteStrategy.HasFlag(IgnoreObsoleteMembersStrategy.Source)) - return Enumerable.Empty(); + if ( + !BuilderContext.SymbolAccessor.TryFindMemberPath(Mapping.TargetType, config.Target.Path, out var foundMemberPath) + || foundMemberPath is not NonEmptyMemberPath targetMemberPath + ) + { + BuilderContext.ReportDiagnostic( + DiagnosticDescriptors.ConfiguredMappingTargetMemberNotFound, + config.Target.FullName, + Mapping.TargetType + ); - return BuilderContext - .SymbolAccessor.GetAllAccessibleMappableMembers(Mapping.SourceType) - .Where(x => BuilderContext.SymbolAccessor.HasAttribute(x.MemberSymbol)) - .Select(x => x.Name); - } + // consume this member config and prevent its further usage + // as it is invalid, and a diagnostic has already been reported + _state.ConsumeMemberConfig(config); + return null; + } - private IEnumerable GetIgnoredTargetMembers() - { - return BuilderContext - .SymbolAccessor.GetAllAccessibleMappableMembers(Mapping.TargetType) - .Where(x => BuilderContext.SymbolAccessor.HasAttribute(x.MemberSymbol)) - .Select(x => x.Name); - } + if (!ResolveMemberConfigSourcePath(config, out var sourceMemberPath)) + return null; - private IEnumerable GetIgnoredSourceMembers() - { - return BuilderContext - .SymbolAccessor.GetAllAccessibleMappableMembers(Mapping.SourceType) - .Where(x => BuilderContext.SymbolAccessor.HasAttribute(x.MemberSymbol)) - .Select(x => x.Name); + return new MemberMappingInfo(sourceMemberPath, targetMemberPath, config); } - private HashSet GetSourceMemberNames() + private bool ResolveMemberConfigSourcePath(MemberMappingConfiguration config, [NotNullWhen(true)] out MemberPath? sourceMemberPath) { - return BuilderContext.SymbolAccessor.GetAllAccessibleMappableMembers(Mapping.SourceType).Select(x => x.Name).ToHashSet(); - } + if (!BuilderContext.SymbolAccessor.TryFindMemberPath(Mapping.SourceType, config.Source.Path, out sourceMemberPath)) + { + BuilderContext.ReportDiagnostic( + DiagnosticDescriptors.ConfiguredMappingSourceMemberNotFound, + config.Source.FullName, + Mapping.SourceType + ); - private Dictionary GetTargetMembers() - { - return BuilderContext - .SymbolAccessor.GetAllAccessibleMappableMembers(Mapping.TargetType) - .ToDictionary(x => x.Name, StringComparer.OrdinalIgnoreCase); - } + // consume this member config and prevent its further usage + // as it is invalid, and a diagnostic has already been reported + _state.ConsumeMemberConfig(config); + return false; + } - private Dictionary> GetMemberConfigurations() - { - return BuilderContext - .Configuration.Members.ExplicitMappings.GroupBy(x => x.Target.Path.First()) - .ToDictionary(x => x.Key, x => x.ToList()); + return true; } - private IReadOnlyCollection GetNestedMemberPaths() + private bool TryFindSourcePath(string targetMemberName, [NotNullWhen(true)] out MemberPath? sourceMemberPath, bool? ignoreCase = null) { - var nestedMemberPaths = new List(); + ignoreCase ??= BuilderContext.Configuration.Mapper.PropertyNameMappingStrategy == PropertyNameMappingStrategy.CaseInsensitive; + var pathCandidates = MemberPathCandidateBuilder.BuildMemberPathCandidates(targetMemberName).Select(cs => cs.ToList()).ToList(); - foreach (var nestedMemberConfig in BuilderContext.Configuration.Members.NestedMappings) - { - if (!BuilderContext.SymbolAccessor.TryFindMemberPath(Mapping.SourceType, nestedMemberConfig.Source.Path, out var memberPath)) - { - BuilderContext.ReportDiagnostic( - DiagnosticDescriptors.ConfiguredMappingNestedMemberNotFound, - nestedMemberConfig.Source.FullName, - Mapping.SourceType - ); - continue; - } - nestedMemberPaths.Add(memberPath); - } + // First, try to find the property on (a sub-path of) the source type itself. (If this is undesired, an Ignore property can be used.) + if (TryFindSourcePath(pathCandidates, ignoreCase.Value, out sourceMemberPath)) + return true; - return nestedMemberPaths; + // Otherwise, search all nested members + return _nestedMappingsContext.TryFindNestedSourcePath(pathCandidates, ignoreCase.Value, out sourceMemberPath); } - private void AddUnmatchedIgnoredTargetMembersDiagnostics() + private MemberMappingInfo? TryGetMemberConfigMappingInfo(IMappableMember targetMember, bool ignoreCase) { - foreach (var notFoundIgnoredMember in _ignoredUnmatchedTargetMemberNames) + if (TryGetMemberConfigs(targetMember.Name, false, out var memberConfigs)) { - if (notFoundIgnoredMember.Contains(StringMemberPath.MemberAccessSeparator, StringComparison.Ordinal)) + var memberConfig = memberConfigs.FirstOrDefault(x => x.Target.Path.Count == 1); + if (memberConfig != null && ResolveMemberConfigSourcePath(memberConfig, out var sourceMember)) { - BuilderContext.ReportDiagnostic(DiagnosticDescriptors.NestedIgnoredTargetMember, notFoundIgnoredMember, Mapping.TargetType); - continue; + return new MemberMappingInfo(sourceMember, new NonEmptyMemberPath(BuilderContext.Target, [targetMember]), memberConfig); } - BuilderContext.ReportDiagnostic(DiagnosticDescriptors.IgnoredTargetMemberNotFound, notFoundIgnoredMember, Mapping.TargetType); } - } - private void AddUnmatchedIgnoredSourceMembersDiagnostics() - { - foreach (var notFoundIgnoredMember in _ignoredUnmatchedSourceMemberNames) + if (ignoreCase && TryGetMemberConfigs(targetMember.Name, true, out memberConfigs)) { - if (notFoundIgnoredMember.Contains(StringMemberPath.MemberAccessSeparator, StringComparison.Ordinal)) + var memberConfig = memberConfigs.FirstOrDefault(x => x.Target.Path.Count == 1); + if (memberConfig != null && ResolveMemberConfigSourcePath(memberConfig, out var sourceMember)) { - BuilderContext.ReportDiagnostic(DiagnosticDescriptors.NestedIgnoredSourceMember, notFoundIgnoredMember, Mapping.TargetType); - continue; + return new MemberMappingInfo(sourceMember, new NonEmptyMemberPath(BuilderContext.Target, [targetMember]), memberConfig); } - BuilderContext.ReportDiagnostic(DiagnosticDescriptors.IgnoredSourceMemberNotFound, notFoundIgnoredMember, Mapping.SourceType); } - } - private void AddUnmatchedTargetMembersDiagnostics() - { - foreach (var memberConfig in MemberConfigsByRootTargetName.Values.SelectMany(x => x)) - { - BuilderContext.ReportDiagnostic( - DiagnosticDescriptors.ConfiguredMappingTargetMemberNotFound, - memberConfig.Target.FullName, - Mapping.TargetType - ); - } - } - - private void AddUnmatchedSourceMembersDiagnostics() - { - if (!BuilderContext.Configuration.Members.RequiredMappingStrategy.HasFlag(RequiredMappingStrategy.Source)) - return; - - foreach (var sourceMemberName in _unmappedSourceMemberNames) - { - BuilderContext.ReportDiagnostic( - DiagnosticDescriptors.SourceMemberNotMapped, - sourceMemberName, - Mapping.SourceType, - Mapping.TargetType - ); - } - } - - private void AddUnusedNestedMembersDiagnostics() - { - foreach (var sourceMemberPath in _unusedNestedMemberPaths) - { - BuilderContext.ReportDiagnostic(DiagnosticDescriptors.NestedMemberNotUsed, sourceMemberPath, Mapping.SourceType); - } - } - - private void AddMappedAndIgnoredTargetMembersDiagnostics() - { - foreach (var targetMemberName in _mappedAndIgnoredTargetMemberNames) - { - BuilderContext.ReportDiagnostic( - DiagnosticDescriptors.IgnoredTargetMemberExplicitlyMapped, - targetMemberName, - Mapping.TargetType - ); - } - } - - private void AddMappedAndIgnoredSourceMembersDiagnostics() - { - foreach (var sourceMemberName in _mappedAndIgnoredSourceMemberNames) - { - BuilderContext.ReportDiagnostic( - DiagnosticDescriptors.IgnoredSourceMemberExplicitlyMapped, - sourceMemberName, - Mapping.SourceType - ); - } - } - - private void AddNoMemberMappedDiagnostic() - { - if (!_hasMemberMapping) - { - BuilderContext.ReportDiagnostic( - DiagnosticDescriptors.NoMemberMappings, - BuilderContext.Source.ToDisplayString(), - BuilderContext.Target.ToDisplayString() - ); - } + return null; } } diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingState.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingState.cs new file mode 100644 index 0000000000..f8b697ad44 --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingState.cs @@ -0,0 +1,139 @@ +using System.Diagnostics.CodeAnalysis; +using Riok.Mapperly.Configuration; +using Riok.Mapperly.Descriptors.Mappings.MemberMappings; +using Riok.Mapperly.Helpers; +using Riok.Mapperly.Symbols; + +namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; + +/// +/// The state of an ongoing member mapping matching process. +/// Contains discovered but unmapped members, ignored members, etc. +/// +/// Source member names which are not used in a member mapping yet. +/// Target member names which are not used in a member mapping yet. +/// A dictionary with all members of the target with a case-insensitive key comparer. +/// All known target members. +/// All configurations by root target member names, which are not yet consumed. +/// All ignored source members names. +internal class MembersMappingState( + HashSet unmappedSourceMemberNames, + HashSet unmappedTargetMemberNames, + IReadOnlyDictionary targetMemberCaseMapping, + IReadOnlyDictionary targetMembers, + Dictionary> memberConfigsByRootTargetName, + IReadOnlyCollection ignoredSourceMemberNames +) +{ + /// + /// All source member names that are not used in a member mapping (yet). + /// + private readonly HashSet _unmappedSourceMemberNames = unmappedSourceMemberNames; + + /// + /// All target member names that are not used in a member mapping (yet). + /// + private readonly HashSet _unmappedTargetMemberNames = unmappedTargetMemberNames; + + public IReadOnlyCollection IgnoredSourceMemberNames { get; } = ignoredSourceMemberNames; + + /// + /// Whether any member mapping has been added. + /// + public bool HasMemberMapping { get; private set; } + + public IEnumerable UnmappedSourceMemberNames => _unmappedSourceMemberNames; + + public IEnumerable UnmappedTargetMembers => _unmappedTargetMemberNames.Select(x => targetMembers[x]); + + public IEnumerable UnusedMemberConfigs => memberConfigsByRootTargetName.Values.SelectMany(x => x); + + public IEnumerable EnumerateUnmappedTargetMembers() => _unmappedTargetMemberNames.Select(x => targetMembers[x]); + + public IEnumerable EnumerateUnmappedOrConfiguredTargetMembers() + { + return _unmappedTargetMemberNames + .Concat(memberConfigsByRootTargetName.Keys) + .Distinct() + .Select(targetMembers.GetValueOrDefault) + .WhereNotNull(); + } + + public void MappingAdded(MemberMappingInfo info, bool ignoreTargetCasing) + { + HasMemberMapping = true; + SetMembersMapped(info, ignoreTargetCasing); + } + + public void SetTargetMemberMapped(IMappableMember targetMember) => SetTargetMemberMapped(targetMember.Name); + + public void SetTargetMemberMapped(string targetName, bool ignoreCase = false) + { + _unmappedTargetMemberNames.Remove(targetName); + + if (ignoreCase && targetMemberCaseMapping.TryGetValue(targetName, out targetName)) + { + _unmappedTargetMemberNames.Remove(targetName); + } + } + + public void SetMembersMapped(MemberMappingInfo info, bool ignoreTargetCasing) + { + SetTargetMemberMapped(info.TargetMember.Path[0].Name, ignoreTargetCasing); + SetSourceMemberMapped(info.SourceMember); + + if (info.Configuration != null) + { + ConsumeMemberConfig(info.Configuration); + } + } + + public void ConsumeMemberConfig(MemberMappingConfiguration config) => + ConsumeMemberConfig(config, config.Target, memberConfigsByRootTargetName); + + public bool TryGetMemberConfigs( + string targetMemberName, + bool ignoreCase, + [NotNullWhen(true)] out IReadOnlyList? memberConfigs + ) + { + if (ignoreCase) + { + targetMemberName = targetMemberCaseMapping.GetValueOrDefault(targetMemberName, targetMemberName); + } + + if (memberConfigsByRootTargetName.TryGetValue(targetMemberName, out var configs)) + { + memberConfigs = configs; + return true; + } + + memberConfigs = null; + return false; + } + + private void SetSourceMemberMapped(MemberPath sourcePath) + { + if (sourcePath.Path.FirstOrDefault() is { } sourceMember) + { + _unmappedSourceMemberNames.Remove(sourceMember.Name); + } + else + { + // Assume all source members are used when the source object is used itself. + _unmappedSourceMemberNames.Clear(); + } + } + + private void ConsumeMemberConfig(T config, StringMemberPath targetPath, Dictionary> configsByRootName) + { + if (!configsByRootName.TryGetValue(targetPath.Path[0], out var configs)) + return; + + configs.Remove(config); + if (configs.Count == 0) + { + configsByRootName.Remove(targetPath.Path[0]); + } + } +} diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingStateBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingStateBuilder.cs new file mode 100644 index 0000000000..78d09f3f00 --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingStateBuilder.cs @@ -0,0 +1,89 @@ +using Riok.Mapperly.Configuration; +using Riok.Mapperly.Diagnostics; +using Riok.Mapperly.Helpers; +using Riok.Mapperly.Symbols; + +namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; + +internal static class MembersMappingStateBuilder +{ + public static MembersMappingState Build(MappingBuilderContext ctx) + { + // build configurations + var configuredTargetMembers = new HashSet(); + var memberConfigsByRootTargetName = BuildMemberConfigurations(ctx, configuredTargetMembers); + + // build all members + var unmappedSourceMemberNames = GetSourceMemberNames(ctx); + var targetMembers = GetTargetMembers(ctx); + + // build ignored members + var ignoredSourceMemberNames = IgnoredMembersBuilder.BuildIgnoredMembers( + ctx, + MappingSourceTarget.Source, + unmappedSourceMemberNames + ); + var ignoredTargetMemberNames = IgnoredMembersBuilder.BuildIgnoredMembers(ctx, MappingSourceTarget.Target, targetMembers.Keys); + + // remove ignored members + unmappedSourceMemberNames.ExceptWith(ignoredSourceMemberNames); + targetMembers.RemoveRange(ignoredTargetMemberNames); + + var targetMemberCaseMapping = targetMembers.Keys.ToDictionary(x => x, x => x, StringComparer.OrdinalIgnoreCase); + var unmappedTargetMemberNames = targetMembers.Keys.ToHashSet(); + return new MembersMappingState( + unmappedSourceMemberNames, + unmappedTargetMemberNames, + targetMemberCaseMapping, + targetMembers, + memberConfigsByRootTargetName, + ignoredSourceMemberNames + ); + } + + private static HashSet GetSourceMemberNames(MappingBuilderContext ctx) + { + return ctx.SymbolAccessor.GetAllAccessibleMappableMembers(ctx.Source).Select(x => x.Name).ToHashSet(); + } + + private static Dictionary GetTargetMembers(MappingBuilderContext ctx) + { + return ctx.SymbolAccessor.GetAllAccessibleMappableMembers(ctx.Target).ToDictionary(x => x.Name); + } + + private static Dictionary> BuildMemberConfigurations( + MappingBuilderContext ctx, + HashSet configuredTargetMembers + ) + { + // order by target path count as objects with less path depth should be mapped first + // to prevent NREs in the generated code + return GetUniqueTargetConfigurations(ctx, configuredTargetMembers, ctx.Configuration.Members.ExplicitMappings, x => x.Target) + .GroupBy(x => x.Target.Path[0]) + .ToDictionary(x => x.Key, x => x.OrderBy(cfg => cfg.Target.Path.Count).ToList()); + } + + private static IEnumerable GetUniqueTargetConfigurations( + MappingBuilderContext ctx, + HashSet configuredTargetMembers, + IEnumerable configs, + Func targetPathSelector + ) + { + foreach (var config in configs) + { + var targetPath = targetPathSelector(config); + if (configuredTargetMembers.Add(targetPath)) + { + yield return config; + continue; + } + + ctx.ReportDiagnostic( + DiagnosticDescriptors.MultipleConfigurationsForTargetMember, + ctx.Target.ToDisplayString(), + targetPath.FullName + ); + } + } +} diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NestedMappingsContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NestedMappingsContext.cs new file mode 100644 index 0000000000..7f48485013 --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NestedMappingsContext.cs @@ -0,0 +1,101 @@ +using System.Diagnostics.CodeAnalysis; +using Riok.Mapperly.Diagnostics; +using Riok.Mapperly.Symbols; + +namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; + +/// +/// Handles nested mappings configured by . +/// +public class NestedMappingsContext +{ + private readonly MappingBuilderContext _context; + private readonly IReadOnlyCollection _paths; + private readonly HashSet _unusedPaths; + + private NestedMappingsContext(MappingBuilderContext context, IReadOnlyCollection paths) + { + _context = context; + _paths = paths; + _unusedPaths = new HashSet(paths, ReferenceEqualityComparer.Instance); + } + + public static NestedMappingsContext Create(MappingBuilderContext ctx) => new(ctx, ResolveNestedMappings(ctx)); + + private static List ResolveNestedMappings(MappingBuilderContext ctx) + { + var nestedMemberPaths = new List(ctx.Configuration.Members.NestedMappings.Count); + + foreach (var nestedMemberConfig in ctx.Configuration.Members.NestedMappings) + { + if (!ctx.SymbolAccessor.TryFindMemberPath(ctx.Source, nestedMemberConfig.Source.Path, out var memberPath)) + { + ctx.ReportDiagnostic( + DiagnosticDescriptors.ConfiguredMappingNestedMemberNotFound, + nestedMemberConfig.Source.FullName, + ctx.Source + ); + continue; + } + + nestedMemberPaths.Add(memberPath); + } + + return nestedMemberPaths; + } + + public bool TryFindNestedSourcePath( + List> pathCandidates, + bool ignoreCase, + [NotNullWhen(true)] out MemberPath? sourceMemberPath + ) + { + foreach (var nestedMemberPath in _paths) + { + if (TryFindNestedSourcePath(pathCandidates, ignoreCase, nestedMemberPath, out sourceMemberPath)) + return true; + } + + sourceMemberPath = default; + return false; + } + + private bool TryFindNestedSourcePath( + List> pathCandidates, + bool ignoreCase, + MemberPath nestedMemberPath, + [NotNullWhen(true)] out MemberPath? sourceMemberPath + ) + { + if ( + _context.SymbolAccessor.TryFindMemberPath( + nestedMemberPath.MemberType, + pathCandidates, + // Use empty ignore list to support ignoring a property for normal search while flattening its properties + Array.Empty(), + ignoreCase, + out var nestedSourceMemberPath + ) + ) + { + sourceMemberPath = new NonEmptyMemberPath(_context.Source, nestedMemberPath.Path.Concat(nestedSourceMemberPath.Path).ToList()); + _unusedPaths.Remove(nestedMemberPath); + return true; + } + + sourceMemberPath = default; + return false; + } + + public void ReportDiagnostics() + { + foreach (var unusedPath in _unusedPaths) + { + _context.ReportDiagnostic( + DiagnosticDescriptors.NestedMemberNotUsed, + unusedPath.ToDisplayString(includeMemberType: false, includeRootType: false), + _context.Source.ToDisplayString() + ); + } + } +} diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewInstanceBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewInstanceBuilderContext.cs index 055118b4a1..5778d489ce 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewInstanceBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewInstanceBuilderContext.cs @@ -1,5 +1,9 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Descriptors.Mappings.MemberMappings; +using Riok.Mapperly.Diagnostics; +using Riok.Mapperly.Symbols; namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; @@ -7,28 +11,42 @@ namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; /// An implementation of . /// /// The type of the mapping. -public class NewInstanceBuilderContext : MembersMappingBuilderContext, INewInstanceBuilderContext +public class NewInstanceBuilderContext(MappingBuilderContext builderContext, T mapping) + : MembersMappingBuilderContext(builderContext, mapping), + INewInstanceBuilderContext where T : INewInstanceObjectMemberMapping { - public IReadOnlyDictionary RootTargetNameCasingMapping { get; } - - public NewInstanceBuilderContext(MappingBuilderContext builderContext, T mapping) - : base(builderContext, mapping) - { - RootTargetNameCasingMapping = MemberConfigsByRootTargetName.ToDictionary(x => x.Key, x => x.Key, StringComparer.OrdinalIgnoreCase); - } - public void AddInitMemberMapping(MemberAssignmentMapping mapping) { - SetSourceMemberMapped(mapping.SourceGetter.MemberPath); Mapping.AddInitMemberMapping(mapping); + MappingAdded(mapping.MemberInfo); } public void AddConstructorParameterMapping(ConstructorParameterMapping mapping) { - var paramName = RootTargetNameCasingMapping.GetValueOrDefault(mapping.Parameter.Name, defaultValue: mapping.Parameter.Name); - MemberConfigsByRootTargetName.Remove(paramName); - SetSourceMemberMapped(mapping.DelegateMapping.SourceGetter.MemberPath); Mapping.AddConstructorParameterMapping(mapping); + MappingAdded(mapping.MemberInfo, true); + } + + public bool TryMatchInitOnlyMember(IMappableMember targetMember, [NotNullWhen(true)] out MemberMappingInfo? memberInfo) + { + if (TryMatchMember(targetMember, out memberInfo)) + return true; + + if (TryGetMemberConfigs(targetMember.Name, false, out var configs)) + { + BuilderContext.ReportDiagnostic( + DiagnosticDescriptors.InitOnlyMemberDoesNotSupportPaths, + Mapping.TargetType, + configs[0].Target.FullName + ); + ConsumeMemberConfig(configs[0]); + return false; + } + + return false; } + + public bool TryMatchParameter(IParameterSymbol parameter, [NotNullWhen(true)] out MemberMappingInfo? memberInfo) => + TryMatchMember(new ConstructorParameterMember(parameter, BuilderContext.SymbolAccessor), true, out memberInfo); } diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewInstanceContainerBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewInstanceContainerBuilderContext.cs index 14a162a387..6992631eed 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewInstanceContainerBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewInstanceContainerBuilderContext.cs @@ -1,5 +1,9 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Descriptors.Mappings.MemberMappings; +using Riok.Mapperly.Diagnostics; +using Riok.Mapperly.Symbols; namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; @@ -8,28 +12,42 @@ namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; /// which supports containers (). /// /// -public class NewInstanceContainerBuilderContext : MembersContainerBuilderContext, INewInstanceBuilderContext +public class NewInstanceContainerBuilderContext(MappingBuilderContext builderContext, T mapping) + : MembersContainerBuilderContext(builderContext, mapping), + INewInstanceBuilderContext where T : INewInstanceObjectMemberMapping, IMemberAssignmentTypeMapping { - public IReadOnlyDictionary RootTargetNameCasingMapping { get; } - - public NewInstanceContainerBuilderContext(MappingBuilderContext builderContext, T mapping) - : base(builderContext, mapping) - { - RootTargetNameCasingMapping = MemberConfigsByRootTargetName.ToDictionary(x => x.Key, x => x.Key, StringComparer.OrdinalIgnoreCase); - } - public void AddInitMemberMapping(MemberAssignmentMapping mapping) { - SetSourceMemberMapped(mapping.SourceGetter.MemberPath); Mapping.AddInitMemberMapping(mapping); + MappingAdded(mapping.MemberInfo); } public void AddConstructorParameterMapping(ConstructorParameterMapping mapping) { - var paramName = RootTargetNameCasingMapping.GetValueOrDefault(mapping.Parameter.Name, defaultValue: mapping.Parameter.Name); - MemberConfigsByRootTargetName.Remove(paramName); - SetSourceMemberMapped(mapping.DelegateMapping.SourceGetter.MemberPath); Mapping.AddConstructorParameterMapping(mapping); + MappingAdded(mapping.MemberInfo, true); + } + + public bool TryMatchInitOnlyMember(IMappableMember targetMember, [NotNullWhen(true)] out MemberMappingInfo? memberInfo) + { + if (TryMatchMember(targetMember, out memberInfo)) + return true; + + if (TryGetMemberConfigs(targetMember.Name, false, out var configs)) + { + BuilderContext.ReportDiagnostic( + DiagnosticDescriptors.InitOnlyMemberDoesNotSupportPaths, + Mapping.TargetType, + configs[0].Target.FullName + ); + ConsumeMemberConfig(configs[0]); + return false; + } + + return false; } + + public bool TryMatchParameter(IParameterSymbol parameter, [NotNullWhen(true)] out MemberMappingInfo? memberInfo) => + TryMatchMember(new ConstructorParameterMember(parameter, BuilderContext.SymbolAccessor), true, out memberInfo); } diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewValueTupleConstructorBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewValueTupleConstructorBuilderContext.cs index e5ec184cb9..1631a936f5 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewValueTupleConstructorBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewValueTupleConstructorBuilderContext.cs @@ -1,5 +1,8 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Descriptors.Mappings.MemberMappings; +using Riok.Mapperly.Symbols; namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; @@ -7,15 +10,96 @@ namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; /// An implementation of . /// /// The type of the mapping. -public class NewValueTupleConstructorBuilderContext(MappingBuilderContext builderContext, T mapping) - : MembersMappingBuilderContext(builderContext, mapping), - INewValueTupleBuilderContext +public class NewValueTupleConstructorBuilderContext : MembersMappingBuilderContext, INewValueTupleBuilderContext where T : INewValueTupleMapping { + private readonly IReadOnlyDictionary _secondarySourceNames; + + /// + /// An implementation of . + /// + /// The type of the mapping. + public NewValueTupleConstructorBuilderContext(MappingBuilderContext builderContext, T mapping) + : base(builderContext, mapping) + { + _secondarySourceNames = mapping.SourceType.IsTupleType + ? BuildSecondarySourceFields() + : new Dictionary(StringComparer.Ordinal); + } + + public bool TryMatchTupleElement(IFieldSymbol member, [NotNullWhen(true)] out MemberMappingInfo? memberInfo) + { + if (TryMatchMember(new FieldMember(member, BuilderContext.SymbolAccessor), null, out memberInfo)) + return true; + + if ( + member.CorrespondingTupleField != null + && !string.Equals(member.CorrespondingTupleField.Name, member.Name, StringComparison.Ordinal) + ) + { + if (TryMatchMember(new FieldMember(member.CorrespondingTupleField, BuilderContext.SymbolAccessor), null, out memberInfo)) + return true; + } + + return false; + } + public void AddTupleConstructorParameterMapping(ValueTupleConstructorParameterMapping mapping) { - MemberConfigsByRootTargetName.Remove(mapping.Parameter.Name); - SetSourceMemberMapped(mapping.DelegateMapping.SourceGetter.MemberPath); Mapping.AddConstructorParameterMapping(mapping); + SetTargetMemberMapped(mapping.Parameter.Name); + MappingAdded(mapping.MemberInfo); + } + + protected override bool TryFindSourcePath( + IReadOnlyList> pathCandidates, + bool ignoreCase, + [NotNullWhen(true)] out MemberPath? sourceMemberPath + ) + { + if (base.TryFindSourcePath(pathCandidates, ignoreCase, out sourceMemberPath)) + return true; + + if (TryFindSecondaryTupleSourceField(pathCandidates, out sourceMemberPath)) + return true; + + return false; + } + + private bool TryFindSecondaryTupleSourceField( + IReadOnlyList> pathCandidates, + [NotNullWhen(true)] out MemberPath? sourceMemberPath + ) + { + foreach (var pathParts in pathCandidates) + { + if (pathParts.Count != 1) + continue; + + var name = pathParts[0]; + if (_secondarySourceNames.TryGetValue(name, out var sourceField)) + { + sourceMemberPath = new NonEmptyMemberPath( + Mapping.SourceType, + [new FieldMember(sourceField, BuilderContext.SymbolAccessor)] + ); + return true; + } + } + + sourceMemberPath = null; + return false; + } + + private Dictionary BuildSecondarySourceFields() + { + return ((INamedTypeSymbol)Mapping.SourceType) + .TupleElements.Where(t => + t.CorrespondingTupleField != null + && !string.Equals(t.Name, t.CorrespondingTupleField.Name, StringComparison.Ordinal) + && !IsIgnoredSourceMember(t.Name) + && !IsIgnoredSourceMember(t.CorrespondingTupleField.Name) + ) + .ToDictionary(t => t.CorrespondingTupleField!.Name, t => t, StringComparer.Ordinal); } } diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewValueTupleExpressionBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewValueTupleExpressionBuilderContext.cs index c05d4c56a7..c87a4acd71 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewValueTupleExpressionBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewValueTupleExpressionBuilderContext.cs @@ -1,5 +1,8 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Descriptors.Mappings.MemberMappings; +using Riok.Mapperly.Symbols; namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; @@ -7,27 +10,96 @@ namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; /// An implementation of . /// /// The type of the mapping. -public class NewValueTupleExpressionBuilderContext(MappingBuilderContext builderContext, T mapping) - : MembersContainerBuilderContext(builderContext, mapping), - INewValueTupleBuilderContext +public class NewValueTupleExpressionBuilderContext : MembersContainerBuilderContext, INewValueTupleBuilderContext where T : INewValueTupleMapping, IMemberAssignmentTypeMapping { + private readonly IReadOnlyDictionary _secondarySourceNames; + + /// + /// An implementation of . + /// + /// The type of the mapping. + public NewValueTupleExpressionBuilderContext(MappingBuilderContext builderContext, T mapping) + : base(builderContext, mapping) + { + _secondarySourceNames = mapping.SourceType.IsTupleType + ? BuildSecondarySourceFields() + : new Dictionary(StringComparer.Ordinal); + } + + public bool TryMatchTupleElement(IFieldSymbol member, [NotNullWhen(true)] out MemberMappingInfo? memberInfo) + { + if (TryMatchMember(new FieldMember(member, BuilderContext.SymbolAccessor), null, out memberInfo)) + return true; + + if ( + member.CorrespondingTupleField != null + && !string.Equals(member.CorrespondingTupleField.Name, member.Name, StringComparison.Ordinal) + ) + { + if (TryMatchMember(new FieldMember(member.CorrespondingTupleField, BuilderContext.SymbolAccessor), null, out memberInfo)) + return true; + } + + return false; + } + public void AddTupleConstructorParameterMapping(ValueTupleConstructorParameterMapping mapping) { - if (MemberConfigsByRootTargetName.TryGetValue(mapping.Parameter.Name, out var value)) + Mapping.AddConstructorParameterMapping(mapping); + SetTargetMemberMapped(mapping.Parameter.Name); + MappingAdded(mapping.MemberInfo); + } + + protected override bool TryFindSourcePath( + IReadOnlyList> pathCandidates, + bool ignoreCase, + [NotNullWhen(true)] out MemberPath? sourceMemberPath + ) + { + if (base.TryFindSourcePath(pathCandidates, ignoreCase, out sourceMemberPath)) + return true; + + if (TryFindSecondaryTupleSourceField(pathCandidates, out sourceMemberPath)) + return true; + + return false; + } + + private bool TryFindSecondaryTupleSourceField( + IReadOnlyList> pathCandidates, + [NotNullWhen(true)] out MemberPath? sourceMemberPath + ) + { + foreach (var pathParts in pathCandidates) { - // remove the mapping used to map the tuple constructor - value.RemoveAll(x => x.Target.Path.Count == 1); + if (pathParts.Count != 1) + continue; - // remove from dictionary and target members if there aren't any more mappings - if (!value.Any()) + var name = pathParts[0]; + if (_secondarySourceNames.TryGetValue(name, out var sourceField)) { - MemberConfigsByRootTargetName.Remove(mapping.Parameter.Name); - TargetMembers.Remove(mapping.Parameter.Name); + sourceMemberPath = new NonEmptyMemberPath( + Mapping.SourceType, + [new FieldMember(sourceField, BuilderContext.SymbolAccessor)] + ); + return true; } } - SetSourceMemberMapped(mapping.DelegateMapping.SourceGetter.MemberPath); - Mapping.AddConstructorParameterMapping(mapping); + sourceMemberPath = null; + return false; + } + + private Dictionary BuildSecondarySourceFields() + { + return ((INamedTypeSymbol)Mapping.SourceType) + .TupleElements.Where(t => + t.CorrespondingTupleField != null + && !string.Equals(t.Name, t.CorrespondingTupleField.Name, StringComparison.Ordinal) + && !IsIgnoredSourceMember(t.Name) + && !IsIgnoredSourceMember(t.CorrespondingTupleField.Name) + ) + .ToDictionary(t => t.CorrespondingTupleField!.Name, t => t, StringComparer.Ordinal); } } diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MemberMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MemberMappingBuilder.cs new file mode 100644 index 0000000000..7f944f4f6c --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MemberMappingBuilder.cs @@ -0,0 +1,189 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; +using Riok.Mapperly.Descriptors.Mappings; +using Riok.Mapperly.Descriptors.Mappings.MemberMappings; +using Riok.Mapperly.Descriptors.Mappings.MemberMappings.SourceValue; +using Riok.Mapperly.Diagnostics; +using Riok.Mapperly.Helpers; +using Riok.Mapperly.Symbols; + +namespace Riok.Mapperly.Descriptors.MappingBodyBuilders; + +/// +/// Builder for member mappings (member of objects). +/// +internal static class MemberMappingBuilder +{ + public enum CodeStyle + { + Expression, + Statement + } + + public static bool TryBuildContainerAssignment( + IMembersContainerBuilderContext ctx, + MemberMappingInfo memberInfo, + out bool requiresNullHandling, + [NotNullWhen(true)] out MemberAssignmentMapping? mapping + ) + { + if (!TryBuild(ctx, memberInfo, CodeStyle.Statement, out var mappedSourceValue)) + { + mapping = null; + requiresNullHandling = false; + return false; + } + + var setterTargetPath = SetterMemberPath.Build(ctx.BuilderContext, memberInfo.TargetMember); + requiresNullHandling = mappedSourceValue is MappedMemberSourceValue { RequiresSourceNullCheck: true }; + mapping = new MemberAssignmentMapping(setterTargetPath, mappedSourceValue, memberInfo); + return true; + } + + public static bool TryBuildAssignment( + IMembersBuilderContext ctx, + MemberMappingInfo memberInfo, + [NotNullWhen(true)] out MemberAssignmentMapping? mapping + ) + { + if (!TryBuild(ctx, memberInfo, CodeStyle.Expression, out var mappedSourceValue)) + { + mapping = null; + return false; + } + + var setterTargetPath = SetterMemberPath.Build(ctx.BuilderContext, memberInfo.TargetMember); + mapping = new MemberAssignmentMapping(setterTargetPath, mappedSourceValue, memberInfo); + return true; + } + + public static bool TryBuild( + IMembersBuilderContext ctx, + MemberMappingInfo memberMappingInfo, + CodeStyle codeStyle, + [NotNullWhen(true)] out ISourceValue? sourceValue + ) + { + var mappingKey = memberMappingInfo.ToTypeMappingKey(); + var delegateMapping = ctx.BuilderContext.FindOrBuildLooseNullableMapping( + mappingKey, + diagnosticLocation: memberMappingInfo.Configuration?.Location + ); + + var sourceMember = memberMappingInfo.SourceMember; + var targetMember = memberMappingInfo.TargetMember; + if (delegateMapping == null) + { + ctx.BuilderContext.ReportDiagnostic( + DiagnosticDescriptors.CouldNotMapMember, + sourceMember.ToDisplayString(), + targetMember.ToDisplayString() + ); + sourceValue = null; + return false; + } + + if (codeStyle == CodeStyle.Statement) + { + sourceValue = BuildBlockNullHandlingMapping(ctx, delegateMapping, sourceMember, targetMember); + return true; + } + + if (!ValidateLoopMapping(ctx, delegateMapping, sourceMember, targetMember)) + { + sourceValue = null; + return false; + } + + sourceValue = BuildInlineNullHandlingMapping(ctx, delegateMapping, sourceMember, targetMember.MemberType); + return true; + } + + private static bool ValidateLoopMapping( + IMembersBuilderContext ctx, + INewInstanceMapping delegateMapping, + MemberPath sourceMember, + NonEmptyMemberPath targetMember + ) + { + if (!ReferenceEqualityComparer.Instance.Equals(delegateMapping, ctx.Mapping)) + return true; + + if (targetMember.Member is ConstructorParameterMember) + { + ctx.BuilderContext.ReportDiagnostic( + DiagnosticDescriptors.ReferenceLoopInCtorMapping, + sourceMember.ToDisplayString(includeMemberType: false), + ctx.Mapping.TargetType, + targetMember.ToDisplayString(includeRootType: false, includeMemberType: false) + ); + } + else + { + ctx.BuilderContext.ReportDiagnostic( + DiagnosticDescriptors.ReferenceLoopInInitOnlyMapping, + sourceMember.ToDisplayString(includeMemberType: false), + targetMember.ToDisplayString(includeMemberType: false) + ); + } + return false; + } + + private static NullMappedMemberSourceValue BuildInlineNullHandlingMapping( + IMembersBuilderContext ctx, + INewInstanceMapping delegateMapping, + MemberPath sourcePath, + ITypeSymbol targetMemberType + ) + { + var getterSourcePath = GetterMemberPath.Build(ctx.BuilderContext, sourcePath); + + var nullFallback = NullFallbackValue.Default; + if (!delegateMapping.SourceType.IsNullable() && sourcePath.IsAnyNullable()) + { + nullFallback = ctx.BuilderContext.GetNullFallbackValue(targetMemberType); + } + + return new NullMappedMemberSourceValue( + delegateMapping, + getterSourcePath, + targetMemberType, + nullFallback, + !ctx.BuilderContext.IsExpression + ); + } + + private static ISourceValue BuildBlockNullHandlingMapping( + IMembersBuilderContext ctx, + INewInstanceMapping delegateMapping, + MemberPath sourceMember, + NonEmptyMemberPath targetMember + ) + { + var getterSourcePath = GetterMemberPath.Build(ctx.BuilderContext, sourceMember); + + // no member of the source path is nullable, no null handling needed + if (!sourceMember.IsAnyNullable()) + { + return new MappedMemberSourceValue(delegateMapping, getterSourcePath, false, true); + } + + // If null property assignments are allowed, + // and the delegate mapping accepts nullable types (and converts it to a non-nullable type), + // or the mapping is synthetic and the target accepts nulls + // 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) + ) + { + return new MappedMemberSourceValue(delegateMapping, getterSourcePath, true, false); + } + + // additional null condition check + // (only map if the source is not null, else may throw depending on settings) + // via RequiresNullCheck + return new MappedMemberSourceValue(delegateMapping, getterSourcePath, false, true); + } +} diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewInstanceObjectMemberMappingBodyBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewInstanceObjectMemberMappingBodyBuilder.cs index b26d785e28..e9d376b541 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewInstanceObjectMemberMappingBodyBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewInstanceObjectMemberMappingBodyBuilder.cs @@ -1,12 +1,10 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis; using Riok.Mapperly.Abstractions; -using Riok.Mapperly.Configuration; using Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Descriptors.Mappings.MemberMappings; using Riok.Mapperly.Diagnostics; -using Riok.Mapperly.Helpers; using Riok.Mapperly.Symbols; namespace Riok.Mapperly.Descriptors.MappingBodyBuilders; @@ -30,140 +28,58 @@ public static void BuildMappingBody(MappingBuilderContext ctx, NewInstanceObject BuildConstructorMapping(mappingCtx); BuildInitMemberMappings(mappingCtx); ObjectMemberMappingBodyBuilder.BuildMappingBody(mappingCtx); + mappingCtx.AddDiagnostics(); } - private static void BuildInitMemberMappings(INewInstanceBuilderContext ctx, bool includeAllMembers = false) + private static void BuildInitMemberMappings( + INewInstanceBuilderContext ctx, + bool includeAllMembers = false + ) { var initOnlyTargetMembers = includeAllMembers - ? ctx.TargetMembers.Values.ToArray() - : ctx.TargetMembers.Values.Where(x => x.CanOnlySetViaInitializer()).ToArray(); + ? ctx.EnumerateUnmappedTargetMembers().ToArray() + : ctx.EnumerateUnmappedTargetMembers().Where(x => x.CanOnlySetViaInitializer()).ToArray(); foreach (var targetMember in initOnlyTargetMembers) { - ctx.TargetMembers.Remove(targetMember.Name); - - if (ctx.MemberConfigsByRootTargetName.Remove(targetMember.Name, out var memberConfigs)) + if (ctx.TryMatchInitOnlyMember(targetMember, out var memberInfo)) { - BuildInitMemberMapping(ctx, targetMember, memberConfigs); + BuildInitMemberMapping(ctx, memberInfo); continue; } - if (!ctx.TryFindNestedSourceMembersPath(targetMember.Name, out var sourceMemberPath)) - { - if (targetMember.IsRequired) - { - ctx.BuilderContext.ReportDiagnostic( - DiagnosticDescriptors.RequiredMemberNotMapped, - targetMember.Name, - ctx.Mapping.TargetType, - ctx.Mapping.SourceType - ); - } - else if (ctx.BuilderContext.Configuration.Members.RequiredMappingStrategy.HasFlag(RequiredMappingStrategy.Target)) - { - ctx.BuilderContext.ReportDiagnostic( - DiagnosticDescriptors.SourceMemberNotFound, - targetMember.Name, - ctx.Mapping.TargetType, - ctx.Mapping.SourceType - ); - } - - continue; - } - - BuildInitMemberMapping(ctx, targetMember, sourceMemberPath); - } - } - - private static void BuildInitMemberMapping( - INewInstanceBuilderContext ctx, - IMappableMember targetMember, - IReadOnlyCollection memberConfigs - ) - { - // add configured mapping - // target paths are not supported (yet), only target properties - if (memberConfigs.Count > 1) - { - ctx.BuilderContext.ReportDiagnostic( - DiagnosticDescriptors.MultipleConfigurationsForInitOnlyMember, - targetMember.Type, - targetMember.Name - ); - } - - var memberConfig = memberConfigs.First(); - if (memberConfig.Target.Path.Count > 1) - { + // set the member mapped as it is an init only member + // diagnostics are already reported + // and no further mapping attempts should be undertaken ctx.BuilderContext.ReportDiagnostic( - DiagnosticDescriptors.InitOnlyMemberDoesNotSupportPaths, - targetMember.Type, - memberConfig.Target.FullName + targetMember.IsRequired ? DiagnosticDescriptors.RequiredMemberNotMapped : DiagnosticDescriptors.SourceMemberNotFound, + targetMember.Name, + ctx.Mapping.TargetType, + ctx.Mapping.SourceType ); - return; - } - - if ( - !ctx.BuilderContext.SymbolAccessor.TryFindMemberPath(ctx.Mapping.SourceType, memberConfig.Source.Path, out var sourceMemberPath) - ) - { - if (ctx.BuilderContext.Configuration.Members.RequiredMappingStrategy.HasFlag(RequiredMappingStrategy.Target)) - { - ctx.BuilderContext.ReportDiagnostic( - DiagnosticDescriptors.SourceMemberNotFound, - targetMember.Name, - ctx.Mapping.TargetType, - ctx.Mapping.SourceType - ); - } - - return; + ctx.SetTargetMemberMapped(targetMember); } - - BuildInitMemberMapping(ctx, targetMember, sourceMemberPath, memberConfig); } private static void BuildInitMemberMapping( - INewInstanceBuilderContext ctx, - IMappableMember targetMember, - MemberPath sourcePath, - MemberMappingConfiguration? memberConfig = null + INewInstanceBuilderContext ctx, + MemberMappingInfo memberInfo ) { - var targetPath = new NonEmptyMemberPath(ctx.Mapping.TargetType, new[] { targetMember }); - if (!ObjectMemberMappingBodyBuilder.ValidateMappingSpecification(ctx, sourcePath, targetPath, true)) - return; - - var mappingKey = new TypeMappingKey(sourcePath.MemberType, targetMember.Type, memberConfig?.ToTypeMappingConfiguration()); - var delegateMapping = ctx.BuilderContext.FindOrBuildLooseNullableMapping(mappingKey, diagnosticLocation: memberConfig?.Location); + // consume member configs + // to ensure no further mappings are created for these configurations, + // even if a mapping validation fails + ctx.ConsumeMemberConfig(memberInfo); - if (delegateMapping == null) - { - ctx.BuilderContext.ReportDiagnostic( - DiagnosticDescriptors.CouldNotMapMember, - sourcePath.ToDisplayString(), - targetPath.ToDisplayString() - ); + if (!ObjectMemberMappingBodyBuilder.ValidateMappingSpecification(ctx, memberInfo, true)) return; - } - if (delegateMapping.Equals(ctx.Mapping)) - { - ctx.BuilderContext.ReportDiagnostic( - DiagnosticDescriptors.ReferenceLoopInInitOnlyMapping, - sourcePath.ToDisplayString(includeMemberType: false), - targetPath.ToDisplayString(includeMemberType: false) - ); + if (!MemberMappingBuilder.TryBuildAssignment(ctx, memberInfo, out var memberAssignmentMapping)) return; - } - var setterTargetPath = SetterMemberPath.Build(ctx.BuilderContext, targetPath); - var memberMapping = ctx.BuildNullMemberMapping(sourcePath, delegateMapping, targetMember.Type); - var memberAssignmentMapping = new MemberAssignmentMapping(setterTargetPath, memberMapping); ctx.AddInitMemberMapping(memberAssignmentMapping); } - private static void BuildConstructorMapping(INewInstanceBuilderContext ctx) + private static void BuildConstructorMapping(INewInstanceBuilderContext ctx) { if (ctx.Mapping.TargetType is not INamedTypeSymbol namedTargetType) { @@ -191,7 +107,7 @@ private static void BuildConstructorMapping(INewInstanceBuilderContext foreach (var ctorCandidate in ctorCandidates) { - if (!TryBuildConstructorMapping(ctx, ctorCandidate, out var mappedTargetMemberNames, out var constructorParameterMappings)) + if (!TryBuildConstructorMapping(ctx, ctorCandidate, out var constructorParameterMappings)) { if (ctx.BuilderContext.SymbolAccessor.HasAttribute(ctorCandidate)) { @@ -205,10 +121,9 @@ private static void BuildConstructorMapping(INewInstanceBuilderContext continue; } - ctx.TargetMembers.RemoveRange(mappedTargetMemberNames); - foreach (var constructorParameterMapping in constructorParameterMappings) + foreach (var mapping in constructorParameterMappings) { - ctx.AddConstructorParameterMapping(constructorParameterMapping); + ctx.AddConstructorParameterMapping(mapping); } return; @@ -220,16 +135,17 @@ private static void BuildConstructorMapping(INewInstanceBuilderContext private static bool TryBuildConstructorMapping( INewInstanceBuilderContext ctx, IMethodSymbol ctor, - [NotNullWhen(true)] out ISet? mappedTargetMemberNames, - [NotNullWhen(true)] out ISet? constructorParameterMappings + [NotNullWhen(true)] out List? constructorParameterMappings ) { - constructorParameterMappings = new HashSet(); - mappedTargetMemberNames = new HashSet(); + constructorParameterMappings = new List(); var skippedOptionalParam = false; foreach (var parameter in ctor.Parameters) { - if (!TryFindConstructorParameterSourcePath(ctx, parameter, out var sourcePath, out var memberConfig)) + if ( + !ctx.TryMatchParameter(parameter, out var memberMappingInfo) + || !MemberMappingBuilder.TryBuild(ctx, memberMappingInfo, MemberMappingBuilder.CodeStyle.Expression, out var sourceValue) + ) { // expressions do not allow skipping of optional parameters if (!parameter.IsOptional || ctx.BuilderContext.IsExpression) @@ -239,90 +155,8 @@ private static bool TryBuildConstructorMapping( continue; } - // nullability is handled inside the member mapping - var parameterType = ctx.BuilderContext.SymbolAccessor.UpgradeNullable(parameter.Type); - var typeMapping = new TypeMappingKey(sourcePath.MemberType, parameterType, memberConfig?.ToTypeMappingConfiguration()); - var delegateMapping = ctx.BuilderContext.FindOrBuildLooseNullableMapping( - typeMapping, - diagnosticLocation: memberConfig?.Location - ); - - if (delegateMapping == null) - { - if (!parameter.IsOptional) - return false; - - skippedOptionalParam = true; - continue; - } - - if (delegateMapping.Equals(ctx.Mapping)) - { - ctx.BuilderContext.ReportDiagnostic( - DiagnosticDescriptors.ReferenceLoopInCtorMapping, - sourcePath.ToDisplayString(includeMemberType: false), - ctx.Mapping.TargetType, - parameter.Name - ); - return false; - } - - var memberMapping = ctx.BuildNullMemberMapping(sourcePath, delegateMapping, parameterType); - var ctorMapping = new ConstructorParameterMapping(parameter, memberMapping, skippedOptionalParam); + var ctorMapping = new ConstructorParameterMapping(parameter, sourceValue, skippedOptionalParam, memberMappingInfo); constructorParameterMappings.Add(ctorMapping); - mappedTargetMemberNames.Add(parameter.Name); - } - - return true; - } - - private static bool TryFindConstructorParameterSourcePath( - INewInstanceBuilderContext ctx, - IParameterSymbol parameter, - [NotNullWhen(true)] out MemberPath? sourcePath, - out MemberMappingConfiguration? memberConfig - ) - { - sourcePath = null; - memberConfig = null; - - if ( - !ctx.RootTargetNameCasingMapping.TryGetValue(parameter.Name, out var parameterName) - || !ctx.MemberConfigsByRootTargetName.TryGetValue(parameterName, out var memberConfigs) - ) - { - return ctx.TryFindNestedSourceMembersPath(parameter.Name, out sourcePath, true); - } - - if (memberConfigs.Count > 1) - { - ctx.BuilderContext.ReportDiagnostic( - DiagnosticDescriptors.MultipleConfigurationsForConstructorParameter, - parameter.Type, - parameter.Name - ); - } - - memberConfig = memberConfigs.First(); - if (memberConfig.Target.Path.Count > 1) - { - ctx.BuilderContext.ReportDiagnostic( - DiagnosticDescriptors.ConstructorParameterDoesNotSupportPaths, - parameter.Type, - memberConfig.Target.FullName - ); - return false; - } - - if (!ctx.BuilderContext.SymbolAccessor.TryFindMemberPath(ctx.Mapping.SourceType, memberConfig.Source.Path, out sourcePath)) - { - ctx.BuilderContext.ReportDiagnostic( - DiagnosticDescriptors.SourceMemberNotFound, - memberConfig.Source.FullName, - ctx.Mapping.TargetType, - ctx.Mapping.SourceType - ); - return false; } return true; diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewValueTupleMappingBodyBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewValueTupleMappingBodyBuilder.cs index c24fcdf548..b67b5baf88 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewValueTupleMappingBodyBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewValueTupleMappingBodyBuilder.cs @@ -1,12 +1,9 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics; using Microsoft.CodeAnalysis; -using Riok.Mapperly.Configuration; using Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Descriptors.Mappings.MemberMappings; using Riok.Mapperly.Diagnostics; -using Riok.Mapperly.Helpers; -using Riok.Mapperly.Symbols; namespace Riok.Mapperly.Descriptors.MappingBodyBuilders; @@ -29,45 +26,39 @@ public static void BuildMappingBody(MappingBuilderContext ctx, NewValueTupleExpr private static void BuildTupleConstructorMapping(INewValueTupleBuilderContext ctx) { - if (ctx.Mapping.TargetType is not INamedTypeSymbol namedTargetType) - { - ctx.BuilderContext.ReportDiagnostic(DiagnosticDescriptors.NoConstructorFound, ctx.BuilderContext.Target); - return; - } + Debug.Assert(ctx.Mapping.TargetType.IsTupleType); + Debug.Assert(ctx.Mapping.TargetType is INamedTypeSymbol); - if (!TryBuildTupleConstructorMapping(ctx, namedTargetType, out var constructorParameterMappings, out var mappedTargetMemberNames)) + if (!TryBuildTupleConstructorMapping(ctx, out var constructorParameterMappings)) { ctx.BuilderContext.ReportDiagnostic(DiagnosticDescriptors.NoConstructorFound, ctx.BuilderContext.Target); return; } - var removableMappedTargetMemberNames = mappedTargetMemberNames.Where(x => !ctx.MemberConfigsByRootTargetName.ContainsKey(x)); - - ctx.TargetMembers.RemoveRange(removableMappedTargetMemberNames); - foreach (var constructorParameterMapping in constructorParameterMappings) + foreach (var mapping in constructorParameterMappings) { - ctx.AddTupleConstructorParameterMapping(constructorParameterMapping); + ctx.AddTupleConstructorParameterMapping(mapping); } } private static bool TryBuildTupleConstructorMapping( INewValueTupleBuilderContext ctx, - INamedTypeSymbol namedTargetType, - out HashSet constructorParameterMappings, - out HashSet mappedTargetMemberNames + out List constructorParameterMappings ) { - mappedTargetMemberNames = new HashSet(); - constructorParameterMappings = new HashSet(); + constructorParameterMappings = new List(); - foreach (var targetMember in namedTargetType.TupleElements) - { - if (!ctx.TargetMembers.ContainsKey(targetMember.Name)) - { - return false; - } + var targetMembers = ctx.EnumerateUnmappedTargetMembers().ToList(); + + // this can only happen if a target member is ignored + // if this is the case, a mapping can never be created... + if (targetMembers.Count != ((INamedTypeSymbol)ctx.Mapping.TargetType).TupleElements.Length) + return false; - if (!TryFindConstructorParameterSourcePath(ctx, targetMember, out var sourcePath, out var memberConfig)) + foreach (var targetMember in targetMembers) + { + var targetField = (IFieldSymbol)targetMember.MemberSymbol; + if (!ctx.TryMatchTupleElement(targetField, out var memberMappingInfo)) { ctx.BuilderContext.ReportDiagnostic( DiagnosticDescriptors.SourceMemberNotFound, @@ -75,123 +66,22 @@ out HashSet mappedTargetMemberNames ctx.Mapping.TargetType, ctx.Mapping.SourceType ); - - return false; - } - - // nullability is handled inside the member expressionMapping - var targetMemberType = ctx.BuilderContext.SymbolAccessor.UpgradeNullable(targetMember.Type); - var mappingKey = new TypeMappingKey(sourcePath.MemberType, targetMemberType, memberConfig?.ToTypeMappingConfiguration()); - var delegateMapping = ctx.BuilderContext.FindOrBuildLooseNullableMapping( - mappingKey, - diagnosticLocation: memberConfig?.Location - ); - if (delegateMapping == null) - { - ctx.BuilderContext.ReportDiagnostic( - DiagnosticDescriptors.CouldNotMapMember, - sourcePath.ToDisplayString(), - FormatTargetMemberForDiagnostic(ctx.Mapping.TargetType, targetMember) - ); + ctx.SetTargetMemberMapped(targetMember); return false; } - if (delegateMapping.Equals(ctx.Mapping)) + if ( + !MemberMappingBuilder.TryBuild(ctx, memberMappingInfo, MemberMappingBuilder.CodeStyle.Expression, out var mappedSourceValue) + ) { - ctx.BuilderContext.ReportDiagnostic( - DiagnosticDescriptors.ReferenceLoopInCtorMapping, - sourcePath.ToDisplayString(includeMemberType: false), - ctx.Mapping.TargetType, - targetMember.Name - ); + ctx.SetTargetMemberMapped(targetMember); return false; } - var memberMapping = ctx.BuildNullMemberMapping(sourcePath, delegateMapping, targetMemberType); - var ctorMapping = new ValueTupleConstructorParameterMapping(targetMember, memberMapping); + var ctorMapping = new ValueTupleConstructorParameterMapping(targetField, mappedSourceValue, memberMappingInfo); constructorParameterMappings.Add(ctorMapping); - mappedTargetMemberNames.Add(targetMember.Name); - } - - return true; - } - - /// - /// Formats the target member in the same way that does. - /// - private static string FormatTargetMemberForDiagnostic(ITypeSymbol targetType, IFieldSymbol targetMember) - { - return $"{targetType.ToDisplayString()}.{targetMember.Name} of type {targetMember.Type.ToDisplayString()}"; - } - - private static bool TryFindConstructorParameterSourcePath( - INewValueTupleBuilderContext ctx, - IFieldSymbol field, - [NotNullWhen(true)] out MemberPath? sourcePath, - out MemberMappingConfiguration? memberConfig - ) - { - sourcePath = null; - memberConfig = null; - - if (!ctx.MemberConfigsByRootTargetName.TryGetValue(field.Name, out var memberConfigs)) - return TryBuildConstructorParameterSourcePath(ctx, field, out sourcePath); - - // remove nested targets - var initMemberPaths = memberConfigs.Where(x => x.Target.Path.Count == 1).ToArray(); - - // if all memberConfigs are nested than do normal mapping - if (initMemberPaths.Length == 0) - return TryBuildConstructorParameterSourcePath(ctx, field, out sourcePath); - - if (initMemberPaths.Length > 1) - { - ctx.BuilderContext.ReportDiagnostic( - DiagnosticDescriptors.MultipleConfigurationsForConstructorParameter, - field.Type, - field.Name - ); - } - - memberConfig = initMemberPaths.First(); - if (ctx.BuilderContext.SymbolAccessor.TryFindMemberPath(ctx.Mapping.SourceType, memberConfig.Source.Path, out sourcePath)) - return true; - - ctx.BuilderContext.ReportDiagnostic( - DiagnosticDescriptors.SourceMemberNotFound, - memberConfig.Source, - ctx.Mapping.TargetType, - ctx.Mapping.SourceType - ); - return false; - } - - private static bool TryBuildConstructorParameterSourcePath( - INewValueTupleBuilderContext ctx, - IFieldSymbol field, - out MemberPath? sourcePath - ) - { - if (ctx.TryFindNestedSourceMembersPath(field.Name, out sourcePath)) - { - return true; } - // if standard matching fails, try to use the positional fields - // if source is a tuple compare the underlying field ie, Item1, Item2 - if (!ctx.Mapping.SourceType.IsTupleType || ctx.Mapping.SourceType is not INamedTypeSymbol namedType) - return false; - - var mappableField = namedType.TupleElements.FirstOrDefault(x => - x.CorrespondingTupleField != default - && !ctx.IgnoredSourceMemberNames.Contains(x.Name) - && string.Equals(field.CorrespondingTupleField!.Name, x.CorrespondingTupleField!.Name) - ); - - if (mappableField == default) - return false; - - sourcePath = new NonEmptyMemberPath(namedType, new[] { new FieldMember(mappableField, ctx.BuilderContext.SymbolAccessor) }); return true; } } diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/ObjectMemberMappingBodyBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/ObjectMemberMappingBodyBuilder.cs index d127ca3194..3970837415 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/ObjectMemberMappingBodyBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/ObjectMemberMappingBodyBuilder.cs @@ -1,11 +1,8 @@ using System.Diagnostics.CodeAnalysis; -using Riok.Mapperly.Abstractions; -using Riok.Mapperly.Configuration; using Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Descriptors.Mappings.MemberMappings; using Riok.Mapperly.Diagnostics; -using Riok.Mapperly.Helpers; using Riok.Mapperly.Symbols; namespace Riok.Mapperly.Descriptors.MappingBodyBuilders; @@ -19,94 +16,43 @@ public static void BuildMappingBody(MappingBuilderContext ctx, IMemberAssignment { var mappingCtx = new MembersContainerBuilderContext(ctx, mapping); BuildMappingBody(mappingCtx); - } - public static void BuildMappingBody(IMembersContainerBuilderContext ctx) - { - foreach (var targetMember in ctx.TargetMembers.Values) + // init only members should not result in unmapped diagnostics for existing target mappings + foreach (var initOnlyTargetMember in mappingCtx.EnumerateUnmappedTargetMembers().Where(x => x.IsInitOnly)) { - if (ctx.MemberConfigsByRootTargetName.Remove(targetMember.Name, out var memberConfigs)) - { - // add all configured mappings - // order by target path count to map less nested items first (otherwise they would overwrite all others) - // eg. target.A = source.B should be mapped before target.A.Id = source.B.Id - foreach (var config in memberConfigs.OrderBy(x => x.Target.Path.Count)) - { - BuildMemberAssignmentMapping(ctx, config); - } - - continue; - } - - if (ctx.TryFindNestedSourceMembersPath(targetMember.Name, out var sourceMemberPath)) - { - BuildMemberAssignmentMapping(ctx, sourceMemberPath, new NonEmptyMemberPath(ctx.Mapping.TargetType, new[] { targetMember })); - continue; - } - - if ( - targetMember.CanSet - && ctx.BuilderContext.Configuration.Members.RequiredMappingStrategy.HasFlag(RequiredMappingStrategy.Target) - ) - { - ctx.BuilderContext.ReportDiagnostic( - DiagnosticDescriptors.SourceMemberNotFound, - targetMember.Name, - ctx.Mapping.TargetType, - ctx.Mapping.SourceType - ); - } + mappingCtx.SetTargetMemberMapped(initOnlyTargetMember); } - - ctx.AddDiagnostics(); + mappingCtx.AddDiagnostics(); } - private static void BuildMemberAssignmentMapping( - IMembersContainerBuilderContext ctx, - MemberMappingConfiguration config - ) + public static void BuildMappingBody(IMembersContainerBuilderContext ctx) { - if ( - !ctx.BuilderContext.SymbolAccessor.TryFindMemberPath(ctx.Mapping.TargetType, config.Target.Path, out var foundMemberPath) - || foundMemberPath is not NonEmptyMemberPath targetMemberPath - ) + foreach (var targetMember in ctx.EnumerateUnmappedOrConfiguredTargetMembers()) { - ctx.BuilderContext.ReportDiagnostic( - DiagnosticDescriptors.ConfiguredMappingTargetMemberNotFound, - config.Target.FullName, - ctx.Mapping.TargetType - ); - return; - } - - if (!ctx.BuilderContext.SymbolAccessor.TryFindMemberPath(ctx.Mapping.SourceType, config.Source.Path, out var sourceMemberPath)) - { - ctx.BuilderContext.ReportDiagnostic( - DiagnosticDescriptors.ConfiguredMappingSourceMemberNotFound, - config.Source.FullName, - ctx.Mapping.SourceType - ); - return; + foreach (var memberMappingInfo in ctx.MatchMembers(targetMember)) + { + BuildMemberAssignmentMapping(ctx, memberMappingInfo); + } } - - BuildMemberAssignmentMapping(ctx, sourceMemberPath, targetMemberPath, config); } [SuppressMessage("Meziantou.Analyzer", "MA0051:MethodIsTooLong")] public static bool ValidateMappingSpecification( IMembersBuilderContext ctx, - MemberPath sourceMemberPath, - NonEmptyMemberPath targetMemberPath, + MemberMappingInfo memberInfo, bool allowInitOnlyMember = false ) { + var sourceMemberPath = memberInfo.SourceMember; + var targetMemberPath = memberInfo.TargetMember; + // the target member path is readonly or not accessible if (!targetMemberPath.Member.CanSet) { ctx.BuilderContext.ReportDiagnostic( DiagnosticDescriptors.CannotMapToReadOnlyMember, - sourceMemberPath.ToDisplayString(), - targetMemberPath.ToDisplayString() + memberInfo.DescribeSource(), + targetMemberPath.ToDisplayString(includeMemberType: false) ); return false; } @@ -122,8 +68,8 @@ public static bool ValidateMappingSpecification( { ctx.BuilderContext.ReportDiagnostic( DiagnosticDescriptors.CannotMapToReadOnlyMember, - sourceMemberPath.ToDisplayString(), - targetMemberPath.ToDisplayString() + memberInfo.DescribeSource(), + targetMemberPath.ToDisplayString(includeMemberType: false) ); return false; } @@ -140,7 +86,7 @@ public static bool ValidateMappingSpecification( { ctx.BuilderContext.ReportDiagnostic( DiagnosticDescriptors.CannotMapToWriteOnlyMemberPath, - sourceMemberPath.ToDisplayString(), + memberInfo.DescribeSource(), targetMemberPath.ToDisplayString() ); return false; @@ -148,7 +94,7 @@ public static bool ValidateMappingSpecification( // cannot assign to intermediate value type, error CS1612 // invalid mapping a value type has a property set - if (!ValidateStructModification(ctx, sourceMemberPath, targetMemberPath)) + if (!ValidateStructModification(ctx, memberInfo)) return false; // a target member path part is init only @@ -157,7 +103,7 @@ public static bool ValidateMappingSpecification( { ctx.BuilderContext.ReportDiagnostic( DiagnosticDescriptors.CannotMapToInitOnlyMemberPath, - sourceMemberPath.ToDisplayString(includeMemberType: false), + memberInfo.DescribeSource(), targetMemberPath.ToDisplayString(includeMemberType: false) ); return false; @@ -197,28 +143,24 @@ public static bool ValidateMappingSpecification( return true; } - private static bool ValidateStructModification( - IMembersBuilderContext ctx, - MemberPath sourceMemberPath, - MemberPath targetMemberPath - ) + private static bool ValidateStructModification(IMembersBuilderContext ctx, MemberMappingInfo memberInfo) { - if (targetMemberPath.Path.Count <= 1) + if (memberInfo.TargetMember.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--) + for (var i = memberInfo.TargetMember.Path.Count - 2; i >= 0; i--) { - var member = targetMemberPath.Path[i]; + var member = memberInfo.TargetMember.Path[i]; if (member is PropertyMember { Type: { IsValueType: true, IsRefLikeType: false } }) { ctx.BuilderContext.ReportDiagnostic( DiagnosticDescriptors.CannotMapToTemporarySourceMember, - sourceMemberPath.ToDisplayString(), - targetMemberPath.ToDisplayString(), - member.Name, - member.Type + memberInfo.DescribeSource(), + memberInfo.TargetMember.ToDisplayString(), + member.Type, + member.Name ); return false; } @@ -232,88 +174,41 @@ MemberPath targetMemberPath private static void BuildMemberAssignmentMapping( IMembersContainerBuilderContext ctx, - MemberPath sourceMemberPath, - NonEmptyMemberPath targetMemberPath, - MemberMappingConfiguration? memberConfig = null + MemberMappingInfo memberMappingInfo ) { - if (TryAddExistingTargetMapping(ctx, sourceMemberPath, targetMemberPath)) - return; + // consume member configs + // to ensure no further mappings are created for these configurations, + // even if a mapping validation fails + ctx.ConsumeMemberConfig(memberMappingInfo); - if (!ValidateMappingSpecification(ctx, sourceMemberPath, targetMemberPath)) + if (TryAddExistingTargetMapping(ctx, memberMappingInfo)) return; - var delegateMapping = TryBuildMemberTypeMapping(ctx, sourceMemberPath, targetMemberPath, memberConfig); - if (delegateMapping == null) + if (!ValidateMappingSpecification(ctx, memberMappingInfo)) return; - var getterSourcePath = GetterMemberPath.Build(ctx.BuilderContext, sourceMemberPath); - var setterTargetPath = SetterMemberPath.Build(ctx.BuilderContext, targetMemberPath); - - // no member of the source path is nullable, no null handling needed - if (!sourceMemberPath.IsAnyNullable()) - { - var memberMapping = new MemberMapping(delegateMapping, getterSourcePath, false, true); - ctx.AddMemberAssignmentMapping(new MemberAssignmentMapping(setterTargetPath, memberMapping)); + if (!MemberMappingBuilder.TryBuildContainerAssignment(ctx, memberMappingInfo, out var requiresNullHandling, out var mapping)) return; - } - // If null property assignments are allowed, - // and the delegate mapping accepts nullable types (and converts it to a non-nullable type), - // or the mapping is synthetic and the target accepts nulls - // 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 && targetMemberPath.Member.IsNullable) - ) + if (requiresNullHandling) { - var memberMapping = new MemberMapping(delegateMapping, getterSourcePath, true, false); - ctx.AddMemberAssignmentMapping(new MemberAssignmentMapping(setterTargetPath, memberMapping)); - return; + ctx.AddNullDelegateMemberAssignmentMapping(mapping); } - - // additional null condition check - // (only map if source is not null, else may throw depending on settings) - ctx.AddNullDelegateMemberAssignmentMapping( - new MemberAssignmentMapping(setterTargetPath, new MemberMapping(delegateMapping, getterSourcePath, false, true)) - ); - } - - private static INewInstanceMapping? TryBuildMemberTypeMapping( - IMembersContainerBuilderContext ctx, - MemberPath sourceMemberPath, - NonEmptyMemberPath targetMemberPath, - MemberMappingConfiguration? memberConfig - ) - { - // nullability is handled inside the member mapping - var typeMapping = new TypeMappingKey( - sourceMemberPath.MemberType, - targetMemberPath.MemberType, - memberConfig?.ToTypeMappingConfiguration() - ); - - var mapping = ctx.BuilderContext.FindOrBuildLooseNullableMapping(typeMapping, diagnosticLocation: memberConfig?.Location); - if (mapping != null) + else { - return mapping; + ctx.AddMemberAssignmentMapping(mapping); } - - // couldn't build the mapping - ctx.BuilderContext.ReportDiagnostic( - DiagnosticDescriptors.CouldNotMapMember, - sourceMemberPath.ToDisplayString(), - targetMemberPath.ToDisplayString() - ); - return null; } private static bool TryAddExistingTargetMapping( IMembersContainerBuilderContext ctx, - MemberPath sourceMemberPath, - NonEmptyMemberPath targetMemberPath + MemberMappingInfo memberMappingInfo ) { + var sourceMemberPath = memberMappingInfo.SourceMember; + var targetMemberPath = memberMappingInfo.TargetMember; + // if the member is readonly // and the target and source path is readable, // we try to create an existing target mapping @@ -326,15 +221,14 @@ NonEmptyMemberPath targetMemberPath return false; } - var mappingKey = new TypeMappingKey(sourceMemberPath.MemberType, targetMemberPath.MemberType); - var existingTargetMapping = ctx.BuilderContext.FindOrBuildExistingTargetMapping(mappingKey); + var existingTargetMapping = ctx.BuilderContext.FindOrBuildExistingTargetMapping(memberMappingInfo.ToTypeMappingKey()); if (existingTargetMapping == null) return false; var getterSourcePath = GetterMemberPath.Build(ctx.BuilderContext, sourceMemberPath); - var setterTargetPath = GetterMemberPath.Build(ctx.BuilderContext, targetMemberPath); + var getterTargetPath = GetterMemberPath.Build(ctx.BuilderContext, targetMemberPath); - var memberMapping = new MemberExistingTargetMapping(existingTargetMapping, getterSourcePath, setterTargetPath); + var memberMapping = new MemberExistingTargetMapping(existingTargetMapping, getterSourcePath, getterTargetPath, memberMappingInfo); ctx.AddMemberAssignmentMapping(memberMapping); return true; } diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs index 3a54e955f6..98e1b49d00 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs @@ -74,9 +74,6 @@ bool ignoreDerivedTypes public ObjectFactoryCollection ObjectFactories { get; } - /// - public IReadOnlyCollection UserMappings => MappingBuilder.UserMappings; - /// public IReadOnlyDictionary NewInstanceMappings => MappingBuilder.NewInstanceMappings; diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/MappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/MappingBuilder.cs index 1860648dde..7834c68ee5 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/MappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/MappingBuilder.cs @@ -1,5 +1,4 @@ using Riok.Mapperly.Descriptors.Mappings; -using Riok.Mapperly.Descriptors.Mappings.UserMappings; using Riok.Mapperly.Symbols; namespace Riok.Mapperly.Descriptors.MappingBuilders; @@ -35,9 +34,6 @@ public class MappingBuilder(MappingCollection mappings, MapperDeclaration mapper NewInstanceObjectMemberMappingBuilder.TryBuildMapping, }; - /// - public IReadOnlyCollection UserMappings => mappings.UserMappings; - /// public IReadOnlyDictionary NewInstanceMappings => mappings.NewInstanceMappings; diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/ConstructorParameterMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/ConstructorParameterMapping.cs index 9ffbf6e775..7afa58daad 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/ConstructorParameterMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/ConstructorParameterMapping.cs @@ -1,5 +1,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Descriptors.Mappings.MemberMappings.SourceValue; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; @@ -7,30 +8,27 @@ namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings; public class ConstructorParameterMapping( IParameterSymbol parameter, - NullMemberMapping delegateMapping, - bool selfOrPreviousIsUnmappedOptional + ISourceValue sourceValue, + bool selfOrPreviousIsUnmappedOptional, + MemberMappingInfo memberInfo ) { - private readonly bool _selfOrPreviousIsUnmappedOptional = selfOrPreviousIsUnmappedOptional; - - /// - /// The parameter of the constructor. - /// Note: the nullability of it may not be "upgraded". - /// - public IParameterSymbol Parameter { get; } = parameter; + public MemberMappingInfo MemberInfo { get; } = memberInfo; - public NullMemberMapping DelegateMapping { get; } = delegateMapping; + private readonly bool _selfOrPreviousIsUnmappedOptional = selfOrPreviousIsUnmappedOptional; + private readonly IParameterSymbol _parameter = parameter; + private readonly ISourceValue _sourceValue = sourceValue; public ArgumentSyntax BuildArgument(TypeMappingBuildContext ctx) { - var argumentExpression = DelegateMapping.Build(ctx); + var argumentExpression = _sourceValue.Build(ctx); var arg = Argument(argumentExpression); - return _selfOrPreviousIsUnmappedOptional ? arg.WithNameColon(SpacedNameColon(Parameter.Name)) : arg; + return _selfOrPreviousIsUnmappedOptional ? arg.WithNameColon(SpacedNameColon(_parameter.Name)) : arg; } protected bool Equals(ConstructorParameterMapping other) => - Parameter.Equals(other.Parameter, SymbolEqualityComparer.Default) - && DelegateMapping.Equals(other.DelegateMapping) + _parameter.Equals(other._parameter, SymbolEqualityComparer.Default) + && _sourceValue.Equals(other._sourceValue) && _selfOrPreviousIsUnmappedOptional == other._selfOrPreviousIsUnmappedOptional; public override bool Equals(object? obj) @@ -51,8 +49,8 @@ public override int GetHashCode() { unchecked { - var hashCode = SymbolEqualityComparer.Default.GetHashCode(Parameter); - hashCode = (hashCode * 397) ^ DelegateMapping.GetHashCode(); + var hashCode = SymbolEqualityComparer.Default.GetHashCode(_parameter); + hashCode = (hashCode * 397) ^ _sourceValue.GetHashCode(); hashCode = (hashCode * 397) ^ _selfOrPreviousIsUnmappedOptional.GetHashCode(); return hashCode; } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/IMemberAssignmentMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/IMemberAssignmentMapping.cs index 98cb878296..14ed9a7b68 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/IMemberAssignmentMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/IMemberAssignmentMapping.cs @@ -1,5 +1,4 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; -using Riok.Mapperly.Symbols; namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings; @@ -8,9 +7,7 @@ namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings; /// public interface IMemberAssignmentMapping { - GetterMemberPath SourceGetter { get; } - - NonEmptyMemberPath TargetPath { get; } + MemberMappingInfo MemberInfo { get; } IEnumerable Build(TypeMappingBuildContext ctx, ExpressionSyntax targetAccess); } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/IMemberMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/IMemberMapping.cs deleted file mode 100644 index c9395cdf61..0000000000 --- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/IMemberMapping.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Riok.Mapperly.Symbols; - -namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings; - -/// -/// Represents a member mapping which accesses a source member and maps it to a certain type. -/// (eg. MapToC(source.A.B)) -/// -public interface IMemberMapping -{ - GetterMemberPath SourceGetter { get; } - - ExpressionSyntax Build(TypeMappingBuildContext ctx); -} diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberAssignmentMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberAssignmentMapping.cs index b3575179ae..9b5bf2681e 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberAssignmentMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberAssignmentMapping.cs @@ -1,31 +1,32 @@ using System.Diagnostics; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Descriptors.Mappings.MemberMappings.SourceValue; using Riok.Mapperly.Symbols; namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings; /// -/// Represents a simple member mapping including an assignment to a target member. -/// (eg. target.A = source.B) +/// Represents a member mapping including an assignment to a target member. +/// (e.g. target.A = source.B or target.A = "fooBar") /// -[DebuggerDisplay("MemberAssignmentMapping({SourceGetter.MemberPath.FullName} => {TargetPath.FullName})")] -public class MemberAssignmentMapping(SetterMemberPath targetPath, IMemberMapping mapping) : IMemberAssignmentMapping +[DebuggerDisplay("MemberAssignmentMapping({_sourceValue} => {_targetPath})")] +public class MemberAssignmentMapping(SetterMemberPath targetPath, ISourceValue sourceValue, MemberMappingInfo memberInfo) + : IMemberAssignmentMapping { - private readonly IMemberMapping _mapping = mapping; + public MemberMappingInfo MemberInfo { get; } = memberInfo; - public GetterMemberPath SourceGetter => _mapping.SourceGetter; - - public NonEmptyMemberPath TargetPath => targetPath.MemberPath; + private readonly ISourceValue _sourceValue = sourceValue; + private readonly SetterMemberPath _targetPath = targetPath; public IEnumerable Build(TypeMappingBuildContext ctx, ExpressionSyntax targetAccess) => ctx.SyntaxFactory.SingleStatement(BuildExpression(ctx, targetAccess)); public ExpressionSyntax BuildExpression(TypeMappingBuildContext ctx, ExpressionSyntax? targetAccess) { - var mappedValue = _mapping.Build(ctx); + var mappedValue = _sourceValue.Build(ctx); // target.SetValue(source.Value); or target.Value = source.Value; - return targetPath.BuildAssignment(targetAccess, mappedValue); + return _targetPath.BuildAssignment(targetAccess, mappedValue); } public override bool Equals(object? obj) @@ -42,16 +43,7 @@ public override bool Equals(object? obj) return Equals((MemberAssignmentMapping)obj); } - public override int GetHashCode() - { - unchecked - { - var hashCode = _mapping.GetHashCode(); - hashCode = (hashCode * 397) ^ SourceGetter.GetHashCode(); - hashCode = (hashCode * 397) ^ TargetPath.GetHashCode(); - return hashCode; - } - } + public override int GetHashCode() => HashCode.Combine(_sourceValue, _targetPath); public static bool operator ==(MemberAssignmentMapping? left, MemberAssignmentMapping? right) => Equals(left, right); @@ -59,6 +51,6 @@ public override int GetHashCode() protected bool Equals(MemberAssignmentMapping other) { - return _mapping.Equals(other._mapping) && SourceGetter.Equals(other.SourceGetter) && TargetPath.Equals(other.TargetPath); + return _sourceValue.Equals(other._sourceValue) && _targetPath.Equals(other._targetPath); } } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberExistingTargetMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberExistingTargetMapping.cs index 3a3fcab0c6..cf7e0fc827 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberExistingTargetMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberExistingTargetMapping.cs @@ -7,16 +7,18 @@ namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings; /// /// A which maps to an existing target instance. /// -public class MemberExistingTargetMapping(IExistingTargetMapping delegateMapping, GetterMemberPath sourcePath, GetterMemberPath targetPath) - : IMemberAssignmentMapping +public class MemberExistingTargetMapping( + IExistingTargetMapping delegateMapping, + GetterMemberPath sourcePath, + GetterMemberPath targetPath, + MemberMappingInfo memberInfo +) : IMemberAssignmentMapping { - public GetterMemberPath SourceGetter { get; } = sourcePath; - - public NonEmptyMemberPath TargetPath => (NonEmptyMemberPath)targetPath.MemberPath; + public MemberMappingInfo MemberInfo { get; } = memberInfo; public IEnumerable Build(TypeMappingBuildContext ctx, ExpressionSyntax targetAccess) { - var source = SourceGetter.BuildAccess(ctx.Source); + var source = sourcePath.BuildAccess(ctx.Source); var target = targetPath.BuildAccess(targetAccess); return delegateMapping.Build(ctx.WithSource(source), target); } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberMappingInfo.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberMappingInfo.cs new file mode 100644 index 0000000000..3f3961396b --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberMappingInfo.cs @@ -0,0 +1,21 @@ +using System.Diagnostics; +using Riok.Mapperly.Configuration; +using Riok.Mapperly.Symbols; + +namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings; + +[DebuggerDisplay("{DebuggerDisplay}")] +public record MemberMappingInfo(MemberPath SourceMember, NonEmptyMemberPath TargetMember, MemberMappingConfiguration? Configuration = null) +{ + private string DebuggerDisplay => $"{SourceMember.FullName} => {TargetMember.FullName}"; + + 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.MemberType, TargetMember.MemberType, Configuration?.ToTypeMappingConfiguration()); + } + + public string DescribeSource() => SourceMember.ToDisplayString(includeMemberType: false); +} diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/ISourceValue.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/ISourceValue.cs new file mode 100644 index 0000000000..30f3dd9129 --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/ISourceValue.cs @@ -0,0 +1,12 @@ +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings.SourceValue; + +/// +/// A source value is the right part of an . +/// It can be a constant value or a mapped member of the mapping source object. +/// +public interface ISourceValue +{ + ExpressionSyntax Build(TypeMappingBuildContext ctx); +} diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/MappedMemberSourceValue.cs similarity index 50% rename from src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberMapping.cs rename to src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/MappedMemberSourceValue.cs index af955c7c4d..e62bc94a6a 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/MappedMemberSourceValue.cs @@ -1,24 +1,24 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Riok.Mapperly.Symbols; -namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings; +namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings.SourceValue; /// -/// Represents a simple implementation without any null handling. -/// (eg. MapToD(source.A.B) or MapToD(source?.A?.B)). +/// A mapped source member without any null handling. +/// (e.g. MapToD(source.A.B) or MapToD(source?.A?.B)). /// -public class MemberMapping( +public class MappedMemberSourceValue( INewInstanceMapping delegateMapping, GetterMemberPath sourceGetter, bool nullConditionalAccess, bool addValuePropertyOnNullable -) : IMemberMapping +) : ISourceValue { - public GetterMemberPath SourceGetter { get; } = sourceGetter; + public bool RequiresSourceNullCheck => !nullConditionalAccess && sourceGetter.MemberPath.IsAnyNullable(); public ExpressionSyntax Build(TypeMappingBuildContext ctx) { - ctx = ctx.WithSource(SourceGetter.BuildAccess(ctx.Source, addValuePropertyOnNullable, nullConditionalAccess)); + ctx = ctx.WithSource(sourceGetter.BuildAccess(ctx.Source, addValuePropertyOnNullable, nullConditionalAccess)); return delegateMapping.Build(ctx); } } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/NullMemberMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/NullMappedMemberSourceValue.cs similarity index 53% rename from src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/NullMemberMapping.cs rename to src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/NullMappedMemberSourceValue.cs index ac4bb64a40..2244b2b185 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/NullMemberMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/NullMappedMemberSourceValue.cs @@ -5,33 +5,32 @@ using Riok.Mapperly.Symbols; using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; -namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings; +namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings.SourceValue; /// -/// Represents a null safe . -/// (eg. source?.A?.B ?? null-substitute or source?.A?.B != null ? MapToD(source.A.B) : null-substitute) +/// A mapped source member with additional null handling. +/// (e.g. source?.A?.B ?? null-substitute or source?.A?.B != null ? MapToD(source.A.B) : null-substitute) /// -[DebuggerDisplay("NullMemberMapping({SourceGetter}: {_delegateMapping})")] -public class NullMemberMapping( +[DebuggerDisplay("NullMappedMemberSourceValue({_sourceGetter}: {_delegateMapping})")] +public class NullMappedMemberSourceValue( INewInstanceMapping delegateMapping, GetterMemberPath sourceGetter, ITypeSymbol targetType, NullFallbackValue nullFallback, bool useNullConditionalAccess -) : IMemberMapping +) : ISourceValue { private readonly INewInstanceMapping _delegateMapping = delegateMapping; private readonly NullFallbackValue _nullFallback = nullFallback; - - public GetterMemberPath SourceGetter { get; } = sourceGetter; + private readonly GetterMemberPath _sourceGetter = sourceGetter; public ExpressionSyntax Build(TypeMappingBuildContext ctx) { // the source type of the delegate mapping is nullable or the source path is not nullable // build mapping with null conditional access - if (_delegateMapping.SourceType.IsNullable() || !SourceGetter.MemberPath.IsAnyNullable()) + if (_delegateMapping.SourceType.IsNullable() || !_sourceGetter.MemberPath.IsAnyNullable()) { - ctx = ctx.WithSource(SourceGetter.BuildAccess(ctx.Source, nullConditional: true)); + ctx = ctx.WithSource(_sourceGetter.BuildAccess(ctx.Source, nullConditional: true)); return _delegateMapping.Build(ctx); } @@ -42,10 +41,10 @@ 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.IsAnyObjectPathNullable())) { - var nullConditionalSourceAccess = SourceGetter.BuildAccess(ctx.Source, nullConditional: true); - var nameofSourceAccess = SourceGetter.BuildAccess(ctx.Source, nullConditional: false); + var nullConditionalSourceAccess = _sourceGetter.BuildAccess(ctx.Source, nullConditional: true); + var nameofSourceAccess = _sourceGetter.BuildAccess(ctx.Source, nullConditional: false); var mapping = _delegateMapping.Build(ctx.WithSource(nullConditionalSourceAccess)); return _nullFallback == NullFallbackValue.Default && targetType.IsNullable() ? mapping @@ -53,15 +52,19 @@ public ExpressionSyntax Build(TypeMappingBuildContext ctx) } var notNullCondition = useNullConditionalAccess - ? IsNotNull(SourceGetter.BuildAccess(ctx.Source, nullConditional: true, skipTrailingNonNullable: true)) - : SourceGetter.MemberPath.BuildNonNullConditionWithoutConditionalAccess(ctx.Source)!; - var sourceMemberAccess = SourceGetter.BuildAccess(ctx.Source, true); + ? IsNotNull(_sourceGetter.BuildAccess(ctx.Source, nullConditional: true, skipTrailingNonNullable: true)) + : _sourceGetter.MemberPath.BuildNonNullConditionWithoutConditionalAccess(ctx.Source)!; + var sourceMemberAccess = _sourceGetter.BuildAccess(ctx.Source, true); ctx = ctx.WithSource(sourceMemberAccess); return Conditional(notNullCondition, _delegateMapping.Build(ctx), NullSubstitute(targetType, sourceMemberAccess, _nullFallback)); } - protected bool Equals(NullMemberMapping other) => - _delegateMapping.Equals(other._delegateMapping) && _nullFallback == other._nullFallback && SourceGetter.Equals(other.SourceGetter); + protected bool Equals(NullMappedMemberSourceValue other) + { + return _delegateMapping.Equals(other._delegateMapping) + && _nullFallback == other._nullFallback + && _sourceGetter.Equals(other._sourceGetter); + } public override bool Equals(object? obj) { @@ -74,21 +77,12 @@ public override bool Equals(object? obj) if (obj.GetType() != GetType()) return false; - return Equals((NullMemberMapping)obj); + return Equals((NullMappedMemberSourceValue)obj); } - public override int GetHashCode() - { - unchecked - { - var hashCode = _delegateMapping.GetHashCode(); - hashCode = (hashCode * 397) ^ (int)_nullFallback; - hashCode = (hashCode * 397) ^ SourceGetter.GetHashCode(); - return hashCode; - } - } + public override int GetHashCode() => HashCode.Combine(_delegateMapping, _nullFallback, _sourceGetter); - public static bool operator ==(NullMemberMapping? left, NullMemberMapping? right) => Equals(left, right); + public static bool operator ==(NullMappedMemberSourceValue? left, NullMappedMemberSourceValue? right) => Equals(left, right); - public static bool operator !=(NullMemberMapping? left, NullMemberMapping? right) => !Equals(left, right); + public static bool operator !=(NullMappedMemberSourceValue? left, NullMappedMemberSourceValue? right) => !Equals(left, right); } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/ValueTupleConstructorParameterMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/ValueTupleConstructorParameterMapping.cs index ac7a464800..879f0274b0 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/ValueTupleConstructorParameterMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/ValueTupleConstructorParameterMapping.cs @@ -1,23 +1,26 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Descriptors.Mappings.MemberMappings.SourceValue; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings; -public class ValueTupleConstructorParameterMapping(IFieldSymbol parameter, NullMemberMapping delegateMapping) +public class ValueTupleConstructorParameterMapping(IFieldSymbol parameter, ISourceValue sourceValue, MemberMappingInfo memberInfo) { + public MemberMappingInfo MemberInfo { get; } = memberInfo; + /// /// The parameter the value tuple. /// Note: the nullability of it may not be "upgraded". /// public IFieldSymbol Parameter { get; } = parameter; - public NullMemberMapping DelegateMapping { get; } = delegateMapping; + private readonly ISourceValue _sourceValue = sourceValue; public ArgumentSyntax BuildArgument(TypeMappingBuildContext ctx, bool emitFieldName) { - var argumentExpression = DelegateMapping.Build(ctx); + var argumentExpression = _sourceValue.Build(ctx); var argument = Argument(argumentExpression); // tuples inside expression cannot use the expression form (A: .., ..) instead new ValueTuple<>(..) must be used @@ -32,7 +35,7 @@ public ArgumentSyntax BuildArgument(TypeMappingBuildContext ctx, bool emitFieldN } protected bool Equals(ValueTupleConstructorParameterMapping other) => - Parameter.Equals(other.Parameter, SymbolEqualityComparer.Default) && DelegateMapping.Equals(other.DelegateMapping); + Parameter.Equals(other.Parameter, SymbolEqualityComparer.Default) && _sourceValue.Equals(other._sourceValue); public override bool Equals(object? obj) { @@ -53,7 +56,7 @@ public override int GetHashCode() unchecked { var hashCode = SymbolEqualityComparer.Default.GetHashCode(Parameter); - hashCode = (hashCode * 397) ^ DelegateMapping.GetHashCode(); + hashCode = (hashCode * 397) ^ _sourceValue.GetHashCode(); return hashCode; } } diff --git a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs index 9b27f01afc..eaf0d5b797 100644 --- a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs +++ b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs @@ -165,16 +165,6 @@ public static class DiagnosticDescriptors true ); - public static readonly DiagnosticDescriptor MultipleConfigurationsForInitOnlyMember = - new( - "RMG017", - "An init only member can have one configuration at max", - "The init only member {0}.{1} can have one configuration at max", - DiagnosticCategories.Mapper, - DiagnosticSeverity.Warning, - true - ); - public static readonly DiagnosticDescriptor SourceMemberNotMapped = new( "RMG020", @@ -245,26 +235,6 @@ public static class DiagnosticDescriptors true ); - public static readonly DiagnosticDescriptor MultipleConfigurationsForConstructorParameter = - new( - "RMG027", - "A constructor parameter can have one configuration at max", - "The constructor parameter at {0}.{1} can have one configuration at max", - DiagnosticCategories.Mapper, - DiagnosticSeverity.Warning, - true - ); - - public static readonly DiagnosticDescriptor ConstructorParameterDoesNotSupportPaths = - new( - "RMG028", - "Constructor parameter cannot handle target paths", - "Cannot map to constructor parameter target path {0}.{1}", - DiagnosticCategories.Mapper, - DiagnosticSeverity.Error, - true - ); - public static readonly DiagnosticDescriptor QueryableProjectionMappingsDoNotSupportReferenceHandling = new( "RMG029", @@ -717,6 +687,16 @@ public static class DiagnosticDescriptors true ); + public static readonly DiagnosticDescriptor MultipleConfigurationsForTargetMember = + new( + "RMG074", + "Multiple mappings are configured for the same target member", + "Multiple mappings are configured for the same target member {0}.{1}", + DiagnosticCategories.Mapper, + DiagnosticSeverity.Error, + true + ); + private static string BuildHelpUri(string id) { #if ENV_NEXT diff --git a/src/Riok.Mapperly/Symbols/ConstructorParameterMember.cs b/src/Riok.Mapperly/Symbols/ConstructorParameterMember.cs new file mode 100644 index 0000000000..7451ba820c --- /dev/null +++ b/src/Riok.Mapperly/Symbols/ConstructorParameterMember.cs @@ -0,0 +1,37 @@ +using System.Diagnostics; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Descriptors; +using Riok.Mapperly.Helpers; + +namespace Riok.Mapperly.Symbols; + +/// +/// A constructor parameter represented as a mappable member. +/// This is semantically not really a member, but it acts as a mapping target +/// and is therefore in terms of the mapping the same. +/// +[DebuggerDisplay("{Name}")] +public class ConstructorParameterMember(IParameterSymbol fieldSymbol, SymbolAccessor accessor) : IMappableMember +{ + private readonly IParameterSymbol _fieldSymbol = fieldSymbol; + + public string Name => _fieldSymbol.Name; + public ITypeSymbol Type { get; } = accessor.UpgradeNullable(fieldSymbol.Type); + public ISymbol MemberSymbol => _fieldSymbol; + public bool IsNullable => _fieldSymbol.NullableAnnotation.IsNullable(); + public bool IsIndexer => false; + public bool CanGet => false; + public bool CanSet => false; + public bool CanSetDirectly => false; + public bool IsInitOnly => true; + public bool IsRequired => !_fieldSymbol.IsOptional; + + public ExpressionSyntax BuildAccess(ExpressionSyntax source, bool nullConditional = false) => + throw new InvalidOperationException("Cannot access a constructor parameter"); + + public override bool Equals(object? obj) => + obj is ConstructorParameterMember other && SymbolEqualityComparer.IncludeNullability.Equals(_fieldSymbol, other._fieldSymbol); + + public override int GetHashCode() => SymbolEqualityComparer.IncludeNullability.GetHashCode(_fieldSymbol); +} diff --git a/src/Riok.Mapperly/Symbols/EmptyMemberPath.cs b/src/Riok.Mapperly/Symbols/EmptyMemberPath.cs index 9e7d259e26..a6546a58ea 100644 --- a/src/Riok.Mapperly/Symbols/EmptyMemberPath.cs +++ b/src/Riok.Mapperly/Symbols/EmptyMemberPath.cs @@ -10,5 +10,6 @@ public class EmptyMemberPath(ITypeSymbol rootType) : MemberPath(rootType, []) public override ITypeSymbol MemberType => RootType; - public override string ToDisplayString(bool includeMemberType = true) => RootType.ToDisplayString(); + public override string ToDisplayString(bool includeRootType = true, bool includeMemberType = true) => + includeRootType ? RootType.ToDisplayString() : string.Empty; } diff --git a/src/Riok.Mapperly/Symbols/MappingSourceTarget.cs b/src/Riok.Mapperly/Symbols/MappingSourceTarget.cs new file mode 100644 index 0000000000..e1865f788e --- /dev/null +++ b/src/Riok.Mapperly/Symbols/MappingSourceTarget.cs @@ -0,0 +1,7 @@ +namespace Riok.Mapperly.Symbols; + +public enum MappingSourceTarget +{ + Source, + Target, +} diff --git a/src/Riok.Mapperly/Symbols/MemberPath.cs b/src/Riok.Mapperly/Symbols/MemberPath.cs index 5d1fd53c98..094b2b1389 100644 --- a/src/Riok.Mapperly/Symbols/MemberPath.cs +++ b/src/Riok.Mapperly/Symbols/MemberPath.cs @@ -139,5 +139,5 @@ public override int GetHashCode() private bool Equals(MemberPath other) => RootType.Equals(other.RootType, SymbolEqualityComparer.IncludeNullability) && Path.SequenceEqual(other.Path); - public abstract string ToDisplayString(bool includeMemberType = true); + public abstract string ToDisplayString(bool includeRootType = true, bool includeMemberType = true); } diff --git a/src/Riok.Mapperly/Symbols/NonEmptyMemberPath.cs b/src/Riok.Mapperly/Symbols/NonEmptyMemberPath.cs index 3d082c1199..ee42f42f4b 100644 --- a/src/Riok.Mapperly/Symbols/NonEmptyMemberPath.cs +++ b/src/Riok.Mapperly/Symbols/NonEmptyMemberPath.cs @@ -24,9 +24,10 @@ public NonEmptyMemberPath(ITypeSymbol rootType, IReadOnlyList p public override ITypeSymbol MemberType => IsAnyNullable() ? Member.Type.WithNullableAnnotation(NullableAnnotation.Annotated) : Member.Type; - public override string ToDisplayString(bool includeMemberType = true) + public override string ToDisplayString(bool includeRootType = true, bool includeMemberType = true) { var ofType = includeMemberType ? $" of type {Member.Type.ToDisplayString()}" : null; - return RootType.ToDisplayString() + MemberAccessSeparator + FullName + ofType; + var rootType = includeRootType ? RootType.ToDisplayString() + MemberAccessSeparator : null; + return rootType + FullName + ofType; } } diff --git a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyConstructorResolverTest.cs b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyConstructorResolverTest.cs index fe036cc87a..e8c26ed6c4 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyConstructorResolverTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyConstructorResolverTest.cs @@ -594,4 +594,20 @@ public void ClassConstructorParameterWithStringFormat() """ ); } + + [Fact] + public Task MapConstructorAndMapPropertyWithPathShouldMapPath() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapProperty("Value1", "ValueC.Value3")] + private partial B Map(A source); + """, + "class A { public int Value1 { get; set; } public C ValueC { get; set; } }", + "class B(int value1, C valueC) { public int Value1 { get; set; } public C ValueC { get; set; } }", + "class C { public int Value2 { get; set; } public int Value3 { get; set; } }" + ); + + return TestHelper.VerifyGenerator(source); + } } diff --git a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyFromSourceTest.cs b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyFromSourceTest.cs index 51c1551c27..617858bc48 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyFromSourceTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyFromSourceTest.cs @@ -37,7 +37,10 @@ public void WithManualMappedNotFoundTargetPropertyShouldDiagnostic() TestHelper .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) .Should() - .HaveDiagnostic(DiagnosticDescriptors.ConfiguredMappingTargetMemberNotFound) + .HaveDiagnostic( + DiagnosticDescriptors.ConfiguredMappingTargetMemberNotFound, + "Specified member Value on mapping target type B was not found" + ) .HaveAssertedAllDiagnostics(); } diff --git a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyTest.cs b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyTest.cs index 4ad497da09..cbf72c3b8e 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyTest.cs @@ -169,6 +169,36 @@ public void WithManualMappedProperty() ); } + [Fact] + public void WithManualMappedPropertyDuplicated() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapProperty(nameof(A.StringValue), nameof(B.StringValue2)] + [MapProperty(nameof(A.StringValue), nameof(B.StringValue2)] + partial B Map(A source); + """, + "class A { public string StringValue { get; set; } }", + "class B { public string StringValue2 { get; set; } }" + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic( + DiagnosticDescriptors.MultipleConfigurationsForTargetMember, + "Multiple mappings are configured for the same target member B.StringValue2" + ) + .HaveAssertedAllDiagnostics() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.StringValue2 = source.StringValue; + return target; + """ + ); + } + [Fact] public void WithPropertyNameMappingStrategyCaseInsensitive() { @@ -460,10 +490,17 @@ public void ModifyingTemporaryStructShouldDiagnostic() TestHelper .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) .Should() - .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotMapped) + .HaveDiagnostic( + DiagnosticDescriptors.SourceMemberNotMapped, + "The member StringValue on the mapping source type A is not mapped to any member on the mapping target type B" + ) + .HaveDiagnostic( + DiagnosticDescriptors.SourceMemberNotFound, + "The member NestedValue on the mapping target type B was not found on the mapping source type A" + ) .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" + "Cannot map from member A.StringValue to member path B.NestedValue.StringValue of type string because C.NestedValue is a value type, returning a temporary value, see CS1612" ) .HaveAssertedAllDiagnostics() .HaveSingleMethodBody( diff --git a/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionTest.cs b/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionTest.cs index 4ca728708d..fb1928a7a9 100644 --- a/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionTest.cs @@ -173,6 +173,24 @@ public Task CtorShouldSkipUnmatchedOptionalParameters() return TestHelper.VerifyGenerator(source); } + [Fact] + public Task CtorWithPathMappingShouldDiagnostic() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + private partial IQueryable Map(IQueryable source); + + [MapProperty("Value1", "ValueC.Value3")] + private partial B MapObj(A source); + """, + "class A { public int Value1 { get; set; } public C ValueC { get; set; } }", + "class B(int value1, C valueC) { public int Value1 { get; set; } public C ValueC { get; set; } }", + "class C { public int Value2 { get; set; } public int Value3 { get; set; } }" + ); + + return TestHelper.VerifyGenerator(source); + } + [Fact] public void WithReferenceHandlingShouldDiagnostic() { diff --git a/test/Riok.Mapperly.Tests/Mapping/UnsafeAccessorTest.cs b/test/Riok.Mapperly.Tests/Mapping/UnsafeAccessorTest.cs index 967dc05d80..aeae1d7122 100644 --- a/test/Riok.Mapperly.Tests/Mapping/UnsafeAccessorTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/UnsafeAccessorTest.cs @@ -249,6 +249,8 @@ public void InitPrivatePropertyShouldNotMap() .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) .Should() .HaveDiagnostic(DiagnosticDescriptors.CannotMapToReadOnlyMember) + .HaveDiagnostic(DiagnosticDescriptors.CannotMapToInitOnlyMemberPath) + .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotFound) .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotMapped) .HaveAssertedAllDiagnostics() .HaveMapMethodBody( @@ -274,6 +276,7 @@ public void RequiredPrivateSetPropertyShouldDiagnostic() .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) .Should() .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotMapped) + .HaveDiagnostic(DiagnosticDescriptors.RequiredMemberNotMapped) .HaveDiagnostic(DiagnosticDescriptors.CannotMapToReadOnlyMember) .HaveAssertedAllDiagnostics() .HaveMapMethodBody( @@ -299,6 +302,7 @@ public void QueryablePrivateToPrivatePropertyShouldNotGenerate() .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) .Should() .HaveDiagnostic(DiagnosticDescriptors.CannotMapToReadOnlyMember) + .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotFound) .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotMapped) .HaveAssertedAllDiagnostics(); } @@ -319,6 +323,7 @@ public void QueryablePrivateToPublicPropertyShouldNotGenerate() .Should() .HaveDiagnostic(DiagnosticDescriptors.CannotMapFromWriteOnlyMember) .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotMapped) + .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotFound) .HaveAssertedAllDiagnostics(); } diff --git a/test/Riok.Mapperly.Tests/Mapping/ValueTupleTest.cs b/test/Riok.Mapperly.Tests/Mapping/ValueTupleTest.cs index 27e382f9e8..7809b6f7aa 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ValueTupleTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ValueTupleTest.cs @@ -251,6 +251,31 @@ public void TupleToTupleWithIgnoredSource() ); } + [Fact] + public void TupleToTupleWithAdditionalSourceTupleFieldShouldDiagnostic() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + partial (int, string) Map((int A, string B, int C) source); + """ + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic( + DiagnosticDescriptors.SourceMemberNotMapped, + "The member C on the mapping source type (int A, string B, int C) is not mapped to any member on the mapping target type (int, string)" + ) + .HaveAssertedAllDiagnostics() + .HaveSingleMethodBody( + """ + var target = (source.A, source.B); + return target; + """ + ); + } + [Fact] public void TupleToTupleWithIgnoredSourceByPosition() { @@ -375,7 +400,10 @@ public void IgnoreTargetTuple() .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) .Should() .HaveDiagnostic(DiagnosticDescriptors.NoConstructorFound) - .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotMapped) + .HaveDiagnostic( + DiagnosticDescriptors.SourceMemberNotMapped, + "The member A on the mapping source type (string, int A) is not mapped to any member on the mapping target type (int, int A)" + ) .HaveAssertedAllDiagnostics() .HaveSingleMethodBody( """ @@ -460,14 +488,14 @@ public void TupleToTupleWithMapProperty() """ [MapProperty("C", "A")] [MapProperty("Item3", "Item2")] + [MapperIgnoreSource("B")] partial (int A, int) Map((int B, int C, int) source); """ ); TestHelper - .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .GenerateMapper(source) .Should() - .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotMapped) .HaveSingleMethodBody( """ var target = (A: source.C, source.Item3); @@ -605,7 +633,7 @@ public void TupleToTupleWithManyMapPropertyShouldDiagnostic() var source = TestSourceBuilder.MapperWithBodyAndTypes( """ [MapProperty("B", "A")] - [MapProperty("B", "A")] + [MapProperty("Item1", "A")] partial (int A, int) Map((int, string B) source); """ ); @@ -613,7 +641,15 @@ public void TupleToTupleWithManyMapPropertyShouldDiagnostic() TestHelper .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) .Should() - .HaveDiagnostic(DiagnosticDescriptors.MultipleConfigurationsForConstructorParameter); + .HaveDiagnostic( + DiagnosticDescriptors.SourceMemberNotMapped, + "The member Item1 on the mapping source type (int, string B) is not mapped to any member on the mapping target type (int A, int)" + ) + .HaveDiagnostic( + DiagnosticDescriptors.MultipleConfigurationsForTargetMember, + "Multiple mappings are configured for the same target member (int A, int).A" + ) + .HaveAssertedAllDiagnostics(); } [Fact] @@ -629,7 +665,11 @@ public void TupleToTupleWithMapPropertyWithImplicitNameShouldDiagnostic() TestHelper .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) .Should() - .HaveDiagnostic(DiagnosticDescriptors.ConfiguredMappingTargetMemberNotFound); + .HaveDiagnostic( + DiagnosticDescriptors.ConfiguredMappingSourceMemberNotFound, + "Specified member Item2 on source type (int C, int D) was not found" + ) + .HaveAssertedAllDiagnostics(); } [Fact] diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyConstructorResolverTest.MapConstructorAndMapPropertyWithPathShouldMapPath#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyConstructorResolverTest.MapConstructorAndMapPropertyWithPathShouldMapPath#Mapper.g.verified.cs new file mode 100644 index 0000000000..bdd2a2099e --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyConstructorResolverTest.MapConstructorAndMapPropertyWithPathShouldMapPath#Mapper.g.verified.cs @@ -0,0 +1,13 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial global::B Map(global::A source) + { + var target = new global::B(source.Value1, source.ValueC); + target.ValueC.Value3 = source.Value1; + return target; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyFlatteningTest.ManualUnflattenedPropertySourcePropertyNotFoundShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyFlatteningTest.ManualUnflattenedPropertySourcePropertyNotFoundShouldDiagnostic.verified.txt index 515ec3f1ad..1d61820b33 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyFlatteningTest.ManualUnflattenedPropertySourcePropertyNotFoundShouldDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyFlatteningTest.ManualUnflattenedPropertySourcePropertyNotFoundShouldDiagnostic.verified.txt @@ -20,6 +20,16 @@ Message: The member MyValueId on the mapping source type A is not mapped to any member on the mapping target type B, Category: Mapper }, + { + Id: RMG012, + Title: Source member was not found for target member, + Severity: Info, + WarningLevel: 1, + Location: : (11,4)-(11,79), + MessageFormat: The member {0} on the mapping target type {1} was not found on the mapping source type {2}, + Message: The member Value on the mapping target type B was not found on the mapping source type A, + Category: Mapper + }, { Id: RMG066, Title: No members are mapped in an object mapping, diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyFlatteningTest.ManualUnflattenedPropertyTargetPropertyNotFoundShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyFlatteningTest.ManualUnflattenedPropertyTargetPropertyNotFoundShouldDiagnostic.verified.txt index 0f3d66f395..59cf90d586 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyFlatteningTest.ManualUnflattenedPropertyTargetPropertyNotFoundShouldDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyFlatteningTest.ManualUnflattenedPropertyTargetPropertyNotFoundShouldDiagnostic.verified.txt @@ -20,6 +20,16 @@ Message: The member MyValueId on the mapping source type A is not mapped to any member on the mapping target type B, Category: Mapper }, + { + Id: RMG012, + Title: Source member was not found for target member, + Severity: Info, + WarningLevel: 1, + Location: : (11,4)-(11,79), + MessageFormat: The member {0} on the mapping target type {1} was not found on the mapping source type {2}, + Message: The member Value on the mapping target type B was not found on the mapping source type A, + Category: Mapper + }, { Id: RMG066, Title: No members are mapped in an object mapping, diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyFlatteningTest.ManualUnflattenedPropertyTargetPropertyPathWriteOnlyShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyFlatteningTest.ManualUnflattenedPropertyTargetPropertyPathWriteOnlyShouldDiagnostic.verified.txt index 5dbf630c80..08cd528513 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyFlatteningTest.ManualUnflattenedPropertyTargetPropertyPathWriteOnlyShouldDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyFlatteningTest.ManualUnflattenedPropertyTargetPropertyPathWriteOnlyShouldDiagnostic.verified.txt @@ -7,7 +7,7 @@ WarningLevel: 1, Location: : (11,4)-(11,76), MessageFormat: Cannot map from {0} to write only member path {1}, - Message: Cannot map from A.MyValueId of type string to write only member path B.Value.Id of type string, + Message: Cannot map from A.MyValueId to write only member path B.Value.Id of type string, Category: Mapper }, { @@ -20,6 +20,16 @@ Message: The member MyValueId on the mapping source type A is not mapped to any member on the mapping target type B, Category: Mapper }, + { + Id: RMG012, + Title: Source member was not found for target member, + Severity: Info, + WarningLevel: 1, + Location: : (11,4)-(11,76), + MessageFormat: The member {0} on the mapping target type {1} was not found on the mapping source type {2}, + Message: The member Value on the mapping target type B was not found on the mapping source type A, + Category: Mapper + }, { Id: RMG066, Title: No members are mapped in an object mapping, diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertyWithConfigurationNotFoundSourcePropertyShouldDiagnostic#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertyWithConfigurationNotFoundSourcePropertyShouldDiagnostic#Mapper.g.verified.cs index 3e2041cd3f..0e3c9db55f 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertyWithConfigurationNotFoundSourcePropertyShouldDiagnostic#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertyWithConfigurationNotFoundSourcePropertyShouldDiagnostic#Mapper.g.verified.cs @@ -6,7 +6,10 @@ public partial class Mapper [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] private partial global::B Map(global::A source) { - var target = new global::B(); + var target = new global::B() + { + StringValue = source.StringValue, + }; return target; } } \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertyWithConfigurationNotFoundSourcePropertyShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertyWithConfigurationNotFoundSourcePropertyShouldDiagnostic.verified.txt index 8e4aa403f5..beef16c901 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertyWithConfigurationNotFoundSourcePropertyShouldDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertyWithConfigurationNotFoundSourcePropertyShouldDiagnostic.verified.txt @@ -1,34 +1,13 @@ { Diagnostics: [ { - Id: RMG012, - Title: Source member was not found for target member, - Severity: Info, - WarningLevel: 1, + Id: RMG006, + Title: Mapping source member not found, + Severity: Error, + WarningLevel: 0, Location: : (11,4)-(11,81), - MessageFormat: The member {0} on the mapping target type {1} was not found on the mapping source type {2}, - Message: The member StringValue on the mapping target type B was not found on the mapping source type A, - Category: Mapper - }, - { - Id: RMG020, - Title: Source member is not mapped to any target member, - Severity: Info, - WarningLevel: 1, - Location: : (11,4)-(11,81), - MessageFormat: The member {0} on the mapping source type {1} is not mapped to any member on the mapping target type {2}, - Message: The member StringValue on the mapping source type A is not mapped to any member on the mapping target type B, - Category: Mapper - }, - { - Id: RMG066, - Title: No members are mapped in an object mapping, - Severity: Warning, - WarningLevel: 1, - Location: : (11,4)-(11,81), - HelpLink: https://localhost:3000/docs/configuration/analyzer-diagnostics/RMG066, - MessageFormat: No members are mapped in the object mapping from {0} to {1}, - Message: No members are mapped in the object mapping from A to B, + MessageFormat: Specified member {0} on source type {1} was not found, + Message: Specified member StringValue2 on source type A was not found, Category: Mapper } ] diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertyWithMultipleConfigurationsShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertyWithMultipleConfigurationsShouldDiagnostic.verified.txt index d95a087b4b..6f5f2c5579 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertyWithMultipleConfigurationsShouldDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertyWithMultipleConfigurationsShouldDiagnostic.verified.txt @@ -1,13 +1,13 @@ { Diagnostics: [ { - Id: RMG017, - Title: An init only member can have one configuration at max, - Severity: Warning, - WarningLevel: 1, + Id: RMG074, + Title: Multiple mappings are configured for the same target member, + Severity: Error, + WarningLevel: 0, Location: : (11,4)-(11,126), - MessageFormat: The init only member {0}.{1} can have one configuration at max, - Message: The init only member string.StringValue can have one configuration at max, + MessageFormat: Multiple mappings are configured for the same target member {0}.{1}, + Message: Multiple mappings are configured for the same target member B.StringValue, Category: Mapper }, { diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertyWithPathConfigurationsShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertyWithPathConfigurationsShouldDiagnostic.verified.txt index 323dd9bc30..cf95bbb1a9 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertyWithPathConfigurationsShouldDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyInitPropertyTest.InitOnlyPropertyWithPathConfigurationsShouldDiagnostic.verified.txt @@ -7,7 +7,17 @@ WarningLevel: 0, Location: : (11,4)-(11,81), MessageFormat: Cannot map to init only member path {0}.{1}, - Message: Cannot map to init only member path C.Nested.Value, + Message: Cannot map to init only member path B.Nested.Value, + Category: Mapper + }, + { + Id: RMG012, + Title: Source member was not found for target member, + Severity: Info, + WarningLevel: 1, + Location: : (11,4)-(11,81), + MessageFormat: The member {0} on the mapping target type {1} was not found on the mapping source type {2}, + Message: The member Nested on the mapping target type B was not found on the mapping source type A, Category: Mapper }, { diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ShouldIgnoreReadOnlyPropertyOnTargetWithDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ShouldIgnoreReadOnlyPropertyOnTargetWithDiagnostic.verified.txt index 8920f96cbf..4877989311 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ShouldIgnoreReadOnlyPropertyOnTargetWithDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ShouldIgnoreReadOnlyPropertyOnTargetWithDiagnostic.verified.txt @@ -7,7 +7,7 @@ WarningLevel: 1, Location: : (11,4)-(11,36), MessageFormat: Cannot map {0} to read only member {1}, - Message: Cannot map A.StringValue2 of type string to read only member B.StringValue2 of type string, + Message: Cannot map A.StringValue2 to read only member B.StringValue2, Category: Mapper }, { diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ShouldIgnoreWriteOnlyPropertyOnSourceWithDiagnostics.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ShouldIgnoreWriteOnlyPropertyOnSourceWithDiagnostics.verified.txt index c658ce475f..7ab9ac8e2c 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ShouldIgnoreWriteOnlyPropertyOnSourceWithDiagnostics.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.ShouldIgnoreWriteOnlyPropertyOnSourceWithDiagnostics.verified.txt @@ -19,6 +19,16 @@ MessageFormat: The member {0} on the mapping source type {1} is not mapped to any member on the mapping target type {2}, Message: The member StringValue2 on the mapping source type A is not mapped to any member on the mapping target type B, Category: Mapper + }, + { + Id: RMG012, + Title: Source member was not found for target member, + Severity: Info, + WarningLevel: 1, + Location: : (11,4)-(11,36), + MessageFormat: The member {0} on the mapping target type {1} was not found on the mapping source type {2}, + Message: The member StringValue2 on the mapping target type B was not found on the mapping source type A, + Category: Mapper } ] } \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualMappedNotFoundSourcePropertyShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualMappedNotFoundSourcePropertyShouldDiagnostic.verified.txt index 24d858c207..9032096aa7 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualMappedNotFoundSourcePropertyShouldDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualMappedNotFoundSourcePropertyShouldDiagnostic.verified.txt @@ -20,6 +20,16 @@ Message: The member StringValue on the mapping source type A is not mapped to any member on the mapping target type B, Category: Mapper }, + { + Id: RMG012, + Title: Source member was not found for target member, + Severity: Info, + WarningLevel: 1, + Location: : (11,4)-(11,89), + MessageFormat: The member {0} on the mapping target type {1} was not found on the mapping source type {2}, + Message: The member StringValue2 on the mapping target type B was not found on the mapping source type A, + Category: Mapper + }, { Id: RMG066, Title: No members are mapped in an object mapping, diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualMappedNotFoundTargetPropertyShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualMappedNotFoundTargetPropertyShouldDiagnostic.verified.txt index 74612ef01d..4dc8f38800 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualMappedNotFoundTargetPropertyShouldDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualMappedNotFoundTargetPropertyShouldDiagnostic.verified.txt @@ -1,15 +1,5 @@ { Diagnostics: [ - { - Id: RMG012, - Title: Source member was not found for target member, - Severity: Info, - WarningLevel: 1, - Location: : (11,4)-(11,96), - MessageFormat: The member {0} on the mapping target type {1} was not found on the mapping source type {2}, - Message: The member StringValue2 on the mapping target type B was not found on the mapping source type A, - Category: Mapper - }, { Id: RMG005, Title: Mapping target member not found, @@ -30,6 +20,16 @@ Message: The member StringValue on the mapping source type A is not mapped to any member on the mapping target type B, Category: Mapper }, + { + Id: RMG012, + Title: Source member was not found for target member, + Severity: Info, + WarningLevel: 1, + Location: : (11,4)-(11,96), + MessageFormat: The member {0} on the mapping target type {1} was not found on the mapping source type {2}, + Message: The member StringValue2 on the mapping target type B was not found on the mapping source type A, + Category: Mapper + }, { Id: RMG066, Title: No members are mapped in an object mapping, diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualNotFoundSourcePropertyShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualNotFoundSourcePropertyShouldDiagnostic.verified.txt index a42a7d73aa..a3bd9549c0 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualNotFoundSourcePropertyShouldDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithManualNotFoundSourcePropertyShouldDiagnostic.verified.txt @@ -20,6 +20,16 @@ Message: The member StringValue on the mapping source type A is not mapped to any member on the mapping target type B, Category: Mapper }, + { + Id: RMG012, + Title: Source member was not found for target member, + Severity: Info, + WarningLevel: 1, + Location: : (11,4)-(11,87), + MessageFormat: The member {0} on the mapping target type {1} was not found on the mapping source type {2}, + Message: The member StringValue2 on the mapping target type B was not found on the mapping source type A, + Category: Mapper + }, { Id: RMG066, Title: No members are mapped in an object mapping, diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPrivateSourceGetterShouldIgnoreAndDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPrivateSourceGetterShouldIgnoreAndDiagnostic.verified.txt index b2c1098a27..e583369d95 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPrivateSourceGetterShouldIgnoreAndDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPrivateSourceGetterShouldIgnoreAndDiagnostic.verified.txt @@ -19,6 +19,16 @@ MessageFormat: The member {0} on the mapping source type {1} is not mapped to any member on the mapping target type {2}, Message: The member StringValue on the mapping source type A is not mapped to any member on the mapping target type B, Category: Mapper + }, + { + Id: RMG012, + Title: Source member was not found for target member, + Severity: Info, + WarningLevel: 1, + Location: : (11,4)-(11,36), + MessageFormat: The member {0} on the mapping target type {1} was not found on the mapping source type {2}, + Message: The member StringValue on the mapping target type B was not found on the mapping source type A, + Category: Mapper } ] } \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPrivateSourcePathGetterShouldIgnoreAndDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPrivateSourcePathGetterShouldIgnoreAndDiagnostic.verified.txt index 28afe1bae9..bfae366c35 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPrivateSourcePathGetterShouldIgnoreAndDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPrivateSourcePathGetterShouldIgnoreAndDiagnostic.verified.txt @@ -19,6 +19,16 @@ MessageFormat: The member {0} on the mapping source type {1} is not mapped to any member on the mapping target type {2}, Message: The member NestedValue on the mapping source type A is not mapped to any member on the mapping target type B, Category: Mapper + }, + { + Id: RMG012, + Title: Source member was not found for target member, + Severity: Info, + WarningLevel: 1, + Location: : (11,4)-(11,36), + MessageFormat: The member {0} on the mapping target type {1} was not found on the mapping source type {2}, + Message: The member NestedValue on the mapping target type B was not found on the mapping source type A, + Category: Mapper } ] } \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPrivateTargetPathGetterShouldIgnoreAndDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPrivateTargetPathGetterShouldIgnoreAndDiagnostic.verified.txt index 13f25d55ad..8b098e41e1 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPrivateTargetPathGetterShouldIgnoreAndDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPrivateTargetPathGetterShouldIgnoreAndDiagnostic.verified.txt @@ -7,7 +7,7 @@ WarningLevel: 1, Location: : (11,4)-(11,36), MessageFormat: Cannot map {0} to read only member {1}, - Message: Cannot map A.NestedValue of type C to read only member B.NestedValue of type D, + Message: Cannot map A.NestedValue to read only member B.NestedValue, Category: Mapper }, { diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPrivateTargetSetterShouldIgnoreAndDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPrivateTargetSetterShouldIgnoreAndDiagnostic.verified.txt index 53eb59841d..8930681809 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPrivateTargetSetterShouldIgnoreAndDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPrivateTargetSetterShouldIgnoreAndDiagnostic.verified.txt @@ -7,7 +7,7 @@ WarningLevel: 1, Location: : (11,4)-(11,36), MessageFormat: Cannot map {0} to read only member {1}, - Message: Cannot map A.StringValue of type string to read only member B.StringValue of type string, + Message: Cannot map A.StringValue to read only member B.StringValue, Category: Mapper }, { diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPropertyNameMappingStrategyCaseSensitive.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPropertyNameMappingStrategyCaseSensitive.verified.txt index e640b50581..1de6b67044 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPropertyNameMappingStrategyCaseSensitive.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithPropertyNameMappingStrategyCaseSensitive.verified.txt @@ -1,23 +1,23 @@ { Diagnostics: [ { - Id: RMG012, - Title: Source member was not found for target member, + Id: RMG020, + Title: Source member is not mapped to any target member, Severity: Info, WarningLevel: 1, Location: : (11,4)-(11,36), - MessageFormat: The member {0} on the mapping target type {1} was not found on the mapping source type {2}, - Message: The member stringvalue on the mapping target type B was not found on the mapping source type A, + MessageFormat: The member {0} on the mapping source type {1} is not mapped to any member on the mapping target type {2}, + Message: The member StringValue on the mapping source type A is not mapped to any member on the mapping target type B, Category: Mapper }, { - Id: RMG020, - Title: Source member is not mapped to any target member, + Id: RMG012, + Title: Source member was not found for target member, Severity: Info, WarningLevel: 1, Location: : (11,4)-(11,36), - MessageFormat: The member {0} on the mapping source type {1} is not mapped to any member on the mapping target type {2}, - Message: The member StringValue on the mapping source type A is not mapped to any member on the mapping target type B, + MessageFormat: The member {0} on the mapping target type {1} was not found on the mapping source type {2}, + Message: The member stringvalue on the mapping target type B was not found on the mapping source type A, Category: Mapper } ] diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithUnmappablePropertyShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithUnmappablePropertyShouldDiagnostic.verified.txt index 70c0b40d75..cc2aa2addf 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithUnmappablePropertyShouldDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithUnmappablePropertyShouldDiagnostic.verified.txt @@ -20,6 +20,16 @@ Message: The member Value on the mapping source type A is not mapped to any member on the mapping target type B, Category: Mapper }, + { + Id: RMG012, + Title: Source member was not found for target member, + Severity: Info, + WarningLevel: 1, + Location: : (11,4)-(11,36), + MessageFormat: The member {0} on the mapping target type {1} was not found on the mapping source type {2}, + Message: The member Value on the mapping target type B was not found on the mapping source type A, + Category: Mapper + }, { Id: RMG066, Title: No members are mapped in an object mapping, diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithUnmatchedPropertyShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithUnmatchedPropertyShouldDiagnostic.verified.txt index fd284271c0..cbdedcb6a3 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithUnmatchedPropertyShouldDiagnostic.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithUnmatchedPropertyShouldDiagnostic.verified.txt @@ -1,23 +1,23 @@ { Diagnostics: [ { - Id: RMG012, - Title: Source member was not found for target member, + Id: RMG020, + Title: Source member is not mapped to any target member, Severity: Info, WarningLevel: 1, Location: : (11,4)-(11,36), - MessageFormat: The member {0} on the mapping target type {1} was not found on the mapping source type {2}, - Message: The member StringValueB on the mapping target type B was not found on the mapping source type A, + MessageFormat: The member {0} on the mapping source type {1} is not mapped to any member on the mapping target type {2}, + Message: The member StringValueA on the mapping source type A is not mapped to any member on the mapping target type B, Category: Mapper }, { - Id: RMG020, - Title: Source member is not mapped to any target member, + Id: RMG012, + Title: Source member was not found for target member, Severity: Info, WarningLevel: 1, Location: : (11,4)-(11,36), - MessageFormat: The member {0} on the mapping source type {1} is not mapped to any member on the mapping target type {2}, - Message: The member StringValueA on the mapping source type A is not mapped to any member on the mapping target type B, + MessageFormat: The member {0} on the mapping target type {1} was not found on the mapping source type {2}, + Message: The member StringValueB on the mapping target type B was not found on the mapping source type A, Category: Mapper } ] diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.CtorWithPathMappingShouldDiagnostic#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.CtorWithPathMappingShouldDiagnostic#Mapper.g.verified.cs new file mode 100644 index 0000000000..2899e6ed66 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.CtorWithPathMappingShouldDiagnostic#Mapper.g.verified.cs @@ -0,0 +1,21 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable source) + { +#nullable disable + return System.Linq.Queryable.Select(source, x => new global::B(x.Value1, x.ValueC)); +#nullable enable + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial global::B MapObj(global::A source) + { + var target = new global::B(source.Value1, source.ValueC); + target.ValueC.Value3 = source.Value1; + return target; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.CtorWithPathMappingShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.CtorWithPathMappingShouldDiagnostic.verified.txt new file mode 100644 index 0000000000..083fa08877 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.CtorWithPathMappingShouldDiagnostic.verified.txt @@ -0,0 +1,14 @@ +{ + Diagnostics: [ + { + Id: RMG005, + Title: Mapping target member not found, + Severity: Error, + WarningLevel: 0, + Location: : (13,0)-(14,35), + MessageFormat: Specified member {0} on mapping target type {1} was not found, + Message: Specified member ValueC.Value3 on mapping target type B was not found, + Category: Mapper + } + ] +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.ReferenceLoopInitProperty.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.ReferenceLoopInitProperty.verified.txt index d80c9fa4b3..3aa1d943f8 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.ReferenceLoopInitProperty.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.ReferenceLoopInitProperty.verified.txt @@ -19,6 +19,16 @@ MessageFormat: The member {0} on the mapping source type {1} is not mapped to any member on the mapping target type {2}, Message: The member Parent on the mapping source type A is not mapped to any member on the mapping target type B, Category: Mapper + }, + { + Id: RMG012, + Title: Source member was not found for target member, + Severity: Info, + WarningLevel: 1, + Location: : (11,4)-(11,84), + MessageFormat: The member {0} on the mapping target type {1} was not found on the mapping source type {2}, + Message: The member Parent on the mapping target type B was not found on the mapping source type A, + Category: Mapper } ] } \ No newline at end of file