diff --git a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs
index e350c7cd9f..f12bf77a1b 100644
--- a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs
+++ b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs
@@ -28,6 +28,23 @@ public sealed class MapperAttribute : Attribute
///
public bool EnumMappingIgnoreCase { get; set; }
+ ///
+ /// Specifies the behaviour in the case when the mapper tries to return null in a mapping method with a non-nullable return type.
+ /// If set to true an is thrown.
+ /// If set to false the mapper tries to return a default value.
+ /// For a this is ,
+ /// for value types default
+ /// and for reference types new() if a parameterless constructor exists or else an is thrown.
+ ///
+ public bool ThrowOnMappingNullMismatch { get; set; } = true;
+
+ ///
+ /// Specifies the behaviour in the case when the mapper tries to set a non-nullable property to a null value.
+ /// If set to true an is thrown.
+ /// If set to false the property assignment is ignored.
+ ///
+ public bool ThrowOnPropertyMappingNullMismatch { get; set; }
+
///
/// Whether to always deep copy objects.
/// Eg. when the type Person[] should be mapped to the same type Person[],
diff --git a/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs b/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs
index cae41ddb98..639586cc68 100644
--- a/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs
@@ -14,6 +14,7 @@ public class DescriptorBuilder
private static readonly IReadOnlyCollection _mappingBuilders = new MappingBuilder[]
{
+ NullableMappingBuilder.TryBuildMapping,
SpecialTypeMappingBuilder.TryBuildMapping,
DirectAssignmentMappingBuilder.TryBuildMapping,
DictionaryMappingBuilder.TryBuildMapping,
@@ -40,7 +41,7 @@ public class DescriptorBuilder
private readonly Dictionary _defaultConfigurations = new();
// this includes mappings to build and already built mappings
- private readonly Dictionary<(ITypeSymbol SourceType, ITypeSymbol TargetType), TypeMapping> _mappings = new();
+ private readonly Dictionary<(ITypeSymbol SourceType, ITypeSymbol TargetType), TypeMapping> _mappings = new(new MappingTupleEqualityComparer());
// additional user defined mappings
// (with same signature as already defined mappings but with different names)
@@ -131,24 +132,47 @@ public MapperDescriptor Build()
_mapperDescriptor.AddTypeMapping(extraMapping);
}
+ // set method names
+ foreach (var methodMapping in _mapperDescriptor.MethodTypeMappings)
+ {
+ methodMapping.SetMethodNameIfNeeded(_methodNameBuilder.Build);
+ }
+
return _mapperDescriptor;
}
- internal TypeMapping? FindOrBuildMapping(
+ public TypeMapping? FindOrBuildMapping(
ITypeSymbol sourceType,
ITypeSymbol targetType)
{
if (FindMapping(sourceType, targetType) is { } foundMapping)
return foundMapping;
- if (TryBuildNewMapping(null, sourceType, targetType) is not { } mapping)
+ if (BuildDelegateMapping(null, sourceType, targetType) is not { } mapping)
return null;
AddMapping(mapping);
return mapping;
}
- public TypeMapping? TryBuildNewMapping(
+ public TypeMapping? FindMapping(ITypeSymbol sourceType, ITypeSymbol targetType)
+ {
+ _mappings.TryGetValue((sourceType, targetType), out var mapping);
+ return mapping;
+ }
+
+ public TypeMapping? FindOrBuildDelegateMapping(
+ ISymbol? userSymbol,
+ ITypeSymbol sourceType,
+ ITypeSymbol targetType)
+ {
+ if (FindMapping(sourceType, targetType) is { } foundMapping)
+ return foundMapping;
+
+ return BuildDelegateMapping(userSymbol, sourceType, targetType);
+ }
+
+ public TypeMapping? BuildDelegateMapping(
ISymbol? userSymbol,
ITypeSymbol sourceType,
ITypeSymbol targetType)
@@ -171,11 +195,6 @@ internal void ReportDiagnostic(DiagnosticDescriptor descriptor, Location? locati
private void BuildMappingBody(MappingBuilderContext ctx, TypeMapping typeMapping)
{
- if (typeMapping is not MethodMapping methodMapping)
- return;
-
- methodMapping.SetMethodNameIfNeeded(_methodNameBuilder.Build);
-
switch (typeMapping)
{
case ObjectPropertyMapping mapping:
@@ -192,7 +211,7 @@ private void AddMapping(TypeMapping mapping)
private void AddUserMapping(TypeMapping mapping)
{
- _methodNameBuilder.Add(((IUserMapping)mapping).Method.Name);
+ _methodNameBuilder.Reserve(((IUserMapping)mapping).Method.Name);
if (mapping.CallableByOtherMappings && FindMapping(mapping.SourceType, mapping.TargetType) is null)
{
AddMapping(mapping);
@@ -202,9 +221,23 @@ private void AddUserMapping(TypeMapping mapping)
_extraMappings.Add(mapping);
}
- private TypeMapping? FindMapping(ITypeSymbol sourceType, ITypeSymbol targetType)
+ private class MappingTupleEqualityComparer : IEqualityComparer<(ITypeSymbol Source, ITypeSymbol Target)>
{
- _mappings.TryGetValue((sourceType, targetType), out var mapping);
- return mapping;
+ public bool Equals((ITypeSymbol Source, ITypeSymbol Target) x, (ITypeSymbol Source, ITypeSymbol Target) y)
+ {
+ return Equals(x.Source, y.Source)
+ && Equals(x.Target, y.Target);
+ }
+
+ public int GetHashCode((ITypeSymbol Source, ITypeSymbol Target) obj)
+ {
+ unchecked
+ {
+ return (SymbolEqualityComparer.Default.GetHashCode(obj.Source) * 397) ^ SymbolEqualityComparer.Default.GetHashCode(obj.Target);
+ }
+ }
+
+ private bool Equals(ITypeSymbol x, ITypeSymbol y)
+ => SymbolEqualityComparer.IncludeNullability.Equals(x, y);
}
}
diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/CtorMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/CtorMappingBuilder.cs
index c7481a9e1b..819f1e109d 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBuilder/CtorMappingBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/CtorMappingBuilder.cs
@@ -1,5 +1,6 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Descriptors.TypeMappings;
+using Riok.Mapperly.Helpers;
namespace Riok.Mapperly.Descriptors.MappingBuilder;
@@ -14,7 +15,8 @@ public static class CtorMappingBuilder
var ctorMethod = namedTarget.InstanceConstructors
.FirstOrDefault(m =>
m.Parameters.Length == 1
- && SymbolEqualityComparer.Default.Equals(m.Parameters[0].Type, ctx.Source));
+ && SymbolEqualityComparer.Default.Equals(m.Parameters[0].Type, ctx.Source)
+ && ctx.Source.HasSameOrStricterNullability(m.Parameters[0].Type));
return ctorMethod == null
? null
diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/EnumMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/EnumMappingBuilder.cs
index 42d659c1dd..0a174fbbe8 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBuilder/EnumMappingBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/EnumMappingBuilder.cs
@@ -26,15 +26,15 @@ public static class EnumMappingBuilder
}
// since enums are immutable they can be directly assigned if they are of the same type
- if (SymbolEqualityComparer.Default.Equals(ctx.Source, ctx.Target))
- return new NullDelegateMapping(ctx.Source, ctx.Target, new DirectAssignmentMapping(ctx.Source.NonNullable()));
+ if (SymbolEqualityComparer.IncludeNullability.Equals(ctx.Source, ctx.Target))
+ return new DirectAssignmentMapping(ctx.Source);
// map enums by strategy
var config = ctx.GetConfigurationOrDefault();
return config.Strategy switch
{
EnumMappingStrategy.ByName => BuildNameMapping(ctx, config.IgnoreCase),
- _ => new NullDelegateMapping(ctx.Source, ctx.Target, new CastMapping(ctx.Source.NonNullable(), ctx.Target.NonNullable())),
+ _ => new CastMapping(ctx.Source, ctx.Target),
};
}
diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/NullableMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/NullableMappingBuilder.cs
new file mode 100644
index 0000000000..22f62509d5
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/NullableMappingBuilder.cs
@@ -0,0 +1,57 @@
+using Microsoft.CodeAnalysis;
+using Riok.Mapperly.Descriptors.TypeMappings;
+using Riok.Mapperly.Diagnostics;
+using Riok.Mapperly.Helpers;
+
+namespace Riok.Mapperly.Descriptors.MappingBuilder;
+
+public static class NullableMappingBuilder
+{
+ public static TypeMapping? TryBuildMapping(MappingBuilderContext ctx)
+ {
+ var sourceIsNullable = ctx.Source.TryGetNonNullable(out var sourceNonNullable);
+ var targetIsNullable = ctx.Target.TryGetNonNullable(out var targetNonNullable);
+ if (!sourceIsNullable && !targetIsNullable)
+ return null;
+
+ var delegateMapping = ctx.BuildDelegateMapping(sourceNonNullable ?? ctx.Source, targetNonNullable ?? ctx.Target);
+ return delegateMapping == null
+ ? null
+ : BuildNullDelegateMapping(ctx, delegateMapping);
+ }
+
+ private static TypeMapping BuildNullDelegateMapping(MappingBuilderContext ctx, TypeMapping mapping)
+ {
+ var nullFallback = GetNullFallbackValue(ctx);
+ return mapping switch
+ {
+ MethodMapping methodMapping => new NullDelegateMethodMapping(
+ ctx.Source,
+ ctx.Target,
+ methodMapping,
+ nullFallback),
+ _ => new NullDelegateMapping(ctx.Source, ctx.Target, mapping, nullFallback),
+ };
+ }
+
+ private static NullFallbackValue GetNullFallbackValue(MappingBuilderContext ctx)
+ {
+ if (ctx.Target.IsNullable())
+ return NullFallbackValue.Default;
+
+ if (ctx.MapperConfiguration.ThrowOnMappingNullMismatch)
+ return NullFallbackValue.ThrowArgumentNullException;
+
+ if (!ctx.Target.IsReferenceType || ctx.Target.IsNullable())
+ return NullFallbackValue.Default;
+
+ if (ctx.Target.SpecialType == SpecialType.System_String)
+ return NullFallbackValue.EmptyString;
+
+ if (ctx.Target.HasAccessibleParameterlessConstructor())
+ return NullFallbackValue.CreateInstance;
+
+ ctx.ReportDiagnostic(DiagnosticDescriptors.NoParameterlessConstructorFound, ctx.Target);
+ return NullFallbackValue.ThrowArgumentNullException;
+ }
+}
diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/ObjectPropertyMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/ObjectPropertyMappingBuilder.cs
index 92719e0c8f..da0a13d335 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBuilder/ObjectPropertyMappingBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/ObjectPropertyMappingBuilder.cs
@@ -16,7 +16,7 @@ public static class ObjectPropertyMappingBuilder
if (ctx.Target.SpecialType != SpecialType.None || ctx.Source.SpecialType != SpecialType.None)
return null;
- return new NewInstanceObjectPropertyMapping(ctx.Source, ctx.Target);
+ return new NewInstanceObjectPropertyMapping(ctx.Source, ctx.Target.NonNullable());
}
public static void BuildMappingBody(MappingBuilderContext ctx, ObjectPropertyMapping mapping)
@@ -99,7 +99,7 @@ private static void AddUnmatchedIgnoredPropertiesDiagnostics(
.FirstOrDefault(p => !p.IsStatic);
}
- private static PropertyMappingDescriptor? BuildPropertyMapping(
+ private static PropertyMapping? BuildPropertyMapping(
MappingBuilderContext ctx,
ObjectPropertyMapping mapping,
IPropertySymbol sourceProperty,
@@ -111,26 +111,20 @@ private static void AddUnmatchedIgnoredPropertiesDiagnostics(
if (sourceProperty.IsWriteOnly)
return null;
- var propertyMapping = ctx.FindOrBuildMapping(sourceProperty.Type.NonNullable(), targetProperty.Type.NonNullable());
- if (propertyMapping == null)
- {
- ctx.ReportDiagnostic(
- DiagnosticDescriptors.CouldNotMapProperty,
- mapping.SourceType,
- sourceProperty.Name,
- sourceProperty.Type,
- mapping.TargetType,
- targetProperty.Name,
- targetProperty.Type);
- return null;
- }
+ // nullability is handled inside the property mapping
+ var delegateMapping = ctx.FindMapping(sourceProperty.Type.UpgradeNullable(), targetProperty.Type.UpgradeNullable())
+ ?? ctx.FindOrBuildMapping(sourceProperty.Type.NonNullable(), targetProperty.Type.NonNullable());
+ if (delegateMapping != null)
+ return new PropertyMapping(sourceProperty, targetProperty, delegateMapping, ctx.MapperConfiguration.ThrowOnPropertyMappingNullMismatch);
- var nullDelegateMapping = new NullDelegateMapping(
- sourceProperty.IsNullable(),
- targetProperty.IsNullable(),
+ ctx.ReportDiagnostic(
+ DiagnosticDescriptors.CouldNotMapProperty,
+ mapping.SourceType,
+ sourceProperty.Name,
sourceProperty.Type,
- targetProperty.Type,
- propertyMapping);
- return new PropertyMappingDescriptor(sourceProperty, targetProperty, nullDelegateMapping);
+ mapping.TargetType,
+ targetProperty.Name,
+ targetProperty.Type);
+ return null;
}
}
diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/UserMethodMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/UserMethodMappingBuilder.cs
index 398a9aab55..d62343bce8 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBuilder/UserMethodMappingBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/UserMethodMappingBuilder.cs
@@ -24,7 +24,7 @@ public static IEnumerable ExtractUserMappings(
public static void BuildMappingBody(MappingBuilderContext ctx, UserDefinedNewInstanceMethodMapping mapping)
{
- mapping.DelegateMapping = ctx.TryBuildNewMapping(mapping.SourceType, mapping.TargetType);
+ mapping.DelegateMapping = ctx.BuildDelegateMapping(mapping.SourceType, mapping.TargetType);
if (mapping.DelegateMapping == null)
{
ctx.ReportDiagnostic(
@@ -80,7 +80,7 @@ private static IEnumerable ExtractMethods(INamedTypeSymbol object
// and is accessible it is a user implemented method mapping
if (!methodSymbol.IsAbstract)
{
- return methodSymbol.IsAccessible()
+ return methodSymbol.IsAccessible(true)
? new UserImplementedMethodMapping(methodSymbol)
: null;
}
diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs
index fa419f78d4..24c06b6803 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs
@@ -26,6 +26,9 @@ public MappingBuilderContext(
public ITypeSymbol Target { get; }
+ public TypeMapping? FindMapping(ITypeSymbol sourceType, ITypeSymbol targetType)
+ => _builder.FindMapping(sourceType, targetType);
+
///
/// Tries to find an existing mapping for the provided types.
/// If none is found, a new one is created.
@@ -41,16 +44,30 @@ public MappingBuilderContext(
public TypeMapping? FindOrBuildMapping(ITypeSymbol sourceType, ITypeSymbol targetType)
=> _builder.FindOrBuildMapping(sourceType, targetType);
+ ///
+ /// Tries to find an existing mapping for the provided types.
+ /// If none is found, a new one is created.
+ /// If a new mapping is created, it is not added to the mapping descriptor (should only be used as a delegate to another mapping)
+ /// and is therefore not accessible by other mappings.
+ /// Configuration / the user symbol is passed from the caller.
+ ///
+ /// The source type.
+ /// The target type.
+ /// The created mapping or null if none could be created.
+ public TypeMapping? FindOrBuildDelegateMapping(ITypeSymbol source, ITypeSymbol target)
+ => _builder.FindOrBuildDelegateMapping(_userSymbol, source, target);
+
///
/// Tries to build a new mapping for the given types.
- /// The built mapping is not added to the mapping descriptor.
+ /// The built mapping is not added to the mapping descriptor (should only be used as a delegate to another mapping)
+ /// and is therefore not accessible by other mappings.
/// Configuration / the user symbol is passed from the caller.
///
/// The source type.
/// The target type.
/// The created mapping or null if none could be created.
- public TypeMapping? TryBuildNewMapping(ITypeSymbol source, ITypeSymbol target)
- => _builder.TryBuildNewMapping(_userSymbol, source, target);
+ public TypeMapping? BuildDelegateMapping(ITypeSymbol source, ITypeSymbol target)
+ => _builder.BuildDelegateMapping(_userSymbol, source, target);
public T GetConfigurationOrDefault() where T : Attribute
{
diff --git a/src/Riok.Mapperly/Descriptors/MethodNameBuilder.cs b/src/Riok.Mapperly/Descriptors/MethodNameBuilder.cs
index fa44689fa9..393abf2cd2 100644
--- a/src/Riok.Mapperly/Descriptors/MethodNameBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MethodNameBuilder.cs
@@ -8,7 +8,7 @@ internal class MethodNameBuilder
private const string MethodNamePrefix = "MapTo";
private readonly HashSet _usedNames = new();
- internal void Add(string name)
+ internal void Reserve(string name)
=> _usedNames.Add(name);
internal string Build(MethodMapping mapping)
diff --git a/src/Riok.Mapperly/Descriptors/PropertyMappingDescriptor.cs b/src/Riok.Mapperly/Descriptors/PropertyMappingDescriptor.cs
deleted file mode 100644
index 0b1fabbab8..0000000000
--- a/src/Riok.Mapperly/Descriptors/PropertyMappingDescriptor.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using System.Diagnostics;
-using Microsoft.CodeAnalysis;
-using Riok.Mapperly.Descriptors.TypeMappings;
-
-namespace Riok.Mapperly.Descriptors;
-
-[DebuggerDisplay("PropertyMapping({Source.Name} => {Target.Name})")]
-public class PropertyMappingDescriptor
-{
- public PropertyMappingDescriptor(
- IPropertySymbol source,
- IPropertySymbol target,
- TypeMapping typeMapping)
- {
- Source = source;
- Target = target;
- TypeMapping = typeMapping;
- }
-
- public IPropertySymbol Source { get; }
-
- public IPropertySymbol Target { get; }
-
- public TypeMapping TypeMapping { get; }
-}
diff --git a/src/Riok.Mapperly/Descriptors/TypeMappings/MethodMapping.cs b/src/Riok.Mapperly/Descriptors/TypeMappings/MethodMapping.cs
index 56284586eb..9ad55105a0 100644
--- a/src/Riok.Mapperly/Descriptors/TypeMappings/MethodMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/TypeMappings/MethodMapping.cs
@@ -40,8 +40,7 @@ public MethodDeclarationSyntax BuildMethod()
return MethodDeclaration(returnType, Identifier(MethodName))
.WithModifiers(TokenList(BuildModifiers()))
.WithParameterList(BuildParameterList())
- .WithBody(Block(BuildBody(IdentifierName(SourceParamName))))
- .WithAttributeLists(List(BuildAttributes(SourceParamName)));
+ .WithBody(Block(BuildBody(IdentifierName(SourceParamName))));
}
public abstract IEnumerable BuildBody(ExpressionSyntax source);
@@ -73,14 +72,4 @@ private ParameterListSyntax BuildParameterList()
{
return ParameterList(CommaSeparatedList(BuildParameters()));
}
-
- private IEnumerable BuildAttributes(string sourceParamName)
- {
- // if target and source types are nullable we add a [return: NotNullIfNotNull("source")] annotation
- if (TargetType.NullableAnnotation == NullableAnnotation.Annotated
- && SourceType.NullableAnnotation == NullableAnnotation.Annotated)
- {
- yield return ReturnNotNullIfNotNullAttribute(sourceParamName);
- }
- }
}
diff --git a/src/Riok.Mapperly/Descriptors/TypeMappings/NewInstanceObjectPropertyMapping.cs b/src/Riok.Mapperly/Descriptors/TypeMappings/NewInstanceObjectPropertyMapping.cs
index b4d246b0fb..748d292bc6 100644
--- a/src/Riok.Mapperly/Descriptors/TypeMappings/NewInstanceObjectPropertyMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/TypeMappings/NewInstanceObjectPropertyMapping.cs
@@ -1,6 +1,5 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
-using Riok.Mapperly.Helpers;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Riok.Mapperly.Emit.SyntaxFactoryHelper;
@@ -19,17 +18,11 @@ public NewInstanceObjectPropertyMapping(
public override IEnumerable BuildBody(ExpressionSyntax source)
{
- // if the source type is nullable, add a null guard.
- if (SourceType.IsNullable())
- {
- yield return IfNullReturn(source, NullSubstitute(TargetType, source));
- }
-
// var target = new T();
yield return CreateInstance(TargetVariableName, TargetType);
// map properties
- foreach (var expression in base.BuildBody(source, IdentifierName(TargetVariableName)))
+ foreach (var expression in BuildBody(source, IdentifierName(TargetVariableName)))
{
yield return expression;
}
diff --git a/src/Riok.Mapperly/Descriptors/TypeMappings/NullDelegateMapping.cs b/src/Riok.Mapperly/Descriptors/TypeMappings/NullDelegateMapping.cs
index bbfcef7d79..1eed32bda3 100644
--- a/src/Riok.Mapperly/Descriptors/TypeMappings/NullDelegateMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/TypeMappings/NullDelegateMapping.cs
@@ -13,71 +13,37 @@ public class NullDelegateMapping : TypeMapping
{
private const string NullableValueProperty = "Value";
- private readonly bool _targetIsNullable;
- private readonly bool _sourceIsNullable;
private readonly TypeMapping _delegateMapping;
-
- public NullDelegateMapping(ITypeSymbol nullableSourceType, ITypeSymbol nullableTargetType, TypeMapping delegateMapping)
- : base(nullableSourceType, nullableTargetType)
- {
- _sourceIsNullable = nullableSourceType.IsNullable();
- _targetIsNullable = nullableTargetType.IsNullable();
- _delegateMapping = delegateMapping;
- }
+ private readonly NullFallbackValue _nullFallbackValue;
public NullDelegateMapping(
- bool sourceIsNullable,
- bool targetIsNullable,
ITypeSymbol nullableSourceType,
ITypeSymbol nullableTargetType,
- TypeMapping delegateMapping)
+ TypeMapping delegateMapping,
+ NullFallbackValue nullFallbackValue)
: base(nullableSourceType, nullableTargetType)
{
- _sourceIsNullable = sourceIsNullable;
- _targetIsNullable = targetIsNullable;
_delegateMapping = delegateMapping;
+ _nullFallbackValue = nullFallbackValue;
}
public override ExpressionSyntax Build(ExpressionSyntax source)
{
+ if (!SourceType.IsNullable() || _delegateMapping.SourceType.IsNullable())
+ return _delegateMapping.Build(source);
+
// source is nullable and the mapping method cannot handle nulls,
// call mapping only if source is not null.
- if (_sourceIsNullable && !_delegateMapping.SourceType.IsNullable())
- {
- // for direct assignments
- // source ?? ;
- if (_delegateMapping is DirectAssignmentMapping)
- {
- return _targetIsNullable
- ? _delegateMapping.Build(source)
- : Coalesce(
- _delegateMapping.Build(source),
- NullSubstitute(TargetType.NonNullable(), source));
- }
-
- // for non direct assignments
- // source == null ? : Map(source)
- // or for nullable value types:
- // source == null ? : Map(source.Value)
- var sourceValue = SourceType.IsNullableValueType()
- ? MemberAccess(source, NullableValueProperty)
- : source;
-
- return ConditionalExpression(
- IsNull(source),
- _targetIsNullable ? DefaultLiteral() : NullSubstitute(TargetType.NonNullable(), source),
- _delegateMapping.Build(sourceValue));
- }
-
- // target can not be nullable and the map method may return null values
- // therefore we replace Map(source) with Map(source) ?? ;
- if (!_targetIsNullable && _delegateMapping.TargetType.IsNullable())
- {
- return Coalesce(
- _delegateMapping.Build(source),
- NullSubstitute(TargetType.NonNullable(), source));
- }
-
- return _delegateMapping.Build(source);
+ // source == null ? : Map(source)
+ // or for nullable value types:
+ // source == null ? : Map(source.Value)
+ var sourceValue = SourceType.IsNullableValueType()
+ ? MemberAccess(source, NullableValueProperty)
+ : source;
+
+ return ConditionalExpression(
+ IsNull(source),
+ NullSubstitute(TargetType.NonNullable(), source, _nullFallbackValue),
+ _delegateMapping.Build(sourceValue));
}
}
diff --git a/src/Riok.Mapperly/Descriptors/TypeMappings/NullDelegateMethodMapping.cs b/src/Riok.Mapperly/Descriptors/TypeMappings/NullDelegateMethodMapping.cs
new file mode 100644
index 0000000000..c81390da28
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/TypeMappings/NullDelegateMethodMapping.cs
@@ -0,0 +1,47 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Riok.Mapperly.Helpers;
+using static Riok.Mapperly.Emit.SyntaxFactoryHelper;
+
+namespace Riok.Mapperly.Descriptors.TypeMappings;
+
+///
+/// Null aware delegate mapping for s.
+/// Abstracts handling null values of the delegated mapping.
+///
+public class NullDelegateMethodMapping : MethodMapping
+{
+ private readonly MethodMapping _delegateMapping;
+ private readonly NullFallbackValue _nullFallbackValue;
+
+ public NullDelegateMethodMapping(
+ ITypeSymbol nullableSourceType,
+ ITypeSymbol nullableTargetType,
+ MethodMapping delegateMapping,
+ NullFallbackValue nullFallbackValue)
+ : base(nullableSourceType, nullableTargetType)
+ {
+ _delegateMapping = delegateMapping;
+ _nullFallbackValue = nullFallbackValue;
+ }
+
+ public override IEnumerable BuildBody(ExpressionSyntax source)
+ {
+ var body = _delegateMapping.BuildBody(source);
+ return AddPreNullHandling(source, body);
+ }
+
+ private IEnumerable AddPreNullHandling(ExpressionSyntax source, IEnumerable body)
+ {
+ if (!SourceType.IsNullable() || _delegateMapping.SourceType.IsNullable())
+ return body;
+
+ // source is nullable and the mapping method cannot handle nulls,
+ // call mapping only if source is not null.
+ // if (source == null)
+ // return ;
+ return body.Prepend(IfNullReturnOrThrow(
+ source,
+ NullSubstitute(TargetType.NonNullable(), source, _nullFallbackValue)));
+ }
+}
diff --git a/src/Riok.Mapperly/Descriptors/TypeMappings/NullFallbackValue.cs b/src/Riok.Mapperly/Descriptors/TypeMappings/NullFallbackValue.cs
new file mode 100644
index 0000000000..4e3e6e24b1
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/TypeMappings/NullFallbackValue.cs
@@ -0,0 +1,9 @@
+namespace Riok.Mapperly.Descriptors.TypeMappings;
+
+public enum NullFallbackValue
+{
+ Default,
+ EmptyString,
+ CreateInstance,
+ ThrowArgumentNullException,
+}
diff --git a/src/Riok.Mapperly/Descriptors/TypeMappings/ObjectPropertyMapping.cs b/src/Riok.Mapperly/Descriptors/TypeMappings/ObjectPropertyMapping.cs
index 747a4869fd..07d88663a0 100644
--- a/src/Riok.Mapperly/Descriptors/TypeMappings/ObjectPropertyMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/TypeMappings/ObjectPropertyMapping.cs
@@ -1,8 +1,5 @@
using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
-using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
-using static Riok.Mapperly.Emit.SyntaxFactoryHelper;
namespace Riok.Mapperly.Descriptors.TypeMappings;
@@ -12,38 +9,15 @@ namespace Riok.Mapperly.Descriptors.TypeMappings;
///
public abstract class ObjectPropertyMapping : MethodMapping
{
- private readonly List _propertyMappings = new();
+ private readonly List _propertyMappings = new();
protected ObjectPropertyMapping(ITypeSymbol sourceType, ITypeSymbol targetType) : base(sourceType, targetType)
{
}
- public void AddPropertyMapping(PropertyMappingDescriptor propertyMapping)
+ public void AddPropertyMapping(PropertyMapping propertyMapping)
=> _propertyMappings.Add(propertyMapping);
internal IEnumerable BuildBody(ExpressionSyntax source, ExpressionSyntax target)
- {
- foreach (var propertyMapping in _propertyMappings)
- {
- yield return PropertyMapping(propertyMapping, source, target);
- }
- }
-
- private static ExpressionStatementSyntax PropertyMapping(
- PropertyMappingDescriptor mapping,
- ExpressionSyntax sourceAccess,
- ExpressionSyntax targetAccess)
- {
- // Map(source.Property)
- var sourcePropertyAccess = MemberAccess(sourceAccess, mapping.Source.Name);
- var sourceMappedExpression = mapping.TypeMapping.Build(sourcePropertyAccess);
-
- // target.Property = Map(source.Property)
- var assignment = AssignmentExpression(
- SyntaxKind.SimpleAssignmentExpression,
- MemberAccess(targetAccess, mapping.Target.Name),
- sourceMappedExpression);
-
- return ExpressionStatement(assignment);
- }
+ => _propertyMappings.Select(x => x.Build(source, target));
}
diff --git a/src/Riok.Mapperly/Descriptors/TypeMappings/PropertyMapping.cs b/src/Riok.Mapperly/Descriptors/TypeMappings/PropertyMapping.cs
new file mode 100644
index 0000000000..97b986a851
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/TypeMappings/PropertyMapping.cs
@@ -0,0 +1,89 @@
+using System.Diagnostics;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Riok.Mapperly.Helpers;
+using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
+using static Riok.Mapperly.Emit.SyntaxFactoryHelper;
+
+namespace Riok.Mapperly.Descriptors.TypeMappings;
+
+[DebuggerDisplay("PropertyMapping({_source.Name} => {_target.Name})")]
+public class PropertyMapping
+{
+ private const string NullableValueProperty = "Value";
+
+ private readonly TypeMapping _mapping;
+ private readonly IPropertySymbol _source;
+ private readonly IPropertySymbol _target;
+ private readonly bool _throwInsteadOfConditionalNullMapping;
+
+ public PropertyMapping(
+ IPropertySymbol source,
+ IPropertySymbol target,
+ TypeMapping mapping,
+ bool throwInsteadOfConditionalNullMapping)
+ {
+ _source = source;
+ _target = target;
+ _mapping = mapping;
+ _throwInsteadOfConditionalNullMapping = throwInsteadOfConditionalNullMapping;
+ }
+
+ public StatementSyntax Build(
+ ExpressionSyntax sourceAccess,
+ ExpressionSyntax targetAccess)
+ {
+ var targetPropertyAccess = MemberAccess(targetAccess, _target.Name);
+ ExpressionSyntax sourcePropertyAccess = MemberAccess(sourceAccess, _source.Name);
+
+ // if source is nullable, but mapping doesn't accept nulls
+ // condition: source != null
+ (var condition, sourcePropertyAccess) = BuildPreMappingCondition(sourcePropertyAccess);
+ var mappedValue = _mapping.Build(sourcePropertyAccess);
+
+ // target.Property = mappedValue;
+ var assignment = AssignmentExpression(
+ SyntaxKind.SimpleAssignmentExpression,
+ targetPropertyAccess,
+ mappedValue);
+ var assignmentExpression = ExpressionStatement(assignment);
+
+ // if (source.Value != null)
+ // target.Value = Map(Source.Name);
+ // else
+ // throw ...
+ return BuildIf(condition, assignmentExpression, sourcePropertyAccess);
+ }
+
+ private StatementSyntax BuildIf(ExpressionSyntax? condition, StatementSyntax assignment, ExpressionSyntax sourcePropertyAccess)
+ {
+ if (condition == null)
+ return assignment;
+
+ var elseClause = _throwInsteadOfConditionalNullMapping
+ ? ElseClause(ExpressionStatement(ThrowNewArgumentNullException(sourcePropertyAccess)))
+ : null;
+ return IfStatement(condition, assignment, elseClause);
+ }
+
+ private (ExpressionSyntax? Condition, ExpressionSyntax SourceAccess) BuildPreMappingCondition(ExpressionSyntax sourceAccess)
+ {
+ if (!_source.IsNullable() || _mapping.SourceType.IsNullable())
+ return (null, sourceAccess);
+
+ // if source is nullable but the mapping does not accept nulls
+ // add not null condition
+ var condition = IsNotNull(sourceAccess);
+
+ // source != null
+ // if the source is a nullable value type
+ // replace source by source.Value for the mapping
+ if (_source.Type.IsNullableValueType())
+ {
+ sourceAccess = MemberAccess(sourceAccess, NullableValueProperty);
+ }
+
+ return (condition, sourceAccess);
+ }
+}
diff --git a/src/Riok.Mapperly/Descriptors/TypeMappings/UserDefinedExistingInstanceMethodMapping.cs b/src/Riok.Mapperly/Descriptors/TypeMappings/UserDefinedExistingInstanceMethodMapping.cs
index 59dcd78c61..1cccb405e4 100644
--- a/src/Riok.Mapperly/Descriptors/TypeMappings/UserDefinedExistingInstanceMethodMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/TypeMappings/UserDefinedExistingInstanceMethodMapping.cs
@@ -15,7 +15,7 @@ public class UserDefinedExistingInstanceMethodMapping : ObjectPropertyMapping, I
public UserDefinedExistingInstanceMethodMapping(
IMethodSymbol method,
bool isAbstractMapperDefinition)
- : base(method.Parameters[0].Type, method.Parameters[1].Type)
+ : base(method.Parameters[0].Type.UpgradeNullable(), method.Parameters[1].Type.UpgradeNullable())
{
Override = isAbstractMapperDefinition;
Accessibility = Accessibility.Public;
diff --git a/src/Riok.Mapperly/Descriptors/TypeMappings/UserDefinedNewInstanceMethodMapping.cs b/src/Riok.Mapperly/Descriptors/TypeMappings/UserDefinedNewInstanceMethodMapping.cs
index 44cd015735..f0693a112d 100644
--- a/src/Riok.Mapperly/Descriptors/TypeMappings/UserDefinedNewInstanceMethodMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/TypeMappings/UserDefinedNewInstanceMethodMapping.cs
@@ -1,5 +1,6 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Riok.Mapperly.Helpers;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Riok.Mapperly.Emit.SyntaxFactoryHelper;
@@ -13,7 +14,7 @@ public class UserDefinedNewInstanceMethodMapping : MethodMapping, IUserMapping
private const string NoMappingComment = "// Could not generate mapping";
public UserDefinedNewInstanceMethodMapping(IMethodSymbol method, bool isAbstractMapperDefinition)
- : base(method.Parameters.Single().Type, method.ReturnType)
+ : base(method.Parameters.Single().Type.UpgradeNullable(), method.ReturnType.UpgradeNullable())
{
Override = isAbstractMapperDefinition;
Accessibility = Accessibility.Public;
diff --git a/src/Riok.Mapperly/Descriptors/TypeMappings/UserImplementedMethodMapping.cs b/src/Riok.Mapperly/Descriptors/TypeMappings/UserImplementedMethodMapping.cs
index c237ee7039..ed3b974487 100644
--- a/src/Riok.Mapperly/Descriptors/TypeMappings/UserImplementedMethodMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/TypeMappings/UserImplementedMethodMapping.cs
@@ -1,5 +1,6 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Riok.Mapperly.Helpers;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Riok.Mapperly.Emit.SyntaxFactoryHelper;
@@ -11,7 +12,7 @@ namespace Riok.Mapperly.Descriptors.TypeMappings;
public class UserImplementedMethodMapping : TypeMapping, IUserMapping
{
public UserImplementedMethodMapping(IMethodSymbol method)
- : base(method.Parameters.Single().Type, method.ReturnType)
+ : base(method.Parameters.Single().Type.UpgradeNullable(), method.ReturnType.UpgradeNullable())
{
Method = method;
}
diff --git a/src/Riok.Mapperly/Emit/SyntaxFactoryHelper.cs b/src/Riok.Mapperly/Emit/SyntaxFactoryHelper.cs
index b49e8b454c..bb5a0c63ea 100644
--- a/src/Riok.Mapperly/Emit/SyntaxFactoryHelper.cs
+++ b/src/Riok.Mapperly/Emit/SyntaxFactoryHelper.cs
@@ -1,6 +1,7 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Riok.Mapperly.Descriptors.TypeMappings;
using Riok.Mapperly.Helpers;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
@@ -13,7 +14,6 @@ public static class SyntaxFactoryHelper
private const string ArgumentNullExceptionClassName = "System.ArgumentNullException";
private const string NotImplementedExceptionClassName = "System.NotImplementedException";
- private const string NotNullIfNotNullAttributeName = "System.Diagnostics.CodeAnalysis.NotNullIfNotNull";
public static readonly IdentifierNameSyntax VarIdentifier = IdentifierName("var");
@@ -42,24 +42,34 @@ public static BinaryExpressionSyntax Coalesce(
public static BinaryExpressionSyntax IsNull(ExpressionSyntax expression)
=> BinaryExpression(SyntaxKind.EqualsExpression, expression, NullLiteral());
- public static ExpressionSyntax NullSubstitute(ITypeSymbol t, ExpressionSyntax argument)
- {
- if (!t.IsReferenceType || t.IsNullable())
- return DefaultLiteral();
-
- if (t.SpecialType == SpecialType.System_String)
- return StringLiteral(string.Empty);
+ public static BinaryExpressionSyntax IsNotNull(ExpressionSyntax expression)
+ => BinaryExpression(SyntaxKind.NotEqualsExpression, expression, NullLiteral());
- return t.HasAccessibleParameterlessConstructor()
- ? CreateInstance(t)
- : ThrowNewArgumentNullException(argument);
+ public static ExpressionSyntax NullSubstitute(ITypeSymbol t, ExpressionSyntax argument, NullFallbackValue nullFallbackValue)
+ {
+ return nullFallbackValue switch
+ {
+ NullFallbackValue.Default => DefaultLiteral(),
+ NullFallbackValue.EmptyString => StringLiteral(string.Empty),
+ NullFallbackValue.CreateInstance => CreateInstance(t),
+ _ => ThrowNewArgumentNullException(argument),
+ };
}
- public static StatementSyntax IfNullReturn(ExpressionSyntax expression, ExpressionSyntax? returnExpression = null)
+ public static StatementSyntax IfNullReturn(ExpressionSyntax expression)
+ => IfStatement(IsNull(expression), ReturnStatement());
+
+ public static StatementSyntax IfNullReturnOrThrow(ExpressionSyntax expression, ExpressionSyntax? returnOrThrowExpression = null)
{
+ StatementSyntax ifExpression = returnOrThrowExpression switch
+ {
+ ThrowExpressionSyntax throwSyntax => ThrowStatement(throwSyntax.Expression),
+ _ => ReturnStatement(returnOrThrowExpression),
+ };
+
return IfStatement(
IsNull(expression),
- ReturnStatement(returnExpression));
+ ifExpression);
}
public static LiteralExpressionSyntax DefaultLiteral()
@@ -74,15 +84,6 @@ public static LiteralExpressionSyntax StringLiteral(string content) =>
public static LiteralExpressionSyntax BooleanLiteral(bool b)
=> LiteralExpression(b ? SyntaxKind.TrueLiteralExpression : SyntaxKind.FalseLiteralExpression);
- public static AttributeListSyntax ReturnNotNullIfNotNullAttribute(string paramName)
- {
- var attribute = Attribute(IdentifierName(NotNullIfNotNullAttributeName))
- .WithArgumentList(AttributeArgumentList(SingletonSeparatedList(AttributeArgument(StringLiteral(paramName)))));
-
- return AttributeList(SingletonSeparatedList(attribute))
- .WithTarget(AttributeTargetSpecifier(Token(SyntaxKind.ReturnKeyword)));
- }
-
public static StatementSyntax ReturnVariable(string identifierName)
=> ReturnStatement(IdentifierName(identifierName));
@@ -95,12 +96,6 @@ public static MemberAccessExpressionSyntax MemberAccess(ExpressionSyntax idExpre
public static InvocationExpressionSyntax NameOf(ExpressionSyntax expression)
=> Invocation(IdentifierName("nameof"), expression);
- public static ElementAccessExpressionSyntax ArrayElementAccess(ExpressionSyntax array, ExpressionSyntax index)
- {
- return ElementAccessExpression(array)
- .WithArgumentList(BracketedArgumentList(SingletonSeparatedList(Argument(index))));
- }
-
public static ThrowExpressionSyntax ThrowArgumentOutOfRangeException(ExpressionSyntax arg)
{
return ThrowExpression(ObjectCreationExpression(IdentifierName(ArgumentOutOfRangeExceptionClassName))
diff --git a/src/Riok.Mapperly/Helpers/NullableSymbolExtensions.cs b/src/Riok.Mapperly/Helpers/NullableSymbolExtensions.cs
index 714d7a1d65..a8c764541a 100644
--- a/src/Riok.Mapperly/Helpers/NullableSymbolExtensions.cs
+++ b/src/Riok.Mapperly/Helpers/NullableSymbolExtensions.cs
@@ -7,6 +7,41 @@ public static class NullableSymbolExtensions
{
private const string NullableGenericTypeName = "System.Nullable";
+ internal static bool HasSameOrStricterNullability(this ITypeSymbol symbol, ITypeSymbol other)
+ {
+ return symbol.NullableAnnotation == NullableAnnotation.NotAnnotated
+ || symbol.UpgradeNullable().NullableAnnotation == other.UpgradeNullable().NullableAnnotation;
+ }
+
+ ///
+ /// Upgrade the nullability of a symbol from to .
+ ///
+ /// The symbol to upgrade.
+ /// The upgraded symbol
+ internal static ITypeSymbol UpgradeNullable(this ITypeSymbol symbol)
+ {
+ TryUpgradeNullable(symbol, out var upgradedSymbol);
+ return upgradedSymbol ?? symbol;
+ }
+
+ ///
+ /// Tries to upgrade the nullability of a symbol from to .
+ ///
+ /// The symbol.
+ /// The upgraded symbol, if an upgrade has taken place, null otherwise.
+ /// Whether an upgrade has taken place.
+ internal static bool TryUpgradeNullable(this ITypeSymbol symbol, [NotNullWhen(true)] out ITypeSymbol? upgradedSymbol)
+ {
+ if (symbol.NullableAnnotation != NullableAnnotation.None)
+ {
+ upgradedSymbol = default;
+ return false;
+ }
+
+ upgradedSymbol = symbol.WithNullableAnnotation(NullableAnnotation.Annotated);
+ return true;
+ }
+
internal static bool TryGetNonNullable(this ITypeSymbol symbol, [NotNullWhen(true)] out ITypeSymbol? nonNullable)
{
if (symbol.NonNullableValueType() is { } t)
diff --git a/src/Riok.Mapperly/Helpers/PropertyEqualityComparer.cs b/src/Riok.Mapperly/Helpers/PropertyEqualityComparer.cs
deleted file mode 100644
index 815c2bf25d..0000000000
--- a/src/Riok.Mapperly/Helpers/PropertyEqualityComparer.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-namespace Riok.Mapperly.Helpers;
-
-public class PropertyEqualityComparer : IEqualityComparer
-{
- private readonly Func _valueSelector;
- private readonly IEqualityComparer _valueComparer;
-
- public PropertyEqualityComparer(Func valueSelector, IEqualityComparer valueComparer)
- {
- _valueSelector = valueSelector;
- _valueComparer = valueComparer;
- }
-
- public bool Equals(T x, T y)
- => _valueComparer.Equals(Value(x), Value(y));
-
- public int GetHashCode(T obj)
- => _valueComparer.GetHashCode(Value(obj));
-
- private TValue Value(T obj)
- => _valueSelector(obj);
-}
diff --git a/src/Riok.Mapperly/Helpers/SymbolExtensions.cs b/src/Riok.Mapperly/Helpers/SymbolExtensions.cs
index dceee09088..85bce8ff74 100644
--- a/src/Riok.Mapperly/Helpers/SymbolExtensions.cs
+++ b/src/Riok.Mapperly/Helpers/SymbolExtensions.cs
@@ -11,14 +11,14 @@ internal static bool HasAttribute(this ISymbol symbol, INamedTypeSymbol attribut
internal static bool IsImmutable(this ISymbol symbol)
=> symbol is INamedTypeSymbol namedSymbol && (namedSymbol.IsReadOnly || namedSymbol.SpecialType == SpecialType.System_String);
- internal static bool IsAccessible(this ISymbol symbol)
- => symbol.DeclaredAccessibility.HasFlag(Accessibility.Protected)
- || symbol.DeclaredAccessibility.HasFlag(Accessibility.Internal)
- || symbol.DeclaredAccessibility.HasFlag(Accessibility.Public);
+ internal static bool IsAccessible(this ISymbol symbol, bool allowProtected = false)
+ => symbol.DeclaredAccessibility.HasFlag(Accessibility.Internal)
+ || symbol.DeclaredAccessibility.HasFlag(Accessibility.Public)
+ || (symbol.DeclaredAccessibility.HasFlag(Accessibility.Protected) && allowProtected);
- internal static bool HasAccessibleParameterlessConstructor(this ITypeSymbol symbol)
+ internal static bool HasAccessibleParameterlessConstructor(this ITypeSymbol symbol, bool allowProtected = false)
=> symbol is INamedTypeSymbol { IsAbstract: false } namedTypeSymbol
- && namedTypeSymbol.Constructors.Any(c => c.Parameters.IsDefaultOrEmpty && c.IsAccessible());
+ && namedTypeSymbol.Constructors.Any(c => c.Parameters.IsDefaultOrEmpty && c.IsAccessible(allowProtected));
internal static bool IsArrayType(this ITypeSymbol symbol)
=> symbol is IArrayTypeSymbol;
diff --git a/test/Riok.Mapperly.Tests/Descriptors/MethodNameBuilderTest.cs b/test/Riok.Mapperly.Tests/Descriptors/MethodNameBuilderTest.cs
index e4c5d973bd..60b04a642f 100644
--- a/test/Riok.Mapperly.Tests/Descriptors/MethodNameBuilderTest.cs
+++ b/test/Riok.Mapperly.Tests/Descriptors/MethodNameBuilderTest.cs
@@ -12,7 +12,7 @@ public class MethodNameBuilderTest
public void ShouldGenerateUniqueMethodNames()
{
var builder = new MethodNameBuilder();
- builder.Add("MapToA");
+ builder.Reserve("MapToA");
builder.Build(NewMethodMappingMock("A"))
.Should()
.BeEquivalentTo("MapToA1");
diff --git a/test/Riok.Mapperly.Tests/Mapping/EnumTest.cs b/test/Riok.Mapperly.Tests/Mapping/EnumTest.cs
index c2ba2c0c2f..497bb76c54 100644
--- a/test/Riok.Mapperly.Tests/Mapping/EnumTest.cs
+++ b/test/Riok.Mapperly.Tests/Mapping/EnumTest.cs
@@ -163,7 +163,7 @@ public void NullableEnumToOtherEnumShouldCastWithNullHandling()
TestHelper.GenerateSingleMapperMethodBody(source)
.Should()
- .Be("return source == null ? default : (E2)source.Value;");
+ .Be("return source == null ? throw new System.ArgumentNullException(nameof(source)) : (E2)source.Value;");
}
[Fact]
diff --git a/test/Riok.Mapperly.Tests/Mapping/NullableTest.cs b/test/Riok.Mapperly.Tests/Mapping/NullableTest.cs
new file mode 100644
index 0000000000..3cf5cef9e6
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/Mapping/NullableTest.cs
@@ -0,0 +1,126 @@
+namespace Riok.Mapperly.Tests.Mapping;
+
+[UsesVerify]
+public class NullableTest
+{
+ [Fact]
+ public void NullableToNonNullableShouldThrow()
+ {
+ var source = TestSourceBuilder.Mapping(
+ "A?",
+ "B",
+ "class A { }",
+ "class B { }");
+
+ TestHelper.GenerateSingleMapperMethodBody(source)
+ .Should()
+ .Be(@"if (source == null)
+ throw new System.ArgumentNullException(nameof(source));
+ var target = new B();
+ return target;".ReplaceLineEndings());
+ }
+
+ [Fact]
+ public void NullableToNullableShouldWork()
+ {
+ var source = TestSourceBuilder.Mapping(
+ "A?",
+ "B?",
+ "class A { }",
+ "class B { }");
+
+ TestHelper.GenerateSingleMapperMethodBody(source)
+ .Should()
+ .Be(@"if (source == null)
+ return default;
+ var target = new B();
+ return target;".ReplaceLineEndings());
+ }
+
+ [Fact]
+ public void NonNullableToNullableShouldWork()
+ {
+ var source = TestSourceBuilder.Mapping(
+ "A",
+ "B?",
+ "class A { }",
+ "class B { }");
+
+ TestHelper.GenerateSingleMapperMethodBody(source)
+ .Should()
+ .Be(@"var target = new B();
+ return target;".ReplaceLineEndings());
+ }
+
+ [Fact]
+ public void NullableToNonNullableWithNoThrowShouldReturnNewInstance()
+ {
+ var source = TestSourceBuilder.Mapping(
+ "A?",
+ "B",
+ TestSourceBuilderOptions.Default with { ThrowOnMappingNullMismatch = false },
+ "class A { }",
+ "class B { }");
+
+ TestHelper.GenerateSingleMapperMethodBody(source)
+ .Should()
+ .Be(@"if (source == null)
+ return new B();
+ var target = new B();
+ return target;".ReplaceLineEndings());
+ }
+
+ [Fact]
+ public Task NullableToNonNullableWithNoThrowNoAccessibleCtorShouldDiagnostic()
+ {
+ var source = TestSourceBuilder.Mapping(
+ "string?",
+ "B",
+ TestSourceBuilderOptions.Default with { ThrowOnMappingNullMismatch = false },
+ "class A { }",
+ "class B { protected B(){} public static B Parse(string v) => new B(); }");
+ return TestHelper.VerifyGenerator(source);
+ }
+
+ [Fact]
+ public void NullableToNonNullableStringWithNoThrowShouldReturnEmptyString()
+ {
+ var source = TestSourceBuilder.Mapping(
+ "A?",
+ "string",
+ TestSourceBuilderOptions.Default with { ThrowOnMappingNullMismatch = false },
+ "class A { }");
+
+ TestHelper.GenerateSingleMapperMethodBody(source)
+ .Should()
+ .Be("return source == null ? \"\" : source.ToString();".ReplaceLineEndings());
+ }
+
+ [Fact]
+ public void NullableToNonNullableValueTypeWithNoThrowShouldReturnDefault()
+ {
+ var source = TestSourceBuilder.Mapping(
+ "DateTime?",
+ "DateTime",
+ TestSourceBuilderOptions.Default with { ThrowOnMappingNullMismatch = false });
+
+ TestHelper.GenerateSingleMapperMethodBody(source)
+ .Should()
+ .Be("return source == null ? default : source.Value;".ReplaceLineEndings());
+ }
+
+ [Fact]
+ public void WithExistingInstanceNullableSource()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ "void Map(A? source, B target)",
+ "class A { public string StringValue { get; set; } }",
+ "class B { public string StringValue { get; set; } }");
+
+ TestHelper.GenerateSingleMapperMethodBody(source)
+ .Should()
+ .Be(@"if (source == null)
+ return;
+ target.StringValue = source.StringValue;".ReplaceLineEndings());
+ }
+}
diff --git a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyTest.cs b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyTest.cs
index 548deb70ce..cb4a0cb5f0 100644
--- a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyTest.cs
+++ b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyTest.cs
@@ -1,3 +1,5 @@
+using Microsoft.CodeAnalysis;
+
namespace Riok.Mapperly.Tests.Mapping;
[UsesVerify]
@@ -102,7 +104,8 @@ public void NullableIntToNonNullableIntProperty()
TestHelper.GenerateSingleMapperMethodBody(source)
.Should()
.Be(@"var target = new B();
- target.Value = source.Value ?? default;
+ if (source.Value != null)
+ target.Value = source.Value.Value;
return target;".ReplaceLineEndings());
}
@@ -118,77 +121,106 @@ public void NullableStringToNonNullableStringProperty()
TestHelper.GenerateSingleMapperMethodBody(source)
.Should()
.Be(@"var target = new B();
- target.Value = source.Value ?? """";
+ if (source.Value != null)
+ target.Value = source.Value;
return target;".ReplaceLineEndings());
}
[Fact]
- public void NullableToNonNullableWithUserImplementedMethodAsAbstractClass()
+ public void NullableClassToNonNullableClassProperty()
{
- var source = @"
-using System;
-using System.Collections.Generic;
-using Riok.Mapperly.Abstractions;
-
-[Mapper]
-public abstract class MyMapper
-{
- public abstract B Map(A source);
+ var source = TestSourceBuilder.Mapping(
+ "A",
+ "B",
+ "class A { public C? Value { get; set; } }",
+ "class B { public D Value { get; set; } }",
+ "class C { public string V {get; set; } }",
+ "class D { public string V {get; set; } }");
- public string? MapString(string s)
- => s;
-}
+ TestHelper.GenerateMapperMethodBody(source)
+ .Should()
+ .Be(@"var target = new B();
+ if (source.Value != null)
+ target.Value = MapToD(source.Value);
+ return target;".ReplaceLineEndings());
+ }
-class A { public string Value { get; set; } }
-class B { public string Value { get; set; } }
-";
+ [Fact]
+ public void NonNullableClassToNullableClassProperty()
+ {
+ var source = TestSourceBuilder.Mapping(
+ "A",
+ "B",
+ "class A { public C Value { get; set; } }",
+ "class B { public D? Value { get; set; } }",
+ "class C { public string V {get; set; } }",
+ "class D { public string V {get; set; } }");
- TestHelper.GenerateSingleMapperMethodBody(source)
+ TestHelper.GenerateMapperMethodBody(source)
.Should()
.Be(@"var target = new B();
- target.Value = MapString(source.Value) ?? """";
+ target.Value = MapToD(source.Value);
return target;".ReplaceLineEndings());
}
[Fact]
- public void NullableToNonNullableWithUserImplementedMethodAsInterface()
+ public void DisabledNullableClassPropertyToNonNullableProperty()
{
- var source = @"
-using System;
-using System.Collections.Generic;
-using Riok.Mapperly.Abstractions;
-
-[Mapper]
-public interface IMyMapper
-{
- B Map(A source);
+ var source = TestSourceBuilder.Mapping(
+ "A",
+ "B",
+ "#nullable disable\n class A { public C Value { get; set; } }\n#nullable enable",
+ "class B { public D Value { get; set; } }",
+ "class C { public string V {get; set; } }",
+ "class D { public string V {get; set; } }");
- string? MapString(string s) => s;
-}
+ TestHelper.GenerateMapperMethodBody(source)
+ .Should()
+ .Be(@"var target = new B();
+ if (source.Value != null)
+ target.Value = MapToD(source.Value);
+ return target;".ReplaceLineEndings());
+ }
-class A { public string Value { get; set; } }
-class B { public string Value { get; set; } }
-";
+ [Fact]
+ public void NullableClassPropertyToDisabledNullableProperty()
+ {
+ var source = TestSourceBuilder.Mapping(
+ "A",
+ "B",
+ "class A { public C? Value { get; set; } }",
+ "#nullable disable\n class B { public D Value { get; set; } }\n#nullable enable",
+ "class C { public string V {get; set; } }",
+ "class D { public string V {get; set; } }");
- TestHelper.GenerateSingleMapperMethodBody(source)
+ TestHelper.GenerateMapperMethodBody(source)
.Should()
.Be(@"var target = new B();
- target.Value = ((IMyMapper)this).MapString(source.Value) ?? """";
+ if (source.Value != null)
+ target.Value = MapToD(source.Value);
return target;".ReplaceLineEndings());
}
[Fact]
- public Task NullableClassToNonNullableClassProperty()
+ public void NullableClassToNonNullableClassPropertyThrow()
{
var source = TestSourceBuilder.Mapping(
"A",
"B",
+ TestSourceBuilderOptions.Default with { ThrowOnPropertyMappingNullMismatch = true },
"class A { public C? Value { get; set; } }",
"class B { public D Value { get; set; } }",
"class C { public string V {get; set; } }",
"class D { public string V {get; set; } }");
- return TestHelper.VerifyGenerator(source);
+ TestHelper.GenerateMapperMethodBody(source)
+ .Should()
+ .Be(@"var target = new B();
+ if (source.Value != null)
+ target.Value = MapToD(source.Value);
+ else
+ throw new System.ArgumentNullException(nameof(source.Value));
+ return target;".ReplaceLineEndings());
}
[Fact]
@@ -292,78 +324,65 @@ public void ShouldUseUserProvidedMappingAsInterface()
}
[Fact]
- public void ShouldUseUserProvidedMappingAsAbstractClass()
+ public void ShouldUseUserProvidedMappingWithDisabledNullability()
{
var mapperBody = @"
-public abstract B Map(A source);
-public D UserImplementedMap(C source) => new D();";
+B Map(A source);
+D UserImplementedMap(C source) => new D();";
var source = TestSourceBuilder.MapperWithBodyAndTypes(
mapperBody,
- TestSourceBuilderOptions.Default with { AsInterface = false },
"class A { public string StringValue { get; set; } public C NestedValue { get; set; } }",
"class B { public string StringValue { get; set; } public D NestedValue { get; set; } }",
"class C {}",
"class D {}");
- TestHelper.GenerateSingleMapperMethodBody(source)
- .Should()
- .Be(@"var target = new B();
- target.StringValue = source.StringValue;
- target.NestedValue = UserImplementedMap(source.NestedValue);
- return target;".ReplaceLineEndings());
- }
-
- [Fact]
- public void NullableToNullable()
- {
- var source = TestSourceBuilder.Mapping(
- "A?",
- "B?",
- "class A { public string StringValue { get; set; } }",
- "class B { public string StringValue { get; set; } }");
-
- TestHelper.GenerateSingleMapperMethodBody(source)
+ TestHelper.GenerateSingleMapperMethodBody(
+ source,
+ TestHelperOptions.Default with { NullableOption = NullableContextOptions.Disable })
.Should()
.Be(@"if (source == null)
return default;
var target = new B();
- target.StringValue = source.StringValue;
+ if (source.StringValue != null)
+ target.StringValue = source.StringValue;
+ target.NestedValue = ((IMapper)this).UserImplementedMap(source.NestedValue);
return target;".ReplaceLineEndings());
}
[Fact]
- public void NullableToNonNullable()
+ public void ShouldUseUserProvidedMappingAsAbstractClass()
{
- var source = TestSourceBuilder.Mapping(
- "A?",
- "B",
- "class A { public string StringValue { get; set; } }",
- "class B { public string StringValue { get; set; } }");
+ var mapperBody = @"
+public abstract B Map(A source);
+public D UserImplementedMap(C source) => new D();";
+
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ mapperBody,
+ TestSourceBuilderOptions.Default with { AsInterface = false },
+ "class A { public string StringValue { get; set; } public C NestedValue { get; set; } }",
+ "class B { public string StringValue { get; set; } public D NestedValue { get; set; } }",
+ "class C {}",
+ "class D {}");
TestHelper.GenerateSingleMapperMethodBody(source)
.Should()
- .Be(@"if (source == null)
- return new B();
- var target = new B();
+ .Be(@"var target = new B();
target.StringValue = source.StringValue;
+ target.NestedValue = UserImplementedMap(source.NestedValue);
return target;".ReplaceLineEndings());
}
[Fact]
- public void CustomClassToNullableCustomClass()
+ public Task WithUnmappablePropertyShouldDiagnostic()
{
var source = TestSourceBuilder.Mapping(
"A",
- "B?",
- "class A { public string StringValue { get; set; } }",
- "class B { public string StringValue { get; set; } }");
+ "B",
+ "class A { public DateTime Value { get; set; } }",
+ "class B { public Version Value { get; set; } }");
- TestHelper.GenerateSingleMapperMethodBody(source)
- .Should()
- .Be(@"var target = new B();
- target.StringValue = source.StringValue;
- return target;".ReplaceLineEndings());
+ return TestHelper.VerifyGenerator(source);
}
[Fact]
diff --git a/test/Riok.Mapperly.Tests/Mapping/ParseTest.cs b/test/Riok.Mapperly.Tests/Mapping/ParseTest.cs
index 4bdb1278d2..7cc873e83f 100644
--- a/test/Riok.Mapperly.Tests/Mapping/ParseTest.cs
+++ b/test/Riok.Mapperly.Tests/Mapping/ParseTest.cs
@@ -26,7 +26,7 @@ public void ParseableBuiltNullableInClass()
var source = TestSourceBuilder.Mapping("string?", "int?");
TestHelper.GenerateSingleMapperMethodBody(source)
.Should()
- .Be("return int.Parse(source);");
+ .Be("return source == null ? default : int.Parse(source);");
}
[Fact]
diff --git a/test/Riok.Mapperly.Tests/Mapping/UserMethodTest.cs b/test/Riok.Mapperly.Tests/Mapping/UserMethodTest.cs
index 16af677256..e24157d59e 100644
--- a/test/Riok.Mapperly.Tests/Mapping/UserMethodTest.cs
+++ b/test/Riok.Mapperly.Tests/Mapping/UserMethodTest.cs
@@ -72,21 +72,6 @@ public void WithExistingInstance()
.Be("target.StringValue = source.StringValue;");
}
- [Fact]
- public void WithExistingInstanceNullableSource()
- {
- var source = TestSourceBuilder.MapperWithBodyAndTypes(
- "void Map(A? source, B target)",
- "class A { public string StringValue { get; set; } }",
- "class B { public string StringValue { get; set; } }");
-
- TestHelper.GenerateSingleMapperMethodBody(source)
- .Should()
- .Be(@"if (source == null)
- return;
- target.StringValue = source.StringValue;".ReplaceLineEndings());
- }
-
[Fact]
public void WithMultipleUserDefinedMethodShouldWork()
{
@@ -133,7 +118,7 @@ public void WithSameNamesShouldGenerateUniqueMethodNames()
TestHelper.GenerateMapperMethodBodies(source)
.Select(x => x.Name)
.Should()
- .BeEquivalentTo("MapToB", "MapToB2");
+ .BeEquivalentTo("MapToB", "MapToB1");
}
[Fact]
diff --git a/test/Riok.Mapperly.Tests/TestHelper.cs b/test/Riok.Mapperly.Tests/TestHelper.cs
index 1bdc05ba0e..0573ca9145 100644
--- a/test/Riok.Mapperly.Tests/TestHelper.cs
+++ b/test/Riok.Mapperly.Tests/TestHelper.cs
@@ -9,32 +9,38 @@ public static class TestHelper
{
public static Task VerifyGenerator(
string source,
- NullableContextOptions nullableOption = NullableContextOptions.Enable,
- LanguageVersion languageVersion = LanguageVersion.Default)
+ TestHelperOptions? options = null)
{
- var driver = Generate(source, nullableOption, languageVersion);
+ var driver = Generate(source, options);
return Verify(driver).ToTask();
}
- public static string GenerateSingleMapperMethodBody(string source, bool allowDiagnostics = false)
+ public static string GenerateSingleMapperMethodBody(string source, TestHelperOptions? options = null)
{
- return GenerateMapperMethodBodies(source, allowDiagnostics)
+ return GenerateMapperMethodBodies(source, options)
.Single()
.Body;
}
- public static string GenerateMapperMethodBody(string source, string methodName = TestSourceBuilder.DefaultMapMethodName, bool allowDiagnostics = false)
+ public static string GenerateMapperMethodBody(
+ string source,
+ string methodName = TestSourceBuilder.DefaultMapMethodName,
+ TestHelperOptions? options = null)
{
- return GenerateMapperMethodBodies(source, allowDiagnostics)
+ return GenerateMapperMethodBodies(source, options)
.Single(x => x.Name == methodName)
.Body;
}
- public static IEnumerable<(string Name, string Body)> GenerateMapperMethodBodies(string source, bool allowDiagnostics = false)
+ public static IEnumerable<(string Name, string Body)> GenerateMapperMethodBodies(
+ string source,
+ TestHelperOptions? options = null)
{
- var result = Generate(source).GetRunResult();
+ options ??= TestHelperOptions.Default;
- if (!allowDiagnostics)
+ var result = Generate(source, options).GetRunResult();
+
+ if (!options.AllowDiagnostics)
{
result.Diagnostics.Should().HaveCount(0);
}
@@ -62,11 +68,12 @@ private static string ExtractBody(MethodDeclarationSyntax methodImpl)
private static GeneratorDriver Generate(
string source,
- NullableContextOptions nullableOption = NullableContextOptions.Enable,
- LanguageVersion languageVersion = LanguageVersion.Default)
+ TestHelperOptions? options)
{
- var syntaxTree = CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(languageVersion));
- var compilation = BuildCompilation(nullableOption, syntaxTree);
+ options ??= TestHelperOptions.Default;
+
+ var syntaxTree = CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(options.LanguageVersion));
+ var compilation = BuildCompilation(options.NullableOption, syntaxTree);
var generator = new MapperGenerator();
GeneratorDriver driver = CSharpGeneratorDriver.Create(generator);
diff --git a/test/Riok.Mapperly.Tests/TestHelperOptions.cs b/test/Riok.Mapperly.Tests/TestHelperOptions.cs
new file mode 100644
index 0000000000..f3b716d07c
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/TestHelperOptions.cs
@@ -0,0 +1,12 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+
+namespace Riok.Mapperly.Tests;
+
+public record TestHelperOptions(
+ NullableContextOptions NullableOption = NullableContextOptions.Enable,
+ LanguageVersion LanguageVersion = LanguageVersion.Default,
+ bool AllowDiagnostics = false)
+{
+ public static readonly TestHelperOptions Default = new();
+}
diff --git a/test/Riok.Mapperly.Tests/TestSourceBuilder.cs b/test/Riok.Mapperly.Tests/TestSourceBuilder.cs
index 81e800918e..6f188c0266 100644
--- a/test/Riok.Mapperly.Tests/TestSourceBuilder.cs
+++ b/test/Riok.Mapperly.Tests/TestSourceBuilder.cs
@@ -1,3 +1,5 @@
+using System.Runtime.CompilerServices;
+
namespace Riok.Mapperly.Tests;
public static class TestSourceBuilder
@@ -36,9 +38,22 @@ public static string MapperWithBody(string body, TestSourceBuilderOptions? optio
private static string BuildAttribute(TestSourceBuilderOptions options)
{
- return options.UseDeepCloning
- ? "[Mapper(UseDeepCloning = true)]"
- : "[Mapper]";
+ var attrs = new[]
+ {
+ Attribute(options.UseDeepCloning),
+ Attribute(options.ThrowOnMappingNullMismatch),
+ Attribute(options.ThrowOnPropertyMappingNullMismatch),
+ };
+
+ return $"[Mapper({string.Join(", ", attrs)})]";
+ }
+
+ private static string Attribute(bool value, [CallerArgumentExpression("value")] string? expression = null)
+ {
+ if (expression == null)
+ throw new ArgumentNullException(nameof(expression));
+
+ return $"{expression.Split(".").Last()} = {(value ? "true" : "false")}";
}
public static string MapperWithBodyAndTypes(string body, params string[] types)
diff --git a/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs b/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs
index e92a6d6990..ee8c362328 100644
--- a/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs
+++ b/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs
@@ -3,7 +3,9 @@ namespace Riok.Mapperly.Tests;
public record TestSourceBuilderOptions(
bool AsInterface = true,
string? Namespace = null,
- bool UseDeepCloning = false)
+ bool UseDeepCloning = false,
+ bool ThrowOnMappingNullMismatch = true,
+ bool ThrowOnPropertyMappingNullMismatch = false)
{
public static readonly TestSourceBuilderOptions Default = new();
public static readonly TestSourceBuilderOptions WithDeepCloning = new(UseDeepCloning: true);
diff --git a/test/Riok.Mapperly.Tests/_snapshots/NullableTest.NullableToNonNullableWithNoThrowNoAccessibleCtorShouldDiagnostic.00.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/NullableTest.NullableToNonNullableWithNoThrowNoAccessibleCtorShouldDiagnostic.00.verified.txt
new file mode 100644
index 0000000000..3edbb81745
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/NullableTest.NullableToNonNullableWithNoThrowNoAccessibleCtorShouldDiagnostic.00.verified.txt
@@ -0,0 +1,17 @@
+{
+ Diagnostics: [
+ {
+ Id: RMG002,
+ Title: No accessible parameterless constructor found,
+ Severity: Error,
+ WarningLevel: 0,
+ Location: : (10,4)-(10,26),
+ Description: ,
+ HelpLink: ,
+ MessageFormat: {0} has no accessible parameterless constructor,
+ Message: B has no accessible parameterless constructor,
+ Category: Mapper,
+ CustomTags: []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/NullableTest.NullableToNonNullableWithNoThrowNoAccessibleCtorShouldDiagnostic.01.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/NullableTest.NullableToNonNullableWithNoThrowNoAccessibleCtorShouldDiagnostic.01.verified.cs
new file mode 100644
index 0000000000..1f826ec69f
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/NullableTest.NullableToNonNullableWithNoThrowNoAccessibleCtorShouldDiagnostic.01.verified.cs
@@ -0,0 +1,10 @@
+//HintName: Mapper.g.cs
+#nullable enable
+public sealed class Mapper : IMapper
+{
+ public static readonly IMapper Instance = new Mapper();
+ public B Map(string? source)
+ {
+ return source == null ? throw new System.ArgumentNullException(nameof(source)) : B.Parse(source);
+ }
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithUnmappablePropertyShouldDiagnostic.00.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithUnmappablePropertyShouldDiagnostic.00.verified.txt
new file mode 100644
index 0000000000..f2a2032e64
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithUnmappablePropertyShouldDiagnostic.00.verified.txt
@@ -0,0 +1,17 @@
+{
+ Diagnostics: [
+ {
+ Id: RMG007,
+ Title: Could not map property,
+ Severity: Error,
+ WarningLevel: 0,
+ Location: : (10,4)-(10,20),
+ Description: ,
+ HelpLink: ,
+ MessageFormat: Could not map property {0}.{1} of type {2} to {3}.{4} of type {5},
+ Message: Could not map property A.Value of type System.DateTime to B.Value of type System.Version,
+ Category: Mapper,
+ CustomTags: []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.NullableClassToNonNullableClassProperty.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithUnmappablePropertyShouldDiagnostic.01.verified.cs
similarity index 53%
rename from test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.NullableClassToNonNullableClassProperty.verified.cs
rename to test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithUnmappablePropertyShouldDiagnostic.01.verified.cs
index 3843746994..77f1e00e63 100644
--- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.NullableClassToNonNullableClassProperty.verified.cs
+++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyTest.WithUnmappablePropertyShouldDiagnostic.01.verified.cs
@@ -6,14 +6,6 @@ public sealed class Mapper : IMapper
public B Map(A source)
{
var target = new B();
- target.Value = source.Value == null ? new D() : MapToD(source.Value);
- return target;
- }
-
- private D MapToD(C source)
- {
- var target = new D();
- target.V = source.V;
return target;
}
}
\ No newline at end of file