diff --git a/docs/docs/configuration/enum.mdx b/docs/docs/configuration/enum.mdx index 20db9a82c3..295165b522 100644 --- a/docs/docs/configuration/enum.mdx +++ b/docs/docs/configuration/enum.mdx @@ -62,15 +62,17 @@ Enum from/to strings mappings can be customized by setting the enum naming strat You can specify the naming strategy using `NamingStrategy` in `MapEnumAttribute` or `EnumNamingStrategy` in `MapperAttribute` and `MapperDefaultsAttribute`. Available naming strategies: -| Name | Description | -| -------------- | ----------------------------------------------- | -| MemberName | Matches enum values using their name. (default) | -| CamelCase | Matches enum values using camelCase. | -| PascalCase | Matches enum values using PascalCase. | -| SnakeCase | Matches enum values using snake_case. | -| UpperSnakeCase | Matches enum values using UPPER_SNAKE_CASE. | -| KebabCase | Matches enum values using kebab-case. | -| UpperKebabCase | Matches enum values using UPPER-KEBAB-CASE. | +| Name | Description | +| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| MemberName | Matches enum values using their name. (default) | +| CamelCase | Matches enum values using camelCase. | +| PascalCase | Matches enum values using PascalCase. | +| SnakeCase | Matches enum values using snake_case. | +| UpperSnakeCase | Matches enum values using UPPER_SNAKE_CASE. | +| KebabCase | Matches enum values using kebab-case. | +| UpperKebabCase | Matches enum values using UPPER-KEBAB-CASE. | +| ComponentModelDescriptionAttribute | Matches enum values using the `Description` property of the `System.ComponentModel.DescriptionAttribute`. If the attribute is not present or the property is null, the member name is used. | +| SerializationEnumMemberAttribute | Matches enum values using the `Value` property of the `System.Runtime.Serialization.EnumMemberAttribute`. If the attribute is not present or the property is null, the member name is used. | Note that explicit enum mappings (`MapEnumValue`) and fallback values (`FallbackValue` in `MapEnum`) are not affected by naming strategies. diff --git a/src/Riok.Mapperly.Abstractions/EnumNamingStrategy.cs b/src/Riok.Mapperly.Abstractions/EnumNamingStrategy.cs index ecbc9711b1..d962805e95 100644 --- a/src/Riok.Mapperly.Abstractions/EnumNamingStrategy.cs +++ b/src/Riok.Mapperly.Abstractions/EnumNamingStrategy.cs @@ -1,3 +1,6 @@ +using System.ComponentModel; +using System.Runtime.Serialization; + namespace Riok.Mapperly.Abstractions; /// @@ -39,4 +42,16 @@ public enum EnumNamingStrategy /// Matches enum values using UPPER-KEBAB-CASE. /// UpperKebabCase, + + /// + /// Matches enum values using + /// or if the attribute is not present on the enum member. + /// + ComponentModelDescriptionAttribute, + + /// + /// Matches enum values using + /// or if the attribute is not present on the enum member. + /// + SerializationEnumMemberAttribute, } diff --git a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs index 664bcc5227..c3004431a7 100644 --- a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs +++ b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs @@ -127,7 +127,7 @@ public class MapperAttribute : Attribute public bool AutoUserMappings { get; set; } = true; /// - /// The default enum naming strategy. + /// Defines the strategy to use when mapping an enum from/to string. /// Can be overwritten on specific enums via mapping method configurations. /// public EnumNamingStrategy EnumNamingStrategy { get; set; } = EnumNamingStrategy.MemberName; diff --git a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt index 01a2c66689..661fac8783 100644 --- a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt +++ b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt @@ -200,3 +200,5 @@ Riok.Mapperly.Abstractions.MapEnumAttribute.NamingStrategy.get -> Riok.Mapperly. Riok.Mapperly.Abstractions.MapEnumAttribute.NamingStrategy.set -> void Riok.Mapperly.Abstractions.MapperAttribute.EnumNamingStrategy.get -> Riok.Mapperly.Abstractions.EnumNamingStrategy Riok.Mapperly.Abstractions.MapperAttribute.EnumNamingStrategy.set -> void +Riok.Mapperly.Abstractions.EnumNamingStrategy.ComponentModelDescriptionAttribute = 7 -> Riok.Mapperly.Abstractions.EnumNamingStrategy +Riok.Mapperly.Abstractions.EnumNamingStrategy.SerializationEnumMemberAttribute = 8 -> Riok.Mapperly.Abstractions.EnumNamingStrategy diff --git a/src/Riok.Mapperly/AnalyzerReleases.Shipped.md b/src/Riok.Mapperly/AnalyzerReleases.Shipped.md index dd7ae0cd63..dfc07857f8 100644 --- a/src/Riok.Mapperly/AnalyzerReleases.Shipped.md +++ b/src/Riok.Mapperly/AnalyzerReleases.Shipped.md @@ -187,7 +187,10 @@ RMG081 | Mapper | Error | A mapping method with additional parameters cann 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 -RMG085 | Mapper | Error | Invalid usage of Fallback value +RMG085 | Mapper | Error | Invalid usage of fallback value +RMG086 | Mapper | Error | The source of the explicit mapping from a string to an enum is not of type string +RMG087 | Mapper | Error | The target of the explicit mapping from an enum to a string is not of type string +RMG088 | Mapper | Info | The attribute to build the name of the enum member is missing ### Removed Rules diff --git a/src/Riok.Mapperly/Configuration/ComponentModelDescriptionAttributeConfiguration.cs b/src/Riok.Mapperly/Configuration/ComponentModelDescriptionAttributeConfiguration.cs new file mode 100644 index 0000000000..da9b94ddb0 --- /dev/null +++ b/src/Riok.Mapperly/Configuration/ComponentModelDescriptionAttributeConfiguration.cs @@ -0,0 +1,10 @@ +namespace Riok.Mapperly.Configuration; + +/// +/// Configuration class to represent +/// +public record ComponentModelDescriptionAttributeConfiguration(string? Description) +{ + public ComponentModelDescriptionAttributeConfiguration() + : this((string?)null) { } +} diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/EnumMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/EnumMappingBuilder.cs new file mode 100644 index 0000000000..63b4f648c4 --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/EnumMappingBuilder.cs @@ -0,0 +1,64 @@ +using System.ComponentModel; +using System.Runtime.Serialization; +using Microsoft.CodeAnalysis; +using Riok.Mapperly.Abstractions; +using Riok.Mapperly.Configuration; +using Riok.Mapperly.Diagnostics; +using Riok.Mapperly.Helpers; + +namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; + +public static class EnumMappingBuilder +{ + internal static string GetMemberName(MappingBuilderContext ctx, IFieldSymbol field) => + GetMemberName(ctx, field, ctx.Configuration.Enum.NamingStrategy); + + private static string GetMemberName(MappingBuilderContext ctx, IFieldSymbol field, EnumNamingStrategy namingStrategy) + { + return namingStrategy switch + { + EnumNamingStrategy.MemberName => field.Name, + EnumNamingStrategy.CamelCase => field.Name.ToCamelCase(), + EnumNamingStrategy.PascalCase => field.Name.ToPascalCase(), + EnumNamingStrategy.SnakeCase => field.Name.ToSnakeCase(), + EnumNamingStrategy.UpperSnakeCase => field.Name.ToUpperSnakeCase(), + EnumNamingStrategy.KebabCase => field.Name.ToKebabCase(), + EnumNamingStrategy.UpperKebabCase => field.Name.ToUpperKebabCase(), + EnumNamingStrategy.ComponentModelDescriptionAttribute => GetComponentModelDescription(ctx, field), + EnumNamingStrategy.SerializationEnumMemberAttribute => GetEnumMemberValue(ctx, field), + _ => throw new ArgumentOutOfRangeException($"{nameof(namingStrategy)} has an unknown value {namingStrategy}"), + }; + } + + private static string GetEnumMemberValue(MappingBuilderContext ctx, IFieldSymbol field) + { + var name = ctx.AttributeAccessor.AccessFirstOrDefault(field)?.Value; + if (name != null) + return name; + + ctx.ReportDiagnostic( + DiagnosticDescriptors.EnumNamingAttributeMissing, + nameof(EnumMemberAttribute), + field.Name, + field.ConstantValue ?? "" + ); + return GetMemberName(ctx, field, EnumNamingStrategy.MemberName); + } + + private static string GetComponentModelDescription(MappingBuilderContext ctx, IFieldSymbol field) + { + var name = ctx + .AttributeAccessor.AccessFirstOrDefault(field) + ?.Description; + if (name != null) + return name; + + ctx.ReportDiagnostic( + DiagnosticDescriptors.EnumNamingAttributeMissing, + nameof(DescriptionAttribute), + field.Name, + field.ConstantValue ?? "" + ); + return GetMemberName(ctx, field, EnumNamingStrategy.MemberName); + } +} diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumToEnumMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumToEnumMappingBuilder.cs index a532739fe4..92e6002e6e 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumToEnumMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumToEnumMappingBuilder.cs @@ -184,7 +184,19 @@ private static EnumFallbackValueMapping BuildFallbackMapping(MappingBuilderConte if (fallbackValue is not { Expression: MemberAccessExpressionSyntax memberAccessExpression }) { - ctx.ReportDiagnostic(DiagnosticDescriptors.InvalidFallbackValue, fallbackValue.Value.Expression.ToFullString()); + ctx.ReportDiagnostic(DiagnosticDescriptors.InvalidEnumMappingFallbackValue, fallbackValue.Value.Expression.ToFullString()); + return new EnumFallbackValueMapping(ctx.Source, ctx.Target); + } + + if (!SymbolEqualityComparer.Default.Equals(ctx.Target, fallbackValue.Value.ConstantValue.Type)) + { + ctx.ReportDiagnostic( + DiagnosticDescriptors.EnumFallbackValueTypeDoesNotMatchTargetEnumType, + fallbackValue, + fallbackValue.Value.ConstantValue.Value ?? 0, + fallbackValue.Value.ConstantValue.Type?.Name ?? "unknown", + ctx.Target + ); return new EnumFallbackValueMapping(ctx.Source, ctx.Target); } @@ -193,25 +205,14 @@ private static EnumFallbackValueMapping BuildFallbackMapping(MappingBuilderConte FullyQualifiedIdentifier(ctx.Target), memberAccessExpression.Name ); - - if (SymbolEqualityComparer.Default.Equals(ctx.Target, fallbackValue.Value.ConstantValue.Type)) - return new EnumFallbackValueMapping(ctx.Source, ctx.Target, fallbackExpression: fallbackExpression); - - ctx.ReportDiagnostic( - DiagnosticDescriptors.EnumFallbackValueTypeDoesNotMatchTargetEnumType, - fallbackValue, - fallbackValue.Value.ConstantValue.Value ?? 0, - fallbackValue.Value.ConstantValue.Type?.Name ?? "unknown", - ctx.Target - ); - return new EnumFallbackValueMapping(ctx.Source, ctx.Target); + return new EnumFallbackValueMapping(ctx.Source, ctx.Target, fallbackExpression: fallbackExpression); } private static IReadOnlyDictionary BuildExplicitValueMappings(MappingBuilderContext ctx) { var explicitMappings = new Dictionary(SymbolEqualityComparer.Default); - var sourceFields = ctx.SymbolAccessor.GetEnumFields(ctx.Source); - var targetFields = ctx.SymbolAccessor.GetEnumFields(ctx.Target); + var sourceFields = ctx.SymbolAccessor.GetEnumFieldsByValue(ctx.Source); + var targetFields = ctx.SymbolAccessor.GetEnumFieldsByValue(ctx.Target); foreach (var (source, target) in ctx.Configuration.Enum.ExplicitMappings) { if (source.ConstantValue.Kind is not TypedConstantKind.Enum) diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumToStringMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumToStringMappingBuilder.cs index 21940abaf2..6fbe7870c3 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumToStringMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumToStringMappingBuilder.cs @@ -1,5 +1,4 @@ using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; using Riok.Mapperly.Abstractions; using Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; using Riok.Mapperly.Descriptors.Mappings; @@ -29,21 +28,11 @@ private static EnumToStringMapping BuildEnumToStringMapping(MappingBuilderContex { var fallbackMapping = BuildFallbackMapping(ctx, out var fallbackStringValue); var enumMemberMappings = BuildEnumMemberMappings(ctx, fallbackStringValue); - - if (fallbackStringValue is not null) - { - enumMemberMappings = enumMemberMappings.Where(m => - !m.TargetSyntax.ToString().Equals(fallbackStringValue, StringComparison.Ordinal) - ); - } - return new EnumToStringMapping(ctx.Source, ctx.Target, enumMemberMappings, fallbackMapping); } private static IEnumerable BuildEnumMemberMappings(MappingBuilderContext ctx, string? fallbackStringValue) { - var namingStrategy = ctx.Configuration.Enum.NamingStrategy; - var ignoredSourceMembers = ctx.Configuration.Enum.IgnoredSourceMembers.ToHashSet(SymbolTypeEqualityComparer.FieldDefault); EnumMappingDiagnosticReporter.AddUnmatchedSourceIgnoredMembers(ctx, ignoredSourceMembers); @@ -54,37 +43,45 @@ private static IEnumerable BuildEnumMemberMappings(MappingBui { // source.Value1 var sourceSyntax = MemberAccess(FullyQualifiedIdentifier(ctx.Source), sourceField.Name); - - var name = sourceField.GetName(namingStrategy); - if (string.Equals(name, fallbackStringValue, StringComparison.Ordinal)) - continue; - - if (explicitValueMappings.TryGetValue(sourceField, out var explicitMapping)) + if (explicitValueMappings.TryGetValue(sourceField, out var memberName)) { + if (string.Equals(fallbackStringValue, memberName, StringComparison.Ordinal)) + continue; + // "explicit_value1" - yield return new EnumMemberMapping(sourceSyntax, explicitMapping); + yield return new EnumMemberMapping(sourceSyntax, StringLiteral(memberName)); continue; } - if (namingStrategy is not EnumNamingStrategy.MemberName) + var name = EnumMappingBuilder.GetMemberName(ctx, sourceField); + if (string.Equals(name, fallbackStringValue, StringComparison.Ordinal)) + continue; + + if (ctx.Configuration.Enum.NamingStrategy == EnumNamingStrategy.MemberName) { - // "value1" - yield return new EnumMemberMapping(sourceSyntax, StringLiteral(name)); + // nameof(source.Value1) + yield return new EnumMemberMapping(sourceSyntax, NameOf(sourceSyntax)); continue; } - // nameof(source.Value1) - yield return new EnumMemberMapping(sourceSyntax, NameOf(sourceSyntax)); + // "value1" + yield return new EnumMemberMapping(sourceSyntax, StringLiteral(name)); } } - private static IReadOnlyDictionary BuildExplicitValueMappings(MappingBuilderContext ctx) + private static IReadOnlyDictionary BuildExplicitValueMappings(MappingBuilderContext ctx) { - var explicitMappings = new Dictionary(SymbolEqualityComparer.Default); - var sourceFields = ctx.SymbolAccessor.GetEnumFields(ctx.Source); + var explicitMappings = new Dictionary(SymbolEqualityComparer.Default); + if (!ctx.Configuration.Enum.HasExplicitConfigurations) + return explicitMappings; + + var sourceFields = ctx.SymbolAccessor.GetEnumFieldsByValue(ctx.Source); foreach (var (source, target) in ctx.Configuration.Enum.ExplicitMappings) { - if (!SymbolEqualityComparer.Default.Equals(source.ConstantValue.Type, ctx.Source)) + if ( + !SymbolEqualityComparer.Default.Equals(source.ConstantValue.Type, ctx.Source) + || !sourceFields.TryGetValue(source.ConstantValue.Value!, out var sourceField) + ) { ctx.ReportDiagnostic( DiagnosticDescriptors.SourceEnumValueDoesNotMatchSourceEnumType, @@ -96,12 +93,13 @@ private static IReadOnlyDictionary BuildExplicit continue; } - if (!sourceFields.TryGetValue(source.ConstantValue.Value!, out var sourceField)) + if (target.ConstantValue.Value is not string targetStringValue) { + ctx.ReportDiagnostic(DiagnosticDescriptors.EnumExplicitMappingTargetNotString); continue; } - if (!explicitMappings.TryAdd(sourceField, target.Expression)) + if (!explicitMappings.TryAdd(sourceField, targetStringValue)) { ctx.ReportDiagnostic(DiagnosticDescriptors.EnumSourceValueDuplicated, sourceField, ctx.Source, ctx.Target); } @@ -121,7 +119,7 @@ private static EnumFallbackValueMapping BuildFallbackMapping(MappingBuilderConte if (fallbackValue.Value.ConstantValue.Value is not string fallbackString) { - ctx.ReportDiagnostic(DiagnosticDescriptors.InvalidFallbackValue, fallbackValue.Value.Expression.ToFullString()); + ctx.ReportDiagnostic(DiagnosticDescriptors.InvalidEnumMappingFallbackValue, fallbackValue.Value.Expression.ToFullString()); return new EnumFallbackValueMapping(ctx.Source, ctx.Target, new ToStringMapping(ctx.Source, ctx.Target)); } diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/StringToEnumMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/StringToEnumMappingBuilder.cs index 6956ebd5b8..0aa5ebd794 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/StringToEnumMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/StringToEnumMappingBuilder.cs @@ -46,19 +46,11 @@ private static EnumFromStringSwitchMapping BuildEnumFromStringSwitchMapping( bool genericEnumParseMethodSupported ) { - var fallbackMapping = BuildFallbackParseMapping(ctx, genericEnumParseMethodSupported, out var fallbackMember); - var enumMemberMappings = BuildEnumMemberMappings(ctx, fallbackMember); // 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. - if (fallbackMember is not null) - { - // no need to explicitly map fallback value - enumMemberMappings = enumMemberMappings.Where(m => - !m.TargetSyntax.ToString().Equals(fallbackMember.Name, StringComparison.Ordinal) - ); - } - + var fallbackMapping = BuildFallbackParseMapping(ctx, genericEnumParseMethodSupported, out var fallbackMember); + var enumMemberMappings = BuildEnumMemberMappings(ctx, fallbackMember); return new EnumFromStringSwitchMapping( ctx.Source, ctx.Target, @@ -70,8 +62,6 @@ bool genericEnumParseMethodSupported private static IEnumerable BuildEnumMemberMappings(MappingBuilderContext ctx, IFieldSymbol? fallbackMember) { - var namingStrategy = ctx.Configuration.Enum.NamingStrategy; - var ignoredTargetMembers = ctx.Configuration.Enum.IgnoredTargetMembers.ToHashSet(SymbolTypeEqualityComparer.FieldDefault); EnumMappingDiagnosticReporter.AddUnmatchedTargetIgnoredMembers(ctx, ignoredTargetMembers); if (fallbackMember is not null) @@ -81,23 +71,37 @@ private static IEnumerable BuildEnumMemberMappings(MappingBui var targetFields = ctx.SymbolAccessor.GetFieldsExcept(ctx.Target, ignoredTargetMembers); var explicitValueMappings = BuildExplicitValueMappings(ctx); + var processedSources = new HashSet(); foreach (var targetField in targetFields) { // source.Value1 var targetSyntax = MemberAccess(FullyQualifiedIdentifier(ctx.Target), targetField.Name); - if (explicitValueMappings.TryGetValue(targetField, out var explicitMappings)) + if (explicitValueMappings.TryGetValue(targetField, out var sourceNames)) { - foreach (var explicitMapping in explicitMappings) + foreach (var sourceName in sourceNames) { + if (!processedSources.Add(sourceName)) + { + ctx.ReportDiagnostic(DiagnosticDescriptors.EnumStringSourceValueDuplicated, sourceName, ctx.Source, ctx.Target); + continue; + } + // "explicit_value1" => source.Value1 - yield return new EnumMemberMapping(explicitMapping, targetSyntax); + yield return new EnumMemberMapping(StringLiteral(sourceName), targetSyntax); } continue; } - if (namingStrategy is EnumNamingStrategy.MemberName) + var name = EnumMappingBuilder.GetMemberName(ctx, targetField); + if (!processedSources.Add(name)) + { + ctx.ReportDiagnostic(DiagnosticDescriptors.EnumStringSourceValueDuplicated, name, ctx.Source, ctx.Target); + continue; + } + + if (ctx.Configuration.Enum.NamingStrategy == EnumNamingStrategy.MemberName) { // nameof(source.Value1) yield return new EnumMemberMapping(NameOf(targetSyntax), targetSyntax); @@ -105,7 +109,6 @@ private static IEnumerable BuildEnumMemberMappings(MappingBui } // "value1" - var name = targetField.GetName(namingStrategy); yield return new EnumMemberMapping(StringLiteral(name), targetSyntax); } } @@ -129,7 +132,23 @@ out IFieldSymbol? fallbackMember if (fallbackValue is not { Expression: MemberAccessExpressionSyntax memberAccessExpression }) { - ctx.ReportDiagnostic(DiagnosticDescriptors.InvalidFallbackValue, fallbackValue.Value.Expression.ToFullString()); + ctx.ReportDiagnostic(DiagnosticDescriptors.InvalidEnumMappingFallbackValue, fallbackValue.Value.Expression.ToFullString()); + return new EnumFallbackValueMapping( + ctx.Source, + ctx.Target, + new EnumFromStringParseMapping(ctx.Source, ctx.Target, genericEnumParseMethodSupported, ctx.Configuration.Enum.IgnoreCase) + ); + } + + if (!SymbolEqualityComparer.Default.Equals(ctx.Target, fallbackValue.Value.ConstantValue.Type)) + { + ctx.ReportDiagnostic( + DiagnosticDescriptors.EnumFallbackValueTypeDoesNotMatchTargetEnumType, + fallbackValue, + fallbackValue.Value.ConstantValue.Value ?? 0, + fallbackValue.Value.ConstantValue.Type?.Name ?? "unknown", + ctx.Target + ); return new EnumFallbackValueMapping( ctx.Source, ctx.Target, @@ -142,35 +161,20 @@ out IFieldSymbol? fallbackMember FullyQualifiedIdentifier(ctx.Target), memberAccessExpression.Name ); - - if (SymbolEqualityComparer.Default.Equals(ctx.Target, fallbackValue.Value.ConstantValue.Type)) - { - fallbackMember = ctx.SymbolAccessor.GetField(ctx.Target, memberAccessExpression.Name.Identifier.Text); - return new EnumFallbackValueMapping(ctx.Source, ctx.Target, fallbackExpression: fallbackExpression); - } - - ctx.ReportDiagnostic( - DiagnosticDescriptors.EnumFallbackValueTypeDoesNotMatchTargetEnumType, - fallbackValue, - fallbackValue.Value.ConstantValue.Value ?? 0, - fallbackValue.Value.ConstantValue.Type?.Name ?? "unknown", - ctx.Target - ); - return new EnumFallbackValueMapping( - ctx.Source, - ctx.Target, - new EnumFromStringParseMapping(ctx.Source, ctx.Target, genericEnumParseMethodSupported, ctx.Configuration.Enum.IgnoreCase) - ); + fallbackMember = ctx.SymbolAccessor.GetField(ctx.Target, memberAccessExpression.Name.Identifier.Text); + return new EnumFallbackValueMapping(ctx.Source, ctx.Target, fallbackExpression: fallbackExpression); } - private static IReadOnlyDictionary> BuildExplicitValueMappings(MappingBuilderContext ctx) + private static IReadOnlyDictionary> BuildExplicitValueMappings(MappingBuilderContext ctx) { - var explicitMappings = new Dictionary>(SymbolTypeEqualityComparer.FieldDefault); - var checkedSources = new HashSet(); - var targetFields = ctx.SymbolAccessor.GetEnumFields(ctx.Target); + var explicitMappings = new Dictionary>(SymbolTypeEqualityComparer.FieldDefault); + var targetFields = ctx.SymbolAccessor.GetEnumFieldsByValue(ctx.Target); foreach (var (source, target) in ctx.Configuration.Enum.ExplicitMappings) { - if (!SymbolEqualityComparer.Default.Equals(target.ConstantValue.Type, ctx.Target)) + if ( + !SymbolEqualityComparer.Default.Equals(target.ConstantValue.Type, ctx.Target) + || !targetFields.TryGetValue(target.ConstantValue.Value!, out var targetField) + ) { ctx.ReportDiagnostic( DiagnosticDescriptors.TargetEnumValueDoesNotMatchTargetEnumType, @@ -182,29 +186,19 @@ private static IReadOnlyDictionary> Buil continue; } - if (!targetFields.TryGetValue(target.ConstantValue.Value!, out var targetField)) + if (source.ConstantValue.Value is not string sourceString) { - continue; - } - - if (!checkedSources.Add(source.ConstantValue.Value)) - { - ctx.ReportDiagnostic( - DiagnosticDescriptors.StringSourceValueDuplicated, - source.Expression.ToFullString(), - ctx.Source, - ctx.Target - ); + ctx.ReportDiagnostic(DiagnosticDescriptors.EnumExplicitMappingSourceNotString); continue; } if (explicitMappings.TryGetValue(targetField, out var sources)) { - sources.Add(source.Expression); + sources.Add(sourceString); } else { - explicitMappings.Add(targetField, [source.Expression]); + explicitMappings.Add(targetField, [sourceString]); } } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/Enums/EnumFallbackValueMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/Enums/EnumFallbackValueMapping.cs index 659a1cf654..5c2efc947b 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/Enums/EnumFallbackValueMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/Enums/EnumFallbackValueMapping.cs @@ -25,9 +25,9 @@ public override ExpressionSyntax Build(TypeMappingBuildContext ctx) if (fallbackMapping is not null) return fallbackMapping.Build(ctx); - if (FallbackExpression is null) - return ThrowArgumentOutOfRangeException(ctx.Source, $"The value of enum {SourceType.Name} is not supported"); + if (FallbackExpression is not null) + return FallbackExpression; - return FallbackExpression; + return ThrowArgumentOutOfRangeException(ctx.Source, $"The value of enum {SourceType.Name} is not supported"); } } diff --git a/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs b/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs index 8a00d6c352..159b2775a9 100644 --- a/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs +++ b/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs @@ -227,11 +227,11 @@ internal IEnumerable GetAllMethods(ITypeSymbol symbol, string nam internal IEnumerable GetAllFields(ITypeSymbol symbol) => GetAllMembers(symbol).OfType(); - internal IFieldSymbol? GetField(ITypeSymbol symbol, string name) => - GetAllFields(symbol).FirstOrDefault(t => string.Equals(t.Name, name, StringComparison.Ordinal)); + internal Dictionary GetEnumFieldsByValue(ITypeSymbol symbol) => + GetAllFields(symbol).GroupBy(x => x.ConstantValue!).ToDictionary(f => f.Key, f => f.First()); - internal Dictionary GetEnumFields(ITypeSymbol symbol) => - GetAllFields(symbol).ToDictionary(f => f.ConstantValue!, f => f); + public IFieldSymbol? GetField(ITypeSymbol symbol, string name) => + GetAllFields(symbol).FirstOrDefault(f => string.Equals(f.Name, name, StringComparison.Ordinal)); internal HashSet GetFieldsExcept(ITypeSymbol symbol, ISet ignoredMembers) => GetAllFields(symbol).Where(x => !ignoredMembers.Remove(x)).ToHashSet(SymbolTypeEqualityComparer.FieldDefault); diff --git a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs index d3c5251ae3..2c2b571ff1 100644 --- a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs +++ b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs @@ -777,7 +777,7 @@ public static class DiagnosticDescriptors true ); - public static readonly DiagnosticDescriptor StringSourceValueDuplicated = + public static readonly DiagnosticDescriptor EnumStringSourceValueDuplicated = new( "RMG084", "String source value is specified multiple times, a source string value may only be specified once", @@ -787,16 +787,46 @@ public static class DiagnosticDescriptors true ); - public static readonly DiagnosticDescriptor InvalidFallbackValue = + public static readonly DiagnosticDescriptor InvalidEnumMappingFallbackValue = new( "RMG085", - "Invalid usage of Fallback value", + "Invalid usage of fallback value", "Fallback value '{0}' is invalid in the this mapping", DiagnosticCategories.Mapper, DiagnosticSeverity.Error, true ); + public static readonly DiagnosticDescriptor EnumExplicitMappingSourceNotString = + new( + "RMG086", + "The source of the explicit mapping from a string to an enum is not of type string", + "The source of the explicit mapping from a string to an enum is not of type string", + DiagnosticCategories.Mapper, + DiagnosticSeverity.Error, + true + ); + + public static readonly DiagnosticDescriptor EnumExplicitMappingTargetNotString = + new( + "RMG087", + "The target of the explicit mapping from an enum to a string is not of type string", + "The target of the explicit mapping from an enum to a string is not of type string", + DiagnosticCategories.Mapper, + DiagnosticSeverity.Error, + true + ); + + public static readonly DiagnosticDescriptor EnumNamingAttributeMissing = + new( + "RMG088", + "The attribute to build the name of the enum member is missing", + "The {0} to build the name of the enum member {1} ({2}) is missing", + DiagnosticCategories.Mapper, + DiagnosticSeverity.Info, + true + ); + private static string BuildHelpUri(string id) { #if ENV_NEXT diff --git a/src/Riok.Mapperly/Helpers/EnumNamingStrategyHelper.cs b/src/Riok.Mapperly/Helpers/EnumNamingStrategyHelper.cs index b4fd5aa4ff..e69de29bb2 100644 --- a/src/Riok.Mapperly/Helpers/EnumNamingStrategyHelper.cs +++ b/src/Riok.Mapperly/Helpers/EnumNamingStrategyHelper.cs @@ -1,20 +0,0 @@ -using Microsoft.CodeAnalysis; -using Riok.Mapperly.Abstractions; - -namespace Riok.Mapperly.Helpers; - -public static class EnumNamingStrategyHelper -{ - public static string GetName(this IFieldSymbol field, EnumNamingStrategy namingStrategy) => - namingStrategy switch - { - EnumNamingStrategy.MemberName => field.Name, - EnumNamingStrategy.CamelCase => field.Name.ToCamelCase(), - EnumNamingStrategy.PascalCase => field.Name.ToPascalCase(), - EnumNamingStrategy.SnakeCase => field.Name.ToSnakeCase(), - EnumNamingStrategy.UpperSnakeCase => field.Name.ToUpperSnakeCase(), - EnumNamingStrategy.KebabCase => field.Name.ToKebabCase(), - EnumNamingStrategy.UpperKebabCase => field.Name.ToUpperKebabCase(), - _ => throw new ArgumentOutOfRangeException($"{nameof(namingStrategy)} has an unknown value {namingStrategy}"), - }; -} diff --git a/test/Riok.Mapperly.Tests/Mapping/EnumNamingStrategyTest.cs b/test/Riok.Mapperly.Tests/Mapping/EnumNamingStrategyTest.cs index b47f4dd99d..1e789d2e0b 100644 --- a/test/Riok.Mapperly.Tests/Mapping/EnumNamingStrategyTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/EnumNamingStrategyTest.cs @@ -1,3 +1,5 @@ +using Riok.Mapperly.Diagnostics; + namespace Riok.Mapperly.Tests.Mapping; public class EnumNamingStrategyTest @@ -10,7 +12,7 @@ public void EnumToStringWithMemberNameNamingStrategy() "public enum E {Abc, BcD, C1D, DEf, EFG, FG1, Gh1, Hi_J}" ); TestHelper - .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .GenerateMapper(source) .Should() .HaveSingleMethodBody( """ @@ -38,7 +40,7 @@ public void EnumToStringWithCamelCaseNamingStrategy() "public enum E {Abc, BcD, C1D, DEf, EFG, FG1, Gh1, Hi_J}" ); TestHelper - .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .GenerateMapper(source) .Should() .HaveSingleMethodBody( """ @@ -66,7 +68,7 @@ public void EnumToStringWithPascalCaseNamingStrategy() "public enum E {Abc, BcD, C1D, DEf, EFG, FG1, Gh1, Hi_J}" ); TestHelper - .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .GenerateMapper(source) .Should() .HaveSingleMethodBody( """ @@ -94,7 +96,7 @@ public void EnumToStringWithSnakeCaseNamingStrategy() "public enum E {Abc, BcD, C1D, DEf, EFG, FG1, Gh1, Hi_J}" ); TestHelper - .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .GenerateMapper(source) .Should() .HaveSingleMethodBody( """ @@ -122,7 +124,7 @@ public void EnumToStringWithUpperSnakeCaseNamingStrategy() "public enum E {Abc, BcD, C1D, DEf, EFG, FG1, Gh1, Hi_J}" ); TestHelper - .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .GenerateMapper(source) .Should() .HaveSingleMethodBody( """ @@ -150,7 +152,7 @@ public void EnumToStringWithKebabCaseNamingStrategy() "public enum E {Abc, BcD, C1D, DEf, EFG, FG1, Gh1, Hi_J}" ); TestHelper - .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .GenerateMapper(source) .Should() .HaveSingleMethodBody( """ @@ -178,7 +180,7 @@ public void EnumToStringWithUpperKebabCaseNamingStrategy() "public enum E {Abc, BcD, C1D, DEf, EFG, FG1, Gh1, Hi_J}" ); TestHelper - .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .GenerateMapper(source) .Should() .HaveSingleMethodBody( """ @@ -198,6 +200,85 @@ public void EnumToStringWithUpperKebabCaseNamingStrategy() ); } + [Fact] + public void EnumToStringWithComponentModelDescriptionAttributeNamingStrategy() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapEnum(EnumMappingStrategy.ByName, NamingStrategy = EnumNamingStrategy.ComponentModelDescriptionAttribute)] public partial string ToStr(E source);", + """ + public enum E + { + [System.ComponentModel.Description("A1")] A, + [System.ComponentModel.Description("")] B, + [System.ComponentModel.Description] C, + D, + } + """ + ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + return source switch + { + global::E.A => "A1", + global::E.B => "", + global::E.C => "C", + global::E.D => "D", + _ => source.ToString(), + }; + """ + ) + .HaveDiagnostics( + DiagnosticDescriptors.EnumNamingAttributeMissing, + "The DescriptionAttribute to build the name of the enum member C (2) is missing", + "The DescriptionAttribute to build the name of the enum member D (3) is missing" + ) + .HaveAssertedAllDiagnostics(); + } + + [Fact] + public void EnumToStringWithSerializationEnumMemberAttributeNamingStrategy() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapEnum(EnumMappingStrategy.ByName, NamingStrategy = EnumNamingStrategy.SerializationEnumMemberAttribute)] public partial string ToStr(E source);", + """ + public enum E + { + [System.Runtime.Serialization.EnumMember(Value = "A1")] A, + [System.Runtime.Serialization.EnumMember(Value = "")] B, + [System.Runtime.Serialization.EnumMember(Value = null)] C, + [System.Runtime.Serialization.EnumMember)] D, + E, + } + """ + ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + return source switch + { + global::E.A => "A1", + global::E.B => "", + global::E.C => "C", + global::E.D => "D", + global::E.E => "E", + _ => source.ToString(), + }; + """ + ) + .HaveDiagnostics( + DiagnosticDescriptors.EnumNamingAttributeMissing, + "The EnumMemberAttribute to build the name of the enum member C (2) is missing", + "The EnumMemberAttribute to build the name of the enum member D (3) is missing", + "The EnumMemberAttribute to build the name of the enum member E (4) is missing" + ) + .HaveAssertedAllDiagnostics(); + } + [Fact] public void EnumFromStringWithMemberNameNamingStrategy() { @@ -206,7 +287,7 @@ public void EnumFromStringWithMemberNameNamingStrategy() "public enum E {Abc, BcD, C1D, DEf, EFG, FG1, Gh1, Hi_J}" ); TestHelper - .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .GenerateMapper(source) .Should() .HaveSingleMethodBody( """ @@ -234,7 +315,7 @@ public void EnumFromStringWithCamelCaseNamingStrategy() "public enum E {Abc, BcD, C1D, DEf, EFG, FG1, Gh1, Hi_J}" ); TestHelper - .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .GenerateMapper(source) .Should() .HaveSingleMethodBody( """ @@ -262,7 +343,7 @@ public void EnumFromStringWithPascalCaseNamingStrategy() "public enum E {Abc, BcD, C1D, DEf, EFG, FG1, Gh1, Hi_J}" ); TestHelper - .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .GenerateMapper(source) .Should() .HaveSingleMethodBody( """ @@ -290,7 +371,7 @@ public void EnumFromStringWithSnakeCaseNamingStrategy() "public enum E {Abc, BcD, C1D, DEf, EFG, FG1, Gh1, Hi_J}" ); TestHelper - .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .GenerateMapper(source) .Should() .HaveSingleMethodBody( """ @@ -318,7 +399,7 @@ public void EnumFromStringWithUpperSnakeCaseNamingStrategy() "public enum E {Abc, BcD, C1D, DEf, EFG, FG1, Gh1, Hi_J}" ); TestHelper - .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .GenerateMapper(source) .Should() .HaveSingleMethodBody( """ @@ -346,7 +427,7 @@ public void EnumFromStringWithKebabCaseNamingStrategy() "public enum E {Abc, BcD, C1D, DEf, EFG, FG1, Gh1, Hi_J}" ); TestHelper - .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .GenerateMapper(source) .Should() .HaveSingleMethodBody( """ @@ -374,7 +455,7 @@ public void EnumFromStringWithUpperKebabCaseNamingStrategy() "public enum E {Abc, BcD, C1D, DEf, EFG, FG1, Gh1, Hi_J}" ); TestHelper - .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .GenerateMapper(source) .Should() .HaveSingleMethodBody( """ @@ -394,6 +475,85 @@ public void EnumFromStringWithUpperKebabCaseNamingStrategy() ); } + [Fact] + public void EnumFromStringWithComponentModelDescriptionAttributeNamingStrategy() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapEnum(EnumMappingStrategy.ByName, NamingStrategy = EnumNamingStrategy.ComponentModelDescriptionAttribute)] public partial E ToEnum(string source);", + """ + public enum E + { + [System.ComponentModel.Description("A1")] A, + [System.ComponentModel.Description("")] B, + [System.ComponentModel.Description] C, + D, + } + """ + ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + return source switch + { + "A1" => global::E.A, + "" => global::E.B, + "C" => global::E.C, + "D" => global::E.D, + _ => System.Enum.Parse(source, false), + }; + """ + ) + .HaveDiagnostics( + DiagnosticDescriptors.EnumNamingAttributeMissing, + "The DescriptionAttribute to build the name of the enum member C (2) is missing", + "The DescriptionAttribute to build the name of the enum member D (3) is missing" + ) + .HaveAssertedAllDiagnostics(); + } + + [Fact] + public void EnumFromStringWithSerializationEnumMemberAttributeNamingStrategy() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapEnum(EnumMappingStrategy.ByName, NamingStrategy = EnumNamingStrategy.SerializationEnumMemberAttribute)] public partial E ToEnum(string source);", + """ + public enum E + { + [System.Runtime.Serialization.EnumMember(Value = "A1")] A, + [System.Runtime.Serialization.EnumMember(Value = "")] B, + [System.Runtime.Serialization.EnumMember(Value = null)] C, + [System.Runtime.Serialization.EnumMember)] D, + E, + } + """ + ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + return source switch + { + "A1" => global::E.A, + "" => global::E.B, + "C" => global::E.C, + "D" => global::E.D, + "E" => global::E.E, + _ => System.Enum.Parse(source, false), + }; + """ + ) + .HaveDiagnostics( + DiagnosticDescriptors.EnumNamingAttributeMissing, + "The EnumMemberAttribute to build the name of the enum member C (2) is missing", + "The EnumMemberAttribute to build the name of the enum member D (3) is missing", + "The EnumMemberAttribute to build the name of the enum member E (4) is missing" + ) + .HaveAssertedAllDiagnostics(); + } + [Fact] public void EnumToStringWithFallbackValue() { @@ -402,7 +562,7 @@ public void EnumToStringWithFallbackValue() "public enum E {Abc, BcD, C1D, DEf, EFG, FG1, Gh1, Hi_J}" ); TestHelper - .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .GenerateMapper(source) .Should() .HaveSingleMethodBody( """ @@ -430,7 +590,7 @@ public void EnumFromStringWithFallbackValue() "public enum E {Abc, BcD, C1D, DEf, EFG, FG1, Gh1, Hi_J}" ); TestHelper - .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .GenerateMapper(source) .Should() .HaveSingleMethodBody( """ @@ -448,4 +608,42 @@ public void EnumFromStringWithFallbackValue() """ ); } + + [Fact] + public void EnumFromStringWithAttributeNamingStrategyAndDuplicatedName() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapEnum(EnumMappingStrategy.ByName, NamingStrategy = EnumNamingStrategy.ComponentModelDescriptionAttribute)] public partial E ToEnum(string source);", + """ + public enum E + { + [System.ComponentModel.Description("A1")] A, + [System.ComponentModel.Description("A1")] B, + C, + } + """ + ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + return source switch + { + "A1" => global::E.A, + "C" => global::E.C, + _ => System.Enum.Parse(source, false), + }; + """ + ) + .HaveDiagnostic( + DiagnosticDescriptors.EnumStringSourceValueDuplicated, + "String source value A1 is specified multiple times, a source string value may only be specified once" + ) + .HaveDiagnostic( + DiagnosticDescriptors.EnumNamingAttributeMissing, + "The DescriptionAttribute to build the name of the enum member C (2) is missing" + ) + .HaveAssertedAllDiagnostics(); + } } diff --git a/test/Riok.Mapperly.Tests/Mapping/EnumToStringExplicitMapTest.cs b/test/Riok.Mapperly.Tests/Mapping/EnumToStringExplicitMapTest.cs index 9e0a4b485f..179a0a99ca 100644 --- a/test/Riok.Mapperly.Tests/Mapping/EnumToStringExplicitMapTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/EnumToStringExplicitMapTest.cs @@ -124,4 +124,37 @@ public void EnumToStringWithExplicitValueSourceEnumTypeMismatch() ) .HaveAssertedAllDiagnostics(); } + + [Fact] + public void EnumToStringWithExplicitValueTargetTypeMismatch() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapEnumValue(E1.A, E1.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.EnumExplicitMappingTargetNotString, + "The target of the explicit mapping from an enum to a string is not of type string" + ) + .HaveAssertedAllDiagnostics(); + } } diff --git a/test/Riok.Mapperly.Tests/Mapping/StringToEnumExplicitMapTest.cs b/test/Riok.Mapperly.Tests/Mapping/StringToEnumExplicitMapTest.cs index 3887102618..34036a2e4f 100644 --- a/test/Riok.Mapperly.Tests/Mapping/StringToEnumExplicitMapTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/StringToEnumExplicitMapTest.cs @@ -80,15 +80,14 @@ public void EnumToStringWithExplicitValueDuplicatedSource() 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" + DiagnosticDescriptors.EnumStringSourceValueDuplicated, + "String source value str-e is specified multiple times, a source string value may only be specified once" ) .HaveAssertedAllDiagnostics(); } @@ -125,4 +124,37 @@ public void EnumToStringWithExplicitValueSourceEnumTypeMismatch() ) .HaveAssertedAllDiagnostics(); } + + [Fact] + public void EnumToStringWithExplicitValueTargetTypeMismatch() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapEnumValue(E1.A, E1.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.EnumExplicitMappingSourceNotString, + "The source of the explicit mapping from a string to an enum is not of type string" + ) + .HaveAssertedAllDiagnostics(); + } }