diff --git a/docs/docs/configuration/enum.mdx b/docs/docs/configuration/enum.mdx index 0529403358..9d7c579780 100644 --- a/docs/docs/configuration/enum.mdx +++ b/docs/docs/configuration/enum.mdx @@ -75,6 +75,23 @@ public partial class CarMapper } ``` +## Ignore enum values + +To ignore an enum value the `MapperIgnoreSourceValue` or `MapperIgnoreTargetValue` attributes can be used. +This is especially useful when applying [strict enum mappings](#strict-enum-mappings). + +```csharp +[Mapper] +public partial class CarMapper +{ + // highlight-start + [MapperIgnoreSourceValue(Fruit.Apple)] + [MapperIgnoreTargetValue(FruitDto.Pineapple)] + // highlight-end + public partial FruitDto Map(Fruit source); +} +``` + ## Fallback value To map to a fallback value instead of throwing when encountering an unknown value, diff --git a/docs/package.json b/docs/package.json index 2baeff32c6..d3fe2827eb 100644 --- a/docs/package.json +++ b/docs/package.json @@ -4,6 +4,7 @@ "private": true, "scripts": { "prebuild": "ts-node prebuild.ts", + "prestart": "ts-node prebuild.ts", "docusaurus": "docusaurus", "start": "docusaurus start", "build": "docusaurus build", diff --git a/src/Riok.Mapperly.Abstractions/MapperIgnoreSourceValueAttribute.cs b/src/Riok.Mapperly.Abstractions/MapperIgnoreSourceValueAttribute.cs new file mode 100644 index 0000000000..ad677427a9 --- /dev/null +++ b/src/Riok.Mapperly.Abstractions/MapperIgnoreSourceValueAttribute.cs @@ -0,0 +1,22 @@ +namespace Riok.Mapperly.Abstractions; + +/// +/// Ignores a source enum value from the mapping. +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +public class MapperIgnoreSourceValueAttribute : Attribute +{ + /// + /// Ignores the specified source enum value from the mapping. + /// + /// The source enum value to ignore. + public MapperIgnoreSourceValueAttribute(object source) + { + SourceValue = (Enum)source; + } + + /// + /// Gets the source enum value which should be ignored from the mapping. + /// + public Enum? SourceValue { get; } +} diff --git a/src/Riok.Mapperly.Abstractions/MapperIgnoreTargetValueAttribute.cs b/src/Riok.Mapperly.Abstractions/MapperIgnoreTargetValueAttribute.cs new file mode 100644 index 0000000000..a0cb7daa78 --- /dev/null +++ b/src/Riok.Mapperly.Abstractions/MapperIgnoreTargetValueAttribute.cs @@ -0,0 +1,22 @@ +namespace Riok.Mapperly.Abstractions; + +/// +/// Ignores a target enum value from the mapping. +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +public class MapperIgnoreTargetValueAttribute : Attribute +{ + /// + /// Ignores the specified target enum value from the mapping. + /// + /// The target enum value to ignore. + public MapperIgnoreTargetValueAttribute(object target) + { + TargetValue = (Enum)target; + } + + /// + /// Gets the target enum value which should be ignored from the mapping. + /// + public Enum? TargetValue { get; } +} diff --git a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt index 193f4726f1..2c2d6862f5 100644 --- a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt +++ b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt @@ -89,3 +89,9 @@ Riok.Mapperly.Abstractions.MapDerivedTypeAttribute.TargetType.get -> System.Type Riok.Mapperly.Abstractions.EnumMappingStrategy.ByValueCheckDefined = 2 -> Riok.Mapperly.Abstractions.EnumMappingStrategy Riok.Mapperly.Abstractions.MapEnumAttribute.FallbackValue.get -> object? Riok.Mapperly.Abstractions.MapEnumAttribute.FallbackValue.set -> void +Riok.Mapperly.Abstractions.MapperIgnoreSourceValueAttribute +Riok.Mapperly.Abstractions.MapperIgnoreSourceValueAttribute.MapperIgnoreSourceValueAttribute(object! source) -> void +Riok.Mapperly.Abstractions.MapperIgnoreTargetValueAttribute +Riok.Mapperly.Abstractions.MapperIgnoreTargetValueAttribute.MapperIgnoreTargetValueAttribute(object! target) -> void +Riok.Mapperly.Abstractions.MapperIgnoreSourceValueAttribute.SourceValue.get -> System.Enum? +Riok.Mapperly.Abstractions.MapperIgnoreTargetValueAttribute.TargetValue.get -> System.Enum? diff --git a/src/Riok.Mapperly/AnalyzerReleases.Shipped.md b/src/Riok.Mapperly/AnalyzerReleases.Shipped.md index a3902060f4..52f90a4fd4 100644 --- a/src/Riok.Mapperly/AnalyzerReleases.Shipped.md +++ b/src/Riok.Mapperly/AnalyzerReleases.Shipped.md @@ -102,3 +102,5 @@ RMG040 | Mapper | Error | A target enum member value does not match the ta RMG041 | Mapper | Error | A source enum member value does not match the source enum type RMG042 | Mapper | Error | The type of the enum fallback value does not match the target enum type RMG043 | Mapper | Warning | Enum fallback values are only supported for the ByName and ByValueCheckDefined strategies, but not for the ByValue strategy +RMG044 | Mapper | Warning | An ignored enum member can not be found on the source enum +RMG045 | Mapper | Warning | An ignored enum member can not be found on the target enum diff --git a/src/Riok.Mapperly/Configuration/EnumMappingConfiguration.cs b/src/Riok.Mapperly/Configuration/EnumMappingConfiguration.cs index 01ab4e5985..0ad52b8d8d 100644 --- a/src/Riok.Mapperly/Configuration/EnumMappingConfiguration.cs +++ b/src/Riok.Mapperly/Configuration/EnumMappingConfiguration.cs @@ -7,5 +7,10 @@ public record EnumMappingConfiguration( EnumMappingStrategy Strategy, bool IgnoreCase, IFieldSymbol? FallbackValue, + IReadOnlyCollection IgnoredSourceMembers, + IReadOnlyCollection IgnoredTargetMembers, IReadOnlyCollection ExplicitMappings -); +) +{ + public bool HasExplicitConfigurations => ExplicitMappings.Count > 0 || IgnoredSourceMembers.Count > 0 || IgnoredTargetMembers.Count > 0; +} diff --git a/src/Riok.Mapperly/Configuration/MapperConfiguration.cs b/src/Riok.Mapperly/Configuration/MapperConfiguration.cs index 9f220ae1a7..5bab50920f 100644 --- a/src/Riok.Mapperly/Configuration/MapperConfiguration.cs +++ b/src/Riok.Mapperly/Configuration/MapperConfiguration.cs @@ -1,6 +1,7 @@ using Microsoft.CodeAnalysis; using Riok.Mapperly.Abstractions; using Riok.Mapperly.Descriptors; +using Riok.Mapperly.Helpers; namespace Riok.Mapperly.Configuration; @@ -18,6 +19,8 @@ public MapperConfiguration(SymbolAccessor symbolAccessor, ISymbol mapperSymbol) Mapper.EnumMappingStrategy, Mapper.EnumMappingIgnoreCase, null, + Array.Empty(), + Array.Empty(), Array.Empty() ), new PropertiesMappingConfiguration(Array.Empty(), Array.Empty(), Array.Empty()), @@ -27,14 +30,14 @@ public MapperConfiguration(SymbolAccessor symbolAccessor, ISymbol mapperSymbol) public MapperAttribute Mapper { get; } - public MappingConfiguration ForMethod(IMethodSymbol? method) + public MappingConfiguration BuildFor(MappingConfigurationReference reference) { - if (method == null) + if (reference.Method == null) return _defaultConfiguration; - var enumConfig = BuildEnumConfig(method); - var propertiesConfig = BuildPropertiesConfig(method); - var derivedTypesConfig = BuildDerivedTypeConfigs(method); + var enumConfig = BuildEnumConfig(reference); + var propertiesConfig = BuildPropertiesConfig(reference.Method); + var derivedTypesConfig = BuildDerivedTypeConfigs(reference.Method); return new MappingConfiguration(enumConfig, propertiesConfig, derivedTypesConfig); } @@ -48,7 +51,11 @@ private IReadOnlyCollection BuildDerivedTypeCon private PropertiesMappingConfiguration BuildPropertiesConfig(IMethodSymbol method) { - var ignoredSourceProperties = _dataAccessor.Access(method).Select(x => x.Source).ToList(); + var ignoredSourceProperties = _dataAccessor + .Access(method) + .Select(x => x.Source) + .WhereNotNull() + .ToList(); var ignoredTargetProperties = _dataAccessor .Access(method) .Select(x => x.Target) @@ -56,19 +63,33 @@ private PropertiesMappingConfiguration BuildPropertiesConfig(IMethodSymbol metho #pragma warning disable CS0618 .Concat(_dataAccessor.Access(method).Select(x => x.Target)) #pragma warning restore CS0618 + .WhereNotNull() .ToList(); var explicitMappings = _dataAccessor.Access(method).ToList(); return new PropertiesMappingConfiguration(ignoredSourceProperties, ignoredTargetProperties, explicitMappings); } - private EnumMappingConfiguration BuildEnumConfig(IMethodSymbol method) + private EnumMappingConfiguration BuildEnumConfig(MappingConfigurationReference configRef) { - var configData = _dataAccessor.AccessFirstOrDefault(method); - var explicitMappings = _dataAccessor.Access(method).ToList(); + if (configRef.Method == null || !configRef.Source.IsEnum() && !configRef.Target.IsEnum()) + return _defaultConfiguration.Enum; + + var configData = _dataAccessor.AccessFirstOrDefault(configRef.Method); + var explicitMappings = _dataAccessor.Access(configRef.Method).ToList(); + var ignoredSources = _dataAccessor + .Access(configRef.Method) + .Select(x => x.Value) + .ToList(); + var ignoredTargets = _dataAccessor + .Access(configRef.Method) + .Select(x => x.Value) + .ToList(); return new EnumMappingConfiguration( configData?.Strategy ?? _defaultConfiguration.Enum.Strategy, configData?.IgnoreCase ?? _defaultConfiguration.Enum.IgnoreCase, configData?.FallbackValue, + ignoredSources, + ignoredTargets, explicitMappings ); } diff --git a/src/Riok.Mapperly/Configuration/MapperIgnoreEnumValueConfiguration.cs b/src/Riok.Mapperly/Configuration/MapperIgnoreEnumValueConfiguration.cs new file mode 100644 index 0000000000..4d9dc75556 --- /dev/null +++ b/src/Riok.Mapperly/Configuration/MapperIgnoreEnumValueConfiguration.cs @@ -0,0 +1,5 @@ +using Microsoft.CodeAnalysis; + +namespace Riok.Mapperly.Configuration; + +public record MapperIgnoreEnumValueConfiguration(IFieldSymbol Value); diff --git a/src/Riok.Mapperly/Configuration/MappingConfigurationReference.cs b/src/Riok.Mapperly/Configuration/MappingConfigurationReference.cs new file mode 100644 index 0000000000..9404129d76 --- /dev/null +++ b/src/Riok.Mapperly/Configuration/MappingConfigurationReference.cs @@ -0,0 +1,5 @@ +using Microsoft.CodeAnalysis; + +namespace Riok.Mapperly.Configuration; + +public record struct MappingConfigurationReference(IMethodSymbol? Method, ITypeSymbol Source, ITypeSymbol Target); diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs index 542d04214c..28cfc8a47c 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs @@ -29,7 +29,7 @@ ITypeSymbol target Source = source; Target = target; UserSymbol = userSymbol; - Configuration = ReadConfiguration(UserSymbol); + Configuration = ReadConfiguration(new MappingConfigurationReference(UserSymbol, source, target)); } protected MappingBuilderContext( diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumMappingBuilder.cs index d3763d050c..5762172a7e 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumMappingBuilder.cs @@ -32,61 +32,40 @@ public static class EnumMappingBuilder return null; // map enums by strategy - var explicitMappings = BuildExplicitValueMapping(ctx); return ctx.Configuration.Enum.Strategy switch { EnumMappingStrategy.ByName when ctx.IsExpression => BuildCastMappingAndDiagnostic(ctx), - EnumMappingStrategy.ByValue when ctx.IsExpression && explicitMappings.Count > 0 => BuildCastMappingAndDiagnostic(ctx), + EnumMappingStrategy.ByValue when ctx is { IsExpression: true, Configuration.Enum.HasExplicitConfigurations: true } + => BuildCastMappingAndDiagnostic(ctx), EnumMappingStrategy.ByValueCheckDefined when ctx.IsExpression => BuildCastMappingAndDiagnostic(ctx), - EnumMappingStrategy.ByName => BuildNameMapping(ctx, explicitMappings), - EnumMappingStrategy.ByValueCheckDefined => BuildEnumToEnumCastMapping(ctx, explicitMappings, true), - _ => BuildEnumToEnumCastMapping(ctx, explicitMappings), + EnumMappingStrategy.ByName => BuildNameMapping(ctx), + EnumMappingStrategy.ByValueCheckDefined => BuildEnumToEnumCastMapping(ctx, checkTargetDefined: true), + _ => BuildEnumToEnumCastMapping(ctx), }; } private static TypeMapping BuildCastMappingAndDiagnostic(MappingBuilderContext ctx) { ctx.ReportDiagnostic( - DiagnosticDescriptors.EnumMappingStrategyByNameNotSupportedInProjectionMappings, + DiagnosticDescriptors.EnumMappingNotSupportedInProjectionMappings, ctx.Source.ToDisplayString(), ctx.Target.ToDisplayString() ); - return BuildEnumToEnumCastMapping(ctx, new Dictionary(SymbolEqualityComparer.Default)); + return BuildEnumToEnumCastMapping(ctx, true); } private static TypeMapping BuildEnumToEnumCastMapping( MappingBuilderContext ctx, - IReadOnlyDictionary explicitMappings, + bool ignoreExplicitAndIgnoredMappings = false, bool checkTargetDefined = false ) { - var explicitMappingSourceNames = explicitMappings.Keys.Select(x => x.Name).ToHashSet(); - var explicitMappingTargetNames = explicitMappings.Values.Select(x => x.Name).ToHashSet(); - var sourceValues = ctx.SymbolAccessor - .GetAllFields(ctx.Source) - .Where(x => !explicitMappingSourceNames.Contains(x.Name)) - .ToDictionary(field => field.Name, field => field.ConstantValue); - var targetValues = ctx.SymbolAccessor - .GetAllFields(ctx.Target) - .Where(x => !explicitMappingTargetNames.Contains(x.Name)) - .ToDictionary(field => field.Name, field => field.ConstantValue); - var targetMemberNames = ctx.SymbolAccessor.GetAllFields(ctx.Target).Select(x => x.Name).ToHashSet(); - - var missingTargetValues = targetValues.Where( - field => - !sourceValues.ContainsValue(field.Value) && ctx.Configuration.Enum.FallbackValue?.ConstantValue?.Equals(field.Value) != true + var enumMemberMappings = BuildEnumMemberMappings( + ctx, + ignoreExplicitAndIgnoredMappings, + static x => x.ConstantValue!, + EqualityComparer.Default ); - foreach (var member in missingTargetValues) - { - ctx.ReportDiagnostic(DiagnosticDescriptors.TargetEnumValueNotMapped, member.Key, member.Value!, ctx.Target, ctx.Source); - } - - var missingSourceValues = sourceValues.Where(field => !targetValues.ContainsValue(field.Value)); - foreach (var member in missingSourceValues) - { - ctx.ReportDiagnostic(DiagnosticDescriptors.SourceEnumValueNotMapped, member.Key, member.Value!, ctx.Source, ctx.Target); - } - var fallbackMapping = BuildFallbackMapping(ctx); if (fallbackMapping.FallbackMember != null && !checkTargetDefined) { @@ -101,86 +80,156 @@ _ when ctx.SymbolAccessor.HasAttribute(ctx.Target) => EnumCastMa _ => EnumCastMapping.CheckDefinedMode.Value, }; - var castFallbackMapping = new EnumCastMapping(ctx.Source, ctx.Target, checkDefinedMode, targetMemberNames, fallbackMapping); - if (explicitMappings.Count == 0) + var castFallbackMapping = new EnumCastMapping( + ctx.Source, + ctx.Target, + checkDefinedMode, + enumMemberMappings.TargetMembers, + fallbackMapping + ); + var differentValueExplicitEnumMappings = enumMemberMappings.ExplicitMemberMappings + .Where(x => x.Key.ConstantValue?.Equals(x.Value.ConstantValue) != true) + .ToDictionary(x => x.Key, x => x.Value, (IEqualityComparer)SymbolEqualityComparer.Default); + + if (differentValueExplicitEnumMappings.Count == 0) return castFallbackMapping; - var explicitNameMappings = explicitMappings - .Where(x => !x.Value.ConstantValue?.Equals(x.Key.ConstantValue) == true) - .ToDictionary(x => x.Key.Name, x => x.Value.Name); return new EnumNameMapping( ctx.Source, ctx.Target, - explicitNameMappings, + differentValueExplicitEnumMappings, new EnumFallbackValueMapping(ctx.Source, ctx.Target, castFallbackMapping) ); } - private static EnumNameMapping BuildNameMapping( - MappingBuilderContext ctx, - IReadOnlyDictionary explicitMappings - ) + private static EnumNameMapping BuildNameMapping(MappingBuilderContext ctx) { var fallbackMapping = BuildFallbackMapping(ctx); - var targetFieldsByName = ctx.SymbolAccessor.GetAllFields(ctx.Target).ToDictionary(x => x.Name); - var sourceFieldsByName = ctx.SymbolAccessor.GetAllFields(ctx.Source).ToDictionary(x => x.Name); + var enumMemberMappings = ctx.Configuration.Enum.IgnoreCase + ? BuildEnumMemberMappings(ctx, false, static x => x.Name, StringComparer.Ordinal, StringComparer.OrdinalIgnoreCase) + : BuildEnumMemberMappings(ctx, false, static x => x.Name, StringComparer.Ordinal); - Func getTargetField; - if (ctx.Configuration.Enum.IgnoreCase) + if (enumMemberMappings.MemberMappings.Count == 0) { - var targetFieldsByNameIgnoreCase = targetFieldsByName - .DistinctBy(x => x.Key, StringComparer.OrdinalIgnoreCase) - .ToDictionary(x => x.Key, x => x.Value, StringComparer.OrdinalIgnoreCase); - getTargetField = source => - explicitMappings.GetValueOrDefault(source) - ?? targetFieldsByName.GetValueOrDefault(source.Name) - ?? targetFieldsByNameIgnoreCase.GetValueOrDefault(source.Name); + ctx.ReportDiagnostic(DiagnosticDescriptors.EnumNameMappingNoOverlappingValuesFound, ctx.Source, ctx.Target); } - else + + return new EnumNameMapping(ctx.Source, ctx.Target, enumMemberMappings.MemberMappings, fallbackMapping); + } + + private static EnumMemberMappings BuildEnumMemberMappings( + MappingBuilderContext ctx, + bool ignoreExplicitAndIgnoredMappings, + Func propertySelector, + params IEqualityComparer[] propertyComparer + ) + { + var ignoredSourceMembers = ignoreExplicitAndIgnoredMappings + ? new HashSet(SymbolEqualityComparer.Default) + : ctx.Configuration.Enum.IgnoredSourceMembers.ToHashSet(); + var ignoredTargetMembers = ignoreExplicitAndIgnoredMappings + ? new HashSet(SymbolEqualityComparer.Default) + : ctx.Configuration.Enum.IgnoredTargetMembers.ToHashSet(); + var explicitMappings = ignoreExplicitAndIgnoredMappings + ? new Dictionary(SymbolEqualityComparer.Default) + : BuildExplicitValueMappings(ctx); + var sourceMembers = ctx.Source.GetMembers().OfType().Where(x => !ignoredSourceMembers.Remove(x)).ToHashSet(); + var targetMembers = ctx.Target.GetMembers().OfType().Where(x => !ignoredTargetMembers.Remove(x)).ToHashSet(); + + var targetMembersByProperty = propertyComparer + .Select(pc => targetMembers.DistinctBy(propertySelector, pc).ToDictionary(propertySelector, x => x, pc)) + .ToList(); + + var mappedTargetMembers = new HashSet(SymbolEqualityComparer.Default); + var mappings = new Dictionary(SymbolEqualityComparer.Default); + foreach (var sourceMember in sourceMembers) { - getTargetField = source => explicitMappings.GetValueOrDefault(source) ?? targetFieldsByName.GetValueOrDefault(source.Name); + if (!explicitMappings.TryGetValue(sourceMember, out var targetMember)) + { + var sourceProperty = propertySelector(sourceMember); + foreach (var targetMembersByPropertyCandidate in targetMembersByProperty) + { + if (targetMembersByPropertyCandidate.TryGetValue(sourceProperty, out targetMember)) + break; + } + + if (targetMember == null) + continue; + } + + mappings.Add(sourceMember, targetMember); + mappedTargetMembers.Add(targetMember); } - var enumMemberMappings = ctx.SymbolAccessor - .GetAllFields(ctx.Source) - .Select(x => (Source: x, Target: getTargetField(x))) - .Where(x => x.Target != null) - .ToDictionary(x => x.Source.Name, x => x.Target!.Name); + AddUnmappedMembersDiagnostics(ctx, mappings, mappedTargetMembers, sourceMembers, targetMembers); + AddUnmatchedIgnoredMembers(ctx, ignoredSourceMembers, ignoredTargetMembers); + return new EnumMemberMappings(mappings, explicitMappings, targetMembers); + } - if (enumMemberMappings.Count == 0) + private static void AddUnmatchedIgnoredMembers( + MappingBuilderContext ctx, + ISet ignoredUnmatchedSourceMembers, + ISet ignoredUnmatchedTargetMembers + ) + { + foreach (var member in ignoredUnmatchedSourceMembers) { - ctx.ReportDiagnostic(DiagnosticDescriptors.EnumNameMappingNoOverlappingValuesFound, ctx.Source, ctx.Target); + ctx.ReportDiagnostic( + DiagnosticDescriptors.IgnoredEnumSourceMemberNotFound, + member.Name, + member.ConstantValue!, + ctx.Source, + ctx.Target + ); + } + + foreach (var member in ignoredUnmatchedTargetMembers) + { + ctx.ReportDiagnostic( + DiagnosticDescriptors.IgnoredEnumTargetMemberNotFound, + member.Name, + member.ConstantValue!, + ctx.Source, + ctx.Target + ); } + } - var missingSourceMembers = sourceFieldsByName.Where(field => !enumMemberMappings.ContainsKey(field.Key)); + private static void AddUnmappedMembersDiagnostics( + MappingBuilderContext ctx, + IReadOnlyDictionary mappings, + ISet mappedTargetMembers, + IEnumerable sourceMembers, + IEnumerable targetMembers + ) + { + var missingSourceMembers = sourceMembers.Where(field => !mappings.ContainsKey(field)); foreach (var member in missingSourceMembers) { ctx.ReportDiagnostic( DiagnosticDescriptors.SourceEnumValueNotMapped, - member.Key, - member.Value.ConstantValue!, + member.Name, + member.ConstantValue!, ctx.Source, ctx.Target ); } - var missingTargetMembers = targetFieldsByName.Where( + var missingTargetMembers = targetMembers.Where( field => - !enumMemberMappings.ContainsValue(field.Key) - && ctx.Configuration.Enum.FallbackValue?.ConstantValue?.Equals(field.Value.ConstantValue) != true + !mappedTargetMembers.Contains(field) + && ctx.Configuration.Enum.FallbackValue?.ConstantValue?.Equals(field.ConstantValue) != true ); foreach (var member in missingTargetMembers) { ctx.ReportDiagnostic( DiagnosticDescriptors.TargetEnumValueNotMapped, - member.Key, - member.Value.ConstantValue!, + member.Name, + member.ConstantValue!, ctx.Target, ctx.Source ); } - - return new EnumNameMapping(ctx.Source, ctx.Target, enumMemberMappings, fallbackMapping); } private static EnumFallbackValueMapping BuildFallbackMapping(MappingBuilderContext ctx) @@ -202,7 +251,7 @@ private static EnumFallbackValueMapping BuildFallbackMapping(MappingBuilderConte return new EnumFallbackValueMapping(ctx.Source, ctx.Target); } - private static IReadOnlyDictionary BuildExplicitValueMapping(MappingBuilderContext ctx) + private static IReadOnlyDictionary BuildExplicitValueMappings(MappingBuilderContext ctx) { var targetFieldsByExplicitValue = new Dictionary(SymbolEqualityComparer.Default); foreach (var (source, target) in ctx.Configuration.Enum.ExplicitMappings) @@ -243,4 +292,10 @@ private static IReadOnlyDictionary BuildExplicitValu return targetFieldsByExplicitValue; } + + private record EnumMemberMappings( + IReadOnlyDictionary MemberMappings, + IReadOnlyDictionary ExplicitMemberMappings, + IReadOnlyCollection TargetMembers + ); } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/Enums/EnumCastMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/Enums/EnumCastMapping.cs index b1fdcab1d7..3838ae1965 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/Enums/EnumCastMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/Enums/EnumCastMapping.cs @@ -12,7 +12,7 @@ namespace Riok.Mapperly.Descriptors.Mappings.Enums; public class EnumCastMapping : CastMapping { private readonly CheckDefinedMode _checkDefinedMode; - private readonly IReadOnlyCollection _targetEnumMemberNames; + private readonly IReadOnlyCollection _targetEnumMembers; private readonly EnumFallbackValueMapping _fallback; public enum CheckDefinedMode @@ -37,13 +37,13 @@ public EnumCastMapping( ITypeSymbol sourceType, ITypeSymbol targetType, CheckDefinedMode checkDefinedMode, - IReadOnlyCollection targetEnumMemberNames, + IReadOnlyCollection targetEnumMembers, EnumFallbackValueMapping fallback ) : base(sourceType, targetType) { _checkDefinedMode = checkDefinedMode; - _targetEnumMemberNames = targetEnumMemberNames; + _targetEnumMembers = targetEnumMembers; _fallback = fallback; } @@ -59,7 +59,7 @@ public override ExpressionSyntax Build(TypeMappingBuildContext ctx) private ExpressionSyntax BuildIsDefinedCondition(ExpressionSyntax convertedSourceValue) { - var allEnumMembers = _targetEnumMemberNames.Select(x => MemberAccess(FullyQualifiedIdentifier(TargetType), x)); + var allEnumMembers = _targetEnumMembers.Select(x => MemberAccess(FullyQualifiedIdentifier(TargetType), x.Name)); return _checkDefinedMode switch { // (TargetEnum)v is TargetEnum.A or TargetEnum.B or ... diff --git a/src/Riok.Mapperly/Descriptors/Mappings/Enums/EnumNameMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/Enums/EnumNameMapping.cs index b1a263e5c3..8a9ae97633 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/Enums/EnumNameMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/Enums/EnumNameMapping.cs @@ -12,13 +12,13 @@ namespace Riok.Mapperly.Descriptors.Mappings.Enums; /// public class EnumNameMapping : MethodMapping { - private readonly IReadOnlyDictionary _enumMemberMappings; + private readonly IReadOnlyDictionary _enumMemberMappings; private readonly EnumFallbackValueMapping _fallback; public EnumNameMapping( ITypeSymbol source, ITypeSymbol target, - IReadOnlyDictionary enumMemberMappings, + IReadOnlyDictionary enumMemberMappings, EnumFallbackValueMapping fallback ) : base(source, target) @@ -31,16 +31,16 @@ public override IEnumerable BuildBody(TypeMappingBuildContext c { // switch for each name to the enum value // eg: Enum1.Value1 => Enum2.Value1, - var arms = _enumMemberMappings.Select(BuildArm).Append(_fallback.BuildDiscardArm(ctx)); + var arms = _enumMemberMappings.Select(x => BuildArm(x.Key, x.Value)).Append(_fallback.BuildDiscardArm(ctx)); var switchExpr = SwitchExpression(ctx.Source).WithArms(CommaSeparatedList(arms, true)); yield return ReturnStatement(switchExpr); } - private SwitchExpressionArmSyntax BuildArm(KeyValuePair sourceTargetField) + private SwitchExpressionArmSyntax BuildArm(IFieldSymbol sourceMemberField, IFieldSymbol targetMemberField) { - var sourceMember = MemberAccess(FullyQualifiedIdentifier(SourceType), sourceTargetField.Key); - var targetMember = MemberAccess(FullyQualifiedIdentifier(TargetType), sourceTargetField.Value); + var sourceMember = MemberAccess(FullyQualifiedIdentifier(SourceType), sourceMemberField.Name); + var targetMember = MemberAccess(FullyQualifiedIdentifier(TargetType), targetMemberField.Name); var pattern = ConstantPattern(sourceMember); return SwitchExpressionArm(pattern, targetMember); } diff --git a/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs index b06445b27f..b20b41b61e 100644 --- a/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs @@ -68,5 +68,5 @@ public void ReportDiagnostic(DiagnosticDescriptor descriptor, ISymbol? location, _diagnostics.Add(Diagnostic.Create(descriptor, nodeLocation ?? _descriptor.Syntax.GetLocation(), messageArgs)); } - protected MappingConfiguration ReadConfiguration(IMethodSymbol? userSymbol) => _configuration.ForMethod(userSymbol); + protected MappingConfiguration ReadConfiguration(MappingConfigurationReference configRef) => _configuration.BuildFor(configRef); } diff --git a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs index 7f9f21a9a5..5666d73ff6 100644 --- a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs +++ b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs @@ -276,10 +276,10 @@ internal static class DiagnosticDescriptors true ); - public static readonly DiagnosticDescriptor EnumMappingStrategyByNameNotSupportedInProjectionMappings = new DiagnosticDescriptor( + public static readonly DiagnosticDescriptor EnumMappingNotSupportedInProjectionMappings = new DiagnosticDescriptor( "RMG032", - "The enum mapping strategy ByName, ByValueCheckDefined and explicit enum mappings cannot be used in projection mappings", - "The enum mapping strategy ByName, ByValueCheckDefined and explicit enum mappings cannot be used in projection mappings to map from {0} to {1}", + "The enum mapping strategy ByName, ByValueCheckDefined, explicit enum mappings and ignored enum values cannot be used in projection mappings", + "The enum mapping strategy ByName, ByValueCheckDefined, explicit enum mappings and ignored enum values cannot be used in projection mappings to map from {0} to {1}", DiagnosticCategories.Mapper, DiagnosticSeverity.Warning, true @@ -383,4 +383,22 @@ internal static class DiagnosticDescriptors DiagnosticSeverity.Warning, true ); + + public static readonly DiagnosticDescriptor IgnoredEnumSourceMemberNotFound = new DiagnosticDescriptor( + "RMG044", + "An ignored enum member can not be found on the source enum", + "Ignored enum member {0} ({1}) on {2} not found on source enum {3}", + DiagnosticCategories.Mapper, + DiagnosticSeverity.Warning, + true + ); + + public static readonly DiagnosticDescriptor IgnoredEnumTargetMemberNotFound = new DiagnosticDescriptor( + "RMG045", + "An ignored enum member can not be found on the target enum", + "Ignored enum member {0} ({1}) not found on target enum {3}", + DiagnosticCategories.Mapper, + DiagnosticSeverity.Warning, + true + ); } diff --git a/test/Riok.Mapperly.IntegrationTests/Mapper/StaticTestMapper.cs b/test/Riok.Mapperly.IntegrationTests/Mapper/StaticTestMapper.cs index bc6b4cd4a9..84c41bbf12 100644 --- a/test/Riok.Mapperly.IntegrationTests/Mapper/StaticTestMapper.cs +++ b/test/Riok.Mapperly.IntegrationTests/Mapper/StaticTestMapper.cs @@ -97,6 +97,12 @@ public static TestObjectDto MapToDto(TestObject src) [MapEnumValue(TestEnumDtoAdditionalValue.Value40, TestEnum.Value30)] public static partial TestEnum MapToEnumByValueWithExplicit(TestEnumDtoAdditionalValue v); + [MapEnum(EnumMappingStrategy.ByName)] + [MapperIgnoreSourceValue(TestEnumDtoAdditionalValue.Value30)] + [MapperIgnoreSourceValue(TestEnumDtoAdditionalValue.Value40)] + [MapperIgnoreTargetValue(TestEnum.Value30)] + public static partial TestEnum MapToEnumByNameWithIgnored(TestEnumDtoAdditionalValue v); + [MapEnum(EnumMappingStrategy.ByValueCheckDefined)] public static partial TestEnum MapToEnumByValueCheckDefined(TestEnumDtoByValue v); diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/StaticMapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/StaticMapperTest.SnapshotGeneratedSource.verified.cs index c396941675..6321bdf662 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/StaticMapperTest.SnapshotGeneratedSource.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/StaticMapperTest.SnapshotGeneratedSource.verified.cs @@ -516,6 +516,16 @@ object x when typeof(TTarget).IsAssignableFrom(typeof(object)) => (TTarget)(obje }; } + public static partial global::Riok.Mapperly.IntegrationTests.Models.TestEnum MapToEnumByNameWithIgnored(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoAdditionalValue v) + { + return v switch + { + global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoAdditionalValue.Value10 => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value10, + global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoAdditionalValue.Value20 => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value20, + _ => throw new System.ArgumentOutOfRangeException(nameof(v), v, "The value of enum TestEnumDtoAdditionalValue is not supported"), + }; + } + public static partial global::Riok.Mapperly.IntegrationTests.Models.TestEnum MapToEnumByValueCheckDefined(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue v) { return (global::Riok.Mapperly.IntegrationTests.Models.TestEnum)v is global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value10 or global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value20 or global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value30 ? (global::Riok.Mapperly.IntegrationTests.Models.TestEnum)v : throw new System.ArgumentOutOfRangeException(nameof(v), v, "The value of enum TestEnumDtoByValue is not supported"); diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/StaticMapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/StaticMapperTest.SnapshotGeneratedSource.verified.cs index 35d4aecd0d..ab6f019a34 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/StaticMapperTest.SnapshotGeneratedSource.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/StaticMapperTest.SnapshotGeneratedSource.verified.cs @@ -515,6 +515,16 @@ object x when typeof(TTarget).IsAssignableFrom(typeof(object)) => (TTarget)(obje }; } + public static partial global::Riok.Mapperly.IntegrationTests.Models.TestEnum MapToEnumByNameWithIgnored(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoAdditionalValue v) + { + return v switch + { + global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoAdditionalValue.Value10 => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value10, + global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoAdditionalValue.Value20 => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value20, + _ => throw new System.ArgumentOutOfRangeException(nameof(v), v, "The value of enum TestEnumDtoAdditionalValue is not supported"), + }; + } + public static partial global::Riok.Mapperly.IntegrationTests.Models.TestEnum MapToEnumByValueCheckDefined(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue v) { return (global::Riok.Mapperly.IntegrationTests.Models.TestEnum)v is global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value10 or global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value20 or global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value30 ? (global::Riok.Mapperly.IntegrationTests.Models.TestEnum)v : throw new System.ArgumentOutOfRangeException(nameof(v), v, "The value of enum TestEnumDtoByValue is not supported"); diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/StaticMapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/StaticMapperTest.SnapshotGeneratedSource.verified.cs index 32f0fbed1a..699997e00c 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/StaticMapperTest.SnapshotGeneratedSource.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/StaticMapperTest.SnapshotGeneratedSource.verified.cs @@ -524,6 +524,16 @@ object x when typeof(TTarget).IsAssignableFrom(typeof(object)) => (TTarget)(obje }; } + public static partial global::Riok.Mapperly.IntegrationTests.Models.TestEnum MapToEnumByNameWithIgnored(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoAdditionalValue v) + { + return v switch + { + global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoAdditionalValue.Value10 => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value10, + global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoAdditionalValue.Value20 => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value20, + _ => throw new System.ArgumentOutOfRangeException(nameof(v), v, "The value of enum TestEnumDtoAdditionalValue is not supported"), + }; + } + public static partial global::Riok.Mapperly.IntegrationTests.Models.TestEnum MapToEnumByValueCheckDefined(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue v) { return (global::Riok.Mapperly.IntegrationTests.Models.TestEnum)v is global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value10 or global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value20 or global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value30 ? (global::Riok.Mapperly.IntegrationTests.Models.TestEnum)v : throw new System.ArgumentOutOfRangeException(nameof(v), v, "The value of enum TestEnumDtoByValue is not supported"); @@ -668,4 +678,4 @@ private static string MapToString1(global::Riok.Mapperly.IntegrationTests.Dto.Te return target; } } -} \ No newline at end of file +} diff --git a/test/Riok.Mapperly.Tests/Mapping/EnumIgnoreTest.cs b/test/Riok.Mapperly.Tests/Mapping/EnumIgnoreTest.cs new file mode 100644 index 0000000000..29cbf680ee --- /dev/null +++ b/test/Riok.Mapperly.Tests/Mapping/EnumIgnoreTest.cs @@ -0,0 +1,40 @@ +using Riok.Mapperly.Diagnostics; + +namespace Riok.Mapperly.Tests.Mapping; + +public class EnumIgnoreTest +{ + [Fact] + public void ByValueWithIgnoredSourceValueShouldWork() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapperIgnoreSourceValue(E1.D), MapEnum(EnumMappingStrategy.ByValue)] partial E2 ToE1(E1 source);", + "enum E1 {A, B, C, D = 100, E}", + "enum E2 {AA, BB, CC}" + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics) + .Should() + .HaveDiagnostic(DiagnosticDescriptors.SourceEnumValueNotMapped, "Enum member E (101) on E1 not found on target enum E2") + .HaveAssertedAllDiagnostics() + .HaveSingleMethodBody("return (global::E2)source;"); + } + + [Fact] + public void ByValueWithIgnoredTargetValueShouldWork() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapperIgnoreTargetValue(E2.DD), MapEnum(EnumMappingStrategy.ByValue)] partial E2 ToE1(E1 source);", + "enum E1 {A, B, C}", + "enum E2 {AA, BB, CC, DD = 100, EE}" + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics) + .Should() + .HaveDiagnostic(DiagnosticDescriptors.TargetEnumValueNotMapped, "Enum member EE (101) on E2 not found on source enum E1") + .HaveAssertedAllDiagnostics() + .HaveSingleMethodBody("return (global::E2)source;"); + } +} diff --git a/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionEnumTest.cs b/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionEnumTest.cs index 23d4cedd1f..23c926f485 100644 --- a/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionEnumTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionEnumTest.cs @@ -41,8 +41,8 @@ TestSourceBuilderOptions.Default with .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) .Should() .HaveDiagnostic( - DiagnosticDescriptors.EnumMappingStrategyByNameNotSupportedInProjectionMappings, - "The enum mapping strategy ByName, ByValueCheckDefined and explicit enum mappings cannot be used in projection mappings to map from C to D" + DiagnosticDescriptors.EnumMappingNotSupportedInProjectionMappings, + "The enum mapping strategy ByName, ByValueCheckDefined, explicit enum mappings and ignored enum values cannot be used in projection mappings to map from C to D" ) .HaveAssertedAllDiagnostics(); } @@ -71,8 +71,8 @@ TestSourceBuilderOptions.Default with .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) .Should() .HaveDiagnostic( - DiagnosticDescriptors.EnumMappingStrategyByNameNotSupportedInProjectionMappings, - "The enum mapping strategy ByName, ByValueCheckDefined and explicit enum mappings cannot be used in projection mappings to map from C to D" + DiagnosticDescriptors.EnumMappingNotSupportedInProjectionMappings, + "The enum mapping strategy ByName, ByValueCheckDefined, explicit enum mappings and ignored enum values cannot be used in projection mappings to map from C to D" ) .HaveDiagnostic(DiagnosticDescriptors.TargetEnumValueNotMapped, "Enum member Value2 (200) on D not found on source enum C") .HaveDiagnostic(DiagnosticDescriptors.SourceEnumValueNotMapped, "Enum member Value2 (100) on C not found on target enum D") diff --git a/test/Riok.Mapperly.Tests/_snapshots/EnumTest.EnumToOtherEnumByNameWithoutOverlap.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/EnumTest.EnumToOtherEnumByNameWithoutOverlap.verified.txt index 6346209e09..ef5d8439d6 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/EnumTest.EnumToOtherEnumByNameWithoutOverlap.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/EnumTest.EnumToOtherEnumByNameWithoutOverlap.verified.txt @@ -1,17 +1,5 @@ { Diagnostics: [ - { - Id: RMG003, - Title: No overlapping enum members found, - Severity: Warning, - WarningLevel: 1, - Location: : (11,4)-(11,69), - Description: , - HelpLink: , - MessageFormat: {0} and {1} don't have overlapping enum member names, mapping will therefore always result in an exception, - Message: E1 and E2 don't have overlapping enum member names, mapping will therefore always result in an exception, - Category: Mapper - }, { Id: RMG038, Title: An enum member could not be found on the target enum, @@ -83,6 +71,18 @@ MessageFormat: Enum member {0} ({1}) on {2} not found on source enum {3}, Message: Enum member F (2) on E2 not found on source enum E1, Category: Mapper + }, + { + Id: RMG003, + Title: No overlapping enum members found, + Severity: Warning, + WarningLevel: 1, + Location: : (11,4)-(11,69), + Description: , + HelpLink: , + MessageFormat: {0} and {1} don't have overlapping enum member names, mapping will therefore always result in an exception, + Message: E1 and E2 don't have overlapping enum member names, mapping will therefore always result in an exception, + Category: Mapper } ] } \ No newline at end of file