From 099d7a82b6a59a99fd9533164547ed73c65db6de Mon Sep 17 00:00:00 2001 From: ni507 Date: Fri, 18 Aug 2023 15:13:17 +0200 Subject: [PATCH] feat: add default mapper attribute for assemblies --- docs/docs/configuration/mapper.mdx | 4 + .../DefaultMapperAttribute.cs | 7 + .../MapperAttribute.cs | 2 +- .../PublicAPI.Shipped.txt | 2 + src/Riok.Mapperly/AnalyzerReleases.Shipped.md | 8 + .../Configuration/AttributeDataAccessor.cs | 48 +++++- .../Configuration/MapperConfiguration.cs | 154 ++++++++---------- .../MergedMapperConfiguration.cs | 104 ++++++++++++ .../Descriptors/DescriptorBuilder.cs | 2 +- .../SimpleMappingBuilderContext.cs | 4 +- .../Descriptors/SymbolAccessor.cs | 16 ++ .../Diagnostics/DiagnosticDescriptors.cs | 9 + .../Helpers/MapperConfigurationBuilder.cs | 52 ++++++ src/Riok.Mapperly/MapperGenerator.cs | 7 + .../Helpers/MapperConfigurationBuilderTest.cs | 90 ++++++++++ .../Riok.Mapperly.Tests/Mapping/MapperTest.cs | 49 ++++++ ...AttributeShouldWork#MyMapper.g.verified.cs | 14 ++ 17 files changed, 476 insertions(+), 96 deletions(-) create mode 100644 src/Riok.Mapperly.Abstractions/DefaultMapperAttribute.cs create mode 100644 src/Riok.Mapperly/Configuration/MergedMapperConfiguration.cs create mode 100644 src/Riok.Mapperly/Helpers/MapperConfigurationBuilder.cs create mode 100644 test/Riok.Mapperly.Tests/Helpers/MapperConfigurationBuilderTest.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/MapperTest.AssemblyAttributeShouldWork#MyMapper.g.verified.cs diff --git a/docs/docs/configuration/mapper.mdx b/docs/docs/configuration/mapper.mdx index fc947dd218..a8378d07ff 100644 --- a/docs/docs/configuration/mapper.mdx +++ b/docs/docs/configuration/mapper.mdx @@ -185,3 +185,7 @@ dotnet_diagnostic.RMG020.severity = error # Unmapped source member ### Strict enum mappings To enforce strict enum mappings set `RMG037` and `RMG038` to error, see [strict enum mappings](./enum.mdx). + +## Default Mapper configuration + +The `DefaultMapperAttribute` allows to set default configurations applied to all mappers on the assembly level. diff --git a/src/Riok.Mapperly.Abstractions/DefaultMapperAttribute.cs b/src/Riok.Mapperly.Abstractions/DefaultMapperAttribute.cs new file mode 100644 index 0000000000..39bcde468b --- /dev/null +++ b/src/Riok.Mapperly.Abstractions/DefaultMapperAttribute.cs @@ -0,0 +1,7 @@ +namespace Riok.Mapperly.Abstractions; + +/// +/// Used to set default mapper values in the assembly. +/// +[AttributeUsage(AttributeTargets.Assembly)] +public sealed class DefaultMapperAttribute : MapperAttribute { } diff --git a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs index 9fc533769a..3e50e23f62 100644 --- a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs +++ b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs @@ -6,7 +6,7 @@ namespace Riok.Mapperly.Abstractions; /// Marks an abstract class or an interface as a mapper. /// [AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class)] -public sealed class MapperAttribute : Attribute +public class MapperAttribute : Attribute { /// /// Strategy on how to match mapping property names. diff --git a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt index f68ab9fc72..3a12aacd81 100644 --- a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt +++ b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt @@ -35,6 +35,8 @@ Riok.Mapperly.Abstractions.MapperAttribute.ThrowOnPropertyMappingNullMismatch.ge Riok.Mapperly.Abstractions.MapperAttribute.ThrowOnPropertyMappingNullMismatch.set -> void Riok.Mapperly.Abstractions.MapperAttribute.UseDeepCloning.get -> bool Riok.Mapperly.Abstractions.MapperAttribute.UseDeepCloning.set -> void +Riok.Mapperly.Abstractions.DefaultMapperAttribute +Riok.Mapperly.Abstractions.DefaultMapperAttribute.DefaultMapperAttribute() -> void Riok.Mapperly.Abstractions.MapperConstructorAttribute Riok.Mapperly.Abstractions.MapperConstructorAttribute.MapperConstructorAttribute() -> void Riok.Mapperly.Abstractions.MapperIgnoreObsoleteMembersAttribute diff --git a/src/Riok.Mapperly/AnalyzerReleases.Shipped.md b/src/Riok.Mapperly/AnalyzerReleases.Shipped.md index 745881de50..dafaadb867 100644 --- a/src/Riok.Mapperly/AnalyzerReleases.Shipped.md +++ b/src/Riok.Mapperly/AnalyzerReleases.Shipped.md @@ -106,3 +106,11 @@ RMG044 | Mapper | Warning | An ignored enum member can not be found on the s RMG045 | Mapper | Warning | An ignored enum member can not be found on the target enum RMG046 | Mapper | Error | The used C# language version is not supported by Mapperly, Mapperly requires at least C# 9.0 RMG047 | Mapper | Error | Cannot map to member path due to modifying a temporary value, see CS1612 + +## Release 3.1 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +RMG048 | Mapper | Error | Cannot use multiple attributes in the same assembly diff --git a/src/Riok.Mapperly/Configuration/AttributeDataAccessor.cs b/src/Riok.Mapperly/Configuration/AttributeDataAccessor.cs index 64e7ddb3f8..59a5d19522 100644 --- a/src/Riok.Mapperly/Configuration/AttributeDataAccessor.cs +++ b/src/Riok.Mapperly/Configuration/AttributeDataAccessor.cs @@ -20,8 +20,13 @@ public AttributeDataAccessor(SymbolAccessor symbolAccessor) _symbolAccessor = symbolAccessor; } - public T AccessSingle(ISymbol symbol) - where T : Attribute => Access(symbol).Single(); + public TData AccessSingle(ISymbol symbol) + where TAttribute : Attribute + where TData : notnull => Access(symbol).Single(); + + public TData? AccessAssemblyFirstOrDefault() + where TAttribute : Attribute + where TData : notnull => AccessAssembly().FirstOrDefault(); public TData? AccessFirstOrDefault(ISymbol symbol) where TAttribute : Attribute @@ -44,12 +49,35 @@ public IEnumerable Access(ISymbol symbol) public IEnumerable Access(ISymbol symbol) where TAttribute : Attribute where TData : notnull + { + var attrDatas = _symbolAccessor.GetAttributes(symbol); + return Access(attrDatas); + } + + /// + /// Reads the assembly attribute data and sets it on a newly created instance of . + /// If has n type parameters, + /// needs to have exactly the same constructors as with additional type arguments. + /// + /// The type of the attribute. + /// The type of the data class. If no type parameters are involved, this is usually the same as . + /// The attribute data. + /// If a property or ctor argument of could not be read on the attribute. + public IEnumerable AccessAssembly() + where TAttribute : Attribute + where TData : notnull + { + var attrDatas = _symbolAccessor.GetAssemblyAttributes(); + return Access(attrDatas); + } + + private IEnumerable Access(IEnumerable attrDatas) + where TAttribute : Attribute + where TData : notnull { var attrType = typeof(TAttribute); var dataType = typeof(TData); - var attrDatas = _symbolAccessor.GetAttributes(symbol); - foreach (var attrData in attrDatas) { var syntax = (AttributeSyntax?)attrData.ApplicationSyntaxReference?.GetSyntax(); @@ -176,8 +204,14 @@ is InvocationExpressionSyntax return null; var enumRoslynType = arg.Type ?? throw new InvalidOperationException("Type is null"); - return targetType == typeof(IFieldSymbol) - ? enumRoslynType.GetFields().First(f => Equals(f.ConstantValue, arg.Value)) - : Enum.ToObject(targetType, arg.Value); + if (targetType == typeof(IFieldSymbol)) + return enumRoslynType.GetFields().First(f => Equals(f.ConstantValue, arg.Value)); + + if (targetType.IsConstructedGenericType && targetType.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + targetType = Nullable.GetUnderlyingType(targetType)!; + } + + return Enum.ToObject(targetType, arg.Value); } } diff --git a/src/Riok.Mapperly/Configuration/MapperConfiguration.cs b/src/Riok.Mapperly/Configuration/MapperConfiguration.cs index b93961c848..2216725ee4 100644 --- a/src/Riok.Mapperly/Configuration/MapperConfiguration.cs +++ b/src/Riok.Mapperly/Configuration/MapperConfiguration.cs @@ -1,101 +1,85 @@ -using Microsoft.CodeAnalysis; using Riok.Mapperly.Abstractions; -using Riok.Mapperly.Descriptors; -using Riok.Mapperly.Helpers; namespace Riok.Mapperly.Configuration; public class MapperConfiguration { - private readonly MappingConfiguration _defaultConfiguration; - private readonly AttributeDataAccessor _dataAccessor; + /// + /// Strategy on how to match mapping property names. + /// + public PropertyNameMappingStrategy? PropertyNameMappingStrategy { get; set; } - public MapperConfiguration(SymbolAccessor symbolAccessor, ISymbol mapperSymbol) - { - _dataAccessor = new AttributeDataAccessor(symbolAccessor); - Mapper = _dataAccessor.AccessSingle(mapperSymbol); - _defaultConfiguration = new MappingConfiguration( - new EnumMappingConfiguration( - Mapper.EnumMappingStrategy, - Mapper.EnumMappingIgnoreCase, - null, - Array.Empty(), - Array.Empty(), - Array.Empty() - ), - new PropertiesMappingConfiguration( - Array.Empty(), - Array.Empty(), - Array.Empty(), - Mapper.IgnoreObsoleteMembersStrategy - ), - Array.Empty() - ); - } + /// + /// The default enum mapping strategy. + /// Can be overwritten on specific enums via mapping method configurations. + /// + public EnumMappingStrategy? EnumMappingStrategy { get; set; } - public MapperAttribute Mapper { get; } + /// + /// Whether the case should be ignored for enum mappings. + /// + public bool? EnumMappingIgnoreCase { get; set; } - public MappingConfiguration BuildFor(MappingConfigurationReference reference) - { - if (reference.Method == null) - return _defaultConfiguration; + /// + /// Specifies the behaviour in the case when the mapper tries to return null in a mapping method with a non-nullable return type. + /// If set to true an is thrown. + /// If set to false the mapper tries to return a default value. + /// For a this is , + /// for value types default + /// and for reference types new() if a parameterless constructor exists or else an is thrown. + /// + public bool? ThrowOnMappingNullMismatch { get; set; } - var enumConfig = BuildEnumConfig(reference); - var propertiesConfig = BuildPropertiesConfig(reference.Method); - var derivedTypesConfig = BuildDerivedTypeConfigs(reference.Method); - return new MappingConfiguration(enumConfig, propertiesConfig, derivedTypesConfig); - } + /// + /// Specifies the behaviour in the case when the mapper tries to set a non-nullable property to a null value. + /// If set to true an is thrown. + /// If set to false the property assignment is ignored. + /// This is ignored for required init properties and projection mappings. + /// + public bool? ThrowOnPropertyMappingNullMismatch { get; set; } - private IReadOnlyCollection BuildDerivedTypeConfigs(IMethodSymbol method) - { - return _dataAccessor - .Access(method) - .Concat(_dataAccessor.Access, DerivedTypeMappingConfiguration>(method)) - .ToList(); - } + /// + /// Specifies whether null values are assigned to the target. + /// If true (default), the source is null, and the target does allow null values, + /// null is assigned. + /// If false, null values are never assigned to the target property. + /// This is ignored for required init properties and projection mappings. + /// + public bool? AllowNullPropertyAssignment { get; set; } - private PropertiesMappingConfiguration BuildPropertiesConfig(IMethodSymbol method) - { - var ignoredSourceProperties = _dataAccessor - .Access(method) - .Select(x => x.Source) - .WhereNotNull() - .ToList(); - var ignoredTargetProperties = _dataAccessor - .Access(method) - .Select(x => x.Target) - .WhereNotNull() - .ToList(); - var explicitMappings = _dataAccessor.Access(method).ToList(); - var ignoreObsolete = _dataAccessor.Access(method).FirstOrDefault() is not { } methodIgnore - ? _defaultConfiguration.Properties.IgnoreObsoleteMembersStrategy - : methodIgnore.IgnoreObsoleteStrategy; + /// + /// Whether to always deep copy objects. + /// Eg. when the type Person[] should be mapped to the same type Person[], + /// when false, the same array is reused. + /// when true, the array and each person is cloned. + /// + public bool? UseDeepCloning { get; set; } - return new PropertiesMappingConfiguration(ignoredSourceProperties, ignoredTargetProperties, explicitMappings, ignoreObsolete); - } + /// + /// Enabled conversions which Mapperly automatically implements. + /// By default all supported type conversions are enabled. + /// + /// Eg. to disable all automatically implemented conversions:
+ /// EnabledConversions = MappingConversionType.None + ///
+ /// + /// Eg. to disable ToString() method calls:
+ /// EnabledConversions = MappingConversionType.All & ~MappingConversionType.ToStringMethod + ///
+ ///
+ public MappingConversionType? EnabledConversions { get; set; } - private EnumMappingConfiguration BuildEnumConfig(MappingConfigurationReference configRef) - { - if (configRef.Method == null || !configRef.Source.IsEnum() && !configRef.Target.IsEnum()) - return _defaultConfiguration.Enum; + /// + /// Enables the reference handling feature. + /// Disabled by default for performance reasons. + /// When enabled, an instance is passed through the mapping methods + /// to keep track of and reuse existing target object instances. + /// + public bool? UseReferenceHandling { get; set; } - 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 - ); - } + /// + /// The ignore obsolete attribute strategy. Determines how marked members are mapped. + /// Defaults to . + /// + public IgnoreObsoleteMembersStrategy? IgnoreObsoleteMembersStrategy { get; set; } } diff --git a/src/Riok.Mapperly/Configuration/MergedMapperConfiguration.cs b/src/Riok.Mapperly/Configuration/MergedMapperConfiguration.cs new file mode 100644 index 0000000000..0b388a74f4 --- /dev/null +++ b/src/Riok.Mapperly/Configuration/MergedMapperConfiguration.cs @@ -0,0 +1,104 @@ +using Microsoft.CodeAnalysis; +using Riok.Mapperly.Abstractions; +using Riok.Mapperly.Descriptors; +using Riok.Mapperly.Helpers; + +namespace Riok.Mapperly.Configuration; + +public class MergedMapperConfiguration +{ + private readonly MappingConfiguration _defaultConfiguration; + private readonly AttributeDataAccessor _dataAccessor; + + public MergedMapperConfiguration(SymbolAccessor symbolAccessor, ISymbol mapperSymbol) + { + _dataAccessor = new AttributeDataAccessor(symbolAccessor); + var mapperConfiguration = _dataAccessor.AccessSingle(mapperSymbol); + var defaultMapperConfiguration = _dataAccessor.AccessAssemblyFirstOrDefault(); + Mapper = MapperConfigurationBuilder.Merge(mapperConfiguration, defaultMapperConfiguration); + + _defaultConfiguration = new MappingConfiguration( + new EnumMappingConfiguration( + Mapper.EnumMappingStrategy, + Mapper.EnumMappingIgnoreCase, + null, + Array.Empty(), + Array.Empty(), + Array.Empty() + ), + new PropertiesMappingConfiguration( + Array.Empty(), + Array.Empty(), + Array.Empty(), + Mapper.IgnoreObsoleteMembersStrategy + ), + Array.Empty() + ); + } + + public MapperAttribute Mapper { get; } + + public MappingConfiguration BuildFor(MappingConfigurationReference reference) + { + if (reference.Method == null) + return _defaultConfiguration; + + var enumConfig = BuildEnumConfig(reference); + var propertiesConfig = BuildPropertiesConfig(reference.Method); + var derivedTypesConfig = BuildDerivedTypeConfigs(reference.Method); + return new MappingConfiguration(enumConfig, propertiesConfig, derivedTypesConfig); + } + + private IReadOnlyCollection BuildDerivedTypeConfigs(IMethodSymbol method) + { + return _dataAccessor + .Access(method) + .Concat(_dataAccessor.Access, DerivedTypeMappingConfiguration>(method)) + .ToList(); + } + + private PropertiesMappingConfiguration BuildPropertiesConfig(IMethodSymbol method) + { + var ignoredSourceProperties = _dataAccessor + .Access(method) + .Select(x => x.Source) + .WhereNotNull() + .ToList(); + var ignoredTargetProperties = _dataAccessor + .Access(method) + .Select(x => x.Target) + .WhereNotNull() + .ToList(); + var explicitMappings = _dataAccessor.Access(method).ToList(); + var ignoreObsolete = _dataAccessor.Access(method).FirstOrDefault() is not { } methodIgnore + ? _defaultConfiguration.Properties.IgnoreObsoleteMembersStrategy + : methodIgnore.IgnoreObsoleteStrategy; + + return new PropertiesMappingConfiguration(ignoredSourceProperties, ignoredTargetProperties, explicitMappings, ignoreObsolete); + } + + private EnumMappingConfiguration BuildEnumConfig(MappingConfigurationReference configRef) + { + 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/Descriptors/DescriptorBuilder.cs b/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs index 1dfe294ed1..0a8a515b07 100644 --- a/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs @@ -35,7 +35,7 @@ SymbolAccessor symbolAccessor _mappingBodyBuilder = new MappingBodyBuilder(_mappings); _builderContext = new SimpleMappingBuilderContext( compilation, - new MapperConfiguration(symbolAccessor, mapperSymbol), + new MergedMapperConfiguration(symbolAccessor, mapperSymbol), wellKnownTypes, _symbolAccessor, _mapperDescriptor, diff --git a/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs index ed81d08f73..5a787186ee 100644 --- a/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs @@ -12,11 +12,11 @@ public class SimpleMappingBuilderContext { private readonly MapperDescriptor _descriptor; private readonly List _diagnostics; - private readonly MapperConfiguration _configuration; + private readonly MergedMapperConfiguration _configuration; public SimpleMappingBuilderContext( Compilation compilation, - MapperConfiguration configuration, + MergedMapperConfiguration configuration, WellKnownTypes types, SymbolAccessor symbolAccessor, MapperDescriptor descriptor, diff --git a/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs b/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs index fca886b285..29ae7cb1f6 100644 --- a/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs +++ b/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs @@ -64,10 +64,23 @@ NullableAnnotation typeParameterUsageNullableAnnotation return true; } + public IEnumerable GetAssemblyAttributes() + where T : Attribute + { + var attributes = _compilation.Assembly.GetAttributes(); + return FilterAttributes(attributes); + } + internal IEnumerable GetAttributes(ISymbol symbol) where T : Attribute { var attributes = GetAttributesCore(symbol); + return FilterAttributes(attributes); + } + + private IEnumerable FilterAttributes(ImmutableArray attributes) + where T : Attribute + { if (attributes.IsEmpty) { yield break; @@ -86,6 +99,9 @@ internal IEnumerable GetAttributes(ISymbol symbol) internal bool HasAttribute(ISymbol symbol) where T : Attribute => GetAttributes(symbol).Any(); + internal bool HasMultipleAssemblyAttribute() + where T : Attribute => GetAssemblyAttributes().Skip(1).Any(); + internal IEnumerable GetAllMethods(ITypeSymbol symbol) => GetAllMembers(symbol).OfType(); internal IEnumerable GetAllMethods(ITypeSymbol symbol, string name) => diff --git a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs index c3bf0975a6..16da919037 100644 --- a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs +++ b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs @@ -419,4 +419,13 @@ public static class DiagnosticDescriptors DiagnosticSeverity.Error, true ); + + public static readonly DiagnosticDescriptor CannotUseMultipleAssemblyAttributes = new DiagnosticDescriptor( + "RMG048", + "Cannot use multiple attributes in the same assembly", + "Cannot use multiple {0} attributes in the same assembly", + DiagnosticCategories.Mapper, + DiagnosticSeverity.Error, + true + ); } diff --git a/src/Riok.Mapperly/Helpers/MapperConfigurationBuilder.cs b/src/Riok.Mapperly/Helpers/MapperConfigurationBuilder.cs new file mode 100644 index 0000000000..aa9957cda2 --- /dev/null +++ b/src/Riok.Mapperly/Helpers/MapperConfigurationBuilder.cs @@ -0,0 +1,52 @@ +using Riok.Mapperly.Abstractions; +using Riok.Mapperly.Configuration; + +namespace Riok.Mapperly.Helpers; + +public static class MapperConfigurationBuilder +{ + public static MapperAttribute Merge(MapperConfiguration mapperConfiguration, MapperConfiguration? defaultMapperConfiguration) + { + var mapper = new MapperAttribute(); + mapper.PropertyNameMappingStrategy = + mapperConfiguration.PropertyNameMappingStrategy + ?? defaultMapperConfiguration?.PropertyNameMappingStrategy + ?? mapper.PropertyNameMappingStrategy; + + mapper.EnumMappingStrategy = + mapperConfiguration.EnumMappingStrategy ?? defaultMapperConfiguration?.EnumMappingStrategy ?? mapper.EnumMappingStrategy; + + mapper.EnumMappingIgnoreCase = + mapperConfiguration.EnumMappingIgnoreCase ?? defaultMapperConfiguration?.EnumMappingIgnoreCase ?? mapper.EnumMappingIgnoreCase; + + mapper.ThrowOnMappingNullMismatch = + mapperConfiguration.ThrowOnMappingNullMismatch + ?? defaultMapperConfiguration?.ThrowOnMappingNullMismatch + ?? mapper.ThrowOnMappingNullMismatch; + + mapper.ThrowOnPropertyMappingNullMismatch = + mapperConfiguration.ThrowOnPropertyMappingNullMismatch + ?? defaultMapperConfiguration?.ThrowOnPropertyMappingNullMismatch + ?? mapper.ThrowOnPropertyMappingNullMismatch; + + mapper.AllowNullPropertyAssignment = + mapperConfiguration.AllowNullPropertyAssignment + ?? defaultMapperConfiguration?.AllowNullPropertyAssignment + ?? mapper.AllowNullPropertyAssignment; + + mapper.UseDeepCloning = mapperConfiguration.UseDeepCloning ?? defaultMapperConfiguration?.UseDeepCloning ?? mapper.UseDeepCloning; + + mapper.EnabledConversions = + mapperConfiguration.EnabledConversions ?? defaultMapperConfiguration?.EnabledConversions ?? mapper.EnabledConversions; + + mapper.UseReferenceHandling = + mapperConfiguration.UseReferenceHandling ?? defaultMapperConfiguration?.UseReferenceHandling ?? mapper.UseReferenceHandling; + + mapper.IgnoreObsoleteMembersStrategy = + mapperConfiguration.IgnoreObsoleteMembersStrategy + ?? defaultMapperConfiguration?.IgnoreObsoleteMembersStrategy + ?? mapper.IgnoreObsoleteMembersStrategy; + + return mapper; + } +} diff --git a/src/Riok.Mapperly/MapperGenerator.cs b/src/Riok.Mapperly/MapperGenerator.cs index 9ab0285259..323f1a4841 100644 --- a/src/Riok.Mapperly/MapperGenerator.cs +++ b/src/Riok.Mapperly/MapperGenerator.cs @@ -83,6 +83,13 @@ CancellationToken cancellationToken if (!symbolAccessor.HasAttribute(mapperSymbol)) continue; + if (symbolAccessor.HasMultipleAssemblyAttribute()) + { + diagnostics.Add( + Diagnostic.Create(DiagnosticDescriptors.CannotUseMultipleAssemblyAttributes, null, typeof(DefaultMapperAttribute)) + ); + } + var builder = new DescriptorBuilder(compilation, mapperSyntax, mapperSymbol, wellKnownTypes, symbolAccessor); var (descriptor, descriptorDiagnostics) = builder.Build(); diff --git a/test/Riok.Mapperly.Tests/Helpers/MapperConfigurationBuilderTest.cs b/test/Riok.Mapperly.Tests/Helpers/MapperConfigurationBuilderTest.cs new file mode 100644 index 0000000000..bf38e7b88b --- /dev/null +++ b/test/Riok.Mapperly.Tests/Helpers/MapperConfigurationBuilderTest.cs @@ -0,0 +1,90 @@ +using Riok.Mapperly.Abstractions; +using Riok.Mapperly.Configuration; +using Riok.Mapperly.Helpers; + +namespace Riok.Mapperly.Tests.Helpers; + +public class MapperConfigurationBuilderTest +{ + [Fact] + public void ShouldMergeMapperConfigurations() + { + var mapperConfiguration = NewMapperConfiguration(); + var defaultMapperConfiguration = new MapperConfiguration + { + PropertyNameMappingStrategy = PropertyNameMappingStrategy.CaseInsensitive, + EnumMappingStrategy = EnumMappingStrategy.ByValue, + EnumMappingIgnoreCase = false, + ThrowOnMappingNullMismatch = false, + ThrowOnPropertyMappingNullMismatch = false, + AllowNullPropertyAssignment = false, + UseDeepCloning = false, + EnabledConversions = MappingConversionType.Dictionary, + UseReferenceHandling = false, + IgnoreObsoleteMembersStrategy = IgnoreObsoleteMembersStrategy.Target, + }; + + var mapper = MapperConfigurationBuilder.Merge(mapperConfiguration, defaultMapperConfiguration); + mapper.PropertyNameMappingStrategy.Should().Be(PropertyNameMappingStrategy.CaseSensitive); + mapper.EnumMappingStrategy.Should().Be(EnumMappingStrategy.ByName); + mapper.EnumMappingIgnoreCase.Should().BeTrue(); + mapper.ThrowOnMappingNullMismatch.Should().BeTrue(); + mapper.ThrowOnPropertyMappingNullMismatch.Should().BeTrue(); + mapper.AllowNullPropertyAssignment.Should().BeTrue(); + mapper.UseDeepCloning.Should().BeTrue(); + mapper.EnabledConversions.Should().Be(MappingConversionType.Constructor); + mapper.UseReferenceHandling.Should().BeTrue(); + mapper.IgnoreObsoleteMembersStrategy.Should().Be(IgnoreObsoleteMembersStrategy.Source); + } + + [Fact] + public void ShouldMergeMapperConfigurationsWithEmptyDefaultMapperConfiguration() + { + var mapperConfiguration = NewMapperConfiguration(); + var mapper = MapperConfigurationBuilder.Merge(mapperConfiguration, null); + mapper.PropertyNameMappingStrategy.Should().Be(PropertyNameMappingStrategy.CaseSensitive); + mapper.EnumMappingStrategy.Should().Be(EnumMappingStrategy.ByName); + mapper.EnumMappingIgnoreCase.Should().BeTrue(); + mapper.ThrowOnMappingNullMismatch.Should().BeTrue(); + mapper.ThrowOnPropertyMappingNullMismatch.Should().BeTrue(); + mapper.AllowNullPropertyAssignment.Should().BeTrue(); + mapper.UseDeepCloning.Should().BeTrue(); + mapper.EnabledConversions.Should().Be(MappingConversionType.Constructor); + mapper.UseReferenceHandling.Should().BeTrue(); + mapper.IgnoreObsoleteMembersStrategy.Should().Be(IgnoreObsoleteMembersStrategy.Source); + } + + [Fact] + public void ShouldMergeMapperConfigurationsWithEmptyMapperConfiguration() + { + var mapperConfiguration = new MapperConfiguration(); + var defaultMapperConfiguration = new MapperConfiguration + { + PropertyNameMappingStrategy = PropertyNameMappingStrategy.CaseInsensitive, + EnumMappingStrategy = EnumMappingStrategy.ByName, + EnumMappingIgnoreCase = true, + }; + + var mapper = MapperConfigurationBuilder.Merge(mapperConfiguration, defaultMapperConfiguration); + mapper.PropertyNameMappingStrategy.Should().Be(PropertyNameMappingStrategy.CaseInsensitive); + mapper.EnumMappingStrategy.Should().Be(EnumMappingStrategy.ByName); + mapper.EnumMappingIgnoreCase.Should().BeTrue(); + } + + private MapperConfiguration NewMapperConfiguration() + { + return new MapperConfiguration + { + PropertyNameMappingStrategy = PropertyNameMappingStrategy.CaseSensitive, + EnumMappingStrategy = EnumMappingStrategy.ByName, + EnumMappingIgnoreCase = true, + ThrowOnMappingNullMismatch = true, + ThrowOnPropertyMappingNullMismatch = true, + AllowNullPropertyAssignment = true, + UseDeepCloning = true, + EnabledConversions = MappingConversionType.Constructor, + UseReferenceHandling = true, + IgnoreObsoleteMembersStrategy = IgnoreObsoleteMembersStrategy.Source, + }; + } +} diff --git a/test/Riok.Mapperly.Tests/Mapping/MapperTest.cs b/test/Riok.Mapperly.Tests/Mapping/MapperTest.cs index 9d475a5d14..9297c31975 100644 --- a/test/Riok.Mapperly.Tests/Mapping/MapperTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/MapperTest.cs @@ -125,4 +125,53 @@ public void LanguageLevelLower9ShouldDiagnostic() ) .HaveAssertedAllDiagnostics(); } + + [Fact] + public Task AssemblyAttributeShouldWork() + { + var source = TestSourceBuilder.CSharp( + """ + using Riok.Mapperly.Abstractions; + + [assembly: Riok.Mapperly.Abstractions.DefaultMapperAttribute(EnumMappingIgnoreCase = true)] + [Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName)] + public partial class MyMapper + { + partial E2 Map(E1 source); + } + + enum E1 { value1 } + enum E2 { Value1 } + """ + ); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public void MultipleAssemblyAttributeShouldDiagnostic() + { + var source = TestSourceBuilder.CSharp( + """ + using Riok.Mapperly.Abstractions; + + [assembly: Riok.Mapperly.Abstractions.DefaultMapperAttribute(EnumMappingIgnoreCase = true)] + [assembly: Riok.Mapperly.Abstractions.DefaultMapperAttribute(EnumMappingStrategy = EnumMappingStrategy.ByValue)] + [Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName)] + public partial class MyMapper + { + partial string FooToBar(string value); + } + """ + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic( + DiagnosticDescriptors.CannotUseMultipleAssemblyAttributes, + "Cannot use multiple Riok.Mapperly.Abstractions.DefaultMapperAttribute attributes in the same assembly" + ) + .HaveAssertedAllDiagnostics(); + } } diff --git a/test/Riok.Mapperly.Tests/_snapshots/MapperTest.AssemblyAttributeShouldWork#MyMapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/MapperTest.AssemblyAttributeShouldWork#MyMapper.g.verified.cs new file mode 100644 index 0000000000..af046a7d3c --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/MapperTest.AssemblyAttributeShouldWork#MyMapper.g.verified.cs @@ -0,0 +1,14 @@ +//HintName: MyMapper.g.cs +// +#nullable enable +public partial class MyMapper +{ + private partial global::E2 Map(global::E1 source) + { + return source switch + { + global::E1.value1 => global::E2.Value1, + _ => throw new System.ArgumentOutOfRangeException(nameof(source), source, "The value of enum E1 is not supported"), + }; + } +}