From b27c01b79b2da32a66fced0c8b4ccebc17d699f0 Mon Sep 17 00:00:00 2001 From: Timothy Makkison Date: Sat, 29 Apr 2023 22:21:22 +0100 Subject: [PATCH] feat: add IgnoreObsoleteMembers --- .../IgnoreObsoleteMembersStrategy.cs | 30 ++ .../MapperAttribute.cs | 6 + .../MapperIgnoreObsoleteMembersAttribute.cs | 22 + .../PublicAPI.Shipped.txt | 10 + .../Configuration/MapperConfiguration.cs | 13 +- .../PropertiesMappingConfiguration.cs | 3 +- .../MembersMappingBuilderContext.cs | 45 ++- src/Riok.Mapperly/Symbols/FieldMember.cs | 1 + src/Riok.Mapperly/Symbols/IMappableMember.cs | 2 + src/Riok.Mapperly/Symbols/PropertyMember.cs | 1 + .../Mapping/IgnoreObsoleteTest.cs | 378 ++++++++++++++++++ .../Mapping/ObjectPropertyInitPropertyTest.cs | 60 +++ test/Riok.Mapperly.Tests/TestSourceBuilder.cs | 1 + .../TestSourceBuilderOptions.cs | 6 +- 14 files changed, 572 insertions(+), 6 deletions(-) create mode 100644 src/Riok.Mapperly.Abstractions/IgnoreObsoleteMembersStrategy.cs create mode 100644 src/Riok.Mapperly.Abstractions/MapperIgnoreObsoleteMembersAttribute.cs create mode 100644 test/Riok.Mapperly.Tests/Mapping/IgnoreObsoleteTest.cs 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 fabcfb3fb2..e5373e22e5 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 44ec09dd24..5d82f97395 100644 --- a/src/Riok.Mapperly/Configuration/MapperConfiguration.cs +++ b/src/Riok.Mapperly/Configuration/MapperConfiguration.cs @@ -20,7 +20,12 @@ public MapperConfiguration(WellKnownTypes wellKnownTypes, ISymbol mapperSymbol) null, Array.Empty() ), - new PropertiesMappingConfiguration(Array.Empty(), Array.Empty(), Array.Empty()), + new PropertiesMappingConfiguration( + Array.Empty(), + Array.Empty(), + Array.Empty(), + Mapper.IgnoreObsoleteMembersStrategy + ), Array.Empty() ); } @@ -58,7 +63,11 @@ private PropertiesMappingConfiguration BuildPropertiesConfig(IMethodSymbol metho #pragma warning restore CS0618 .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(IMethodSymbol method) diff --git a/src/Riok.Mapperly/Configuration/PropertiesMappingConfiguration.cs b/src/Riok.Mapperly/Configuration/PropertiesMappingConfiguration.cs index bf53692e59..f769b23c94 100644 --- a/src/Riok.Mapperly/Configuration/PropertiesMappingConfiguration.cs +++ b/src/Riok.Mapperly/Configuration/PropertiesMappingConfiguration.cs @@ -5,5 +5,6 @@ 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 6af0811164..65e621f5c7 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingBuilderContext.cs @@ -21,11 +21,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 +40,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 +76,37 @@ private HashSet InitIgnoredUnmatchedProperties(IEnumerable allPr return unmatched; } + private IEnumerable GetIgnoredObsoleteTargetMembers() + { + var obsoleteStrategy = BuilderContext.Configuration.Properties.IgnoreObsoleteMembersStrategy; + + if (!obsoleteStrategy.HasFlag(IgnoreObsoleteMembersStrategy.Target)) + return Enumerable.Empty(); + + var obsoleteAttribute = BuilderContext.Types.Get(); + return Mapping.TargetType + .GetAccessibleMappableMembers() + .Where(x => x.MemberSymbol.HasAttribute(obsoleteAttribute)) + .Select(x => x.Name); + } + + private IEnumerable GetIgnoredObsoleteSourceMembers() + { + var obsoleteStrategy = BuilderContext.Configuration.Properties.IgnoreObsoleteMembersStrategy; + + if (obsoleteStrategy.HasFlag(IgnoreObsoleteMembersStrategy.Source)) + { + var obsoleteAttribute = BuilderContext.Types.Get(); + + return Mapping.SourceType + .GetAccessibleMappableMembers() + .Where(x => x.MemberSymbol.HasAttribute(obsoleteAttribute)) + .Select(x => x.Name); + } + + return Enumerable.Empty(); + } + private HashSet GetSourceMemberNames() { return Mapping.SourceType.GetAccessibleMappableMembers().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.Tests/Mapping/IgnoreObsoleteTest.cs b/test/Riok.Mapperly.Tests/Mapping/IgnoreObsoleteTest.cs new file mode 100644 index 0000000000..f8562e5607 --- /dev/null +++ b/test/Riok.Mapperly.Tests/Mapping/IgnoreObsoleteTest.cs @@ -0,0 +1,378 @@ +using Riok.Mapperly.Abstractions; +using Riok.Mapperly.Diagnostics; + +namespace Riok.Mapperly.Tests.Mapping; + +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 c0f4505e62..98af91f17d 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 bcb313570f..d50121d5a2 100644 --- a/test/Riok.Mapperly.Tests/TestSourceBuilder.cs +++ b/test/Riok.Mapperly.Tests/TestSourceBuilder.cs @@ -78,6 +78,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;