diff --git a/docs/docs/configuration/enum.mdx b/docs/docs/configuration/enum.mdx index 067bde498a..f8cb6a3b7c 100644 --- a/docs/docs/configuration/enum.mdx +++ b/docs/docs/configuration/enum.mdx @@ -59,7 +59,7 @@ The `IgnoreCase` property allows to opt in for case insensitive mappings (defaul ## Manually mapped enum values To explicitly map enum values the `MapEnumValueAttibute` can be used. -Attribute is only valid on mapping methods with enums as parameters. +Attribute is only valid on enum-to-enum, enum-to-string and string-to-enum mappings. ```csharp [Mapper] @@ -70,6 +70,16 @@ public partial class CarMapper [MapEnumValue(CarFeature.AWD, CarFeatureDto.AllWheelDrive)] // highlight-end public partial CarFeatureDto MapFeature(CarFeature feature); + + // highlight-start + [MapEnumValue("AWD", CarFeatureDto.AllWheelDrive)] + // highlight-end + public partial CarFeatureDto MapFeatureFromString(string feature); + + // highlight-start + [MapEnumValue(CarFeatureDto.AllWheelDrive, "AWD")] + // highlight-end + public partial string MapFeatureToString(CarFeatureDto feature); } ``` @@ -80,7 +90,7 @@ This is especially useful when applying [strict enum mappings](#strict-enum-mapp ```csharp [Mapper] -public partial class CarMapper +public partial class FruitMapper { // highlight-start [MapperIgnoreSourceValue(Fruit.Apple)] diff --git a/src/Riok.Mapperly.Abstractions/MapEnumValueAttribute.cs b/src/Riok.Mapperly.Abstractions/MapEnumValueAttribute.cs index 8decbd5fd0..a97d81dd0d 100644 --- a/src/Riok.Mapperly.Abstractions/MapEnumValueAttribute.cs +++ b/src/Riok.Mapperly.Abstractions/MapEnumValueAttribute.cs @@ -12,21 +12,21 @@ public sealed class MapEnumValueAttribute : Attribute /// /// Customizes how enum values are mapped /// - /// The enum value to map from - /// The enum value to map to + /// The value to map from + /// The value to map to public MapEnumValueAttribute(object source, object target) { - Source = (Enum)source; - Target = (Enum)target; + Source = source; + Target = target; } /// /// What to map to /// - public Enum Target { get; } + public object Target { get; } /// /// What to map from /// - public Enum Source { get; } + public object Source { get; } } diff --git a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt index abdaf0953c..f40e11a59d 100644 --- a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt +++ b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt @@ -15,8 +15,6 @@ Riok.Mapperly.Abstractions.MapEnumAttribute.MapEnumAttribute(Riok.Mapperly.Abstr Riok.Mapperly.Abstractions.MapEnumAttribute.Strategy.get -> Riok.Mapperly.Abstractions.EnumMappingStrategy Riok.Mapperly.Abstractions.MapEnumValueAttribute Riok.Mapperly.Abstractions.MapEnumValueAttribute.MapEnumValueAttribute(object! source, object! target) -> void -Riok.Mapperly.Abstractions.MapEnumValueAttribute.Source.get -> System.Enum! -Riok.Mapperly.Abstractions.MapEnumValueAttribute.Target.get -> System.Enum! Riok.Mapperly.Abstractions.MapperAttribute Riok.Mapperly.Abstractions.MapperAttribute.EnabledConversions.get -> Riok.Mapperly.Abstractions.MappingConversionType Riok.Mapperly.Abstractions.MapperAttribute.EnabledConversions.set -> void @@ -188,3 +186,5 @@ Riok.Mapperly.Abstractions.MapPropertyAttribute.MapPropertyAttribute(string! sou Riok.Mapperly.Abstractions.MapPropertyAttribute.MapPropertyAttribute(string![]! source, string! target) -> void Riok.Mapperly.Abstractions.MapperAttribute.IncludedConstructors.get -> Riok.Mapperly.Abstractions.MemberVisibility Riok.Mapperly.Abstractions.MapperAttribute.IncludedConstructors.set -> void +Riok.Mapperly.Abstractions.MapEnumValueAttribute.Source.get -> object! +Riok.Mapperly.Abstractions.MapEnumValueAttribute.Target.get -> object! diff --git a/src/Riok.Mapperly/AnalyzerReleases.Shipped.md b/src/Riok.Mapperly/AnalyzerReleases.Shipped.md index d4457c6082..cdcd8920d0 100644 --- a/src/Riok.Mapperly/AnalyzerReleases.Shipped.md +++ b/src/Riok.Mapperly/AnalyzerReleases.Shipped.md @@ -186,6 +186,7 @@ RMG038 | Mapper | Warning | An enum member could not be found on the target RMG081 | Mapper | Error | A mapping method with additional parameters cannot be a default mapping RMG082 | Mapper | Warning | An additional mapping method parameter is not mapped RMG083 | Mapper | Info | Cannot map to read only type +RMG084 | Mapper | Error | Multiple mappings are configured for the same source string ### Removed Rules @@ -195,4 +196,3 @@ RMG017 | Mapper | Warning | An init only member can have one configuration a RMG026 | Mapper | Info | Cannot map from indexed member 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/EnumValueMappingConfiguration.cs b/src/Riok.Mapperly/Configuration/EnumValueMappingConfiguration.cs index 53b7f3a7f4..4aec1630fa 100644 --- a/src/Riok.Mapperly/Configuration/EnumValueMappingConfiguration.cs +++ b/src/Riok.Mapperly/Configuration/EnumValueMappingConfiguration.cs @@ -1,4 +1,3 @@ -using Microsoft.CodeAnalysis; using Riok.Mapperly.Abstractions; namespace Riok.Mapperly.Configuration; @@ -9,4 +8,4 @@ namespace Riok.Mapperly.Configuration; /// /// The source constant of the enum value mapping. /// The target constant of the enum value mapping. -public record EnumValueMappingConfiguration(IFieldSymbol Source, IFieldSymbol Target); +public record EnumValueMappingConfiguration(AttributeValue Source, AttributeValue Target); diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/EnumMappingDiagnosticReporter.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/EnumMappingDiagnosticReporter.cs new file mode 100644 index 0000000000..c4a31174df --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/EnumMappingDiagnosticReporter.cs @@ -0,0 +1,88 @@ +using Microsoft.CodeAnalysis; +using Riok.Mapperly.Abstractions; +using Riok.Mapperly.Diagnostics; + +namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; + +internal static class EnumMappingDiagnosticReporter +{ + public static void AddUnmatchedSourceIgnoredMembers(MappingBuilderContext ctx, ISet ignoredSourceMembers) + { + var sourceFields = ctx.SymbolAccessor.GetAllFields(ctx.Source).ToHashSet(); + var unmatchedSourceMembers = ignoredSourceMembers.Where(m => !sourceFields.Contains(m)); + + foreach (var member in unmatchedSourceMembers) + { + ctx.ReportDiagnostic( + DiagnosticDescriptors.IgnoredEnumSourceMemberNotFound, + member.Name, + member.ConstantValue!, + ctx.Source, + ctx.Target + ); + } + } + + public static void AddUnmatchedTargetIgnoredMembers(MappingBuilderContext ctx, ISet ignoredTargetMembers) + { + var targetFields = ctx.SymbolAccessor.GetAllFields(ctx.Target).ToHashSet(); + var unmatchedTargetMembers = ignoredTargetMembers.Where(m => !targetFields.Contains(m)); + + foreach (var member in unmatchedTargetMembers) + { + ctx.ReportDiagnostic( + DiagnosticDescriptors.IgnoredEnumTargetMemberNotFound, + member.Name, + member.ConstantValue!, + ctx.Source, + ctx.Target + ); + } + } + + public static void AddUnmappedTargetMembersDiagnostics( + MappingBuilderContext ctx, + ISet mappings, + IEnumerable targetMembers + ) + { + if (!ctx.Configuration.Enum.RequiredMappingStrategy.HasFlag(RequiredMappingStrategy.Target)) + return; + + var missingTargetMembers = targetMembers.Where(field => + !mappings.Contains(field) && ctx.Configuration.Enum.FallbackValue?.ConstantValue?.Equals(field.ConstantValue) != true + ); + foreach (var member in missingTargetMembers) + { + ctx.ReportDiagnostic( + DiagnosticDescriptors.TargetEnumValueNotMapped, + member.Name, + member.ConstantValue!, + ctx.Target, + ctx.Source + ); + } + } + + public static void AddUnmappedSourceMembersDiagnostics( + MappingBuilderContext ctx, + ISet mappings, + IEnumerable sourceMembers + ) + { + if (!ctx.Configuration.Enum.RequiredMappingStrategy.HasFlag(RequiredMappingStrategy.Source)) + return; + + var missingSourceMembers = sourceMembers.Where(field => !mappings.Contains(field)); + foreach (var member in missingSourceMembers) + { + ctx.ReportDiagnostic( + DiagnosticDescriptors.SourceEnumValueNotMapped, + member.Name, + member.ConstantValue!, + ctx.Source, + ctx.Target + ); + } + } +} diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumToEnumMappingBuilder.cs similarity index 67% rename from src/Riok.Mapperly/Descriptors/MappingBuilders/EnumMappingBuilder.cs rename to src/Riok.Mapperly/Descriptors/MappingBuilders/EnumToEnumMappingBuilder.cs index 437852ddd8..ae8f1e0a80 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumToEnumMappingBuilder.cs @@ -1,5 +1,6 @@ using Microsoft.CodeAnalysis; using Riok.Mapperly.Abstractions; +using Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Descriptors.Mappings.Enums; using Riok.Mapperly.Diagnostics; @@ -7,7 +8,7 @@ namespace Riok.Mapperly.Descriptors.MappingBuilders; -public static class EnumMappingBuilder +public static class EnumToEnumMappingBuilder { public static INewInstanceMapping? TryBuildMapping(MappingBuilderContext ctx) { @@ -134,16 +135,8 @@ params IEqualityComparer[] propertyComparer var explicitMappings = ignoreExplicitAndIgnoredMappings ? new Dictionary(SymbolEqualityComparer.Default) : BuildExplicitValueMappings(ctx); - var sourceMembers = ctx - .Source.GetMembers() - .OfType() - .Where(x => !ignoredSourceMembers.Remove(x)) - .ToHashSet(SymbolTypeEqualityComparer.FieldDefault); - var targetMembers = ctx - .Target.GetMembers() - .OfType() - .Where(x => !ignoredTargetMembers.Remove(x)) - .ToHashSet(SymbolTypeEqualityComparer.FieldDefault); + var sourceMembers = ctx.SymbolAccessor.GetFieldsExcept(ctx.Source, ignoredSourceMembers); + var targetMembers = ctx.SymbolAccessor.GetFieldsExcept(ctx.Target, ignoredTargetMembers); var targetMembersByProperty = propertyComparer .Select(pc => targetMembers.DistinctBy(propertySelector, pc).ToDictionary(propertySelector, x => x, pc)) @@ -170,98 +163,13 @@ params IEqualityComparer[] propertyComparer mappedTargetMembers.Add(targetMember); } - AddUnmappedMembersDiagnostics(ctx, mappings, mappedTargetMembers, sourceMembers, targetMembers); - AddUnmatchedIgnoredMembers(ctx, ignoredSourceMembers, ignoredTargetMembers); + EnumMappingDiagnosticReporter.AddUnmappedSourceMembersDiagnostics(ctx, mappings.Keys.ToHashSet(), sourceMembers); + EnumMappingDiagnosticReporter.AddUnmappedTargetMembersDiagnostics(ctx, mappedTargetMembers, targetMembers); + EnumMappingDiagnosticReporter.AddUnmatchedSourceIgnoredMembers(ctx, ignoredSourceMembers); + EnumMappingDiagnosticReporter.AddUnmatchedTargetIgnoredMembers(ctx, ignoredTargetMembers); return new EnumMemberMappings(mappings, explicitMappings, targetMembers); } - private static void AddUnmatchedIgnoredMembers( - MappingBuilderContext ctx, - ISet ignoredUnmatchedSourceMembers, - ISet ignoredUnmatchedTargetMembers - ) - { - foreach (var member in ignoredUnmatchedSourceMembers) - { - 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 - ); - } - } - - private static void AddUnmappedMembersDiagnostics( - MappingBuilderContext ctx, - IReadOnlyDictionary mappings, - ISet mappedTargetMembers, - IEnumerable sourceMembers, - IEnumerable targetMembers - ) - { - AddUnmappedSourceMembersDiagnostics(ctx, mappings, sourceMembers); - AddUnmappedTargetMembersDiagnostics(ctx, mappedTargetMembers, targetMembers); - } - - private static void AddUnmappedTargetMembersDiagnostics( - MappingBuilderContext ctx, - ISet mappedTargetMembers, - IEnumerable targetMembers - ) - { - if (!ctx.Configuration.Enum.RequiredMappingStrategy.HasFlag(RequiredMappingStrategy.Target)) - return; - - var missingTargetMembers = targetMembers.Where(field => - !mappedTargetMembers.Contains(field) && ctx.Configuration.Enum.FallbackValue?.ConstantValue?.Equals(field.ConstantValue) != true - ); - foreach (var member in missingTargetMembers) - { - ctx.ReportDiagnostic( - DiagnosticDescriptors.TargetEnumValueNotMapped, - member.Name, - member.ConstantValue!, - ctx.Target, - ctx.Source - ); - } - } - - private static void AddUnmappedSourceMembersDiagnostics( - MappingBuilderContext ctx, - IReadOnlyDictionary mappings, - IEnumerable sourceMembers - ) - { - if (!ctx.Configuration.Enum.RequiredMappingStrategy.HasFlag(RequiredMappingStrategy.Source)) - return; - - var missingSourceMembers = sourceMembers.Where(field => !mappings.ContainsKey(field)); - foreach (var member in missingSourceMembers) - { - ctx.ReportDiagnostic( - DiagnosticDescriptors.SourceEnumValueNotMapped, - member.Name, - member.ConstantValue!, - ctx.Source, - ctx.Target - ); - } - } - private static EnumFallbackValueMapping BuildFallbackMapping(MappingBuilderContext ctx) { var fallbackValue = ctx.Configuration.Enum.FallbackValue; @@ -283,44 +191,72 @@ private static EnumFallbackValueMapping BuildFallbackMapping(MappingBuilderConte private static IReadOnlyDictionary BuildExplicitValueMappings(MappingBuilderContext ctx) { - var targetFieldsByExplicitValue = new Dictionary(SymbolEqualityComparer.Default); + var explicitMappings = new Dictionary(SymbolEqualityComparer.Default); + var sourceFields = ctx.SymbolAccessor.GetEnumFields(ctx.Source); + var targetFields = ctx.SymbolAccessor.GetEnumFields(ctx.Target); foreach (var (source, target) in ctx.Configuration.Enum.ExplicitMappings) { - if (!SymbolEqualityComparer.Default.Equals(source.Type, ctx.Source)) + if (source.ConstantValue.Kind is not TypedConstantKind.Enum) + { + ctx.ReportDiagnostic( + DiagnosticDescriptors.MapValueTypeMismatch, + source.Expression.ToFullString(), + source.ConstantValue.Type?.ToDisplayString() ?? "unknown", + ctx.Source + ); + continue; + } + + if (target.ConstantValue.Kind is not TypedConstantKind.Enum) + { + ctx.ReportDiagnostic( + DiagnosticDescriptors.MapValueTypeMismatch, + target.Expression.ToFullString(), + target.ConstantValue.Type?.ToDisplayString() ?? "unknown", + ctx.Target + ); + continue; + } + + if (!SymbolEqualityComparer.Default.Equals(source.ConstantValue.Type, ctx.Source)) { ctx.ReportDiagnostic( DiagnosticDescriptors.SourceEnumValueDoesNotMatchSourceEnumType, - source, - source.ConstantValue ?? 0, - source.Type, + target.Expression.ToFullString(), + target.ConstantValue.Value ?? 0, + target.ConstantValue.Type?.ToDisplayString() ?? "unknown", ctx.Source ); continue; } - if (!SymbolEqualityComparer.Default.Equals(target.Type, ctx.Target)) + if (!SymbolEqualityComparer.Default.Equals(target.ConstantValue.Type, ctx.Target)) { ctx.ReportDiagnostic( DiagnosticDescriptors.TargetEnumValueDoesNotMatchTargetEnumType, - target, - target.ConstantValue ?? 0, - target.Type, + source.Expression.ToFullString(), + source.ConstantValue.Value ?? 0, + source.ConstantValue.Type?.ToDisplayString() ?? "unknown", ctx.Target ); continue; } - if (targetFieldsByExplicitValue.ContainsKey(source)) + if ( + !sourceFields.TryGetValue(source.ConstantValue.Value!, out var sourceField) + || !targetFields.TryGetValue(target.ConstantValue.Value!, out var targetField) + ) { - ctx.ReportDiagnostic(DiagnosticDescriptors.EnumSourceValueDuplicated, source, ctx.Source, ctx.Target); + continue; } - else + + if (!explicitMappings.TryAdd(sourceField, targetField)) { - targetFieldsByExplicitValue.Add(source, target); + ctx.ReportDiagnostic(DiagnosticDescriptors.EnumSourceValueDuplicated, sourceField, ctx.Source, ctx.Target); } } - return targetFieldsByExplicitValue; + return explicitMappings; } private record EnumMemberMappings( diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumToStringMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumToStringMappingBuilder.cs index d2108761aa..17822cec4d 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumToStringMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumToStringMappingBuilder.cs @@ -1,7 +1,10 @@ using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Riok.Mapperly.Abstractions; +using Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Descriptors.Mappings.Enums; +using Riok.Mapperly.Diagnostics; using Riok.Mapperly.Helpers; namespace Riok.Mapperly.Descriptors.MappingBuilders; @@ -18,6 +21,47 @@ public static class EnumToStringMappingBuilder // to string => use an optimized method of Enum.ToString which would use slow reflection // use Enum.ToString as fallback (for ex. for flags) - return new EnumToStringMapping(ctx.Source, ctx.Target, ctx.SymbolAccessor.GetAllFields(ctx.Source)); + return BuildEnumToStringMapping(ctx); + } + + private static EnumToStringMapping BuildEnumToStringMapping(MappingBuilderContext ctx) + { + var ignoredSourceMembers = ctx.Configuration.Enum.IgnoredSourceMembers.ToHashSet(SymbolTypeEqualityComparer.FieldDefault); + var explicitMappings = BuildExplicitValueMappings(ctx); + + EnumMappingDiagnosticReporter.AddUnmatchedSourceIgnoredMembers(ctx, ignoredSourceMembers); + return new EnumToStringMapping(ctx.Source, ctx.Target, ctx.SymbolAccessor.GetAllFields(ctx.Source), explicitMappings); + } + + private static IReadOnlyDictionary BuildExplicitValueMappings(MappingBuilderContext ctx) + { + var explicitMappings = new Dictionary(SymbolEqualityComparer.Default); + var sourceFields = ctx.SymbolAccessor.GetEnumFields(ctx.Source); + foreach (var (source, target) in ctx.Configuration.Enum.ExplicitMappings) + { + if (!SymbolEqualityComparer.Default.Equals(source.ConstantValue.Type, ctx.Source)) + { + ctx.ReportDiagnostic( + DiagnosticDescriptors.SourceEnumValueDoesNotMatchSourceEnumType, + source.Expression.ToFullString(), + source.ConstantValue.Value ?? 0, + source.ConstantValue.Type?.ToDisplayString() ?? "unknown", + ctx.Source + ); + continue; + } + + if (!sourceFields.TryGetValue(source.ConstantValue.Value!, out var sourceField)) + { + continue; + } + + if (!explicitMappings.TryAdd(sourceField, target.Expression)) + { + ctx.ReportDiagnostic(DiagnosticDescriptors.EnumSourceValueDuplicated, sourceField, ctx.Source, ctx.Target); + } + } + + return explicitMappings; } } diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/MappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/MappingBuilder.cs index a9a098fcb6..1045ccc1d8 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/MappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/MappingBuilder.cs @@ -26,7 +26,7 @@ public class MappingBuilder(MappingCollection mappings, MapperDeclaration mapper CtorMappingBuilder.TryBuildMapping, StringToEnumMappingBuilder.TryBuildMapping, EnumToStringMappingBuilder.TryBuildMapping, - EnumMappingBuilder.TryBuildMapping, + EnumToEnumMappingBuilder.TryBuildMapping, DateTimeToDateOnlyMappingBuilder.TryBuildMapping, DateTimeToTimeOnlyMappingBuilder.TryBuildMapping, ExplicitCastMappingBuilder.TryBuildMapping, diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/StringToEnumMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/StringToEnumMappingBuilder.cs index 92849ed084..5c3c0b99ec 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/StringToEnumMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/StringToEnumMappingBuilder.cs @@ -1,5 +1,7 @@ using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Riok.Mapperly.Abstractions; +using Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Descriptors.Mappings.Enums; using Riok.Mapperly.Diagnostics; @@ -33,6 +35,17 @@ public static class StringToEnumMappingBuilder ); } + return BuildEnumFromStringSwitchMapping(ctx, genericEnumParseMethodSupported); + } + + private static EnumFromStringSwitchMapping BuildEnumFromStringSwitchMapping( + MappingBuilderContext ctx, + bool genericEnumParseMethodSupported + ) + { + var ignoredTargetMembers = ctx.Configuration.Enum.IgnoredTargetMembers.ToHashSet(SymbolTypeEqualityComparer.FieldDefault); + var explicitMappings = BuildExplicitValueMappings(ctx); + // from string => use an optimized method of Enum.Parse which would use slow reflection // however we currently don't support all features of Enum.Parse yet (ex. flags) // therefore we use Enum.Parse as fallback. @@ -44,7 +57,15 @@ public static class StringToEnumMappingBuilder members = members.Where(x => fallbackMapping.FallbackMember.ConstantValue?.Equals(x.ConstantValue) != true); } - return new EnumFromStringSwitchMapping(ctx.Source, ctx.Target, members, ctx.Configuration.Enum.IgnoreCase, fallbackMapping); + EnumMappingDiagnosticReporter.AddUnmatchedTargetIgnoredMembers(ctx, ignoredTargetMembers); + return new EnumFromStringSwitchMapping( + ctx.Source, + ctx.Target, + members, + ctx.Configuration.Enum.IgnoreCase, + fallbackMapping, + explicitMappings + ); } private static EnumFallbackValueMapping BuildFallbackParseMapping(MappingBuilderContext ctx, bool genericEnumParseMethodSupported) @@ -75,4 +96,52 @@ private static EnumFallbackValueMapping BuildFallbackParseMapping(MappingBuilder new EnumFromStringParseMapping(ctx.Source, ctx.Target, genericEnumParseMethodSupported, ctx.Configuration.Enum.IgnoreCase) ); } + + private static IReadOnlyDictionary> BuildExplicitValueMappings(MappingBuilderContext ctx) + { + var explicitMappings = new Dictionary>(SymbolTypeEqualityComparer.FieldDefault); + var checkedSources = new HashSet(); + var targetFields = ctx.SymbolAccessor.GetEnumFields(ctx.Target); + foreach (var (source, target) in ctx.Configuration.Enum.ExplicitMappings) + { + if (!SymbolEqualityComparer.Default.Equals(target.ConstantValue.Type, ctx.Target)) + { + ctx.ReportDiagnostic( + DiagnosticDescriptors.TargetEnumValueDoesNotMatchTargetEnumType, + target.Expression.ToFullString(), + target.ConstantValue.Value ?? 0, + target.ConstantValue.Type?.ToDisplayString() ?? "unknown", + ctx.Target + ); + continue; + } + + if (!targetFields.TryGetValue(target.ConstantValue.Value!, out var targetField)) + { + continue; + } + + if (!checkedSources.Add(source.ConstantValue.Value)) + { + ctx.ReportDiagnostic( + DiagnosticDescriptors.StringSourceValueDuplicated, + source.Expression.ToFullString(), + ctx.Source, + ctx.Target + ); + continue; + } + + if (explicitMappings.TryGetValue(targetField, out var sources)) + { + sources.Add(source.Expression); + } + else + { + explicitMappings.Add(targetField, [source.Expression]); + } + } + + return explicitMappings; + } } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/Enums/EnumFromStringSwitchMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/Enums/EnumFromStringSwitchMapping.cs index 2fba1dfd12..f049ff52a7 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/Enums/EnumFromStringSwitchMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/Enums/EnumFromStringSwitchMapping.cs @@ -17,7 +17,8 @@ public class EnumFromStringSwitchMapping( ITypeSymbol targetType, IEnumerable enumMembers, bool ignoreCase, - EnumFallbackValueMapping fallbackMapping + EnumFallbackValueMapping fallbackMapping, + IReadOnlyDictionary> explicitMappings ) : NewInstanceMethodMapping(sourceType, targetType) { private const string IgnoreCaseSwitchDesignatedVariableName = "s"; @@ -27,7 +28,7 @@ EnumFallbackValueMapping fallbackMapping public override IEnumerable BuildBody(TypeMappingBuildContext ctx) { // switch for each name to the enum value - var arms = ignoreCase ? BuildArmsIgnoreCase(ctx) : enumMembers.Select(BuildArm); + var arms = ignoreCase ? BuildArmsIgnoreCase(ctx) : BuildArms(); arms = arms.Append(fallbackMapping.BuildDiscardArm(ctx)); var switchExpr = ctx.SyntaxFactory.Switch(ctx.Source, arms); @@ -37,24 +38,43 @@ public override IEnumerable BuildBody(TypeMappingBuildContext c private IEnumerable BuildArmsIgnoreCase(TypeMappingBuildContext ctx) { var ignoreCaseSwitchDesignatedVariableName = ctx.NameBuilder.New(IgnoreCaseSwitchDesignatedVariableName); - return enumMembers.Select(f => BuildArmIgnoreCase(ignoreCaseSwitchDesignatedVariableName, f)); + foreach (var field in enumMembers) + { + // source.Value1 + var typeMemberAccess = MemberAccess(field.ContainingType.NonNullable().FullyQualifiedIdentifierName(), field.Name); + if (explicitMappings.TryGetValue(field, out var sourceExpressions)) + { + // add switch arm for each source string + foreach (var sourceExpression in sourceExpressions) + { + yield return BuildArmIgnoreCase(ignoreCaseSwitchDesignatedVariableName, typeMemberAccess, sourceExpression); + } + } + else + { + yield return BuildArmIgnoreCase(ignoreCaseSwitchDesignatedVariableName, typeMemberAccess, NameOf(typeMemberAccess)); + } + } } - private SwitchExpressionArmSyntax BuildArmIgnoreCase(string ignoreCaseSwitchDesignatedVariableName, IFieldSymbol field) + private static SwitchExpressionArmSyntax BuildArmIgnoreCase( + string ignoreCaseSwitchDesignatedVariableName, + MemberAccessExpressionSyntax typeMemberAccess, + ExpressionSyntax stringExpression + ) { // { } s var pattern = RecursivePattern() .WithPropertyPatternClause(PropertyPatternClause().AddTrailingSpace()) .WithDesignation(SingleVariableDesignation(Identifier(ignoreCaseSwitchDesignatedVariableName))); - // source.Value1 - var typeMemberAccess = MemberAccess(field.ContainingType.NonNullable().FullyQualifiedIdentifierName(), field.Name); - // when s.Equals(nameof(source.Value1), StringComparison.OrdinalIgnoreCase) + // or (if explicit mapping exists) + // when s.Equals("VALUE-1", StringComparison.OrdinalIgnoreCase) var whenClause = SwitchWhen( InvocationWithoutIndention( MemberAccess(ignoreCaseSwitchDesignatedVariableName, StringEqualsMethodName), - NameOf(typeMemberAccess), + stringExpression, IdentifierName(StringComparisonFullName) ) ); @@ -63,11 +83,27 @@ private SwitchExpressionArmSyntax BuildArmIgnoreCase(string ignoreCaseSwitchDesi return SwitchArm(pattern, typeMemberAccess).WithWhenClause(whenClause); } - private SwitchExpressionArmSyntax BuildArm(IFieldSymbol field) + private IEnumerable BuildArms() { - // nameof(source.Value1) => source.Value1; - var typeMemberAccess = MemberAccess(FullyQualifiedIdentifier(field.ContainingType), field.Name); - var pattern = ConstantPattern(NameOf(typeMemberAccess)); - return SwitchArm(pattern, typeMemberAccess); + foreach (var field in enumMembers) + { + // source.Value1 + var typeMemberAccess = MemberAccess(field.ContainingType.NonNullable().FullyQualifiedIdentifierName(), field.Name); + if (explicitMappings.TryGetValue(field, out var sourceExpressions)) + { + // add switch arm for each source string + foreach (var sourceExpression in sourceExpressions) + { + yield return BuildArm(typeMemberAccess, sourceExpression); + } + } + else + { + yield return BuildArm(typeMemberAccess, NameOf(typeMemberAccess)); + } + } } + + private static SwitchExpressionArmSyntax BuildArm(MemberAccessExpressionSyntax typeMemberAccess, ExpressionSyntax patternSyntax) => + SwitchArm(ConstantPattern(patternSyntax), typeMemberAccess); } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/Enums/EnumToStringMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/Enums/EnumToStringMapping.cs index 7cbe156048..99dac5e532 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/Enums/EnumToStringMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/Enums/EnumToStringMapping.cs @@ -11,8 +11,12 @@ namespace Riok.Mapperly.Descriptors.Mappings.Enums; /// Uses a switch expression for performance reasons (in comparison to ). /// Only supports defined enum values and no flags. /// -public class EnumToStringMapping(ITypeSymbol sourceType, ITypeSymbol targetType, IEnumerable enumMembers) - : NewInstanceMethodMapping(sourceType, targetType) +public class EnumToStringMapping( + ITypeSymbol sourceType, + ITypeSymbol targetType, + IEnumerable enumMembers, + IReadOnlyDictionary explicitMappings +) : NewInstanceMethodMapping(sourceType, targetType) { private const string ToStringMethodName = nameof(Enum.ToString); @@ -23,6 +27,7 @@ public override IEnumerable BuildBody(TypeMappingBuildContext c // switch for each name to the enum value // eg: Enum1.Value1 => "Value1" + // or: Enum1.Value1 => "value_1" (if explicit mapping exists) var arms = enumMembers.Select(BuildArm).Append(fallbackArm); var switchExpr = ctx.SyntaxFactory.Switch(ctx.Source, arms); yield return ctx.SyntaxFactory.Return(switchExpr); @@ -30,9 +35,18 @@ public override IEnumerable BuildBody(TypeMappingBuildContext c private SwitchExpressionArmSyntax BuildArm(IFieldSymbol field) { + // source.Value1 var typeMemberAccess = MemberAccess(FullyQualifiedIdentifier(field.ContainingType.NonNullable()), field.Name); var pattern = ConstantPattern(typeMemberAccess); + if (explicitMappings.TryGetValue(field, out var expression)) + { + // source.Value1 => "VALUE-1" + return SwitchArm(pattern, expression); + } + + // nameof(source.Value1) var nameOf = NameOf(typeMemberAccess); + // source.Value1 => nameof(source.Value1) return SwitchArm(pattern, nameOf); } } diff --git a/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs b/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs index 56fce1d9a0..0ce5014b1b 100644 --- a/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs +++ b/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs @@ -227,6 +227,12 @@ internal IEnumerable GetAllMethods(ITypeSymbol symbol, string nam internal IEnumerable GetAllFields(ITypeSymbol symbol) => GetAllMembers(symbol).OfType(); + internal Dictionary GetEnumFields(ITypeSymbol symbol) => + GetAllFields(symbol).ToDictionary(f => f.ConstantValue!, f => f); + + internal HashSet GetFieldsExcept(ITypeSymbol symbol, ISet ignoredMembers) => + GetAllFields(symbol).Where(x => !ignoredMembers.Remove(x)).ToHashSet(SymbolTypeEqualityComparer.FieldDefault); + internal IReadOnlyCollection GetAllMembers(ITypeSymbol symbol) { if (_allMembers.TryGetValue(symbol, out var members)) diff --git a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs index bc07bc8307..1143680e95 100644 --- a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs +++ b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs @@ -777,6 +777,16 @@ public static class DiagnosticDescriptors true ); + public static readonly DiagnosticDescriptor StringSourceValueDuplicated = + new( + "RMG084", + "String source value is specified multiple times, a source string value may only be specified once", + "String source value {0} is specified multiple times, a source string value may only be specified once", + DiagnosticCategories.Mapper, + DiagnosticSeverity.Error, + true + ); + private static string BuildHelpUri(string id) { #if ENV_NEXT diff --git a/test/Riok.Mapperly.Tests/Mapping/EnumExplicitMapTest.cs b/test/Riok.Mapperly.Tests/Mapping/EnumToEnumExplicitMapTest.cs similarity index 82% rename from test/Riok.Mapperly.Tests/Mapping/EnumExplicitMapTest.cs rename to test/Riok.Mapperly.Tests/Mapping/EnumToEnumExplicitMapTest.cs index a2f62fc8bb..0ef598b50d 100644 --- a/test/Riok.Mapperly.Tests/Mapping/EnumExplicitMapTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/EnumToEnumExplicitMapTest.cs @@ -2,7 +2,7 @@ namespace Riok.Mapperly.Tests.Mapping; -public class EnumExplicitMapTest +public class EnumToEnumExplicitMapTest { [Fact] public void EnumByNameWithExplicitValue() @@ -100,11 +100,66 @@ public void EnumByNameWithExplicitValueDuplicatedSource() .HaveDiagnostic( DiagnosticDescriptors.EnumSourceValueDuplicated, "Enum source value E2.e is specified multiple times, a source enum value may only be specified once" - ); + ) + .HaveDiagnostic(DiagnosticDescriptors.SourceEnumValueNotMapped, "Enum member d (103) on E2 not found on target enum E1") + .HaveDiagnostics( + DiagnosticDescriptors.TargetEnumValueNotMapped, + "Enum member D (3) on E1 not found on source enum E2", + "Enum member F (6) on E1 not found on source enum E2" + ) + .HaveAssertedAllDiagnostics(); + } + + [Fact] + public void EnumByNameWithExplicitValueSourceTypeMismatch() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapEnumValue(\"A\", E1.A), MapEnum(EnumMappingStrategy.ByName)] public partial E1 ToE1(E2 source);", + "public enum E1 {A}", + "public enum E2 {A}" + ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + return source switch + { + global::E2.A => global::E1.A, + _ => throw new System.ArgumentOutOfRangeException(nameof(source), source, "The value of enum E2 is not supported"), + }; + """ + ) + .HaveDiagnostic(DiagnosticDescriptors.MapValueTypeMismatch, "Cannot assign constant value \"A\" of type string to E2") + .HaveAssertedAllDiagnostics(); } [Fact] public void EnumByNameWithExplicitValueTargetTypeMismatch() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapEnumValue(E2.A, \"A\"), MapEnum(EnumMappingStrategy.ByName)] public partial E1 ToE1(E2 source);", + "public enum E1 {A}", + "public enum E2 {A}" + ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + return source switch + { + global::E2.A => global::E1.A, + _ => throw new System.ArgumentOutOfRangeException(nameof(source), source, "The value of enum E2 is not supported"), + }; + """ + ) + .HaveDiagnostic(DiagnosticDescriptors.MapValueTypeMismatch, "Cannot assign constant value \"A\" of type string to E1") + .HaveAssertedAllDiagnostics(); + } + + [Fact] + public void EnumByNameWithExplicitValueTargetEnumTypeMismatch() { var source = TestSourceBuilder.MapperWithBodyAndTypes( "[MapEnumValue(E2.A, E2.A), MapEnum(EnumMappingStrategy.ByName)] public partial E1 ToE1(E2 source);", @@ -131,7 +186,7 @@ public void EnumByNameWithExplicitValueTargetTypeMismatch() } [Fact] - public void EnumByNameWithExplicitValueSourceTypeMismatch() + public void EnumByNameWithExplicitValueSourceEnumTypeMismatch() { var source = TestSourceBuilder.MapperWithBodyAndTypes( "[MapEnumValue(E1.A, E1.A), MapEnum(EnumMappingStrategy.ByName)] public partial E1 ToE1(E2 source);", diff --git a/test/Riok.Mapperly.Tests/Mapping/EnumToStringExplicitMapTest.cs b/test/Riok.Mapperly.Tests/Mapping/EnumToStringExplicitMapTest.cs new file mode 100644 index 0000000000..9e0a4b485f --- /dev/null +++ b/test/Riok.Mapperly.Tests/Mapping/EnumToStringExplicitMapTest.cs @@ -0,0 +1,127 @@ +using Riok.Mapperly.Diagnostics; + +namespace Riok.Mapperly.Tests.Mapping; + +public class EnumToStringExplicitMapTest +{ + [Fact] + public void EnumToStringWithExplicitValue() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapEnumValue(E.e, \"str-e\")] public partial string ToStr(E source);", + "public enum E {A = 100, B, C, d, e, E, f}" + ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + return source switch + { + global::E.A => nameof(global::E.A), + global::E.B => nameof(global::E.B), + global::E.C => nameof(global::E.C), + global::E.d => nameof(global::E.d), + global::E.e => "str-e", + global::E.E => nameof(global::E.E), + global::E.f => nameof(global::E.f), + _ => source.ToString(), + }; + """ + ) + .HaveAssertedAllDiagnostics(); + } + + [Fact] + public void EnumToStringWithExplicitValueMultipleSourcesToOneString() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapEnumValue(E.e, \"str-e\"), MapEnumValue(E.E, \"str-e\")] public partial string ToStr(E source);", + "public enum E {A = 100, B, C, d, e, E, f}" + ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + return source switch + { + global::E.A => nameof(global::E.A), + global::E.B => nameof(global::E.B), + global::E.C => nameof(global::E.C), + global::E.d => nameof(global::E.d), + global::E.e => "str-e", + global::E.E => "str-e", + global::E.f => nameof(global::E.f), + _ => source.ToString(), + }; + """ + ) + .HaveAssertedAllDiagnostics(); + } + + [Fact] + public void EnumToStringWithExplicitValueDuplicatedSource() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapEnumValue(E.e, \"str1-e\"), MapEnumValue(E.e, \"str2-e\")] public partial string ToStr(E source);", + "public enum E {A = 100, B, C, d, e, E, f}" + ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + return source switch + { + global::E.A => nameof(global::E.A), + global::E.B => nameof(global::E.B), + global::E.C => nameof(global::E.C), + global::E.d => nameof(global::E.d), + global::E.e => "str1-e", + global::E.E => nameof(global::E.E), + global::E.f => nameof(global::E.f), + _ => source.ToString(), + }; + """ + ) + .HaveDiagnostic( + DiagnosticDescriptors.EnumSourceValueDuplicated, + "Enum source value E.e is specified multiple times, a source enum value may only be specified once" + ) + .HaveAssertedAllDiagnostics(); + } + + [Fact] + public void EnumToStringWithExplicitValueSourceEnumTypeMismatch() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapEnumValue(E2.A, \"str-A\"))] public partial string ToStr(E1 source);", + "public enum E1 {A = 100, B, C, d, e, E, f}", + "public enum E2 {A}" + ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + return source switch + { + global::E1.A => nameof(global::E1.A), + global::E1.B => nameof(global::E1.B), + global::E1.C => nameof(global::E1.C), + global::E1.d => nameof(global::E1.d), + global::E1.e => nameof(global::E1.e), + global::E1.E => nameof(global::E1.E), + global::E1.f => nameof(global::E1.f), + _ => source.ToString(), + }; + """ + ) + .HaveDiagnostic( + DiagnosticDescriptors.SourceEnumValueDoesNotMatchSourceEnumType, + "Enum member E2.A (0) on E2 does not match type of source enum E1" + ) + .HaveAssertedAllDiagnostics(); + } +} diff --git a/test/Riok.Mapperly.Tests/Mapping/StringToEnumExplicitMapTest.cs b/test/Riok.Mapperly.Tests/Mapping/StringToEnumExplicitMapTest.cs new file mode 100644 index 0000000000..3887102618 --- /dev/null +++ b/test/Riok.Mapperly.Tests/Mapping/StringToEnumExplicitMapTest.cs @@ -0,0 +1,128 @@ +using Riok.Mapperly.Diagnostics; + +namespace Riok.Mapperly.Tests.Mapping; + +public class StringToEnumExplicitMapTest +{ + [Fact] + public void EnumFromStringWithExplicitValue() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapEnumValue(\"str-e\", E.e)] public partial E FromStr(string source);", + "public enum E {A = 100, B, C, d, e, E, f}" + ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + return source switch + { + nameof(global::E.A) => global::E.A, + nameof(global::E.B) => global::E.B, + nameof(global::E.C) => global::E.C, + nameof(global::E.d) => global::E.d, + "str-e" => global::E.e, + nameof(global::E.E) => global::E.E, + nameof(global::E.f) => global::E.f, + _ => System.Enum.Parse(source, false), + }; + """ + ) + .HaveAssertedAllDiagnostics(); + } + + [Fact] + public void EnumFromStringWithExplicitValueMultipleSourcesToOneEnum() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapEnumValue(\"str-e1\", E.e), MapEnumValue(\"str-e2\", E.e)] public partial E FromStr(string source);", + "public enum E {A = 100, B, C, d, e, E, f}" + ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + return source switch + { + nameof(global::E.A) => global::E.A, + nameof(global::E.B) => global::E.B, + nameof(global::E.C) => global::E.C, + nameof(global::E.d) => global::E.d, + "str-e1" => global::E.e, + "str-e2" => global::E.e, + nameof(global::E.E) => global::E.E, + nameof(global::E.f) => global::E.f, + _ => System.Enum.Parse(source, false), + }; + """ + ) + .HaveAssertedAllDiagnostics(); + } + + [Fact] + public void EnumToStringWithExplicitValueDuplicatedSource() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapEnumValue(\"str-e\", E.e), MapEnumValue(\"str-e\", E.E)] public partial E FromStr(string source);", + "public enum E {A = 100, B, C, d, e, E, f}" + ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + return source switch + { + nameof(global::E.A) => global::E.A, + nameof(global::E.B) => global::E.B, + nameof(global::E.C) => global::E.C, + nameof(global::E.d) => global::E.d, + "str-e" => global::E.e, + nameof(global::E.E) => global::E.E, + nameof(global::E.f) => global::E.f, + _ => System.Enum.Parse(source, false), + }; + """ + ) + .HaveDiagnostic( + DiagnosticDescriptors.StringSourceValueDuplicated, + "String source value \"str-e\" is specified multiple times, a source string value may only be specified once" + ) + .HaveAssertedAllDiagnostics(); + } + + [Fact] + public void EnumToStringWithExplicitValueSourceEnumTypeMismatch() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapEnumValue(\"str-A\", E2.A))] public partial E1 FromStr(string source);", + "public enum E1 {A = 100, B, C, d, e, E, f}", + "public enum E2 {A}" + ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + return source switch + { + nameof(global::E1.A) => global::E1.A, + nameof(global::E1.B) => global::E1.B, + nameof(global::E1.C) => global::E1.C, + nameof(global::E1.d) => global::E1.d, + nameof(global::E1.e) => global::E1.e, + nameof(global::E1.E) => global::E1.E, + nameof(global::E1.f) => global::E1.f, + _ => System.Enum.Parse(source, false), + }; + """ + ) + .HaveDiagnostic( + DiagnosticDescriptors.TargetEnumValueDoesNotMatchTargetEnumType, + "Enum member E2.A (0) on E2 does not match type of target enum E1" + ) + .HaveAssertedAllDiagnostics(); + } +}