diff --git a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs
index a8c7de5750..f6832b3e5d 100644
--- a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs
+++ b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs
@@ -86,4 +86,10 @@ public class MapperAttribute : Attribute
/// Defaults to .
///
public IgnoreObsoleteMembersStrategy IgnoreObsoleteMembersStrategy { get; set; } = IgnoreObsoleteMembersStrategy.None;
+
+ ///
+ /// Defines the strategy used when emitting warnings for unmapped members.
+ /// By default this is , emitting warnings for unmapped source and target members.
+ ///
+ public RequiredMappingStrategy RequiredMappingStrategy { get; set; } = RequiredMappingStrategy.Both;
}
diff --git a/src/Riok.Mapperly.Abstractions/MapperRequiredMappingAttribute.cs b/src/Riok.Mapperly.Abstractions/MapperRequiredMappingAttribute.cs
new file mode 100644
index 0000000000..45fed9ecc6
--- /dev/null
+++ b/src/Riok.Mapperly.Abstractions/MapperRequiredMappingAttribute.cs
@@ -0,0 +1,22 @@
+namespace Riok.Mapperly.Abstractions;
+
+///
+/// Defines the strategy used when emitting warnings for unmapped members.
+///
+[AttributeUsage(AttributeTargets.Method)]
+public sealed class MapperRequiredMappingAttribute : Attribute
+{
+ ///
+ /// Defines the strategy used when emitting warnings for unmapped members.
+ ///
+ /// The strategy used when emitting warnings for unmapped members.
+ public MapperRequiredMappingAttribute(RequiredMappingStrategy requiredMappingStrategy)
+ {
+ RequiredMappingStrategy = requiredMappingStrategy;
+ }
+
+ ///
+ /// The strategy used when emitting warnings for unmapped members.
+ ///
+ public RequiredMappingStrategy RequiredMappingStrategy { get; }
+}
diff --git a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
index 80b1edf4ec..80be9349f8 100644
--- a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
+++ b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
@@ -109,3 +109,13 @@ Riok.Mapperly.Abstractions.UseStaticMapperAttribute
Riok.Mapperly.Abstractions.UseStaticMapperAttribute.UseStaticMapperAttribute(System.Type! mapperType) -> void
Riok.Mapperly.Abstractions.UseStaticMapperAttribute
Riok.Mapperly.Abstractions.UseStaticMapperAttribute.UseStaticMapperAttribute() -> void
+Riok.Mapperly.Abstractions.MapperAttribute.RequiredMappingStrategy.get -> Riok.Mapperly.Abstractions.RequiredMappingStrategy
+Riok.Mapperly.Abstractions.MapperAttribute.RequiredMappingStrategy.set -> void
+Riok.Mapperly.Abstractions.MapperRequiredMappingAttribute
+Riok.Mapperly.Abstractions.MapperRequiredMappingAttribute.MapperRequiredMappingAttribute(Riok.Mapperly.Abstractions.RequiredMappingStrategy requiredMappingStrategy) -> void
+Riok.Mapperly.Abstractions.MapperRequiredMappingAttribute.RequiredMappingStrategy.get -> Riok.Mapperly.Abstractions.RequiredMappingStrategy
+Riok.Mapperly.Abstractions.RequiredMappingStrategy
+Riok.Mapperly.Abstractions.RequiredMappingStrategy.Both = -1 -> Riok.Mapperly.Abstractions.RequiredMappingStrategy
+Riok.Mapperly.Abstractions.RequiredMappingStrategy.None = 0 -> Riok.Mapperly.Abstractions.RequiredMappingStrategy
+Riok.Mapperly.Abstractions.RequiredMappingStrategy.Source = 1 -> Riok.Mapperly.Abstractions.RequiredMappingStrategy
+Riok.Mapperly.Abstractions.RequiredMappingStrategy.Target = 2 -> Riok.Mapperly.Abstractions.RequiredMappingStrategy
diff --git a/src/Riok.Mapperly.Abstractions/RequiredMappingStrategy.cs b/src/Riok.Mapperly.Abstractions/RequiredMappingStrategy.cs
new file mode 100644
index 0000000000..b7593fba1b
--- /dev/null
+++ b/src/Riok.Mapperly.Abstractions/RequiredMappingStrategy.cs
@@ -0,0 +1,28 @@
+namespace Riok.Mapperly.Abstractions;
+
+///
+/// Defines the strategy used when emitting warnings for unmapped members.
+///
+[Flags]
+public enum RequiredMappingStrategy
+{
+ ///
+ /// Warnings are not emitted for unmapped source or target members.
+ ///
+ None = 0,
+
+ ///
+ /// Warnings are emitted for both unmapped source and target members.
+ ///
+ Both = ~None,
+
+ ///
+ /// Warnings are emitted for unmapped source members but not for target members.
+ ///
+ Source = 1 << 0,
+
+ ///
+ /// Warnings are emitted for unmapped target members but not for source members.
+ ///
+ Target = 1 << 1
+}
diff --git a/src/Riok.Mapperly/Configuration/MapperConfiguration.cs b/src/Riok.Mapperly/Configuration/MapperConfiguration.cs
index b234aa9e13..270049f41a 100644
--- a/src/Riok.Mapperly/Configuration/MapperConfiguration.cs
+++ b/src/Riok.Mapperly/Configuration/MapperConfiguration.cs
@@ -92,4 +92,10 @@ public record MapperConfiguration
/// Defaults to .
///
public IgnoreObsoleteMembersStrategy? IgnoreObsoleteMembersStrategy { get; init; }
+
+ ///
+ /// Defines the strategy used when emitting warnings for unmapped members.
+ /// By default this is , emitting warnings for unmapped source and target members.
+ ///
+ public RequiredMappingStrategy? RequiredMappingStrategy { get; init; }
}
diff --git a/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs b/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs
index 5e386056f9..f83f5843b0 100644
--- a/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs
+++ b/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs
@@ -46,6 +46,11 @@ public static MapperAttribute Merge(MapperConfiguration mapperConfiguration, Map
?? defaultMapperConfiguration.IgnoreObsoleteMembersStrategy
?? mapper.IgnoreObsoleteMembersStrategy;
+ mapper.RequiredMappingStrategy =
+ mapperConfiguration.RequiredMappingStrategy
+ ?? defaultMapperConfiguration.RequiredMappingStrategy
+ ?? mapper.RequiredMappingStrategy;
+
return mapper;
}
}
diff --git a/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs b/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs
index 795eb0ae9b..31eb60f357 100644
--- a/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs
+++ b/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs
@@ -32,7 +32,8 @@ MapperConfiguration defaultMapperConfiguration
Array.Empty(),
Array.Empty(),
Array.Empty(),
- Mapper.IgnoreObsoleteMembersStrategy
+ Mapper.IgnoreObsoleteMembersStrategy,
+ Mapper.RequiredMappingStrategy
),
Array.Empty()
);
@@ -75,8 +76,17 @@ private PropertiesMappingConfiguration BuildPropertiesConfig(IMethodSymbol metho
var ignoreObsolete = _dataAccessor.Access(method).FirstOrDefault() is not { } methodIgnore
? _defaultConfiguration.Properties.IgnoreObsoleteMembersStrategy
: methodIgnore.IgnoreObsoleteStrategy;
+ var requiredMapping = _dataAccessor.Access(method).FirstOrDefault() is not { } methodWarnUnmapped
+ ? _defaultConfiguration.Properties.RequiredMappingStrategy
+ : methodWarnUnmapped.RequiredMappingStrategy;
- return new PropertiesMappingConfiguration(ignoredSourceProperties, ignoredTargetProperties, explicitMappings, ignoreObsolete);
+ return new PropertiesMappingConfiguration(
+ ignoredSourceProperties,
+ ignoredTargetProperties,
+ explicitMappings,
+ ignoreObsolete,
+ requiredMapping
+ );
}
private EnumMappingConfiguration BuildEnumConfig(MappingConfigurationReference configRef)
diff --git a/src/Riok.Mapperly/Configuration/PropertiesMappingConfiguration.cs b/src/Riok.Mapperly/Configuration/PropertiesMappingConfiguration.cs
index f991cc536a..9d65612eb8 100644
--- a/src/Riok.Mapperly/Configuration/PropertiesMappingConfiguration.cs
+++ b/src/Riok.Mapperly/Configuration/PropertiesMappingConfiguration.cs
@@ -6,5 +6,6 @@ public record PropertiesMappingConfiguration(
IReadOnlyCollection IgnoredSources,
IReadOnlyCollection IgnoredTargets,
IReadOnlyCollection ExplicitMappings,
- IgnoreObsoleteMembersStrategy IgnoreObsoleteMembersStrategy
+ IgnoreObsoleteMembersStrategy IgnoreObsoleteMembersStrategy,
+ RequiredMappingStrategy RequiredMappingStrategy
);
diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingBuilderContext.cs
index d91d570023..fe11e2cc21 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingBuilderContext.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingBuilderContext.cs
@@ -166,6 +166,9 @@ private void AddUnmatchedTargetMembersDiagnostics()
private void AddUnmatchedSourceMembersDiagnostics()
{
+ if (!BuilderContext.Configuration.Properties.RequiredMappingStrategy.HasFlag(RequiredMappingStrategy.Source))
+ return;
+
foreach (var sourceMemberName in _unmappedSourceMemberNames)
{
BuilderContext.ReportDiagnostic(
diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewInstanceObjectMemberMappingBodyBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewInstanceObjectMemberMappingBodyBuilder.cs
index 0ca922df48..1154db4e38 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewInstanceObjectMemberMappingBodyBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewInstanceObjectMemberMappingBodyBuilder.cs
@@ -59,12 +59,25 @@ out var sourceMemberPath
)
)
{
- ctx.BuilderContext.ReportDiagnostic(
- targetMember.IsRequired ? DiagnosticDescriptors.RequiredMemberNotMapped : DiagnosticDescriptors.SourceMemberNotFound,
- targetMember.Name,
- ctx.Mapping.TargetType,
- ctx.Mapping.SourceType
- );
+ if (targetMember.IsRequired)
+ {
+ ctx.BuilderContext.ReportDiagnostic(
+ DiagnosticDescriptors.RequiredMemberNotMapped,
+ targetMember.Name,
+ ctx.Mapping.TargetType,
+ ctx.Mapping.SourceType
+ );
+ }
+ else if (ctx.BuilderContext.Configuration.Properties.RequiredMappingStrategy.HasFlag(RequiredMappingStrategy.Target))
+ {
+ ctx.BuilderContext.ReportDiagnostic(
+ DiagnosticDescriptors.SourceMemberNotFound,
+ targetMember.Name,
+ ctx.Mapping.TargetType,
+ ctx.Mapping.SourceType
+ );
+ }
+
continue;
}
@@ -104,12 +117,16 @@ IReadOnlyCollection memberConfigs
!ctx.BuilderContext.SymbolAccessor.TryFindMemberPath(ctx.Mapping.SourceType, memberConfig.Source.Path, out var sourceMemberPath)
)
{
- ctx.BuilderContext.ReportDiagnostic(
- DiagnosticDescriptors.SourceMemberNotFound,
- targetMember.Name,
- ctx.Mapping.TargetType,
- ctx.Mapping.SourceType
- );
+ if (ctx.BuilderContext.Configuration.Properties.RequiredMappingStrategy.HasFlag(RequiredMappingStrategy.Target))
+ {
+ ctx.BuilderContext.ReportDiagnostic(
+ DiagnosticDescriptors.SourceMemberNotFound,
+ targetMember.Name,
+ ctx.Mapping.TargetType,
+ ctx.Mapping.SourceType
+ );
+ }
+
return;
}
diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/ObjectMemberMappingBodyBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/ObjectMemberMappingBodyBuilder.cs
index d30e8b1a4d..164276c191 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/ObjectMemberMappingBodyBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/ObjectMemberMappingBodyBuilder.cs
@@ -54,7 +54,10 @@ out var sourceMemberPath
continue;
}
- if (targetMember.CanSet)
+ if (
+ targetMember.CanSet
+ && ctx.BuilderContext.Configuration.Properties.RequiredMappingStrategy.HasFlag(RequiredMappingStrategy.Target)
+ )
{
ctx.BuilderContext.ReportDiagnostic(
DiagnosticDescriptors.SourceMemberNotFound,
diff --git a/test/Riok.Mapperly.IntegrationTests/Mapper/StaticTestMapper.cs b/test/Riok.Mapperly.IntegrationTests/Mapper/StaticTestMapper.cs
index 1f1c71ec7a..a5623620ac 100644
--- a/test/Riok.Mapperly.IntegrationTests/Mapper/StaticTestMapper.cs
+++ b/test/Riok.Mapperly.IntegrationTests/Mapper/StaticTestMapper.cs
@@ -30,6 +30,7 @@ public static partial class StaticTestMapper
[MapperIgnoreSource(nameof(TestObject.IgnoredIntValue))]
[MapperIgnoreTarget(nameof(TestObjectDto.IgnoredStringValue))]
[MapperIgnoreObsoleteMembers]
+ [MapperRequiredMapping(RequiredMappingStrategy.Target)]
public static partial TestObjectDto MapToDtoExt(this TestObject src);
public static TestObjectDto MapToDto(TestObject src)
diff --git a/test/Riok.Mapperly.IntegrationTests/Models/TestObject.cs b/test/Riok.Mapperly.IntegrationTests/Models/TestObject.cs
index f2a273ba73..21e1d94064 100644
--- a/test/Riok.Mapperly.IntegrationTests/Models/TestObject.cs
+++ b/test/Riok.Mapperly.IntegrationTests/Models/TestObject.cs
@@ -26,6 +26,8 @@ public TestObject(int ctorValue, int unknownValue = 10, int ctorValue2 = 100)
public int RequiredValue { get; init; }
#endif
+ public int UnmappedValue => 10;
+
public string StringValue { get; set; } = string.Empty;
public string RenamedStringValue { get; set; } = string.Empty;
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningMapperTest.RunMappingShouldWork_NET6_0.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningMapperTest.RunMappingShouldWork_NET6_0.verified.txt
index 97299a4121..a6fbf2c826 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningMapperTest.RunMappingShouldWork_NET6_0.verified.txt
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningMapperTest.RunMappingShouldWork_NET6_0.verified.txt
@@ -4,6 +4,7 @@
IntValue: 10,
IntInitOnlyValue: 3,
RequiredValue: 4,
+ UnmappedValue: 10,
StringValue: fooBar,
RenamedStringValue: fooBar2,
Flattening: {
@@ -27,6 +28,7 @@
CtorValue: 5,
CtorValue2: 100,
RequiredValue: 4,
+ UnmappedValue: 10,
StringValue: ,
RenamedStringValue: ,
Flattening: {},
@@ -45,6 +47,7 @@
CtorValue2: 100,
IntValue: 99,
RequiredValue: 98,
+ UnmappedValue: 10,
StringValue: ,
RenamedStringValue: ,
Flattening: {},
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.RunMappingShouldWork_NET6_0.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.RunMappingShouldWork_NET6_0.verified.txt
index 128e6b3084..23d9668a6f 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.RunMappingShouldWork_NET6_0.verified.txt
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.RunMappingShouldWork_NET6_0.verified.txt
@@ -54,6 +54,7 @@
CtorValue2: 100,
IntValue: 99,
RequiredValue: 98,
+ UnmappedValue: 10,
StringValue: ,
RenamedStringValue: ,
Flattening: {},
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.RunExtensionMappingShouldWork_NET6_0.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.RunExtensionMappingShouldWork_NET6_0.verified.txt
index 6b3cde8e51..257a3273fd 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.RunExtensionMappingShouldWork_NET6_0.verified.txt
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.RunExtensionMappingShouldWork_NET6_0.verified.txt
@@ -49,6 +49,7 @@
CtorValue2: 100,
IntValue: 99,
RequiredValue: 98,
+ UnmappedValue: 10,
StringValue: ,
RenamedStringValue: ,
Flattening: {},
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.RunMappingShouldWork_NET6_0.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.RunMappingShouldWork_NET6_0.verified.txt
index 4812c0f332..9f4d61564e 100644
--- a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.RunMappingShouldWork_NET6_0.verified.txt
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.RunMappingShouldWork_NET6_0.verified.txt
@@ -54,6 +54,7 @@
CtorValue2: 100,
IntValue: 99,
RequiredValue: 98,
+ UnmappedValue: 10,
StringValue: ,
RenamedStringValue: ,
Flattening: {},
diff --git a/test/Riok.Mapperly.Tests/Helpers/MapperConfigurationBuilderTest.cs b/test/Riok.Mapperly.Tests/Helpers/MapperConfigurationBuilderTest.cs
index 1e1e06617f..4b153f7096 100644
--- a/test/Riok.Mapperly.Tests/Helpers/MapperConfigurationBuilderTest.cs
+++ b/test/Riok.Mapperly.Tests/Helpers/MapperConfigurationBuilderTest.cs
@@ -21,6 +21,7 @@ public void ShouldMergeMapperConfigurations()
EnabledConversions = MappingConversionType.Dictionary,
UseReferenceHandling = false,
IgnoreObsoleteMembersStrategy = IgnoreObsoleteMembersStrategy.Target,
+ RequiredMappingStrategy = RequiredMappingStrategy.Target,
};
var mapper = MapperConfigurationMerger.Merge(mapperConfiguration, defaultMapperConfiguration);
@@ -34,6 +35,7 @@ public void ShouldMergeMapperConfigurations()
mapper.EnabledConversions.Should().Be(MappingConversionType.Constructor);
mapper.UseReferenceHandling.Should().BeTrue();
mapper.IgnoreObsoleteMembersStrategy.Should().Be(IgnoreObsoleteMembersStrategy.Source);
+ mapper.RequiredMappingStrategy.Should().Be(RequiredMappingStrategy.Source);
}
[Fact]
@@ -51,6 +53,7 @@ public void ShouldMergeMapperConfigurationsWithEmptyDefaultMapperConfiguration()
mapper.EnabledConversions.Should().Be(MappingConversionType.Constructor);
mapper.UseReferenceHandling.Should().BeTrue();
mapper.IgnoreObsoleteMembersStrategy.Should().Be(IgnoreObsoleteMembersStrategy.Source);
+ mapper.RequiredMappingStrategy.Should().Be(RequiredMappingStrategy.Source);
}
[Fact]
@@ -84,6 +87,7 @@ private MapperConfiguration NewMapperConfiguration()
EnabledConversions = MappingConversionType.Constructor,
UseReferenceHandling = true,
IgnoreObsoleteMembersStrategy = IgnoreObsoleteMembersStrategy.Source,
+ RequiredMappingStrategy = RequiredMappingStrategy.Source,
};
}
}
diff --git a/test/Riok.Mapperly.Tests/Mapping/RequiredMappingTest.cs b/test/Riok.Mapperly.Tests/Mapping/RequiredMappingTest.cs
new file mode 100644
index 0000000000..476f010c9a
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/Mapping/RequiredMappingTest.cs
@@ -0,0 +1,242 @@
+using Riok.Mapperly.Abstractions;
+using Riok.Mapperly.Diagnostics;
+
+namespace Riok.Mapperly.Tests.Mapping;
+
+[UsesVerify]
+public class RequiredMappingTest
+{
+ [Fact]
+ public void ClassAttributeRequiredMappingSource()
+ {
+ var source = TestSourceBuilder.Mapping(
+ "A",
+ "B",
+ TestSourceBuilderOptions.WithRequiredMappingStrategy(RequiredMappingStrategy.Source),
+ "class A { public int Value { get; set; } public int UnmappedSource { get; set; } }",
+ "class B { public int Value { get; set; } public int UnmappedTarget { get; set; } }"
+ );
+
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics)
+ .Should()
+ .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotMapped)
+ .HaveAssertedAllDiagnostics()
+ .HaveSingleMethodBody(
+ """
+ var target = new global::B();
+ target.Value = source.Value;
+ return target;
+ """
+ );
+ }
+
+ [Fact]
+ public void ClassAttributeRequiredMappingTarget()
+ {
+ var source = TestSourceBuilder.Mapping(
+ "A",
+ "B",
+ TestSourceBuilderOptions.WithRequiredMappingStrategy(RequiredMappingStrategy.Target),
+ "class A { public int Value { get; set; } public int UnmappedSource { get; set; } }",
+ "class B { public int Value { get; set; } public int UnmappedTarget { get; set; } }"
+ );
+
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics)
+ .Should()
+ .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotFound)
+ .HaveAssertedAllDiagnostics()
+ .HaveSingleMethodBody(
+ """
+ var target = new global::B();
+ target.Value = source.Value;
+ return target;
+ """
+ );
+ }
+
+ [Fact]
+ public void ClassAttributeRequiredMappingBoth()
+ {
+ var source = TestSourceBuilder.Mapping(
+ "A",
+ "B",
+ TestSourceBuilderOptions.WithRequiredMappingStrategy(RequiredMappingStrategy.Both),
+ "class A { public int Value { get; set; } public int UnmappedSource { get; set; } }",
+ "class B { public int Value { get; set; } public int UnmappedTarget { get; set; } }"
+ );
+
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics)
+ .Should()
+ .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotFound)
+ .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotMapped)
+ .HaveAssertedAllDiagnostics()
+ .HaveSingleMethodBody(
+ """
+ var target = new global::B();
+ target.Value = source.Value;
+ return target;
+ """
+ );
+ }
+
+ [Fact]
+ public void ClassAttributeRequiredMappingNone()
+ {
+ var source = TestSourceBuilder.Mapping(
+ "A",
+ "B",
+ TestSourceBuilderOptions.WithRequiredMappingStrategy(RequiredMappingStrategy.None),
+ "class A { public int Value { get; set; } public int UnmappedSource { get; set; } }",
+ "class B { public int Value { get; set; } public int UnmappedTarget { get; set; } }"
+ );
+
+ TestHelper
+ .GenerateMapper(source)
+ .Should()
+ .HaveAssertedAllDiagnostics()
+ .HaveSingleMethodBody(
+ """
+ var target = new global::B();
+ target.Value = source.Value;
+ return target;
+ """
+ );
+ }
+
+ [Fact]
+ public void MethodAttributeRequiredMappingSource()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ "[MapperRequiredMappingAttribute(RequiredMappingStrategy.Source)] partial B Map(A source);",
+ "class A { public int Value { get; set; } public int UnmappedSource { get; set; } }",
+ "class B { public int Value { get; set; } public int UnmappedTarget { get; set; } }"
+ );
+
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics)
+ .Should()
+ .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotMapped)
+ .HaveAssertedAllDiagnostics()
+ .HaveSingleMethodBody(
+ """
+ var target = new global::B();
+ target.Value = source.Value;
+ return target;
+ """
+ );
+ }
+
+ [Fact]
+ public void MethodAttributeRequiredMappingTarget()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ "[MapperRequiredMappingAttribute(RequiredMappingStrategy.Target)] partial B Map(A source);",
+ "class A { public int Value { get; set; } public int UnmappedSource { get; set; } }",
+ "class B { public int Value { get; set; } public int UnmappedTarget { get; set; } }"
+ );
+
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics)
+ .Should()
+ .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotFound)
+ .HaveAssertedAllDiagnostics()
+ .HaveSingleMethodBody(
+ """
+ var target = new global::B();
+ target.Value = source.Value;
+ return target;
+ """
+ );
+ }
+
+ [Fact]
+ public void MethodAttributeRequiredMappingBoth()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ "[MapperRequiredMappingAttribute(RequiredMappingStrategy.Both)] partial B Map(A source);",
+ "class A { public int Value { get; set; } public int UnmappedSource { get; set; } }",
+ "class B { public int Value { get; set; } public int UnmappedTarget { get; set; } }"
+ );
+
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics)
+ .Should()
+ .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotFound)
+ .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotMapped)
+ .HaveAssertedAllDiagnostics()
+ .HaveSingleMethodBody(
+ """
+ var target = new global::B();
+ target.Value = source.Value;
+ return target;
+ """
+ );
+ }
+
+ [Fact]
+ public void MethodAttributeRequiredMappingNone()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ "[MapperRequiredMappingAttribute(RequiredMappingStrategy.None)] partial B Map(A source);",
+ "class A { public int Value { get; set; } public int UnmappedSource { get; set; } }",
+ "class B { public int Value { get; set; } public int UnmappedTarget { get; set; } }"
+ );
+
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics)
+ .Should()
+ .HaveAssertedAllDiagnostics()
+ .HaveSingleMethodBody(
+ """
+ var target = new global::B();
+ target.Value = source.Value;
+ return target;
+ """
+ );
+ }
+
+ [Fact]
+ public void MethodAttributeOverridesClass()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ "[MapperRequiredMappingAttribute(RequiredMappingStrategy.Target)] partial B Map(A source);",
+ TestSourceBuilderOptions.WithRequiredMappingStrategy(RequiredMappingStrategy.Source),
+ "class A { public int Value { get; set; } public int UnmappedSource { get; set; } }",
+ "class B { public int Value { get; set; } public int UnmappedTarget { get; set; } }"
+ );
+
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics)
+ .Should()
+ .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotFound)
+ .HaveAssertedAllDiagnostics()
+ .HaveSingleMethodBody(
+ """
+ var target = new global::B();
+ target.Value = source.Value;
+ return target;
+ """
+ );
+ }
+
+ [Fact]
+ public void AllowUnmappedTargetTupleShouldDiagnostic()
+ {
+ var source = TestSourceBuilder.Mapping(
+ "A",
+ "(int MyValue, int Value)",
+ TestSourceBuilderOptions.WithRequiredMappingStrategy(RequiredMappingStrategy.Target),
+ "class A { public int Value { get; set; } }"
+ );
+
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.AllowAllDiagnostics)
+ .Should()
+ .HaveDiagnostic(DiagnosticDescriptors.NoConstructorFound)
+ .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotFound)
+ .HaveAssertedAllDiagnostics();
+ }
+}
diff --git a/test/Riok.Mapperly.Tests/TestSourceBuilder.cs b/test/Riok.Mapperly.Tests/TestSourceBuilder.cs
index 0d39237741..41f2ad5545 100644
--- a/test/Riok.Mapperly.Tests/TestSourceBuilder.cs
+++ b/test/Riok.Mapperly.Tests/TestSourceBuilder.cs
@@ -90,6 +90,7 @@ private static string BuildAttribute(TestSourceBuilderOptions options)
Attribute(options.EnumMappingStrategy),
Attribute(options.EnumMappingIgnoreCase),
Attribute(options.IgnoreObsoleteMembersStrategy),
+ Attribute(options.RequiredMappingStrategy),
}.WhereNotNull();
return $"[Mapper({string.Join(", ", attrs)})]";
diff --git a/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs b/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs
index 69c28d2f06..8824846829 100644
--- a/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs
+++ b/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs
@@ -14,7 +14,8 @@ public record TestSourceBuilderOptions(
MappingConversionType? EnabledConversions = null,
EnumMappingStrategy? EnumMappingStrategy = null,
bool? EnumMappingIgnoreCase = null,
- IgnoreObsoleteMembersStrategy? IgnoreObsoleteMembersStrategy = null
+ IgnoreObsoleteMembersStrategy? IgnoreObsoleteMembersStrategy = null,
+ RequiredMappingStrategy? RequiredMappingStrategy = null
)
{
public const string DefaultMapperClassName = "Mapper";
@@ -26,6 +27,9 @@ public record TestSourceBuilderOptions(
public static TestSourceBuilderOptions WithIgnoreObsolete(IgnoreObsoleteMembersStrategy ignoreObsoleteStrategy) =>
new(IgnoreObsoleteMembersStrategy: ignoreObsoleteStrategy);
+ public static TestSourceBuilderOptions WithRequiredMappingStrategy(RequiredMappingStrategy requiredMappingStrategy) =>
+ new(RequiredMappingStrategy: requiredMappingStrategy);
+
public static TestSourceBuilderOptions WithDisabledMappingConversion(params MappingConversionType[] conversionTypes)
{
var enabled = MappingConversionType.All;