diff --git a/src/Riok.Mapperly/Configuration/AttributeDataAccessor.cs b/src/Riok.Mapperly/Configuration/AttributeDataAccessor.cs index 2f1b00322d..f6f6873b6d 100644 --- a/src/Riok.Mapperly/Configuration/AttributeDataAccessor.cs +++ b/src/Riok.Mapperly/Configuration/AttributeDataAccessor.cs @@ -35,23 +35,41 @@ public bool HasAttribute(ISymbol symbol) public IEnumerable Access(ISymbol symbol) where TAttribute : Attribute => Access(symbol); + public IEnumerable TryAccess(IEnumerable data) + where TAttribute : Attribute => TryAccess(data); + + public IEnumerable Access(ISymbol symbol) + where TAttribute : Attribute + where TData : notnull + { + var attrDatas = symbolAccessor.GetAttributes(symbol); + return Access(attrDatas); + } + + public IEnumerable TryAccess(IEnumerable attributes) + where TAttribute : Attribute + where TData : notnull + { + var attrDatas = symbolAccessor.TryGetAttributes(attributes); + return Access(attrDatas); + } + /// /// Reads the attribute data and sets it on a newly created instance of . /// If has n type parameters, /// needs to have an accessible ctor with the parameters 0 to n-1 to be of type . /// needs to have exactly the same constructors as with additional type arguments. /// - /// The symbol on which the attributes should be read. + /// The attributes data. /// The type of the attribute. /// The type of the data class. If no type parameters are involved, this is usually the same as . /// The attribute data. /// If a property or ctor argument of could not be read on the attribute. - public IEnumerable Access(ISymbol symbol) + public IEnumerable Access(IEnumerable attributes) where TAttribute : Attribute where TData : notnull { - var attrDatas = symbolAccessor.GetAttributes(symbol); - foreach (var attrData in attrDatas) + foreach (var attrData in symbolAccessor.GetAttributes(attributes)) { yield return Access(attrData, symbolAccessor); } diff --git a/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs b/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs index 442f732c6a..609794df03 100644 --- a/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs @@ -40,7 +40,7 @@ public DescriptorBuilder( MapperConfiguration defaultMapperConfiguration ) { - _mapperDescriptor = new MapperDescriptor(mapperDeclaration, _methodNameBuilder); + _mapperDescriptor = new MapperDescriptor(mapperDeclaration, _methodNameBuilder, compilationContext.Compilation.LanguageVersion); _symbolAccessor = symbolAccessor; _types = compilationContext.Types; _mappingBodyBuilder = new MappingBodyBuilder(_mappings); diff --git a/src/Riok.Mapperly/Descriptors/MapperDescriptor.cs b/src/Riok.Mapperly/Descriptors/MapperDescriptor.cs index a7b4efbff2..16253f2d4d 100644 --- a/src/Riok.Mapperly/Descriptors/MapperDescriptor.cs +++ b/src/Riok.Mapperly/Descriptors/MapperDescriptor.cs @@ -1,5 +1,6 @@ using System.Text; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Descriptors.UnsafeAccess; @@ -18,10 +19,11 @@ public class MapperDescriptor public bool Static { get; set; } - public MapperDescriptor(MapperDeclaration declaration, UniqueNameBuilder nameBuilder) + public MapperDescriptor(MapperDeclaration declaration, UniqueNameBuilder nameBuilder, LanguageVersion languageVersion) { _declaration = declaration; NameBuilder = nameBuilder; + LanguageVersion = languageVersion; Name = BuildName(declaration.Symbol); UnsafeAccessorName = nameBuilder.New(AccessorClassName); @@ -31,6 +33,8 @@ public MapperDescriptor(MapperDeclaration declaration, UniqueNameBuilder nameBui } } + public LanguageVersion LanguageVersion { get; } + public string Name { get; } public string? Namespace { get; } diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/ConvertStaticMethodMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/ConvertStaticMethodMappingBuilder.cs index 0e720de9c9..616c372ab1 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/ConvertStaticMethodMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/ConvertStaticMethodMappingBuilder.cs @@ -21,15 +21,12 @@ public static class ConvertStaticMethodMappingBuilder allTargetMethods, GetTargetStaticMethodNames(ctx), ctx.Source, - ctx.Target, nonNullableTarget, targetIsNullable ); if (mapping is not null) - { return mapping; - } var allSourceMethods = ctx.SymbolAccessor.GetAllMethods(ctx.Source); @@ -44,7 +41,6 @@ public static class ConvertStaticMethodMappingBuilder allSourceMethods.ToList(), GetSourceStaticMethodNames(ctx), ctx.Source, - ctx.Target, nonNullableTarget, targetIsNullable ); @@ -81,7 +77,6 @@ private static bool IsDateTimeToTimeOnlyConversion(MappingBuilderContext ctx) List allMethods, IEnumerable methodNames, ITypeSymbol sourceType, - ITypeSymbol targetType, ITypeSymbol nonNullableTargetType, bool targetIsNullable ) diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/NullableMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/NullableMappingBuilder.cs index f27d1b0e71..10ff2ab47d 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/NullableMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/NullableMappingBuilder.cs @@ -13,7 +13,6 @@ public static class NullableMappingBuilder return null; var delegateMapping = ctx.BuildMapping(mappingKey, MappingBuildingOptions.KeepUserSymbol); - return delegateMapping == null ? null : BuildNullDelegateMapping(ctx, delegateMapping); } diff --git a/src/Riok.Mapperly/Descriptors/MappingCollection.cs b/src/Riok.Mapperly/Descriptors/MappingCollection.cs index 05cb0a2f26..9fac28a2e0 100644 --- a/src/Riok.Mapperly/Descriptors/MappingCollection.cs +++ b/src/Riok.Mapperly/Descriptors/MappingCollection.cs @@ -234,9 +234,11 @@ public void AddNamedUserMapping(string? name, TUserMapping mapping) public MappingCollectionAddResult TryAddAsDefault(T mapping, TypeMappingConfiguration config) { var mappingKey = new TypeMappingKey(mapping, config); - return _defaultMappings.TryAdd(mappingKey, mapping) + var result = _defaultMappings.TryAdd(mappingKey, mapping) ? MappingCollectionAddResult.Added : MappingCollectionAddResult.NotAddedDuplicated; + AddAdditionalMappings(mapping, config); + return result; } public MappingCollectionAddResult AddUserMapping(TUserMapping mapping, bool? isDefault, string? name) @@ -291,7 +293,16 @@ private MappingCollectionAddResult AddDefaultUserMapping(T mapping) _duplicatedNonDefaultUserMappings.Remove(mappingKey); _defaultMappings[mappingKey] = mapping; + AddAdditionalMappings(mapping, TypeMappingConfiguration.Default); return MappingCollectionAddResult.Added; } + + private void AddAdditionalMappings(T mapping, TypeMappingConfiguration config) + { + foreach (var additionalKey in mapping.BuildAdditionalMappingKeys(config)) + { + _defaultMappings.TryAdd(additionalKey, mapping); + } + } } } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/DerivedTypeSwitchMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/DerivedTypeSwitchMapping.cs index abbb46f0fb..e05f87af67 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/DerivedTypeSwitchMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/DerivedTypeSwitchMapping.cs @@ -1,6 +1,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Riok.Mapperly.Emit.Syntax; +using Riok.Mapperly.Helpers; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; @@ -11,11 +12,11 @@ namespace Riok.Mapperly.Descriptors.Mappings; /// by implementing a type switch over known types and performs the provided mapping for each type. /// public class DerivedTypeSwitchMapping(ITypeSymbol sourceType, ITypeSymbol targetType, IReadOnlyCollection typeMappings) - : NewInstanceMapping(sourceType, targetType) + : NewInstanceMethodMapping(sourceType, targetType) { private const string GetTypeMethodName = nameof(GetType); - public override ExpressionSyntax Build(TypeMappingBuildContext ctx) + public override IEnumerable BuildBody(TypeMappingBuildContext ctx) { // _ => throw new ArgumentException(msg, nameof(ctx.Source)), var sourceTypeExpr = ctx.SyntaxFactory.Invocation(MemberAccess(ctx.Source, GetTypeMethodName)); @@ -32,14 +33,15 @@ public override ExpressionSyntax Build(TypeMappingBuildContext ctx) // source switch { A x => MapToADto(x), B x => MapToBDto(x) } var (typeArmContext, typeArmVariableName) = ctx.WithNewSource(); var arms = typeMappings.Select(x => BuildSwitchArm(typeArmVariableName, x.SourceType, x.Build(typeArmContext))).Append(fallbackArm); - return ctx.SyntaxFactory.Switch(ctx.Source, arms); + var switchExpression = ctx.SyntaxFactory.Switch(ctx.Source, arms); + return [ctx.SyntaxFactory.Return(switchExpression)]; } private SwitchExpressionArmSyntax BuildSwitchArm(string typeArmVariableName, ITypeSymbol type, ExpressionSyntax mapping) { // A x => MapToADto(x), var declaration = DeclarationPattern( - FullyQualifiedIdentifier(type).AddTrailingSpace(), + FullyQualifiedIdentifier(type.NonNullable()).AddTrailingSpace(), SingleVariableDesignation(Identifier(typeArmVariableName)) ); return SwitchArm(declaration, mapping); diff --git a/src/Riok.Mapperly/Descriptors/Mappings/ExistingTarget/ExistingTargetMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/ExistingTarget/ExistingTargetMapping.cs index 448286889a..75d6c56465 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/ExistingTarget/ExistingTargetMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/ExistingTarget/ExistingTargetMapping.cs @@ -26,5 +26,7 @@ protected ExistingTargetMapping(ITypeSymbol sourceType, ITypeSymbol targetType) public virtual bool IsSynthetic => false; + public IEnumerable BuildAdditionalMappingKeys(TypeMappingConfiguration config) => []; + public abstract IEnumerable Build(TypeMappingBuildContext ctx, ExpressionSyntax target); } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/ExistingTarget/ObjectMemberExistingTargetMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/ExistingTarget/ObjectMemberExistingTargetMapping.cs index 6f6a2420ba..a8470087a5 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/ExistingTarget/ObjectMemberExistingTargetMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/ExistingTarget/ObjectMemberExistingTargetMapping.cs @@ -19,4 +19,6 @@ public class ObjectMemberExistingTargetMapping(ITypeSymbol sourceType, ITypeSymb public ITypeSymbol TargetType { get; } = targetType; public bool IsSynthetic => false; + + public IEnumerable BuildAdditionalMappingKeys(TypeMappingConfiguration config) => []; } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/ITypeMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/ITypeMapping.cs index 65506ac17a..059cf516ca 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/ITypeMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/ITypeMapping.cs @@ -9,4 +9,6 @@ public interface ITypeMapping : IMapping /// Gets a value indicating whether this mapping produces any code or can be omitted completely (eg. direct assignments or delegate mappings). /// bool IsSynthetic { get; } + + IEnumerable BuildAdditionalMappingKeys(TypeMappingConfiguration config); } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs index b8a3604dfd..7aa0dbb7ea 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs @@ -78,6 +78,8 @@ ITypeSymbol targetType public bool IsSynthetic => false; + public virtual IEnumerable BuildAdditionalMappingKeys(TypeMappingConfiguration config) => []; + public virtual ExpressionSyntax Build(TypeMappingBuildContext ctx) => ctx.SyntaxFactory.Invocation( MethodName, @@ -91,7 +93,7 @@ public virtual MethodDeclarationSyntax BuildMethod(SourceEmitterContext ctx) SourceParameter.Name, ReferenceHandlerParameter?.Name, ctx.NameBuilder.NewScope(), - ctx.SyntaxFactory.AddIndentation() + ctx.SyntaxFactory ); var parameters = BuildParameterList(); @@ -101,8 +103,8 @@ public virtual MethodDeclarationSyntax BuildMethod(SourceEmitterContext ctx) return MethodDeclaration(returnType.AddTrailingSpace(), Identifier(MethodName)) .WithModifiers(TokenList(BuildModifiers(ctx.IsStatic))) .WithParameterList(parameters) - .WithAttributeLists(ctx.SyntaxFactory.GeneratedCodeAttributeList()) - .WithBody(ctx.SyntaxFactory.Block(BuildBody(typeMappingBuildContext))); + .WithAttributeLists(BuildAttributes(typeMappingBuildContext)) + .WithBody(ctx.SyntaxFactory.Block(BuildBody(typeMappingBuildContext.AddIndentation()))); } public abstract IEnumerable BuildBody(TypeMappingBuildContext ctx); @@ -121,6 +123,9 @@ internal virtual void EnableReferenceHandling(INamedTypeSymbol iReferenceHandler ); } + protected internal virtual SyntaxList BuildAttributes(TypeMappingBuildContext ctx) => + [ctx.SyntaxFactory.GeneratedCodeAttribute()]; + protected virtual ParameterListSyntax BuildParameterList() => ParameterList(IsExtensionMethod, [SourceParameter, ReferenceHandlerParameter, .. AdditionalSourceParameters]); diff --git a/src/Riok.Mapperly/Descriptors/Mappings/NewInstanceMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/NewInstanceMapping.cs index 4bcb6925f3..2eb0fc8614 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/NewInstanceMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/NewInstanceMapping.cs @@ -25,5 +25,7 @@ protected NewInstanceMapping(ITypeSymbol sourceType, ITypeSymbol targetType) /// public virtual bool IsSynthetic => false; + public virtual IEnumerable BuildAdditionalMappingKeys(TypeMappingConfiguration config) => []; + public abstract ExpressionSyntax Build(TypeMappingBuildContext ctx); } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/NoOpMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/NoOpMapping.cs index a6acd2eef4..7fe9134565 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/NoOpMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/NoOpMapping.cs @@ -10,5 +10,7 @@ public class NoOpMapping(ITypeSymbol sourceType, ITypeSymbol targetType) : IExis public ITypeSymbol TargetType => targetType; public bool IsSynthetic => true; + public IEnumerable BuildAdditionalMappingKeys(TypeMappingConfiguration config) => []; + public IEnumerable Build(TypeMappingBuildContext ctx, ExpressionSyntax target) => []; } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/NullDelegateMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/NullDelegateMethodMapping.cs index 2da0f50df7..6c4877fe91 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/NullDelegateMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/NullDelegateMethodMapping.cs @@ -16,6 +16,37 @@ public class NullDelegateMethodMapping( NullFallbackValue nullFallbackValue ) : NewInstanceMethodMapping(nullableSourceType, nullableTargetType) { + public override IEnumerable BuildAdditionalMappingKeys(TypeMappingConfiguration config) + { + // if the fallback value is not nullable, + // this mapping never returns null. + // add the following mapping keys: + // null => null (added by default) + // null => non-null + // non-null => non-null + if (!nullFallbackValue.IsNullable(TargetType)) + { + return + [ + new TypeMappingKey(SourceType, TargetType.NonNullable(), config), + new TypeMappingKey(SourceType.NonNullable(), TargetType.NonNullable(), config), + ]; + } + + // this mapping never returns null for non-null input values + // and is guarded with [return: NotNullIfNotNull] + // therefore this mapping can also be used as mapping for non-null values. + return [new TypeMappingKey(delegateMapping, config)]; + } + + protected internal override SyntaxList BuildAttributes(TypeMappingBuildContext ctx) + { + if (!TargetType.IsNullable() || !nullFallbackValue.IsNullable(TargetType)) + return base.BuildAttributes(ctx); + + return [.. base.BuildAttributes(ctx), ctx.SyntaxFactory.ReturnNotNullIfNotNullAttribute(ctx.Source)]; + } + public override IEnumerable BuildBody(TypeMappingBuildContext ctx) { var body = delegateMapping.BuildBody(ctx); diff --git a/src/Riok.Mapperly/Descriptors/Mappings/NullFallbackValueExtensions.cs b/src/Riok.Mapperly/Descriptors/Mappings/NullFallbackValueExtensions.cs new file mode 100644 index 0000000000..12daee8fde --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/Mappings/NullFallbackValueExtensions.cs @@ -0,0 +1,10 @@ +using Microsoft.CodeAnalysis; +using Riok.Mapperly.Helpers; + +namespace Riok.Mapperly.Descriptors.Mappings; + +internal static class NullFallbackValueExtensions +{ + public static bool IsNullable(this NullFallbackValue fallbackValue, ITypeSymbol targetType) => + fallbackValue == NullFallbackValue.Default && targetType.IsNullable(); +} diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs index 3c9db851a5..c07acb7f86 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs @@ -1,6 +1,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Riok.Mapperly.Abstractions.ReferenceHandling; +using Riok.Mapperly.Helpers; using Riok.Mapperly.Symbols; namespace Riok.Mapperly.Descriptors.Mappings.UserMappings; @@ -21,6 +22,8 @@ bool enableReferenceHandling public new IMethodSymbol Method { get; } = method; + private MethodMapping? DelegateMethodMapping => _delegateMapping as MethodMapping; + public bool? Default { get; } = isDefault; public bool IsExternal => false; @@ -33,6 +36,18 @@ bool enableReferenceHandling public void SetDelegateMapping(INewInstanceMapping mapping) => _delegateMapping = mapping; + public override IEnumerable BuildAdditionalMappingKeys(TypeMappingConfiguration config) + { + // null is never returned if the source value is not null + if (TargetType.IsNullable()) + { + yield return new TypeMappingKey(SourceType.NonNullable(), TargetType.NonNullable(), config); + } + } + + protected internal override SyntaxList BuildAttributes(TypeMappingBuildContext ctx) => + DelegateMethodMapping?.BuildAttributes(ctx) ?? base.BuildAttributes(ctx); + public override ExpressionSyntax Build(TypeMappingBuildContext ctx) { return InternalReferenceHandlingEnabled ? _delegateMapping?.Build(ctx) ?? base.Build(ctx) : base.Build(ctx); @@ -67,10 +82,7 @@ public override IEnumerable BuildBody(TypeMappingBuildContext c return [ctx.SyntaxFactory.Return(_delegateMapping.Build(ctx))]; } - if (_delegateMapping is MethodMapping delegateMethodMapping) - return delegateMethodMapping.BuildBody(ctx); - - return [ctx.SyntaxFactory.Return(_delegateMapping.Build(ctx))]; + return DelegateMethodMapping?.BuildBody(ctx) ?? [ctx.SyntaxFactory.Return(_delegateMapping.Build(ctx))]; } internal override void EnableReferenceHandling(INamedTypeSymbol iReferenceHandlerType) diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedMethodMapping.cs index d504c22025..a1d41c2970 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedMethodMapping.cs @@ -1,5 +1,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Helpers; using Riok.Mapperly.Symbols; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; @@ -16,24 +17,51 @@ public class UserImplementedMethodMapping( MethodParameter sourceParameter, ITypeSymbol targetType, MethodParameter? referenceHandlerParameter, - bool isExternal + bool isExternal, + UserImplementedMethodMapping.TargetNullability targetNullability ) : NewInstanceMapping(sourceParameter.Type, targetType), INewInstanceUserMapping { + public enum TargetNullability + { + NeverNull, + NotNullIfSourceNotNull, + Nullable, + } + public IMethodSymbol Method { get; } = method; public bool? Default { get; } = isDefault; + public bool IsExternal { get; } = isExternal; + public override IEnumerable BuildAdditionalMappingKeys(TypeMappingConfiguration config) + { + var keys = base.BuildAdditionalMappingKeys(config); + switch (targetNullability) + { + case TargetNullability.NeverNull when TargetType.IsNullable(): + keys = keys.Append(new TypeMappingKey(SourceType, TargetType.NonNullable())); + goto case TargetNullability.NotNullIfSourceNotNull; + case TargetNullability.NotNullIfSourceNotNull: + keys = keys.Append(new TypeMappingKey(SourceType.NonNullable(), TargetType.NonNullable())); + break; + } + + return keys; + } + public override ExpressionSyntax Build(TypeMappingBuildContext ctx) { // if the user implemented method is on an interface, // we explicitly cast to be able to use the default interface implementation or explicit implementations if (Method.ReceiverType?.TypeKind != TypeKind.Interface) + { return ctx.SyntaxFactory.Invocation( receiver == null ? IdentifierName(Method.Name) : MemberAccess(receiver, Method.Name), sourceParameter.WithArgument(ctx.Source), referenceHandlerParameter?.WithArgument(ctx.ReferenceHandler) ); + } var castedReceiver = CastExpression( FullyQualifiedIdentifier(Method.ReceiverType!), diff --git a/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs b/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs index cd39a2344f..dd4fd0ddca 100644 --- a/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs +++ b/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs @@ -196,15 +196,24 @@ internal ITypeSymbol NonNullableIfNullableReferenceTypesDisabled(ITypeSymbol typ } internal IEnumerable GetAttributes(ISymbol symbol) + where T : Attribute => GetAttributes(GetAttributesCore(symbol)); + + internal IEnumerable TryGetAttributes(IEnumerable attributes) where T : Attribute { - var attributes = GetAttributesCore(symbol); - if (attributes.IsEmpty) - { - yield break; - } + var attributeSymbol = compilationContext.Types.TryGet(typeof(T).FullName ?? ""); + return attributeSymbol == null ? [] : GetAttributes(attributeSymbol, attributes); + } + internal IEnumerable GetAttributes(IEnumerable attributes) + where T : Attribute + { var attributeSymbol = compilationContext.Types.Get(); + return GetAttributes(attributeSymbol, attributes); + } + + internal IEnumerable GetAttributes(ITypeSymbol attributeSymbol, IEnumerable attributes) + { foreach (var attr in attributes) { if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass?.ConstructedFrom ?? attr.AttributeClass, attributeSymbol)) @@ -228,6 +237,9 @@ internal static IEnumerable GetAttributesSkipCache(ISymbol symbol internal bool HasAttribute(ISymbol symbol) where T : Attribute => GetAttributes(symbol).Any(); + internal bool TryHasAttribute(IEnumerable symbol) + where T : Attribute => TryGetAttributes(symbol).Any(); + internal IEnumerable GetAllMethods(ITypeSymbol symbol) => GetAllMembers(symbol).OfType(); internal IEnumerable GetAllMethods(ITypeSymbol symbol, string name) => diff --git a/src/Riok.Mapperly/Descriptors/TypeMappingKey.cs b/src/Riok.Mapperly/Descriptors/TypeMappingKey.cs index c499bf2e4a..81197c8f8f 100644 --- a/src/Riok.Mapperly/Descriptors/TypeMappingKey.cs +++ b/src/Riok.Mapperly/Descriptors/TypeMappingKey.cs @@ -11,7 +11,7 @@ public readonly struct TypeMappingKey( ITypeSymbol target, TypeMappingConfiguration? config = null, bool includeNullability = true -) +) : IEquatable { private static readonly IEqualityComparer _comparer = SymbolEqualityComparer.IncludeNullability; @@ -28,11 +28,10 @@ public TypeMappingKey(ITypeMapping mapping, TypeMappingConfiguration? config = n public TypeMappingKey TargetNonNullable() => new(Source, Target.NonNullable(), Configuration); - public override bool Equals(object? obj) => - obj is TypeMappingKey other - && _comparer.Equals(Source, other.Source) - && _comparer.Equals(Target, other.Target) - && Configuration.Equals(other.Configuration); + public override bool Equals(object? obj) => obj is TypeMappingKey other && Equals(other); + + public bool Equals(TypeMappingKey other) => + _comparer.Equals(Source, other.Source) && _comparer.Equals(Target, other.Target) && Configuration.Equals(other.Configuration); public override int GetHashCode() { diff --git a/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeConstructorAccessor.cs b/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeConstructorAccessor.cs index 0470f2c868..b60c27472c 100644 --- a/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeConstructorAccessor.cs +++ b/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeConstructorAccessor.cs @@ -18,8 +18,8 @@ public MethodDeclarationSyntax BuildAccessorMethod(SourceEmitterContext ctx) { var typeToCreate = IdentifierName(symbol.ContainingType.FullyQualifiedIdentifierName()).AddTrailingSpace(); var parameters = ParameterList(symbol.Parameters); - var attributeList = ctx.SyntaxFactory.UnsafeAccessorAttributeList(UnsafeAccessorType.Constructor); - return PublicStaticExternMethod(ctx, typeToCreate, methodName, parameters, attributeList); + var attribute = ctx.SyntaxFactory.UnsafeAccessorAttribute(UnsafeAccessorType.Constructor); + return PublicStaticExternMethod(ctx, typeToCreate, methodName, parameters, [attribute]); } public ExpressionSyntax CreateInstance( diff --git a/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeFieldAccessor.cs b/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeFieldAccessor.cs index 1c46f7410b..09c4c3d81e 100644 --- a/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeFieldAccessor.cs +++ b/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeFieldAccessor.cs @@ -31,11 +31,11 @@ public MethodDeclarationSyntax BuildAccessorMethod(SourceEmitterContext ctx) var target = Parameter(symbol.ContainingType.FullyQualifiedIdentifierName(), targetName, true); var parameters = ParameterList(CommaSeparatedList(target)); - var attributeList = ctx.SyntaxFactory.UnsafeAccessorAttributeList(UnsafeAccessorType.Field, symbol.Name); + var attribute = ctx.SyntaxFactory.UnsafeAccessorAttribute(UnsafeAccessorType.Field, symbol.Name); var returnType = RefType(IdentifierName(symbol.Type.FullyQualifiedIdentifierName()).AddTrailingSpace()) .WithRefKeyword(Token(TriviaList(), SyntaxKind.RefKeyword, TriviaList(Space))); - return PublicStaticExternMethod(ctx, returnType, methodName, parameters, attributeList); + return PublicStaticExternMethod(ctx, returnType, methodName, parameters, [attribute]); } public ExpressionSyntax BuildAccess(ExpressionSyntax? baseAccess, bool nullConditional = false) diff --git a/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeGetPropertyAccessor.cs b/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeGetPropertyAccessor.cs index e412a4ffa3..a482d3d866 100644 --- a/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeGetPropertyAccessor.cs +++ b/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeGetPropertyAccessor.cs @@ -28,13 +28,13 @@ public MethodDeclarationSyntax BuildAccessorMethod(SourceEmitterContext ctx) var source = Parameter(symbol.ContainingType.FullyQualifiedIdentifierName(), sourceName, true); var parameters = ParameterList(CommaSeparatedList(source)); - var attributeList = ctx.SyntaxFactory.UnsafeAccessorAttributeList(UnsafeAccessorType.Method, $"get_{symbol.Name}"); + var attribute = ctx.SyntaxFactory.UnsafeAccessorAttribute(UnsafeAccessorType.Method, $"get_{symbol.Name}"); return PublicStaticExternMethod( ctx, IdentifierName(symbol.Type.FullyQualifiedIdentifierName()).AddTrailingSpace(), methodName, parameters, - attributeList + [attribute] ); } diff --git a/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeSetPropertyAccessor.cs b/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeSetPropertyAccessor.cs index f6dce54f0b..5d682c7b6a 100644 --- a/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeSetPropertyAccessor.cs +++ b/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeSetPropertyAccessor.cs @@ -34,14 +34,14 @@ public MethodDeclarationSyntax BuildAccessorMethod(SourceEmitterContext ctx) var targetValue = Parameter(symbol.Type.FullyQualifiedIdentifierName(), valueName); var parameters = ParameterList(CommaSeparatedList(target, targetValue)); - var attributeList = ctx.SyntaxFactory.UnsafeAccessorAttributeList(UnsafeAccessorType.Method, $"set_{symbol.Name}"); + var attribute = ctx.SyntaxFactory.UnsafeAccessorAttribute(UnsafeAccessorType.Method, $"set_{symbol.Name}"); return PublicStaticExternMethod( ctx, PredefinedType(Token(SyntaxKind.VoidKeyword)).AddTrailingSpace(), methodName, parameters, - attributeList + [attribute] ); } diff --git a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs index b5b8994a70..26f6643d19 100644 --- a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs +++ b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis; using Riok.Mapperly.Abstractions; using Riok.Mapperly.Configuration; @@ -133,12 +134,11 @@ bool isExternal if (!valid || !UserMappingMethodParameterExtractor.BuildParameters(ctx, method, false, out var parameters)) { - if (hasAttribute) - { - var name = receiver == null ? method.Name : receiver + method.Name; - ctx.ReportDiagnostic(DiagnosticDescriptors.UnsupportedMappingMethodSignature, method, name); - } + if (!hasAttribute) + return null; + var name = receiver == null ? method.Name : receiver + method.Name; + ctx.ReportDiagnostic(DiagnosticDescriptors.UnsupportedMappingMethodSignature, method, name); return null; } @@ -158,17 +158,40 @@ bool isExternal ); } + var (targetType, targetTypeNullability) = BuildTargetType(ctx, method, parameters.Source.Name); return new UserImplementedMethodMapping( receiver, method, userMappingConfig.Default, parameters.Source, - ctx.SymbolAccessor.UpgradeNullable(method.ReturnType), + targetType, parameters.ReferenceHandler, - isExternal + isExternal, + targetTypeNullability ); } + private static (ITypeSymbol, UserImplementedMethodMapping.TargetNullability) BuildTargetType( + SimpleMappingBuilderContext ctx, + IMethodSymbol method, + string sourceParameterName + ) + { + var targetType = ctx.SymbolAccessor.UpgradeNullable(method.ReturnType); + if (!targetType.IsNullable() || ctx.SymbolAccessor.TryHasAttribute(method.GetReturnTypeAttributes())) + { + return (targetType, UserImplementedMethodMapping.TargetNullability.NeverNull); + } + + var targetNotNullIfSourceNotNull = ctx + .AttributeAccessor.TryAccess(method.GetReturnTypeAttributes()) + .Any(attr => string.Equals(attr.ParameterName, sourceParameterName, StringComparison.Ordinal)); + var nullability = targetNotNullIfSourceNotNull + ? UserImplementedMethodMapping.TargetNullability.NotNullIfSourceNotNull + : UserImplementedMethodMapping.TargetNullability.Nullable; + return (targetType, nullability); + } + private static IUserMapping? BuildUserDefinedMapping(SimpleMappingBuilderContext ctx, IMethodSymbol methodSymbol) { if (!methodSymbol.IsPartialDefinition) diff --git a/src/Riok.Mapperly/Emit/SourceEmitter.cs b/src/Riok.Mapperly/Emit/SourceEmitter.cs index 5275b7e367..d82714a2dc 100644 --- a/src/Riok.Mapperly/Emit/SourceEmitter.cs +++ b/src/Riok.Mapperly/Emit/SourceEmitter.cs @@ -13,7 +13,7 @@ public static class SourceEmitter public static CompilationUnitSyntax Build(MapperDescriptor descriptor, CancellationToken cancellationToken) { - var ctx = new SourceEmitterContext(descriptor.Static, descriptor.NameBuilder, new SyntaxFactoryHelper()); + var ctx = new SourceEmitterContext(descriptor.Static, descriptor.NameBuilder, new SyntaxFactoryHelper(descriptor.LanguageVersion)); ctx = IndentForMapper(ctx, descriptor.Symbol); var memberCtx = ctx.AddIndentation(); diff --git a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Attribute.cs b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Attribute.cs index 829774c676..42aea7be23 100644 --- a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Attribute.cs +++ b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Attribute.cs @@ -1,4 +1,3 @@ -using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; @@ -13,16 +12,7 @@ public partial struct SyntaxFactoryHelper private static readonly IdentifierNameSyntax _unsafeAccessorKindName = IdentifierName(UnsafeAccessorKindName); - public SyntaxList AttributeList(string name, params ExpressionSyntax[] arguments) - { - var args = CommaSeparatedList(arguments.Select(AttributeArgument)); - - var attribute = Attribute(IdentifierName(name)).WithArgumentList(AttributeArgumentList(args)); - - return SingletonList(SyntaxFactory.AttributeList(SingletonSeparatedList(attribute)).AddTrailingLineFeed(Indentation)); - } - - public SyntaxList UnsafeAccessorAttributeList(UnsafeAccessorType type, string? name = null) + public AttributeListSyntax UnsafeAccessorAttribute(UnsafeAccessorType type, string? name = null) { var unsafeAccessType = type switch { @@ -34,9 +24,21 @@ public SyntaxList UnsafeAccessorAttributeList(UnsafeAccesso var kind = MemberAccess(_unsafeAccessorKindName, IdentifierName(unsafeAccessType)); if (name == null) - return AttributeList(UnsafeAccessorName, kind); + return Attribute(UnsafeAccessorName, kind); + + return Attribute(UnsafeAccessorName, kind, Assignment(IdentifierName(UnsafeAccessorNameArgument), StringLiteral(name))); + } + + private AttributeListSyntax Attribute(string name, params ExpressionSyntax[] arguments) + { + var args = CommaSeparatedList(arguments.Select(AttributeArgument)); + var attribute = SyntaxFactory.Attribute(IdentifierName(name)); + if (args.Count > 0) + { + attribute = attribute.WithArgumentList(AttributeArgumentList(args)); + } - return AttributeList(UnsafeAccessorName, kind, Assignment(IdentifierName(UnsafeAccessorNameArgument), StringLiteral(name))); + return AttributeList(SingletonSeparatedList(attribute)).AddTrailingLineFeed(Indentation); } public enum UnsafeAccessorType diff --git a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.GeneratedCode.cs b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.GeneratedCode.cs index 504bd64690..9cbba15517 100644 --- a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.GeneratedCode.cs +++ b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.GeneratedCode.cs @@ -1,5 +1,4 @@ using System.Reflection; -using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Riok.Mapperly.Emit.Syntax; @@ -9,9 +8,9 @@ public partial struct SyntaxFactoryHelper private const string GeneratedCodeAttributeName = "global::System.CodeDom.Compiler.GeneratedCode"; private static readonly AssemblyName _generatorAssemblyName = typeof(SyntaxFactoryHelper).Assembly.GetName(); - public SyntaxList GeneratedCodeAttributeList() + public AttributeListSyntax GeneratedCodeAttribute() { - return AttributeList( + return Attribute( GeneratedCodeAttributeName, StringLiteral(_generatorAssemblyName.Name), StringLiteral(_generatorAssemblyName.Version.ToString()) diff --git a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Method.cs b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Method.cs index 6035dda45d..4769d754cd 100644 --- a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Method.cs +++ b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Method.cs @@ -12,7 +12,7 @@ public static MethodDeclarationSyntax PublicStaticExternMethod( TypeSyntax returnType, string methodName, ParameterListSyntax parameterList, - SyntaxList attributeList + SyntaxList attributes ) { return MethodDeclaration(returnType, Identifier(methodName)) @@ -24,7 +24,7 @@ SyntaxList attributeList ) ) .WithParameterList(parameterList) - .WithAttributeLists(ctx.SyntaxFactory.GeneratedCodeAttributeList().AddRange(attributeList)) + .WithAttributeLists([ctx.SyntaxFactory.GeneratedCodeAttribute(), .. attributes]) .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)); } } diff --git a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.NullAttributes.cs b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.NullAttributes.cs new file mode 100644 index 0000000000..06221f7f86 --- /dev/null +++ b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.NullAttributes.cs @@ -0,0 +1,16 @@ +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace Riok.Mapperly.Emit.Syntax; + +public partial struct SyntaxFactoryHelper +{ + private const string NotNullIfNotNullAttributeName = "global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull"; + + public AttributeListSyntax ReturnNotNullIfNotNullAttribute(ExpressionSyntax source) + { + return Attribute(NotNullIfNotNullAttributeName, ParameterNameOfOrStringLiteral(source)) + .WithTarget(AttributeTargetSpecifier(Token(SyntaxKind.ReturnKeyword)).AddTrailingSpace()); + } +} diff --git a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.String.cs b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.String.cs index 493930d552..589fa559e8 100644 --- a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.String.cs +++ b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.String.cs @@ -21,6 +21,17 @@ public partial struct SyntaxFactoryHelper public static InvocationExpressionSyntax NameOf(ExpressionSyntax expression) => InvocationWithoutIndention(_nameofIdentifier, expression); + private ExpressionSyntax ParameterNameOfOrStringLiteral(ExpressionSyntax expression) + { +#if ROSLYN4_4_OR_GREATER + // nameof(parameter) was introduced in c# 11.0 + if (languageVersion >= LanguageVersion.CSharp11) + return NameOf(expression); +#endif + + return StringLiteral(expression.ToFullString()); + } + public static IdentifierNameSyntax FullyQualifiedIdentifier(ITypeSymbol typeSymbol) => IdentifierName(typeSymbol.FullyQualifiedIdentifierName()); diff --git a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.SymbolDeclaration.cs b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.SymbolDeclaration.cs index 3b45aca5d1..990cc8fe87 100644 --- a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.SymbolDeclaration.cs +++ b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.SymbolDeclaration.cs @@ -25,7 +25,7 @@ public ClassDeclarationSyntax Class(string name, SyntaxTokenList modifiers, Synt .WithKeyword(TrailingSpacedToken(SyntaxKind.ClassKeyword)) .WithOpenBraceToken(LeadingLineFeedToken(SyntaxKind.OpenBraceToken)) .WithCloseBraceToken(LeadingLineFeedToken(SyntaxKind.CloseBraceToken)) - .WithAttributeLists(isPartial ? [] : GeneratedCodeAttributeList()) + .WithAttributeLists(isPartial ? [] : [GeneratedCodeAttribute()]) .AddLeadingLineFeed(Indentation); } @@ -48,7 +48,7 @@ public TypeDeclarationSyntax TypeDeclaration(TypeDeclarationSyntax syntax, Synta .WithKeyword(TrailingSpacedToken(syntax.Keyword.Kind())) .WithOpenBraceToken(LeadingLineFeedToken(SyntaxKind.OpenBraceToken)) .WithCloseBraceToken(LeadingLineFeedToken(SyntaxKind.CloseBraceToken)) - .WithAttributeLists(isPartial ? [] : GeneratedCodeAttributeList()) + .WithAttributeLists(isPartial ? [] : [GeneratedCodeAttribute()]) .AddLeadingLineFeed(Indentation); } } diff --git a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.cs b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.cs index 6c3639b836..386fa4226c 100644 --- a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.cs +++ b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.cs @@ -1,3 +1,4 @@ +using System.Runtime.InteropServices; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -6,22 +7,24 @@ namespace Riok.Mapperly.Emit.Syntax; // useful to create syntax factories: https://roslynquoter.azurewebsites.net/ and https://sharplab.io/ -public readonly partial struct SyntaxFactoryHelper +[StructLayout(LayoutKind.Auto)] +public readonly partial struct SyntaxFactoryHelper(LanguageVersion languageVersion) { private const int ConditionalMultilineThreshold = 70; public static readonly IdentifierNameSyntax VarIdentifier = IdentifierName("var").AddTrailingSpace(); - private SyntaxFactoryHelper(int indentation) + private SyntaxFactoryHelper(int indentation, LanguageVersion languageVersion) + : this(languageVersion) { Indentation = indentation; } public int Indentation { get; } - public SyntaxFactoryHelper AddIndentation() => new(Indentation + 1); + public SyntaxFactoryHelper AddIndentation() => new(Indentation + 1, languageVersion); - public SyntaxFactoryHelper RemoveIndentation() => new(Indentation - 1); + public SyntaxFactoryHelper RemoveIndentation() => new(Indentation - 1, languageVersion); public static AssignmentExpressionSyntax Assignment(ExpressionSyntax target, ExpressionSyntax source, bool coalesce) { diff --git a/src/Riok.Mapperly/MapperGenerator.cs b/src/Riok.Mapperly/MapperGenerator.cs index 58273bb024..7142075ecf 100644 --- a/src/Riok.Mapperly/MapperGenerator.cs +++ b/src/Riok.Mapperly/MapperGenerator.cs @@ -36,7 +36,12 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .CompilationProvider.Combine(nestedCompilations) .Select( static (c, _) => - new CompilationContext(c.Left, new WellKnownTypes(c.Left), c.Right.ToImmutableArray(), new FileNameBuilder()) + new CompilationContext( + (CSharpCompilation)c.Left, + new WellKnownTypes(c.Left), + c.Right.ToImmutableArray(), + new FileNameBuilder() + ) ) .WithTrackingName(MapperGeneratorStepNames.BuildCompilationContext); diff --git a/src/Riok.Mapperly/Symbols/CompilationContext.cs b/src/Riok.Mapperly/Symbols/CompilationContext.cs index bec42d76a9..5d082b34c1 100644 --- a/src/Riok.Mapperly/Symbols/CompilationContext.cs +++ b/src/Riok.Mapperly/Symbols/CompilationContext.cs @@ -1,12 +1,13 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Riok.Mapperly.Descriptors; using Riok.Mapperly.Helpers; namespace Riok.Mapperly.Symbols; public sealed record CompilationContext( - Compilation Compilation, + CSharpCompilation Compilation, WellKnownTypes Types, ImmutableArray NestedCompilations, FileNameBuilder FileNameBuilder diff --git a/test/Riok.Mapperly.IntegrationTests/Mapper/NullableDisabledMapper.cs b/test/Riok.Mapperly.IntegrationTests/Mapper/NullableDisabledMapper.cs new file mode 100644 index 0000000000..5daabbb5bf --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/Mapper/NullableDisabledMapper.cs @@ -0,0 +1,44 @@ +#nullable disable + +using System.Diagnostics.CodeAnalysis; +using Riok.Mapperly.Abstractions; + +namespace Riok.Mapperly.IntegrationTests.Mapper +{ + [Mapper] + public static partial class NullableDisabledMapper + { + public static partial MyDto Map(this MyClass src); + + [return: NotNullIfNotNull(nameof(src))] + private static MyNestedDto MapNested(this MyNestedClass src) + { + if (src == null) + return null; + + return new MyNestedDto(src.V); + } + + public class MyClass + { + public string StringValue { get; set; } + + public int IntValue { get; set; } + + public MyNestedClass Nested { get; set; } + } + + public record MyNestedClass(int V); + + public class MyDto + { + public string StringValue { get; set; } + + public int IntValue { get; set; } + + public MyNestedDto Nested { get; set; } + } + + public record MyNestedDto(int V); + } +} diff --git a/test/Riok.Mapperly.IntegrationTests/NullableDisabledMapperTest.cs b/test/Riok.Mapperly.IntegrationTests/NullableDisabledMapperTest.cs new file mode 100644 index 0000000000..25dd8cd649 --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/NullableDisabledMapperTest.cs @@ -0,0 +1,31 @@ +using System.Threading.Tasks; +using Riok.Mapperly.IntegrationTests.Mapper; +using VerifyXunit; +using Xunit; + +namespace Riok.Mapperly.IntegrationTests +{ + public class NullableDisabledMapperTest : BaseMapperTest + { + [Fact] + public Task SnapshotGeneratedSource() + { + var path = GetGeneratedMapperFilePath(nameof(NullableDisabledMapper)); + return Verifier.VerifyFile(path); + } + + [Fact] + public Task RunMappingShouldWork() + { + var v = NullableDisabledMapper.Map( + new NullableDisabledMapper.MyClass + { + Nested = new NullableDisabledMapper.MyNestedClass(10), + IntValue = 11, + StringValue = "foo", + } + ); + return Verifier.Verify(v); + } + } +} diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/NullableDisabledMapperTest.RunMappingShouldWork.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/NullableDisabledMapperTest.RunMappingShouldWork.verified.txt new file mode 100644 index 0000000000..175bcfa49b --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/NullableDisabledMapperTest.RunMappingShouldWork.verified.txt @@ -0,0 +1,7 @@ +{ + StringValue: foo, + IntValue: 11, + Nested: { + V: 10 + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/NullableDisabledMapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/NullableDisabledMapperTest.SnapshotGeneratedSource.verified.cs new file mode 100644 index 0000000000..184f684933 --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/NullableDisabledMapperTest.SnapshotGeneratedSource.verified.cs @@ -0,0 +1,20 @@ +// +#nullable enable +namespace Riok.Mapperly.IntegrationTests.Mapper +{ + public static partial class NullableDisabledMapper + { + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(src))] + public static partial global::Riok.Mapperly.IntegrationTests.Mapper.NullableDisabledMapper.MyDto? Map(this global::Riok.Mapperly.IntegrationTests.Mapper.NullableDisabledMapper.MyClass? src) + { + if (src == null) + return default; + var target = new global::Riok.Mapperly.IntegrationTests.Mapper.NullableDisabledMapper.MyDto(); + target.StringValue = src.StringValue; + target.IntValue = src.IntValue; + target.Nested = MapNested(src.Nested); + return target; + } + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/MapperGenerationResultAssertions.cs b/test/Riok.Mapperly.Tests/MapperGenerationResultAssertions.cs index 9f203f3779..e34978e8fa 100644 --- a/test/Riok.Mapperly.Tests/MapperGenerationResultAssertions.cs +++ b/test/Riok.Mapperly.Tests/MapperGenerationResultAssertions.cs @@ -161,7 +161,8 @@ public MapperGenerationResultAssertions HaveOnlyMethods(params string[] methodNa public MapperGenerationResultAssertions HaveMethodBody(string methodName, [StringSyntax(StringSyntax.CSharp)] string mapperMethodBody) { - _mapper.Methods[methodName].Body.Should().Be(mapperMethodBody.ReplaceLineEndings().Trim(), $"Method: {methodName}"); + var body = _mapper.Methods[methodName].Body; + body.Should().Be(mapperMethodBody.ReplaceLineEndings().Trim(), $"Method: {methodName}"); return this; } diff --git a/test/Riok.Mapperly.Tests/Mapping/DerivedTypeTest.cs b/test/Riok.Mapperly.Tests/Mapping/DerivedTypeTest.cs index 660325bd8f..f2c8dfe33b 100644 --- a/test/Riok.Mapperly.Tests/Mapping/DerivedTypeTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/DerivedTypeTest.cs @@ -99,6 +99,24 @@ public Task WithInterfaceSourceAndTargetNullableShouldWork() return TestHelper.VerifyGenerator(source); } + [Fact] + public Task NullableDisabledExistingMethodShouldBeUsed() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapDerivedType] + public partial BaseDto Map(Base src); + + public partial ADto MapA(A src); + """, + "class A(int value) : Base { public int Value { get; set; } }", + "class ADto(int value) : BaseDto;", + "class Base;", + "class BaseDto;" + ); + return TestHelper.VerifyGenerator(source, TestHelperOptions.DisabledNullable); + } + [Fact] public Task WithObjectShouldWork() { diff --git a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyTest.cs b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyTest.cs index 6bec33d801..54983cef15 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyTest.cs @@ -319,6 +319,66 @@ private D UserImplementedMap(C source) ); } + [Fact] + public void ShouldUseNotNullIfNotNullUserImplementedMapping() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + public partial B Map(A source); + + [UserMapping(Default = true)] + [return: System.Diagnostics.CodeAnalysis.NotNullIfNotNull("source")] + private D? UserImplementedMap(C? source) => source == null ? null : new D(); + """, + "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 + .GenerateMapper(source) + .Should() + .HaveMapMethodBody( + """ + var target = new global::B(); + target.StringValue = source.StringValue; + target.NestedValue = UserImplementedMap(source.NestedValue); + return target; + """ + ); + } + + [Fact] + public void ShouldUseNotNullUserImplementedMapping() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + public partial B Map(A source); + + [UserMapping(Default = true)] + [return: System.Diagnostics.CodeAnalysis.NotNull] + private D? UserImplementedMap(C? source) => new D(); + """, + "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 + .GenerateMapper(source) + .Should() + .HaveMapMethodBody( + """ + var target = new global::B(); + target.StringValue = source.StringValue; + target.NestedValue = UserImplementedMap(source.NestedValue); + return target; + """ + ); + } + [Fact] public Task WithUnmappablePropertyShouldDiagnostic() { diff --git a/test/Riok.Mapperly.Tests/TestHelper.cs b/test/Riok.Mapperly.Tests/TestHelper.cs index ee802c57c6..c244128009 100644 --- a/test/Riok.Mapperly.Tests/TestHelper.cs +++ b/test/Riok.Mapperly.Tests/TestHelper.cs @@ -79,7 +79,7 @@ public static GeneratorDriver GenerateTracked(Compilation compilation) return driver.RunGenerators(compilation); } - public static Compilation BuildCompilation([StringSyntax(StringSyntax.CSharp)] string source, TestHelperOptions? options) + public static CSharpCompilation BuildCompilation([StringSyntax(StringSyntax.CSharp)] string source, TestHelperOptions? options) { options ??= TestHelperOptions.Default; var syntaxTree = CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(options.LanguageVersion)); diff --git a/test/Riok.Mapperly.Tests/_snapshots/DerivedTypeTest.NullableDisabledExistingMethodShouldBeUsed#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/DerivedTypeTest.NullableDisabledExistingMethodShouldBeUsed#Mapper.g.verified.cs new file mode 100644 index 0000000000..e72b0aab0a --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/DerivedTypeTest.NullableDisabledExistingMethodShouldBeUsed#Mapper.g.verified.cs @@ -0,0 +1,28 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(src))] + public partial global::BaseDto? Map(global::Base? src) + { + if (src == null) + return default; + return src switch + { + global::A x => MapA(x), + _ => throw new System.ArgumentException($"Cannot map {src.GetType()} to BaseDto as there is no known derived type mapping", nameof(src)), + }; + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(src))] + public partial global::ADto? MapA(global::A? src) + { + if (src == null) + return default; + var target = new global::ADto(src.Value); + return target; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/DerivedTypeTest.WithInterfaceSourceAndTargetNullableShouldWork#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/DerivedTypeTest.WithInterfaceSourceAndTargetNullableShouldWork#Mapper.g.verified.cs index cb18e4ac59..b82422deda 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/DerivedTypeTest.WithInterfaceSourceAndTargetNullableShouldWork#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/DerivedTypeTest.WithInterfaceSourceAndTargetNullableShouldWork#Mapper.g.verified.cs @@ -4,9 +4,12 @@ public partial class Mapper { [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(src))] public partial global::B? Map(global::A? src) { - return src == null ? default : src switch + if (src == null) + return default; + return src switch { global::AImpl1 x => MapToBImpl1(x), global::AImpl2 x => MapToBImpl2(x), diff --git a/test/Riok.Mapperly.Tests/_snapshots/DerivedTypeTest.WithInterfaceSourceNullableShouldWork#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/DerivedTypeTest.WithInterfaceSourceNullableShouldWork#Mapper.g.verified.cs index 8a51c08f79..3f46c0a91d 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/DerivedTypeTest.WithInterfaceSourceNullableShouldWork#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/DerivedTypeTest.WithInterfaceSourceNullableShouldWork#Mapper.g.verified.cs @@ -6,7 +6,9 @@ public partial class Mapper [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] public partial global::B Map(global::A? src) { - return src == null ? throw new System.ArgumentNullException(nameof(src)) : src switch + if (src == null) + throw new System.ArgumentNullException(nameof(src)); + return src switch { global::AImpl1 x => MapToBImpl1(x), global::AImpl2 x => MapToBImpl2(x), diff --git a/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.ArrayToCollectionShouldUpgradeNullability#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.ArrayToCollectionShouldUpgradeNullability#Mapper.g.verified.cs index 3181272259..02aca5da43 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.ArrayToCollectionShouldUpgradeNullability#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.ArrayToCollectionShouldUpgradeNullability#Mapper.g.verified.cs @@ -4,6 +4,7 @@ public partial class Mapper { [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(source))] private partial global::B? Map(global::A? source) { if (source == null) diff --git a/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.ArrayToListShouldUpgradeNullability#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.ArrayToListShouldUpgradeNullability#Mapper.g.verified.cs index 3181272259..02aca5da43 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.ArrayToListShouldUpgradeNullability#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.ArrayToListShouldUpgradeNullability#Mapper.g.verified.cs @@ -4,6 +4,7 @@ public partial class Mapper { [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(source))] private partial global::B? Map(global::A? source) { if (source == null) diff --git a/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.ArrayToReadOnlyCollectionShouldUpgradeNullability#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.ArrayToReadOnlyCollectionShouldUpgradeNullability#Mapper.g.verified.cs index dfbe7c31b1..6fe6d41ee2 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.ArrayToReadOnlyCollectionShouldUpgradeNullability#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.ArrayToReadOnlyCollectionShouldUpgradeNullability#Mapper.g.verified.cs @@ -4,6 +4,7 @@ public partial class Mapper { [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(source))] private partial global::B? Map(global::A? source) { if (source == null) diff --git a/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.CollectionToReadOnlyCollectionShouldUpgradeNullability#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.CollectionToReadOnlyCollectionShouldUpgradeNullability#Mapper.g.verified.cs index 992d643d12..ff7c3d80e7 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.CollectionToReadOnlyCollectionShouldUpgradeNullability#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.CollectionToReadOnlyCollectionShouldUpgradeNullability#Mapper.g.verified.cs @@ -4,6 +4,7 @@ public partial class Mapper { [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(source))] private partial global::B? Map(global::A? source) { if (source == null) diff --git a/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.ShouldUpgradeNullabilityInDisabledNullableContextInSelectClause#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.ShouldUpgradeNullabilityInDisabledNullableContextInSelectClause#Mapper.g.verified.cs index 4014199870..84822ab804 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.ShouldUpgradeNullabilityInDisabledNullableContextInSelectClause#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.ShouldUpgradeNullabilityInDisabledNullableContextInSelectClause#Mapper.g.verified.cs @@ -4,6 +4,7 @@ public partial class Mapper { [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(source))] private partial global::B? Map(global::A? source) { if (source == null) @@ -21,6 +22,7 @@ public partial class Mapper } [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(source))] private global::D? MapToD(global::C? source) { if (source == null) diff --git a/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.ShouldUpgradeNullabilityOfGenericInDisabledNullableContext#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.ShouldUpgradeNullabilityOfGenericInDisabledNullableContext#Mapper.g.verified.cs index 4054f7b143..d9398bdb8c 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.ShouldUpgradeNullabilityOfGenericInDisabledNullableContext#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.ShouldUpgradeNullabilityOfGenericInDisabledNullableContext#Mapper.g.verified.cs @@ -4,6 +4,7 @@ public partial class Mapper { [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(source))] private partial global::System.Collections.Generic.IList? Map(global::System.Collections.Generic.IList? source) { if (source == null) @@ -17,6 +18,7 @@ public partial class Mapper } [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(source))] private global::B? MapToB(global::A? source) { if (source == null) diff --git a/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithGenericSourceAndTargetInNullableDisabledContext#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithGenericSourceAndTargetInNullableDisabledContext#Mapper.g.verified.cs index cda75d7b2d..041e1b7648 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithGenericSourceAndTargetInNullableDisabledContext#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithGenericSourceAndTargetInNullableDisabledContext#Mapper.g.verified.cs @@ -24,6 +24,7 @@ public partial class Mapper } [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(source))] private partial global::D? MapToD(global::C? source) { if (source == null) diff --git a/test/Riok.Mapperly.Tests/_snapshots/NullableTest.ShouldUpgradeArrayElementNullabilityInDisabledNullableContext#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/NullableTest.ShouldUpgradeArrayElementNullabilityInDisabledNullableContext#Mapper.g.verified.cs index 60b13752ad..56a694d922 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/NullableTest.ShouldUpgradeArrayElementNullabilityInDisabledNullableContext#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/NullableTest.ShouldUpgradeArrayElementNullabilityInDisabledNullableContext#Mapper.g.verified.cs @@ -4,6 +4,7 @@ public partial class Mapper { [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(source))] private partial global::B?[]? Map(global::A?[]? source) { if (source == null) @@ -17,6 +18,7 @@ public partial class Mapper } [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(source))] private global::B? MapToB(global::A? source) { if (source == null) diff --git a/test/Riok.Mapperly.Tests/_snapshots/NullableTest.ShouldUpgradeGenericNullabilityInDisabledNullableContext#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/NullableTest.ShouldUpgradeGenericNullabilityInDisabledNullableContext#Mapper.g.verified.cs index ff5d5c3282..8cdde4a48e 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/NullableTest.ShouldUpgradeGenericNullabilityInDisabledNullableContext#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/NullableTest.ShouldUpgradeGenericNullabilityInDisabledNullableContext#Mapper.g.verified.cs @@ -10,6 +10,7 @@ public partial class Mapper } [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(source))] private global::B? MapToB(global::A? source) { if (source == null) diff --git a/test/Riok.Mapperly.Tests/_snapshots/NullableTest.ShouldUpgradeNullabilityInDisabledNullableContext#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/NullableTest.ShouldUpgradeNullabilityInDisabledNullableContext#Mapper.g.verified.cs index 4b24be952f..4f3957bc20 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/NullableTest.ShouldUpgradeNullabilityInDisabledNullableContext#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/NullableTest.ShouldUpgradeNullabilityInDisabledNullableContext#Mapper.g.verified.cs @@ -4,6 +4,7 @@ public partial class Mapper { [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(source))] private partial global::B? Map(global::A? source) { if (source == null) diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyNullableTest.NullableToNullablePropertyWithAnotherNullableToNonNullableMappingShouldDirectAssign#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyNullableTest.NullableToNullablePropertyWithAnotherNullableToNonNullableMappingShouldDirectAssign#Mapper.g.verified.cs index 55d55effd7..e883112d07 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyNullableTest.NullableToNullablePropertyWithAnotherNullableToNonNullableMappingShouldDirectAssign#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyNullableTest.NullableToNullablePropertyWithAnotherNullableToNonNullableMappingShouldDirectAssign#Mapper.g.verified.cs @@ -4,6 +4,7 @@ public partial class Mapper { [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(source))] public static partial global::ADest? Map(global::A? source) { if (source == null) diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyNullableTest.ShouldUpgradeNullabilityInDisabledNullableContextInArrayProperty#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyNullableTest.ShouldUpgradeNullabilityInDisabledNullableContextInArrayProperty#Mapper.g.verified.cs index 4014199870..84822ab804 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyNullableTest.ShouldUpgradeNullabilityInDisabledNullableContextInArrayProperty#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyNullableTest.ShouldUpgradeNullabilityInDisabledNullableContextInArrayProperty#Mapper.g.verified.cs @@ -4,6 +4,7 @@ public partial class Mapper { [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(source))] private partial global::B? Map(global::A? source) { if (source == null) @@ -21,6 +22,7 @@ public partial class Mapper } [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(source))] private global::D? MapToD(global::C? source) { if (source == null) diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyNullableTest.ShouldUpgradeNullabilityInDisabledNullableContextInGenericProperty#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyNullableTest.ShouldUpgradeNullabilityInDisabledNullableContextInGenericProperty#Mapper.g.verified.cs index 9a77251f1a..d311346f3b 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyNullableTest.ShouldUpgradeNullabilityInDisabledNullableContextInGenericProperty#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyNullableTest.ShouldUpgradeNullabilityInDisabledNullableContextInGenericProperty#Mapper.g.verified.cs @@ -4,6 +4,7 @@ public partial class Mapper { [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(source))] private partial global::B? Map(global::A? source) { if (source == null) @@ -21,6 +22,7 @@ public partial class Mapper } [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(source))] private global::D? MapToD(global::C? source) { if (source == null) diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyNullableTest.ShouldUpgradeNullabilityInDisabledNullableContextInNestedProperty#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyNullableTest.ShouldUpgradeNullabilityInDisabledNullableContextInNestedProperty#Mapper.g.verified.cs index d5b91e98f8..bf708e0158 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyNullableTest.ShouldUpgradeNullabilityInDisabledNullableContextInNestedProperty#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyNullableTest.ShouldUpgradeNullabilityInDisabledNullableContextInNestedProperty#Mapper.g.verified.cs @@ -4,6 +4,7 @@ public partial class Mapper { [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(source))] private partial global::B? Map(global::A? source) { if (source == null) diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullableTest.ClassToClassMemberMappingDisabledNullableContext#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullableTest.ClassToClassMemberMappingDisabledNullableContext#Mapper.g.verified.cs index 8e47b99754..7934830b17 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullableTest.ClassToClassMemberMappingDisabledNullableContext#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullableTest.ClassToClassMemberMappingDisabledNullableContext#Mapper.g.verified.cs @@ -1,9 +1,10 @@ -//HintName: Mapper.g.cs +//HintName: Mapper.g.cs // #nullable enable public partial class Mapper { [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(q))] public partial global::System.Linq.IQueryable? Map(global::System.Linq.IQueryable? q) { if (q == null) @@ -20,6 +21,7 @@ public partial class Mapper } [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(source))] private partial global::B? Map(global::A? source) { if (source == null) diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullableTest.NestedPropertyWithDeepCloneable#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullableTest.NestedPropertyWithDeepCloneable#Mapper.g.verified.cs index 94e5f8f6da..0323a57932 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullableTest.NestedPropertyWithDeepCloneable#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullableTest.NestedPropertyWithDeepCloneable#Mapper.g.verified.cs @@ -4,6 +4,7 @@ public partial class Mapper { [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(source))] public partial global::System.Linq.IQueryable? Map(global::System.Linq.IQueryable? source) { if (source == null) @@ -21,6 +22,7 @@ public partial class Mapper } [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(source))] public partial global::B? MapConfig(global::A? source) { if (source == null) diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullableTest.RecordToClassDisabledNullableContext#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullableTest.RecordToClassDisabledNullableContext#Mapper.g.verified.cs index 4c734f420b..c5a43dd6b6 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullableTest.RecordToClassDisabledNullableContext#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullableTest.RecordToClassDisabledNullableContext#Mapper.g.verified.cs @@ -4,6 +4,7 @@ public partial class Mapper { [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(source))] private partial global::System.Linq.IQueryable? Map(global::System.Linq.IQueryable? source) { if (source == null) diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullableTest.RecordToRecordMemberMappingDisabledNullableContext#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullableTest.RecordToRecordMemberMappingDisabledNullableContext#Mapper.g.verified.cs index e278019f42..4423ef3a0b 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullableTest.RecordToRecordMemberMappingDisabledNullableContext#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullableTest.RecordToRecordMemberMappingDisabledNullableContext#Mapper.g.verified.cs @@ -1,9 +1,10 @@ -//HintName: Mapper.g.cs +//HintName: Mapper.g.cs // #nullable enable public partial class Mapper { [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(q))] public partial global::System.Linq.IQueryable? Map(global::System.Linq.IQueryable? q) { if (q == null) @@ -14,6 +15,7 @@ public partial class Mapper } [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(source))] private partial global::B? Map(global::A? source) { if (source == null) diff --git a/test/Riok.Mapperly.Tests/_snapshots/RuntimeTargetTypeMappingTest.WithNullableObjectSourceAndTargetTypeShouldIncludeNullables#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/RuntimeTargetTypeMappingTest.WithNullableObjectSourceAndTargetTypeShouldIncludeNullables#Mapper.g.verified.cs index 23ac4201a8..d17fb7b97e 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/RuntimeTargetTypeMappingTest.WithNullableObjectSourceAndTargetTypeShouldIncludeNullables#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/RuntimeTargetTypeMappingTest.WithNullableObjectSourceAndTargetTypeShouldIncludeNullables#Mapper.g.verified.cs @@ -26,6 +26,7 @@ string x when targetType.IsAssignableFrom(typeof(int)) => MapStringToInt(x), } [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(source))] private partial global::D? MapToD(global::C? source) { if (source == null)