diff --git a/docs/docs/configuration/mapper.md b/docs/docs/configuration/mapper.mdx similarity index 58% rename from docs/docs/configuration/mapper.md rename to docs/docs/configuration/mapper.mdx index 3a95a795c9..2e0febe4df 100644 --- a/docs/docs/configuration/mapper.md +++ b/docs/docs/configuration/mapper.mdx @@ -3,11 +3,14 @@ sidebar_position: 0 description: Define a mapper with Mapperly. --- +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + # Mapper configuration The `MapperAttribute` provides options to customize the generated mapper class. -## Copy behaviour +## Copy behavior By default, Mapperly does not create deep copies of objects to improve performance. If an object can be directly assigned to the target, it will do so @@ -56,6 +59,51 @@ public partial class CarMapper } ``` +### Ignore obsolete members stratgey + +By default, mapperly will map source/target members marked with `ObsoleteAttribute`. This can be changed by setting the `IgnoreObsoleteMembersStrategy` of a method with `MapperIgnoreObsoleteMembersAttribute`, or by setting the `IgnoreObsoleteMembersStrategy` option of the `MapperAttribute`. + +| Name | Description | +| ------ | ------------------------------------------------------------------------------- | +| None | Will map members marked with the `Obsolete` attribute (default) | +| Both | Ignores source and target members that are mapped with the `Obsolete` attribute | +| Source | Ignores source members that are mapped with the `Obsolete` attribute | +| Target | Ignores target members that are mapped with the `Obsolete` attribute | + + + + +Sets the `IgnoreObsoleteMembersStrategy` for all methods within the mapper, by default it is `None` allowing obsolete source and target members to be mapped. This can be overriden by individual mapping methods using `MapperIgnoreObsoleteMembersAttribute`. + +```csharp +// highlight-start +[Mapper(IgnoreObsoleteMembersStrategy = IgnoreObsoleteMembersStrategy.Both)] +// highlight-end +public partial class CarMapper +{ + ... +} +``` + + + + +Method will use the provided ignore obsolete mapping strategy, otherwise the `MapperAttribute` property `IgnoreObsoleteMembersStrategy` will be used. + +```csharp +[Mapper] +public partial class CarMapper +{ + // highlight-start + [MapperIgnoreObsoleteMembers(IgnoreObsoleteMembersStrategy.Both)] + // highlight-end + public partial CarMakeDto MapMake(CarMake make); +} +``` + + + + ### Property name mapping strategy By default, property and field names are matched using a case sensitive strategy. diff --git a/src/Riok.Mapperly.Abstractions/IgnoreObsoleteMembersStrategy.cs b/src/Riok.Mapperly.Abstractions/IgnoreObsoleteMembersStrategy.cs new file mode 100644 index 0000000000..ce9d0fbe0d --- /dev/null +++ b/src/Riok.Mapperly.Abstractions/IgnoreObsoleteMembersStrategy.cs @@ -0,0 +1,30 @@ +namespace Riok.Mapperly.Abstractions; + +/// +/// Defines the strategy to use when mapping members marked with . +/// Note that will always map marked members, +/// even if they are ignored. +/// +[Flags] +public enum IgnoreObsoleteMembersStrategy +{ + /// + /// Maps marked members. + /// + None = 0, + + /// + /// Will not map marked source or target members. + /// + Both = ~None, + + /// + /// Ignores source marked members. + /// + Source = 1 << 0, + + /// + /// Ignores target marked members. + /// + Target = 1 << 1, +} diff --git a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs index 21cb2f386c..6aa1f5e792 100644 --- a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs +++ b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs @@ -70,4 +70,10 @@ public sealed class MapperAttribute : Attribute /// to keep track of and reuse existing target object instances. /// public bool UseReferenceHandling { get; set; } + + /// + /// The ignore obsolete attribute strategy. Determines how marked members are mapped. + /// Defaults to . + /// + public IgnoreObsoleteMembersStrategy IgnoreObsoleteMembersStrategy { get; set; } = IgnoreObsoleteMembersStrategy.None; } diff --git a/src/Riok.Mapperly.Abstractions/MapperIgnoreObsoleteMembersAttribute.cs b/src/Riok.Mapperly.Abstractions/MapperIgnoreObsoleteMembersAttribute.cs new file mode 100644 index 0000000000..e798fb3956 --- /dev/null +++ b/src/Riok.Mapperly.Abstractions/MapperIgnoreObsoleteMembersAttribute.cs @@ -0,0 +1,22 @@ +namespace Riok.Mapperly.Abstractions; + +/// +/// Specifies options for obsolete ignoring strategy. +/// +[AttributeUsage(AttributeTargets.Method)] +public sealed class MapperIgnoreObsoleteMembersAttribute : Attribute +{ + /// + /// Specifies options for obsolete ignoring strategy. + /// + /// The strategy to be used to map marked members. Defaults to . + public MapperIgnoreObsoleteMembersAttribute(IgnoreObsoleteMembersStrategy ignoreObsoleteStrategy = IgnoreObsoleteMembersStrategy.Both) + { + IgnoreObsoleteStrategy = ignoreObsoleteStrategy; + } + + /// + /// The strategy used to map marked members. + /// + public IgnoreObsoleteMembersStrategy IgnoreObsoleteStrategy { get; } +} diff --git a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt index 2c2d6862f5..0bf47f01ee 100644 --- a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt +++ b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt @@ -3,6 +3,11 @@ Riok.Mapperly.Abstractions.EnumMappingStrategy Riok.Mapperly.Abstractions.EnumMappingStrategy.ByName = 1 -> Riok.Mapperly.Abstractions.EnumMappingStrategy Riok.Mapperly.Abstractions.EnumMappingStrategy.ByValue = 0 -> Riok.Mapperly.Abstractions.EnumMappingStrategy +Riok.Mapperly.Abstractions.IgnoreObsoleteMembersStrategy +Riok.Mapperly.Abstractions.IgnoreObsoleteMembersStrategy.Both = -1 -> Riok.Mapperly.Abstractions.IgnoreObsoleteMembersStrategy +Riok.Mapperly.Abstractions.IgnoreObsoleteMembersStrategy.None = 0 -> Riok.Mapperly.Abstractions.IgnoreObsoleteMembersStrategy +Riok.Mapperly.Abstractions.IgnoreObsoleteMembersStrategy.Source = 1 -> Riok.Mapperly.Abstractions.IgnoreObsoleteMembersStrategy +Riok.Mapperly.Abstractions.IgnoreObsoleteMembersStrategy.Target = 2 -> Riok.Mapperly.Abstractions.IgnoreObsoleteMembersStrategy Riok.Mapperly.Abstractions.MapEnumAttribute Riok.Mapperly.Abstractions.MapEnumAttribute.IgnoreCase.get -> bool Riok.Mapperly.Abstractions.MapEnumAttribute.IgnoreCase.set -> void @@ -19,6 +24,8 @@ Riok.Mapperly.Abstractions.MapperAttribute.EnumMappingIgnoreCase.get -> bool Riok.Mapperly.Abstractions.MapperAttribute.EnumMappingIgnoreCase.set -> void Riok.Mapperly.Abstractions.MapperAttribute.EnumMappingStrategy.get -> Riok.Mapperly.Abstractions.EnumMappingStrategy Riok.Mapperly.Abstractions.MapperAttribute.EnumMappingStrategy.set -> void +Riok.Mapperly.Abstractions.MapperAttribute.IgnoreObsoleteMembersStrategy.get -> Riok.Mapperly.Abstractions.IgnoreObsoleteMembersStrategy +Riok.Mapperly.Abstractions.MapperAttribute.IgnoreObsoleteMembersStrategy.set -> void Riok.Mapperly.Abstractions.MapperAttribute.MapperAttribute() -> void Riok.Mapperly.Abstractions.MapperAttribute.PropertyNameMappingStrategy.get -> Riok.Mapperly.Abstractions.PropertyNameMappingStrategy Riok.Mapperly.Abstractions.MapperAttribute.PropertyNameMappingStrategy.set -> void @@ -33,6 +40,9 @@ Riok.Mapperly.Abstractions.MapperConstructorAttribute.MapperConstructorAttribute Riok.Mapperly.Abstractions.MapperIgnoreAttribute Riok.Mapperly.Abstractions.MapperIgnoreAttribute.MapperIgnoreAttribute(string! target) -> void Riok.Mapperly.Abstractions.MapperIgnoreAttribute.Target.get -> string! +Riok.Mapperly.Abstractions.MapperIgnoreObsoleteMembersAttribute +Riok.Mapperly.Abstractions.MapperIgnoreObsoleteMembersAttribute.IgnoreObsoleteStrategy.get -> Riok.Mapperly.Abstractions.IgnoreObsoleteMembersStrategy +Riok.Mapperly.Abstractions.MapperIgnoreObsoleteMembersAttribute.MapperIgnoreObsoleteMembersAttribute(Riok.Mapperly.Abstractions.IgnoreObsoleteMembersStrategy ignoreObsoleteStrategy = (Riok.Mapperly.Abstractions.IgnoreObsoleteMembersStrategy)-1) -> void Riok.Mapperly.Abstractions.MapperIgnoreSourceAttribute Riok.Mapperly.Abstractions.MapperIgnoreSourceAttribute.MapperIgnoreSourceAttribute(string! source) -> void Riok.Mapperly.Abstractions.MapperIgnoreSourceAttribute.Source.get -> string! diff --git a/src/Riok.Mapperly/Configuration/MapperConfiguration.cs b/src/Riok.Mapperly/Configuration/MapperConfiguration.cs index 5bab50920f..bd5f65c54d 100644 --- a/src/Riok.Mapperly/Configuration/MapperConfiguration.cs +++ b/src/Riok.Mapperly/Configuration/MapperConfiguration.cs @@ -23,7 +23,12 @@ public MapperConfiguration(SymbolAccessor symbolAccessor, ISymbol mapperSymbol) Array.Empty(), Array.Empty() ), - new PropertiesMappingConfiguration(Array.Empty(), Array.Empty(), Array.Empty()), + new PropertiesMappingConfiguration( + Array.Empty(), + Array.Empty(), + Array.Empty(), + Mapper.IgnoreObsoleteMembersStrategy + ), Array.Empty() ); } @@ -66,7 +71,11 @@ private PropertiesMappingConfiguration BuildPropertiesConfig(IMethodSymbol metho .WhereNotNull() .ToList(); var explicitMappings = _dataAccessor.Access(method).ToList(); - return new PropertiesMappingConfiguration(ignoredSourceProperties, ignoredTargetProperties, explicitMappings); + 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) diff --git a/src/Riok.Mapperly/Configuration/PropertiesMappingConfiguration.cs b/src/Riok.Mapperly/Configuration/PropertiesMappingConfiguration.cs index ccd3bca757..f991cc536a 100644 --- a/src/Riok.Mapperly/Configuration/PropertiesMappingConfiguration.cs +++ b/src/Riok.Mapperly/Configuration/PropertiesMappingConfiguration.cs @@ -1,7 +1,10 @@ +using Riok.Mapperly.Abstractions; + namespace Riok.Mapperly.Configuration; public record PropertiesMappingConfiguration( IReadOnlyCollection IgnoredSources, IReadOnlyCollection IgnoredTargets, - IReadOnlyCollection ExplicitMappings + IReadOnlyCollection ExplicitMappings, + IgnoreObsoleteMembersStrategy IgnoreObsoleteMembersStrategy ); diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingBuilderContext.cs index cb09e44455..c635e30708 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingBuilderContext.cs @@ -1,3 +1,4 @@ +using Riok.Mapperly.Abstractions; using Riok.Mapperly.Configuration; using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Diagnostics; @@ -21,11 +22,17 @@ protected MembersMappingBuilderContext(MappingBuilderContext builderContext, T m { BuilderContext = builderContext; Mapping = mapping; + MemberConfigsByRootTargetName = GetMemberConfigurations(); _unmappedSourceMemberNames = GetSourceMemberNames(); TargetMembers = GetTargetMembers(); - IgnoredSourceMemberNames = builderContext.Configuration.Properties.IgnoredSources; + IgnoredSourceMemberNames = builderContext.Configuration.Properties.IgnoredSources + .Concat(GetIgnoredObsoleteSourceMembers()) + .ToHashSet(); + var ignoredTargetMemberNames = builderContext.Configuration.Properties.IgnoredTargets + .Concat(GetIgnoredObsoleteTargetMembers()) + .ToHashSet(); _ignoredUnmatchedSourceMemberNames = InitIgnoredUnmatchedProperties(IgnoredSourceMemberNames, _unmappedSourceMemberNames); _ignoredUnmatchedTargetMemberNames = InitIgnoredUnmatchedProperties( @@ -34,9 +41,13 @@ protected MembersMappingBuilderContext(MappingBuilderContext builderContext, T m ); _unmappedSourceMemberNames.ExceptWith(IgnoredSourceMemberNames); - TargetMembers.RemoveRange(builderContext.Configuration.Properties.IgnoredTargets); MemberConfigsByRootTargetName = GetMemberConfigurations(); + + // remove explicitly mapped ignored targets from ignoredTargetMemberNames + // then remove all ignored targets from TargetMembers, leaving unignored and explicitly mapped ignored members + ignoredTargetMemberNames.ExceptWith(MemberConfigsByRootTargetName.Keys); + TargetMembers.RemoveRange(ignoredTargetMemberNames); } public MappingBuilderContext BuilderContext { get; } @@ -66,6 +77,32 @@ private HashSet InitIgnoredUnmatchedProperties(IEnumerable allPr return unmatched; } + private IEnumerable GetIgnoredObsoleteTargetMembers() + { + var obsoleteStrategy = BuilderContext.Configuration.Properties.IgnoreObsoleteMembersStrategy; + + if (!obsoleteStrategy.HasFlag(IgnoreObsoleteMembersStrategy.Target)) + return Enumerable.Empty(); + + return BuilderContext.SymbolAccessor + .GetAllAccessibleMappableMembers(Mapping.TargetType) + .Where(x => BuilderContext.SymbolAccessor.HasAttribute(x.MemberSymbol)) + .Select(x => x.Name); + } + + private IEnumerable GetIgnoredObsoleteSourceMembers() + { + var obsoleteStrategy = BuilderContext.Configuration.Properties.IgnoreObsoleteMembersStrategy; + + if (!obsoleteStrategy.HasFlag(IgnoreObsoleteMembersStrategy.Source)) + return Enumerable.Empty(); + + return BuilderContext.SymbolAccessor + .GetAllAccessibleMappableMembers(Mapping.SourceType) + .Where(x => BuilderContext.SymbolAccessor.HasAttribute(x.MemberSymbol)) + .Select(x => x.Name); + } + private HashSet GetSourceMemberNames() { return BuilderContext.SymbolAccessor.GetAllAccessibleMappableMembers(Mapping.SourceType).Select(x => x.Name).ToHashSet(); diff --git a/src/Riok.Mapperly/Symbols/FieldMember.cs b/src/Riok.Mapperly/Symbols/FieldMember.cs index a2c5bc755c..67065063b2 100644 --- a/src/Riok.Mapperly/Symbols/FieldMember.cs +++ b/src/Riok.Mapperly/Symbols/FieldMember.cs @@ -14,6 +14,7 @@ public FieldMember(IFieldSymbol fieldSymbol) public string Name => _fieldSymbol.Name; public ITypeSymbol Type => _fieldSymbol.Type; + public ISymbol MemberSymbol => _fieldSymbol; public bool IsNullable => _fieldSymbol.NullableAnnotation == NullableAnnotation.Annotated || Type.IsNullable(); public bool IsIndexer => false; public bool CanGet => !_fieldSymbol.IsReadOnly && _fieldSymbol.IsAccessible(); diff --git a/src/Riok.Mapperly/Symbols/IMappableMember.cs b/src/Riok.Mapperly/Symbols/IMappableMember.cs index bebdf5fee5..9eb6a873ee 100644 --- a/src/Riok.Mapperly/Symbols/IMappableMember.cs +++ b/src/Riok.Mapperly/Symbols/IMappableMember.cs @@ -12,6 +12,8 @@ public interface IMappableMember ITypeSymbol Type { get; } + ISymbol MemberSymbol { get; } + bool IsNullable { get; } bool IsIndexer { get; } diff --git a/src/Riok.Mapperly/Symbols/PropertyMember.cs b/src/Riok.Mapperly/Symbols/PropertyMember.cs index 1a825ef584..4180059569 100644 --- a/src/Riok.Mapperly/Symbols/PropertyMember.cs +++ b/src/Riok.Mapperly/Symbols/PropertyMember.cs @@ -14,6 +14,7 @@ internal PropertyMember(IPropertySymbol propertySymbol) public string Name => _propertySymbol.Name; public ITypeSymbol Type => _propertySymbol.Type; + public ISymbol MemberSymbol => _propertySymbol; public bool IsNullable => _propertySymbol.NullableAnnotation == NullableAnnotation.Annotated || Type.IsNullable(); public bool IsIndexer => _propertySymbol.IsIndexer; public bool CanGet => !_propertySymbol.IsWriteOnly && _propertySymbol.GetMethod?.IsAccessible() != false; diff --git a/test/Riok.Mapperly.IntegrationTests/Dto/TestObjectDto.cs b/test/Riok.Mapperly.IntegrationTests/Dto/TestObjectDto.cs index 2e0c554ea2..574d0fcb92 100644 --- a/test/Riok.Mapperly.IntegrationTests/Dto/TestObjectDto.cs +++ b/test/Riok.Mapperly.IntegrationTests/Dto/TestObjectDto.cs @@ -111,6 +111,9 @@ public TestObjectDto(int ctorValue, int unknownValue = 10, int ctorValue2 = 100) public int IgnoredIntValue { get; set; } + [Obsolete] + public int IgnoredObsoleteValue { get; set; } + public DateOnly DateTimeValueTargetDateOnly { get; set; } public TimeOnly DateTimeValueTargetTimeOnly { get; set; } diff --git a/test/Riok.Mapperly.IntegrationTests/Mapper/DeepCloningMapper.cs b/test/Riok.Mapperly.IntegrationTests/Mapper/DeepCloningMapper.cs index 937db5f544..b7573d4e79 100644 --- a/test/Riok.Mapperly.IntegrationTests/Mapper/DeepCloningMapper.cs +++ b/test/Riok.Mapperly.IntegrationTests/Mapper/DeepCloningMapper.cs @@ -11,6 +11,7 @@ public static partial class DeepCloningMapper [MapperIgnoreSource(nameof(TestObject.IgnoredIntValue))] [MapperIgnoreSource(nameof(TestObject.IgnoredStringValue))] [MapperIgnoreSource(nameof(TestObject.ImmutableHashSetValue))] + [MapperIgnoreObsoleteMembers] public static partial TestObject Copy(TestObject src); } } diff --git a/test/Riok.Mapperly.IntegrationTests/Mapper/ProjectionMapper.cs b/test/Riok.Mapperly.IntegrationTests/Mapper/ProjectionMapper.cs index 723a75792c..6469e0066f 100644 --- a/test/Riok.Mapperly.IntegrationTests/Mapper/ProjectionMapper.cs +++ b/test/Riok.Mapperly.IntegrationTests/Mapper/ProjectionMapper.cs @@ -19,6 +19,7 @@ public static partial class ProjectionMapper [MapperIgnoreTarget(nameof(TestObjectDtoProjection.IgnoredIntValue))] [MapperIgnoreSource(nameof(TestObjectProjection.IgnoredStringValue))] [MapProperty(nameof(TestObjectProjection.RenamedStringValue), nameof(TestObjectDtoProjection.RenamedStringValue2))] + [MapperIgnoreObsoleteMembers] private static partial TestObjectDtoProjection ProjectToDto(this TestObjectProjection testObject); private static TestObjectDtoManuallyMappedProjection? MapManual(string str) diff --git a/test/Riok.Mapperly.IntegrationTests/Mapper/StaticTestMapper.cs b/test/Riok.Mapperly.IntegrationTests/Mapper/StaticTestMapper.cs index 84c41bbf12..f4d3d458cb 100644 --- a/test/Riok.Mapperly.IntegrationTests/Mapper/StaticTestMapper.cs +++ b/test/Riok.Mapperly.IntegrationTests/Mapper/StaticTestMapper.cs @@ -29,6 +29,7 @@ public static partial class StaticTestMapper [MapperIgnoreSource(nameof(TestObject.IgnoredIntValue))] [MapperIgnoreTarget(nameof(TestObjectDto.IgnoredStringValue))] + [MapperIgnoreObsoleteMembers] public static partial TestObjectDto MapToDtoExt(this TestObject src); public static TestObjectDto MapToDto(TestObject src) @@ -53,6 +54,7 @@ public static TestObjectDto MapToDto(TestObject src) )] [MapperIgnoreSource(nameof(TestObject.IgnoredIntValue))] [MapperIgnoreTarget(nameof(TestObjectDto.IgnoredIntValue))] + [MapperIgnoreObsoleteMembers] private static partial TestObjectDto MapToDtoInternal(TestObject testObject); // disable obsolete warning, as the obsolete attribute should still be tested. diff --git a/test/Riok.Mapperly.IntegrationTests/Mapper/TestMapper.cs b/test/Riok.Mapperly.IntegrationTests/Mapper/TestMapper.cs index 05aa8f2b9c..d913d5a542 100644 --- a/test/Riok.Mapperly.IntegrationTests/Mapper/TestMapper.cs +++ b/test/Riok.Mapperly.IntegrationTests/Mapper/TestMapper.cs @@ -47,6 +47,7 @@ public TestObjectDto MapToDto(TestObject src) nameof(TestObject.NullableUnflatteningIdValue), nameof(TestObjectDto.NullableUnflattening) + "." + nameof(TestObjectDto.NullableUnflattening.IdValue) )] + [MapperIgnoreObsoleteMembers] private partial TestObjectDto MapToDtoInternal(TestObject testObject); // disable obsolete warning, as the obsolete attribute should still be tested. diff --git a/test/Riok.Mapperly.Tests/Mapping/IgnoreObsoleteTest.cs b/test/Riok.Mapperly.Tests/Mapping/IgnoreObsoleteTest.cs new file mode 100644 index 0000000000..1dd5768588 --- /dev/null +++ b/test/Riok.Mapperly.Tests/Mapping/IgnoreObsoleteTest.cs @@ -0,0 +1,379 @@ +using Riok.Mapperly.Abstractions; +using Riok.Mapperly.Diagnostics; + +namespace Riok.Mapperly.Tests.Mapping; + +[UsesVerify] +public class IgnoreObsoleteTest +{ + private const string _classA = """ + class A + { + public int Value { get; set; } + + [Obsolete] + public int Ignored { get; set; } + } + """; + + private const string _classB = """ + class B + { + public int Value { get; set; } + + [Obsolete] + public int Ignored { get; set; } + } + """; + + [Fact] + public void ClassAttributeIgnoreObsoleteNone() + { + var source = TestSourceBuilder.Mapping( + "A", + "B", + TestSourceBuilderOptions.WithIgnoreObsolete(IgnoreObsoleteMembersStrategy.None), + _classA, + _classB + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.Value = source.Value; + target.Ignored = source.Ignored; + return target; + """ + ); + } + + [Fact] + public void ClassAttributeIgnoreObsoleteBoth() + { + var source = TestSourceBuilder.Mapping( + "A", + "B", + TestSourceBuilderOptions.WithIgnoreObsolete(IgnoreObsoleteMembersStrategy.Both), + _classA, + _classB + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.Value = source.Value; + return target; + """ + ); + } + + [Fact] + public void ClassAttributeIgnoreSourceShouldDiagnostic() + { + var source = TestSourceBuilder.Mapping( + "A", + "B", + TestSourceBuilderOptions.WithIgnoreObsolete(IgnoreObsoleteMembersStrategy.Source), + _classA, + _classB + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.Value = source.Value; + return target; + """ + ) + .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotFound); + } + + [Fact] + public void ClassAttributeIgnoreTargetShouldDiagnostic() + { + var source = TestSourceBuilder.Mapping( + "A", + "B", + TestSourceBuilderOptions.WithIgnoreObsolete(IgnoreObsoleteMembersStrategy.Target), + _classA, + _classB + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.Value = source.Value; + return target; + """ + ) + .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotMapped); + } + + [Fact] + public void MethodAttributeIgnoreObsoleteNone() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapperIgnoreObsoleteMembers(IgnoreObsoleteMembersStrategy.None)] partial B Map(A source);", + _classA, + _classB + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.Value = source.Value; + target.Ignored = source.Ignored; + return target; + """ + ); + } + + [Fact] + public void MethodAttributeIgnoreObsoleteBoth() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes("[MapperIgnoreObsoleteMembers] partial B Map(A source);", _classA, _classB); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.Value = source.Value; + return target; + """ + ); + } + + [Fact] + public void MethodAttributeIgnoreObsoleteSourceShouldDiagnostic() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapperIgnoreObsoleteMembers(IgnoreObsoleteMembersStrategy.Source)] partial B Map(A source);", + _classA, + _classB + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.Value = source.Value; + return target; + """ + ) + .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotFound); + } + + [Fact] + public void MethodAttributeIgnoreObsoleteTargetShouldDiagnostic() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapperIgnoreObsoleteMembers(IgnoreObsoleteMembersStrategy.Target)] partial B Map(A source);", + _classA, + _classB + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.Value = source.Value; + return target; + """ + ) + .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotMapped); + } + + [Fact] + public void MethodAttributeOverridesClassAttribute() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapperIgnoreObsoleteMembers(IgnoreObsoleteMembersStrategy.None)] partial B Map(A source);", + TestSourceBuilderOptions.WithIgnoreObsolete(IgnoreObsoleteMembersStrategy.Both), + _classA, + _classB + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.Value = source.Value; + target.Ignored = source.Ignored; + return target; + """ + ); + } + + [Fact] + public void MapPropertyOverridesIgnoreObsoleteBoth() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapProperty("Ignored", "Ignored")] + [MapperIgnoreObsoleteMembers] + partial B Map(A source); + """, + _classA, + _classB + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.Value = source.Value; + target.Ignored = source.Ignored; + return target; + """ + ); + } + + [Fact] + public void MapPropertyOverridesIgnoreObsoleteSource() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapProperty("Ignored", "Ignored")] + [MapperIgnoreObsoleteMembers(IgnoreObsoleteMembersStrategy.Source)] + partial B Map(A source); + """, + _classA, + _classB + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.Value = source.Value; + target.Ignored = source.Ignored; + return target; + """ + ); + } + + [Fact] + public void MapPropertyOverridesIgnoreObsoleteTarget() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapProperty("Ignored", "Ignored")] + [MapperIgnoreObsoleteMembers(IgnoreObsoleteMembersStrategy.Target)] + partial B Map(A source); + """, + _classA, + _classB + ); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.Value = source.Value; + target.Ignored = source.Ignored; + return target; + """ + ); + } + + [Fact] + public void MapInitPropertyWhenIgnoreObsoleteTarget() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapProperty("Ignored", "Ignored")] + [MapperIgnoreObsoleteMembers(IgnoreObsoleteMembersStrategy.Target)] + partial B Map(A source); + """, + _classA, + """ + class B + { + public int Value { get; set; } + + [Obsolete] + public int Ignored { get; init; } + } + """ + ); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::B() + { + Ignored = source.Ignored + }; + target.Value = source.Value; + return target; + """ + ); + } + + [Fact] + public void MapRequiredPropertyWhenIgnoreObsoleteTarget() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapProperty("Ignored", "Ignored")] + [MapperIgnoreObsoleteMembers(IgnoreObsoleteMembersStrategy.Target)] + partial B Map(A source); + """, + _classA, + """ + class B + { + public int Value { get; set; } + + [Obsolete] + public required int Ignored { get; set; } + } + """ + ); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::B() + { + Ignored = source.Ignored + }; + target.Value = source.Value; + return target; + """ + ); + } +} diff --git a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyInitPropertyTest.cs b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyInitPropertyTest.cs index 7aa3fe1ff5..5f678290f8 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyInitPropertyTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyInitPropertyTest.cs @@ -312,6 +312,66 @@ public void RequiredProperty() ); } + [Fact] + public void IgnoredTargetRequiredPropertyWithConfiguration() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapProperty("StringValue", "StringValue")] + [MapperIgnoreTarget("StringValue")] + partial B Map(A source); + """, + "A", + "B", + "class A { public string StringValue { get; init; } public int IntValue { get; set; } }", + "class B { public required string StringValue { get; set; } public int IntValue { get; set; } }" + ); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::B() + { + StringValue = source.StringValue + }; + target.IntValue = source.IntValue; + return target; + """ + ); + } + + [Fact] + public void IgnoredTargetInitPropertyWithConfiguration() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapProperty("StringValue", "StringValue")] + [MapperIgnoreTarget("StringValue")] + partial B Map(A source); + """, + "A", + "B", + "class A { public string StringValue { get; init; } public int IntValue { get; set; } }", + "class B { public string StringValue { get; init; } public int IntValue { get; set; } }" + ); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::B() + { + StringValue = source.StringValue + }; + target.IntValue = source.IntValue; + return target; + """ + ); + } + [Fact] public Task RequiredPropertySourceNotFoundShouldDiagnostic() { diff --git a/test/Riok.Mapperly.Tests/TestSourceBuilder.cs b/test/Riok.Mapperly.Tests/TestSourceBuilder.cs index 724cc92778..bee56a2910 100644 --- a/test/Riok.Mapperly.Tests/TestSourceBuilder.cs +++ b/test/Riok.Mapperly.Tests/TestSourceBuilder.cs @@ -85,6 +85,7 @@ private static string BuildAttribute(TestSourceBuilderOptions options) Attribute(options.PropertyNameMappingStrategy), Attribute(options.EnumMappingStrategy), Attribute(options.EnumMappingIgnoreCase), + Attribute(options.IgnoreObsoleteMembersStrategy), }; return $"[Mapper({string.Join(", ", attrs)})]"; diff --git a/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs b/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs index 6fbbf2256a..fcc0c8684c 100644 --- a/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs +++ b/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs @@ -11,13 +11,17 @@ public record TestSourceBuilderOptions( PropertyNameMappingStrategy PropertyNameMappingStrategy = PropertyNameMappingStrategy.CaseSensitive, MappingConversionType EnabledConversions = MappingConversionType.All, EnumMappingStrategy EnumMappingStrategy = EnumMappingStrategy.ByValue, - bool EnumMappingIgnoreCase = false + bool EnumMappingIgnoreCase = false, + IgnoreObsoleteMembersStrategy IgnoreObsoleteMembersStrategy = IgnoreObsoleteMembersStrategy.None ) { public static readonly TestSourceBuilderOptions Default = new(); public static readonly TestSourceBuilderOptions WithDeepCloning = new(UseDeepCloning: true); public static readonly TestSourceBuilderOptions WithReferenceHandling = new(UseReferenceHandling: true); + public static TestSourceBuilderOptions WithIgnoreObsolete(IgnoreObsoleteMembersStrategy ignoreObsoleteStrategy) => + new(IgnoreObsoleteMembersStrategy: ignoreObsoleteStrategy); + public static TestSourceBuilderOptions WithDisabledMappingConversion(params MappingConversionType[] conversionTypes) { var enabled = MappingConversionType.All;