Skip to content

Commit

Permalink
feat: add IgnoreObsoleteMembers
Browse files Browse the repository at this point in the history
  • Loading branch information
TimothyMakkison committed Apr 29, 2023
1 parent e5e2d1c commit 825ac39
Show file tree
Hide file tree
Showing 10 changed files with 309 additions and 3 deletions.
10 changes: 10 additions & 0 deletions src/Riok.Mapperly.Abstractions/IgnoreObsoleteStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Riok.Mapperly.Abstractions;

[Flags]
public enum IgnoreObsoleteStrategy
{
None = 0,
Both = ~None,
Source = 1 << 0,
Target = 1 << 1,
}
5 changes: 5 additions & 0 deletions src/Riok.Mapperly.Abstractions/MapperAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,9 @@ public sealed class MapperAttribute : Attribute
/// to keep track of and reuse existing target object instances.
/// </summary>
public bool UseReferenceHandling { get; set; }

/// <summary>
/// Sets the ingore Obsolete attribute strategy.
/// </summary>
public IgnoreObsoleteStrategy IgnoreObsoleteStrategy { get; set; } = IgnoreObsoleteStrategy.None;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Riok.Mapperly.Abstractions;

/// <summary>
/// Specifies options for obsolete ignoring strategy.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public sealed class MapperIgnoreObsoleteMembersAttribute : Attribute
{
public MapperIgnoreObsoleteMembersAttribute(IgnoreObsoleteStrategy ignoreObsoleteStrategy = IgnoreObsoleteStrategy.Both)
{
IgnoreObsoleteStrategy = ignoreObsoleteStrategy;
}

public IgnoreObsoleteStrategy IgnoreObsoleteStrategy { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,19 +66,60 @@ private HashSet<string> InitIgnoredUnmatchedProperties(IEnumerable<string> allPr

private HashSet<string> GetIgnoredTargetMembers()
{
var obsoleteMembers = GetIgnoredObsoleteTargetMembers();

return BuilderContext
.ListConfiguration<MapperIgnoreTargetAttribute>()
.Select(x => x.Target)
// deprecated MapperIgnoreAttribute, but it is still supported by Mapperly.
#pragma warning disable CS0618
.Concat(BuilderContext.ListConfiguration<MapperIgnoreAttribute>().Select(x => x.Target))
#pragma warning restore CS0618
.Concat(obsoleteMembers)
.ToHashSet();
}

private IEnumerable<string> GetIgnoredObsoleteTargetMembers()
{
var obosoleteAttribute = BuilderContext.ListConfiguration<MapperIgnoreObsoleteMembersAttribute>().FirstOrDefault();

if (
obosoleteAttribute is { IgnoreObsoleteStrategy: IgnoreObsoleteStrategy.Target or IgnoreObsoleteStrategy.Both }
|| BuilderContext.MapperConfiguration.IgnoreObsoleteStrategy is IgnoreObsoleteStrategy.Target or IgnoreObsoleteStrategy.Both
)
{
return Mapping.SourceType
.GetAccessibleMappableMembers()
.Where(x => x.MemberSymbol.HasAttribute(BuilderContext.Types.ObsoleteAttribute))
.Select(x => x.Name);
}

return Enumerable.Empty<string>();
}

private HashSet<string> GetIgnoredSourceMembers()
{
return BuilderContext.ListConfiguration<MapperIgnoreSourceAttribute>().Select(x => x.Source).ToHashSet();
var obsoleteMembers = GetIgnoredObsoleteSourceMembers();

return BuilderContext.ListConfiguration<MapperIgnoreSourceAttribute>().Select(x => x.Source).Concat(obsoleteMembers).ToHashSet();
}

private IEnumerable<string> GetIgnoredObsoleteSourceMembers()
{
var obosoleteAttribute = BuilderContext.ListConfiguration<MapperIgnoreObsoleteMembersAttribute>().FirstOrDefault();

if (
obosoleteAttribute is { IgnoreObsoleteStrategy: IgnoreObsoleteStrategy.Source or IgnoreObsoleteStrategy.Both }
|| BuilderContext.MapperConfiguration.IgnoreObsoleteStrategy is IgnoreObsoleteStrategy.Source or IgnoreObsoleteStrategy.Both
)
{
return Mapping.SourceType
.GetAccessibleMappableMembers()
.Where(x => x.MemberSymbol.HasAttribute(BuilderContext.Types.ObsoleteAttribute))
.Select(x => x.Name);
}

return Enumerable.Empty<string>();
}

private HashSet<string> GetSourceMemberNames()
Expand Down
1 change: 1 addition & 0 deletions src/Riok.Mapperly/Symbols/FieldMember.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 2 additions & 0 deletions src/Riok.Mapperly/Symbols/IMappableMember.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ public interface IMappableMember

ITypeSymbol Type { get; }

ISymbol MemberSymbol { get; }

bool IsNullable { get; }

bool IsIndexer { get; }
Expand Down
1 change: 1 addition & 0 deletions src/Riok.Mapperly/Symbols/PropertyMember.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
227 changes: 227 additions & 0 deletions test/Riok.Mapperly.Tests/Mapping/IgnoreObsoleteTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Diagnostics;

namespace Riok.Mapperly.Tests.Mapping;

public class IgnoreObsoleteTest
{
private readonly string _classA = """
class A
{
public int Value { get; set;}

[Obsolete]
public int Ignored { get; set;}
}
""";

private readonly 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(IgnoreObsoleteStrategy.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(IgnoreObsoleteStrategy.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(IgnoreObsoleteStrategy.Source),
_classA,
_classB
);

TestHelper
.GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics)
.Should()
.HaveSingleMethodBody(
"""
var target = new global::B();
target.Value = source.Value;
return target;
"""
)
.HaveDiagnostic(new(DiagnosticDescriptors.SourceMemberNotFound));
}

[Fact]
public void ClassAttributeIgnoreTargetShouldDiagnostic()
{
var source = TestSourceBuilder.Mapping(
"A",
"B",
TestSourceBuilderOptions.WithIgnoreObsolete(IgnoreObsoleteStrategy.Target),
_classA,
_classB
);

TestHelper
.GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics)
.Should()
.HaveSingleMethodBody(
"""
var target = new global::B();
target.Value = source.Value;
return target;
"""
)
.HaveDiagnostic(new(DiagnosticDescriptors.SourceMemberNotMapped));
}

[Fact]
public void MethodAttributeIgnoreObsoleteNone()
{
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"[MapperIgnoreObsoleteMembers(IgnoreObsoleteStrategy.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(IgnoreObsoleteStrategy.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(new(DiagnosticDescriptors.SourceMemberNotFound));
}

[Fact]
public void MethodAttributeIgnoreObsoleteTargetShouldDiagnostic()
{
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"[MapperIgnoreObsoleteMembers(IgnoreObsoleteStrategy.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(new(DiagnosticDescriptors.SourceMemberNotMapped));
}

[Fact]
public void MethodAttributeOverridesClassAttribute()
{
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"[MapperIgnoreObsoleteMembers(IgnoreObsoleteStrategy.None)] partial B Map(A source);",
TestSourceBuilderOptions.WithIgnoreObsolete(IgnoreObsoleteStrategy.Both),
_classA,
_classB
);

TestHelper
.GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics)
.Should()
.HaveSingleMethodBody(
"""
var target = new global::B();
target.Value = source.Value;
return target;
"""
);
}
}
2 changes: 1 addition & 1 deletion test/Riok.Mapperly.Tests/TestSourceBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ private static string BuildAttribute(TestSourceBuilderOptions options)
Attribute(options.EnabledConversions),
Attribute(options.PropertyNameMappingStrategy),
Attribute(options.EnumMappingStrategy),
Attribute(options.IgnoreObsoleteStrategy),
};

return $"[Mapper({string.Join(", ", attrs)})]";
}

Expand Down
6 changes: 5 additions & 1 deletion test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,17 @@ public record TestSourceBuilderOptions(
bool ThrowOnPropertyMappingNullMismatch = false,
PropertyNameMappingStrategy PropertyNameMappingStrategy = PropertyNameMappingStrategy.CaseSensitive,
MappingConversionType EnabledConversions = MappingConversionType.All,
EnumMappingStrategy EnumMappingStrategy = EnumMappingStrategy.ByValue
EnumMappingStrategy EnumMappingStrategy = EnumMappingStrategy.ByValue,
IgnoreObsoleteStrategy IgnoreObsoleteStrategy = IgnoreObsoleteStrategy.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(IgnoreObsoleteStrategy ignoreObsoleteStrategy) =>
new(IgnoreObsoleteStrategy: ignoreObsoleteStrategy);

public static TestSourceBuilderOptions WithDisabledMappingConversion(params MappingConversionType[] conversionTypes)
{
var enabled = MappingConversionType.All;
Expand Down

0 comments on commit 825ac39

Please sign in to comment.