From 3b217fc324785f664f516fdca0d2178be3edcd78 Mon Sep 17 00:00:00 2001 From: latonz Date: Wed, 12 Feb 2025 10:15:39 +0100 Subject: [PATCH] feat: add NotNullIfNotNull attribute on generated methods, support it on user implemented methods If nullable attributes are not available, the attribute is not emitted and --- .../Configuration/AttributeDataAccessor.cs | 26 ++++++-- .../MapperConfigurationReader.cs | 9 ++- .../Configuration/MappingConfiguration.cs | 4 +- .../Descriptors/DescriptorBuilder.cs | 6 +- .../Descriptors/MapperDescriptor.cs | 5 +- .../ConvertStaticMethodMappingBuilder.cs | 5 -- .../MappingBuilders/NullableMappingBuilder.cs | 9 ++- .../QueryableMappingBuilder.cs | 2 +- .../Descriptors/MappingCollection.cs | 13 +++- .../Mappings/DerivedTypeSwitchMapping.cs | 10 ++-- .../ExistingTarget/ExistingTargetMapping.cs | 2 + .../ObjectMemberExistingTargetMapping.cs | 2 + .../Descriptors/Mappings/ITypeMapping.cs | 2 + .../Descriptors/Mappings/MethodMapping.cs | 11 +++- .../Mappings/NewInstanceMapping.cs | 2 + .../Descriptors/Mappings/NoOpMapping.cs | 2 + .../Mappings/NullDelegateMethodMapping.cs | 31 +++++++++- .../Mappings/NullFallbackValueExtensions.cs | 10 ++++ .../Mappings/QueryableProjectionMapping.cs | 13 +++- .../UserDefinedNewInstanceMethodMapping.cs | 20 +++++-- .../UserImplementedMethodMapping.cs | 30 +++++++++- .../Descriptors/SupportedFeatures.cs | 24 ++++++++ .../Descriptors/SymbolAccessor.cs | 22 +++++-- .../Descriptors/TypeMappingKey.cs | 11 ++-- .../UnsafeAccess/UnsafeConstructorAccessor.cs | 4 +- .../UnsafeAccess/UnsafeFieldAccessor.cs | 4 +- .../UnsafeAccess/UnsafeGetPropertyAccessor.cs | 4 +- .../UnsafeAccess/UnsafeSetPropertyAccessor.cs | 4 +- .../Descriptors/UserMethodMappingExtractor.cs | 37 +++++++++--- .../Descriptors/WellKnownTypes.cs | 2 + src/Riok.Mapperly/Emit/SourceEmitter.cs | 15 ++++- .../Syntax/SyntaxFactoryHelper.Attribute.cs | 28 +++++---- .../SyntaxFactoryHelper.GeneratedCode.cs | 5 +- .../Emit/Syntax/SyntaxFactoryHelper.Method.cs | 5 +- .../Emit/Syntax/SyntaxFactoryHelper.Null.cs | 14 ++++- .../SyntaxFactoryHelper.NullAttributes.cs | 16 +++++ .../Emit/Syntax/SyntaxFactoryHelper.String.cs | 5 ++ .../SyntaxFactoryHelper.SymbolDeclaration.cs | 4 +- .../Emit/Syntax/SyntaxFactoryHelper.cs | 12 ++-- .../Emit/UnsafeAccessorEmitter.cs | 4 +- src/Riok.Mapperly/MapperGenerator.cs | 27 ++++++++- .../Symbols/CompilationContext.cs | 4 +- .../CircularReferenceMapperTest.cs | 2 + .../DerivedMapperTest.cs | 4 ++ .../Mapper/NullableDisabledMapper.cs | 48 +++++++++++++++ .../MapperDefaultsTest.cs | 2 + .../NestedMapperInterfaceTest.cs | 2 + .../NestedMapperRecordTest.cs | 2 + .../NestedMapperTest.cs | 2 + .../NullableDisabledMapperTest.cs | 33 ++++++++++ .../Riok.Mapperly.IntegrationTests.csproj | 6 +- .../UseExternalMapperTest.cs | 2 + ...erTest.SnapshotGeneratedSource.verified.cs | 2 +- ...SnapshotGeneratedSource_NET6_0.verified.cs | 35 +++++++++++ ...erTest.SnapshotGeneratedSource.verified.cs | 2 +- ...pshotGeneratedSourceBaseMapper.verified.cs | 4 +- ...neratedSourceBaseMapper_NET6_0.verified.cs | 19 ++++++ ...otGeneratedSourceDerivedMapper.verified.cs | 4 +- ...tGeneratedSourceDerivedMapper2.verified.cs | 4 +- ...tedSourceDerivedMapper2_NET6_0.verified.cs | 19 ++++++ ...atedSourceDerivedMapper_NET6_0.verified.cs | 13 ++++ ...tsTest.SnapshotGeneratedSource.verified.cs | 2 +- ...SnapshotGeneratedSource_NET6_0.verified.cs | 18 ++++++ ...erTest.SnapshotGeneratedSource.verified.cs | 2 +- ...ceTest.SnapshotGeneratedSource.verified.cs | 2 +- ...SnapshotGeneratedSource_NET6_0.verified.cs | 16 +++++ ...rdTest.SnapshotGeneratedSource.verified.cs | 2 +- ...SnapshotGeneratedSource_NET6_0.verified.cs | 19 ++++++ ...erTest.SnapshotGeneratedSource.verified.cs | 2 +- ...SnapshotGeneratedSource_NET6_0.verified.cs | 19 ++++++ ...pperTest.RunMappingShouldWork.verified.txt | 7 +++ ...erTest.SnapshotGeneratedSource.verified.cs | 19 ++++++ ...SnapshotGeneratedSource_NET6_0.verified.cs | 20 +++++++ ...SnapshotGeneratedSource_NET7_0.verified.cs | 20 +++++++ ...erTest.SnapshotGeneratedSource.verified.cs | 6 +- ...erTest.SnapshotGeneratedSource.verified.cs | 2 +- ...erTest.SnapshotGeneratedSource.verified.cs | 4 +- ...SnapshotGeneratedSource_NET6_0.verified.cs | 15 +++++ .../Helpers/GenericTypeCheckerTest.cs | 8 ++- .../MapperGenerationResultAssertions.cs | 3 +- .../Mapping/DerivedTypeTest.cs | 18 ++++++ .../Mapping/ObjectPropertyTest.cs | 60 +++++++++++++++++++ test/Riok.Mapperly.Tests/TestHelper.cs | 2 +- ...ingMethodShouldBeUsed#Mapper.g.verified.cs | 28 +++++++++ ...getNullableShouldWork#Mapper.g.verified.cs | 5 +- ...rceNullableShouldWork#Mapper.g.verified.cs | 4 +- ...uldUpgradeNullability#Mapper.g.verified.cs | 1 + ...uldUpgradeNullability#Mapper.g.verified.cs | 1 + ...uldUpgradeNullability#Mapper.g.verified.cs | 1 + ...uldUpgradeNullability#Mapper.g.verified.cs | 1 + ...ContextInSelectClause#Mapper.g.verified.cs | 2 + ...sabledNullableContext#Mapper.g.verified.cs | 2 + ...llableDisabledContext#Mapper.g.verified.cs | 1 + ...sabledNullableContext#Mapper.g.verified.cs | 2 + ...sabledNullableContext#Mapper.g.verified.cs | 1 + ...sabledNullableContext#Mapper.g.verified.cs | 1 + ...ingShouldDirectAssign#Mapper.g.verified.cs | 1 + ...ontextInArrayProperty#Mapper.g.verified.cs | 2 + ...textInGenericProperty#Mapper.g.verified.cs | 2 + ...ntextInNestedProperty#Mapper.g.verified.cs | 1 + ...sabledNullableContext#Mapper.g.verified.cs | 4 +- ...ertyWithDeepCloneable#Mapper.g.verified.cs | 2 + ...sabledNullableContext#Mapper.g.verified.cs | 1 + ...sabledNullableContext#Mapper.g.verified.cs | 4 +- ...houldIncludeNullables#Mapper.g.verified.cs | 1 + 105 files changed, 878 insertions(+), 126 deletions(-) create mode 100644 src/Riok.Mapperly/Descriptors/Mappings/NullFallbackValueExtensions.cs create mode 100644 src/Riok.Mapperly/Descriptors/SupportedFeatures.cs create mode 100644 src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.NullAttributes.cs create mode 100644 test/Riok.Mapperly.IntegrationTests/Mapper/NullableDisabledMapper.cs create mode 100644 test/Riok.Mapperly.IntegrationTests/NullableDisabledMapperTest.cs create mode 100644 test/Riok.Mapperly.IntegrationTests/_snapshots/CircularReferenceMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs create mode 100644 test/Riok.Mapperly.IntegrationTests/_snapshots/DerivedMapperTest.SnapshotGeneratedSourceBaseMapper_NET6_0.verified.cs create mode 100644 test/Riok.Mapperly.IntegrationTests/_snapshots/DerivedMapperTest.SnapshotGeneratedSourceDerivedMapper2_NET6_0.verified.cs create mode 100644 test/Riok.Mapperly.IntegrationTests/_snapshots/DerivedMapperTest.SnapshotGeneratedSourceDerivedMapper_NET6_0.verified.cs create mode 100644 test/Riok.Mapperly.IntegrationTests/_snapshots/MapperDefaultsTest.SnapshotGeneratedSource_NET6_0.verified.cs create mode 100644 test/Riok.Mapperly.IntegrationTests/_snapshots/NestedMapperInterfaceTest.SnapshotGeneratedSource_NET6_0.verified.cs create mode 100644 test/Riok.Mapperly.IntegrationTests/_snapshots/NestedMapperRecordTest.SnapshotGeneratedSource_NET6_0.verified.cs create mode 100644 test/Riok.Mapperly.IntegrationTests/_snapshots/NestedMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs create mode 100644 test/Riok.Mapperly.IntegrationTests/_snapshots/NullableDisabledMapperTest.RunMappingShouldWork.verified.txt create mode 100644 test/Riok.Mapperly.IntegrationTests/_snapshots/NullableDisabledMapperTest.SnapshotGeneratedSource.verified.cs create mode 100644 test/Riok.Mapperly.IntegrationTests/_snapshots/NullableDisabledMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs create mode 100644 test/Riok.Mapperly.IntegrationTests/_snapshots/NullableDisabledMapperTest.SnapshotGeneratedSource_NET7_0.verified.cs create mode 100644 test/Riok.Mapperly.IntegrationTests/_snapshots/UseExternalMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/DerivedTypeTest.NullableDisabledExistingMethodShouldBeUsed#Mapper.g.verified.cs diff --git a/src/Riok.Mapperly/Configuration/AttributeDataAccessor.cs b/src/Riok.Mapperly/Configuration/AttributeDataAccessor.cs index 2f1b00322d..d8dc59d98e 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 attrDatas.Select(a => Access(a)); + } + /// /// 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/Configuration/MapperConfigurationReader.cs b/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs index 1cda0c8523..91d7bb6130 100644 --- a/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs +++ b/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs @@ -15,7 +15,8 @@ public MapperConfigurationReader( AttributeDataAccessor dataAccessor, WellKnownTypes types, ISymbol mapperSymbol, - MapperConfiguration defaultMapperConfiguration + MapperConfiguration defaultMapperConfiguration, + SupportedFeatures supportedFeatures ) { _dataAccessor = dataAccessor; @@ -38,7 +39,8 @@ MapperConfiguration defaultMapperConfiguration ), new MembersMappingConfiguration([], [], [], [], [], mapper.IgnoreObsoleteMembersStrategy, mapper.RequiredMappingStrategy), [], - mapper.UseDeepCloning + mapper.UseDeepCloning, + supportedFeatures ); } @@ -61,7 +63,8 @@ DiagnosticCollection diagnostics enumConfig, membersConfig, derivedTypesConfig, - supportsDeepCloning && MapperConfiguration.Mapper.UseDeepCloning + supportsDeepCloning && MapperConfiguration.Mapper.UseDeepCloning, + MapperConfiguration.SupportedFeatures ); } diff --git a/src/Riok.Mapperly/Configuration/MappingConfiguration.cs b/src/Riok.Mapperly/Configuration/MappingConfiguration.cs index 92df8eb87f..07c285b03d 100644 --- a/src/Riok.Mapperly/Configuration/MappingConfiguration.cs +++ b/src/Riok.Mapperly/Configuration/MappingConfiguration.cs @@ -1,4 +1,5 @@ using Riok.Mapperly.Abstractions; +using Riok.Mapperly.Descriptors; namespace Riok.Mapperly.Configuration; @@ -7,5 +8,6 @@ public record MappingConfiguration( EnumMappingConfiguration Enum, MembersMappingConfiguration Members, IReadOnlyCollection DerivedTypes, - bool UseDeepCloning + bool UseDeepCloning, + SupportedFeatures SupportedFeatures ); diff --git a/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs b/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs index 442f732c6a..0887e19cd4 100644 --- a/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs @@ -40,7 +40,8 @@ public DescriptorBuilder( MapperConfiguration defaultMapperConfiguration ) { - _mapperDescriptor = new MapperDescriptor(mapperDeclaration, _methodNameBuilder); + var supportedFeatures = SupportedFeatures.Build(compilationContext.Types, symbolAccessor, compilationContext.ParseLanguageVersion); + _mapperDescriptor = new MapperDescriptor(mapperDeclaration, _methodNameBuilder, supportedFeatures); _symbolAccessor = symbolAccessor; _types = compilationContext.Types; _mappingBodyBuilder = new MappingBodyBuilder(_mappings); @@ -51,7 +52,8 @@ MapperConfiguration defaultMapperConfiguration attributeAccessor, _types, mapperDeclaration.Symbol, - defaultMapperConfiguration + defaultMapperConfiguration, + supportedFeatures ); _diagnostics = new DiagnosticCollection(mapperDeclaration.Syntax.GetLocation()); diff --git a/src/Riok.Mapperly/Descriptors/MapperDescriptor.cs b/src/Riok.Mapperly/Descriptors/MapperDescriptor.cs index a7b4efbff2..c70a5a207a 100644 --- a/src/Riok.Mapperly/Descriptors/MapperDescriptor.cs +++ b/src/Riok.Mapperly/Descriptors/MapperDescriptor.cs @@ -18,10 +18,11 @@ public class MapperDescriptor public bool Static { get; set; } - public MapperDescriptor(MapperDeclaration declaration, UniqueNameBuilder nameBuilder) + public MapperDescriptor(MapperDeclaration declaration, UniqueNameBuilder nameBuilder, SupportedFeatures supportedFeatures) { _declaration = declaration; NameBuilder = nameBuilder; + SupportedFeatures = supportedFeatures; Name = BuildName(declaration.Symbol); UnsafeAccessorName = nameBuilder.New(AccessorClassName); @@ -31,6 +32,8 @@ public MapperDescriptor(MapperDeclaration declaration, UniqueNameBuilder nameBui } } + public SupportedFeatures SupportedFeatures { 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..09e9267afb 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); } @@ -52,7 +51,13 @@ private static INewInstanceMapping BuildNullDelegateMapping(MappingBuilderContex return mapping switch { - NewInstanceMethodMapping methodMapping => new NullDelegateMethodMapping(ctx.Source, ctx.Target, methodMapping, nullFallback), + NewInstanceMethodMapping methodMapping => new NullDelegateMethodMapping( + ctx.Source, + ctx.Target, + methodMapping, + nullFallback, + ctx.Configuration.SupportedFeatures.NullableAttributes + ), _ => new NullDelegateMapping(ctx.Source, ctx.Target, mapping, nullFallback), }; } diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/QueryableMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/QueryableMappingBuilder.cs index e98738d584..6c99955768 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/QueryableMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/QueryableMappingBuilder.cs @@ -35,7 +35,7 @@ public static class QueryableMappingBuilder ctx.ReportDiagnostic(DiagnosticDescriptors.QueryableProjectionMappingsDoNotSupportReferenceHandling); } - return new QueryableProjectionMapping(ctx.Source, ctx.Target, mapping); + return new QueryableProjectionMapping(ctx.Source, ctx.Target, mapping, ctx.Configuration.SupportedFeatures.NullableAttributes); } private static TypeMappingKey TryBuildMappingKey(MappingBuilderContext ctx, ITypeSymbol sourceType, ITypeSymbol targetType) 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..a3a7da7ded 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) => + SingletonList(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..ae18cf395b 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/NullDelegateMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/NullDelegateMethodMapping.cs @@ -13,9 +13,38 @@ public class NullDelegateMethodMapping( ITypeSymbol nullableSourceType, ITypeSymbol nullableTargetType, MethodMapping delegateMapping, - NullFallbackValue nullFallbackValue + NullFallbackValue nullFallbackValue, + bool nullableAttributesSupported ) : 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)) + { + yield return new TypeMappingKey(SourceType, TargetType.NonNullable(), config); + yield return new TypeMappingKey(SourceType.NonNullable(), TargetType.NonNullable(), config); + yield break; + } + + // 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. + yield return new TypeMappingKey(delegateMapping, config); + } + + protected internal override SyntaxList BuildAttributes(TypeMappingBuildContext ctx) + { + return !nullableAttributesSupported || !TargetType.IsNullable() || !nullFallbackValue.IsNullable(TargetType) + ? base.BuildAttributes(ctx) + : base.BuildAttributes(ctx).Add(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/QueryableProjectionMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/QueryableProjectionMapping.cs index 05f0613302..c2957746f1 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/QueryableProjectionMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/QueryableProjectionMapping.cs @@ -9,8 +9,12 @@ namespace Riok.Mapperly.Descriptors.Mappings; /// A projections queryable mapping /// to map from one generic to another. /// -public class QueryableProjectionMapping(ITypeSymbol sourceType, ITypeSymbol targetType, INewInstanceMapping delegateMapping) - : NewInstanceMethodMapping(sourceType, targetType) +public class QueryableProjectionMapping( + ITypeSymbol sourceType, + ITypeSymbol targetType, + INewInstanceMapping delegateMapping, + bool supportsNullableAttributes +) : NewInstanceMethodMapping(sourceType, targetType) { private const string QueryableReceiverName = "System.Linq.Queryable"; private const string SelectMethodName = nameof(Queryable.Select); @@ -28,7 +32,10 @@ public override IEnumerable BuildBody(TypeMappingBuildContext c var select = ctx.SyntaxFactory.StaticInvocation(QueryableReceiverName, SelectMethodName, ctx.Source, projectionLambda); var returnStatement = ctx.SyntaxFactory.Return(select); var leadingTrivia = returnStatement.GetLeadingTrivia().Insert(0, ElasticCarriageReturnLineFeed).Insert(1, Nullable(false)); - var trailingTrivia = returnStatement.GetTrailingTrivia().Insert(0, ElasticCarriageReturnLineFeed).Insert(1, Nullable(true)); + var trailingTrivia = returnStatement + .GetTrailingTrivia() + .Insert(0, ElasticCarriageReturnLineFeed) + .Insert(1, Nullable(true, !supportsNullableAttributes)); returnStatement = returnStatement.WithLeadingTrivia(leadingTrivia).WithTrailingTrivia(trailingTrivia); return [returnStatement]; } 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/SupportedFeatures.cs b/src/Riok.Mapperly/Descriptors/SupportedFeatures.cs new file mode 100644 index 0000000000..f61fb2b2ea --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/SupportedFeatures.cs @@ -0,0 +1,24 @@ +using System.Runtime.InteropServices; +using Microsoft.CodeAnalysis.CSharp; + +namespace Riok.Mapperly.Descriptors; + +[StructLayout(LayoutKind.Auto)] +public readonly struct SupportedFeatures +{ + public static SupportedFeatures Build(WellKnownTypes types, SymbolAccessor accessor, LanguageVersion parseLanguageVersion) + { + return new() + { +#if ROSLYN4_4_OR_GREATER + // nameof(parameter) was introduced in c# 11.0 + NameOfParameter = parseLanguageVersion >= LanguageVersion.CSharp11, +#endif + NullableAttributes = types.NotNullIfNotNullAttribute != null && accessor.IsDirectlyAccessible(types.NotNullIfNotNullAttribute), + }; + } + + public bool NameOfParameter { get; private init; } + + public bool NullableAttributes { get; private init; } +} 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/Descriptors/WellKnownTypes.cs b/src/Riok.Mapperly/Descriptors/WellKnownTypes.cs index bbb26ca5cd..af3b6cd595 100644 --- a/src/Riok.Mapperly/Descriptors/WellKnownTypes.cs +++ b/src/Riok.Mapperly/Descriptors/WellKnownTypes.cs @@ -12,6 +12,8 @@ public class WellKnownTypes(Compilation compilation) public INamedTypeSymbol? TimeOnly => TryGet("System.TimeOnly"); + public INamedTypeSymbol? NotNullIfNotNullAttribute => TryGet("System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute"); + public ITypeSymbol GetArrayType(ITypeSymbol type) => compilation.CreateArrayTypeSymbol(type, elementNullableAnnotation: type.NullableAnnotation).NonNullable(); diff --git a/src/Riok.Mapperly/Emit/SourceEmitter.cs b/src/Riok.Mapperly/Emit/SourceEmitter.cs index 5275b7e367..d76191a425 100644 --- a/src/Riok.Mapperly/Emit/SourceEmitter.cs +++ b/src/Riok.Mapperly/Emit/SourceEmitter.cs @@ -13,7 +13,11 @@ 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.SupportedFeatures) + ); ctx = IndentForMapper(ctx, descriptor.Symbol); var memberCtx = ctx.AddIndentation(); @@ -26,7 +30,7 @@ public static CompilationUnitSyntax Build(MapperDescriptor descriptor, Cancellat #if ROSLYN4_7_OR_GREATER if (descriptor.UnsafeAccessors.Count > 0) { - var unsafeAccessorClass = UnsafeAccessorEmitter.BuildUnsafeAccessorClass(descriptor, cancellationToken, ctx); + var unsafeAccessorClass = UnsafeAccessorEmitter.BuildUnsafeAccessorClass(ctx, descriptor, cancellationToken); compilationUnitMembers.Add(unsafeAccessorClass); } #endif @@ -39,7 +43,12 @@ public static CompilationUnitSyntax Build(MapperDescriptor descriptor, Cancellat return CompilationUnit() .WithMembers(compilationUnitMemberSyntaxList) - .WithLeadingTrivia(Comment(AutoGeneratedComment), ElasticCarriageReturnLineFeed, Nullable(true), ElasticCarriageReturnLineFeed); + .WithLeadingTrivia( + Comment(AutoGeneratedComment), + ElasticCarriageReturnLineFeed, + Nullable(true, !descriptor.SupportedFeatures.NullableAttributes), + ElasticCarriageReturnLineFeed + ); } private static IEnumerable BuildMembers( 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..8e2ed83718 100644 --- a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Method.cs +++ b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Method.cs @@ -12,9 +12,10 @@ public static MethodDeclarationSyntax PublicStaticExternMethod( TypeSyntax returnType, string methodName, ParameterListSyntax parameterList, - SyntaxList attributeList + SyntaxList attributes ) { + attributes = attributes.Insert(0, ctx.SyntaxFactory.GeneratedCodeAttribute()); return MethodDeclaration(returnType, Identifier(methodName)) .WithModifiers( TokenList( @@ -24,7 +25,7 @@ SyntaxList attributeList ) ) .WithParameterList(parameterList) - .WithAttributeLists(ctx.SyntaxFactory.GeneratedCodeAttributeList().AddRange(attributeList)) + .WithAttributeLists(attributes) .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)); } } diff --git a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Null.cs b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Null.cs index 8a96647f80..74a8cd11cf 100644 --- a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Null.cs +++ b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Null.cs @@ -19,9 +19,19 @@ public static AssignmentExpressionSyntax CoalesceAssignment(ExpressionSyntax tar ); } - public static SyntaxTrivia Nullable(bool enabled) + public static SyntaxTrivia Nullable(bool enabled, bool annotationsOnly = false) { - return Trivia(NullableDirectiveTrivia(LeadingSpacedToken(enabled ? SyntaxKind.EnableKeyword : SyntaxKind.DisableKeyword), true)); + var nullableDirective = NullableDirectiveTrivia( + LeadingSpacedToken(enabled ? SyntaxKind.EnableKeyword : SyntaxKind.DisableKeyword), + true + ); + + if (annotationsOnly) + { + nullableDirective = nullableDirective.WithTargetToken(LeadingSpacedToken(SyntaxKind.AnnotationsKeyword)); + } + + return Trivia(nullableDirective); } public static BinaryExpressionSyntax Coalesce(ExpressionSyntax expr, ExpressionSyntax coalesceExpr) => 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..63b84c457b 100644 --- a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.String.cs +++ b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.String.cs @@ -21,6 +21,11 @@ public partial struct SyntaxFactoryHelper public static InvocationExpressionSyntax NameOf(ExpressionSyntax expression) => InvocationWithoutIndention(_nameofIdentifier, expression); + private ExpressionSyntax ParameterNameOfOrStringLiteral(ExpressionSyntax expression) + { + return supportedFeatures.NameOfParameter ? NameOf(expression) : 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..e50d5b17f3 100644 --- a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.cs +++ b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.cs @@ -1,27 +1,31 @@ +using System.Runtime.InteropServices; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Descriptors; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; 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(SupportedFeatures supportedFeatures) { private const int ConditionalMultilineThreshold = 70; public static readonly IdentifierNameSyntax VarIdentifier = IdentifierName("var").AddTrailingSpace(); - private SyntaxFactoryHelper(int indentation) + private SyntaxFactoryHelper(int indentation, SupportedFeatures supportedFeatures) + : this(supportedFeatures) { Indentation = indentation; } public int Indentation { get; } - public SyntaxFactoryHelper AddIndentation() => new(Indentation + 1); + public SyntaxFactoryHelper AddIndentation() => new(Indentation + 1, supportedFeatures); - public SyntaxFactoryHelper RemoveIndentation() => new(Indentation - 1); + public SyntaxFactoryHelper RemoveIndentation() => new(Indentation - 1, supportedFeatures); public static AssignmentExpressionSyntax Assignment(ExpressionSyntax target, ExpressionSyntax source, bool coalesce) { diff --git a/src/Riok.Mapperly/Emit/UnsafeAccessorEmitter.cs b/src/Riok.Mapperly/Emit/UnsafeAccessorEmitter.cs index ff1ad75043..10921d2086 100644 --- a/src/Riok.Mapperly/Emit/UnsafeAccessorEmitter.cs +++ b/src/Riok.Mapperly/Emit/UnsafeAccessorEmitter.cs @@ -11,9 +11,9 @@ namespace Riok.Mapperly.Emit; public static class UnsafeAccessorEmitter { public static MemberDeclarationSyntax BuildUnsafeAccessorClass( + SourceEmitterContext ctx, MapperDescriptor descriptor, - CancellationToken cancellationToken, - SourceEmitterContext ctx + CancellationToken cancellationToken ) { var accessorCtx = ctx.AddIndentation(); diff --git a/src/Riok.Mapperly/MapperGenerator.cs b/src/Riok.Mapperly/MapperGenerator.cs index 58273bb024..a6303a7e8b 100644 --- a/src/Riok.Mapperly/MapperGenerator.cs +++ b/src/Riok.Mapperly/MapperGenerator.cs @@ -34,9 +34,19 @@ public void Initialize(IncrementalGeneratorInitializationContext context) // build the compilation context var compilationContext = context .CompilationProvider.Combine(nestedCompilations) + .Combine(context.ParseOptionsProvider) .Select( static (c, _) => - new CompilationContext(c.Left, new WellKnownTypes(c.Left), c.Right.ToImmutableArray(), new FileNameBuilder()) + { + var ((compilation, nestedCompilations), parseOptions) = c; + return new CompilationContext( + (CSharpCompilation)compilation, + ((CSharpParseOptions)parseOptions).LanguageVersion, + new WellKnownTypes(compilation), + nestedCompilations.ToImmutableArray(), + new FileNameBuilder() + ); + } ) .WithTrackingName(MapperGeneratorStepNames.BuildCompilationContext); @@ -116,12 +126,23 @@ private static MapperConfiguration BuildDefaults(CompilationContext compilationC private static IEnumerable BuildCompilationDiagnostics(Compilation compilation) { - if (compilation is CSharpCompilation { LanguageVersion: < LanguageVersion.CSharp9 } cSharpCompilation) + if (compilation is not CSharpCompilation csCompilation) { yield return Diagnostic.Create( DiagnosticDescriptors.LanguageVersionNotSupported, null, - cSharpCompilation.LanguageVersion.ToDisplayString(), + "", + LanguageVersion.CSharp9.ToDisplayString() + ); + yield break; + } + + if (csCompilation.LanguageVersion < LanguageVersion.CSharp9) + { + yield return Diagnostic.Create( + DiagnosticDescriptors.LanguageVersionNotSupported, + null, + csCompilation.LanguageVersion.ToDisplayString(), LanguageVersion.CSharp9.ToDisplayString() ); } diff --git a/src/Riok.Mapperly/Symbols/CompilationContext.cs b/src/Riok.Mapperly/Symbols/CompilationContext.cs index bec42d76a9..b55068bc2a 100644 --- a/src/Riok.Mapperly/Symbols/CompilationContext.cs +++ b/src/Riok.Mapperly/Symbols/CompilationContext.cs @@ -1,12 +1,14 @@ 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, + LanguageVersion ParseLanguageVersion, WellKnownTypes Types, ImmutableArray NestedCompilations, FileNameBuilder FileNameBuilder diff --git a/test/Riok.Mapperly.IntegrationTests/CircularReferenceMapperTest.cs b/test/Riok.Mapperly.IntegrationTests/CircularReferenceMapperTest.cs index dcc35fb49e..fb506e2da4 100644 --- a/test/Riok.Mapperly.IntegrationTests/CircularReferenceMapperTest.cs +++ b/test/Riok.Mapperly.IntegrationTests/CircularReferenceMapperTest.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using FluentAssertions; +using Riok.Mapperly.IntegrationTests.Helpers; using Riok.Mapperly.IntegrationTests.Mapper; using Riok.Mapperly.IntegrationTests.Models; using VerifyXunit; @@ -27,6 +28,7 @@ public void ShouldMapCircularReference() } [Fact] + [VersionedSnapshot(Versions.NET6_0)] public Task SnapshotGeneratedSource() { var path = GetGeneratedMapperFilePath(nameof(CircularReferenceMapper)); diff --git a/test/Riok.Mapperly.IntegrationTests/DerivedMapperTest.cs b/test/Riok.Mapperly.IntegrationTests/DerivedMapperTest.cs index c2f4962345..bba935238f 100644 --- a/test/Riok.Mapperly.IntegrationTests/DerivedMapperTest.cs +++ b/test/Riok.Mapperly.IntegrationTests/DerivedMapperTest.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using FluentAssertions; +using Riok.Mapperly.IntegrationTests.Helpers; using Riok.Mapperly.IntegrationTests.Mapper; using VerifyXunit; using Xunit; @@ -9,6 +10,7 @@ namespace Riok.Mapperly.IntegrationTests public class DerivedMapperTest : BaseMapperTest { [Fact] + [VersionedSnapshot(Versions.NET6_0)] public Task SnapshotGeneratedSourceBaseMapper() { var path = GetGeneratedMapperFilePath(nameof(BaseMapper)); @@ -16,6 +18,7 @@ public Task SnapshotGeneratedSourceBaseMapper() } [Fact] + [VersionedSnapshot(Versions.NET6_0)] public Task SnapshotGeneratedSourceDerivedMapper() { var path = GetGeneratedMapperFilePath(nameof(DerivedMapper)); @@ -23,6 +26,7 @@ public Task SnapshotGeneratedSourceDerivedMapper() } [Fact] + [VersionedSnapshot(Versions.NET6_0)] public Task SnapshotGeneratedSourceDerivedMapper2() { var path = GetGeneratedMapperFilePath(nameof(DerivedMapper2)); diff --git a/test/Riok.Mapperly.IntegrationTests/Mapper/NullableDisabledMapper.cs b/test/Riok.Mapperly.IntegrationTests/Mapper/NullableDisabledMapper.cs new file mode 100644 index 0000000000..743e339156 --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/Mapper/NullableDisabledMapper.cs @@ -0,0 +1,48 @@ +#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); + +#if NET7_0_OR_GREATER + [return: NotNullIfNotNull(nameof(src))] +#elif NET5_0_OR_GREATER + [return: NotNullIfNotNull("src")] +#endif + 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/MapperDefaultsTest.cs b/test/Riok.Mapperly.IntegrationTests/MapperDefaultsTest.cs index fe9e828d61..06d917977f 100644 --- a/test/Riok.Mapperly.IntegrationTests/MapperDefaultsTest.cs +++ b/test/Riok.Mapperly.IntegrationTests/MapperDefaultsTest.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using FluentAssertions; +using Riok.Mapperly.IntegrationTests.Helpers; using Riok.Mapperly.IntegrationTests.Mapper; using VerifyXunit; using Xunit; @@ -9,6 +10,7 @@ namespace Riok.Mapperly.IntegrationTests public class MapperDefaultsTest : BaseMapperTest { [Fact] + [VersionedSnapshot(Versions.NET6_0)] public Task SnapshotGeneratedSource() { var path = GetGeneratedMapperFilePath(nameof(EnumMapper)); diff --git a/test/Riok.Mapperly.IntegrationTests/NestedMapperInterfaceTest.cs b/test/Riok.Mapperly.IntegrationTests/NestedMapperInterfaceTest.cs index 0b215c1d2b..28c055e78e 100644 --- a/test/Riok.Mapperly.IntegrationTests/NestedMapperInterfaceTest.cs +++ b/test/Riok.Mapperly.IntegrationTests/NestedMapperInterfaceTest.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using FluentAssertions; +using Riok.Mapperly.IntegrationTests.Helpers; using Riok.Mapperly.IntegrationTests.Mapper; using VerifyXunit; using Xunit; @@ -9,6 +10,7 @@ namespace Riok.Mapperly.IntegrationTests public class NestedMapperInterfaceTest : BaseMapperTest { [Fact] + [VersionedSnapshot(Versions.NET6_0)] public Task SnapshotGeneratedSource() { var path = GetGeneratedMapperFilePath($"{nameof(INestedTestMapper)}.{nameof(NestedTestMapper.TestNesting.NestedMapper)}"); diff --git a/test/Riok.Mapperly.IntegrationTests/NestedMapperRecordTest.cs b/test/Riok.Mapperly.IntegrationTests/NestedMapperRecordTest.cs index de966f9a96..566611f36d 100644 --- a/test/Riok.Mapperly.IntegrationTests/NestedMapperRecordTest.cs +++ b/test/Riok.Mapperly.IntegrationTests/NestedMapperRecordTest.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using FluentAssertions; +using Riok.Mapperly.IntegrationTests.Helpers; using Riok.Mapperly.IntegrationTests.Mapper; using VerifyXunit; using Xunit; @@ -9,6 +10,7 @@ namespace Riok.Mapperly.IntegrationTests public class NestedMapperRecordTest : BaseMapperTest { [Fact] + [VersionedSnapshot(Versions.NET6_0)] public Task SnapshotGeneratedSource() { var path = GetGeneratedMapperFilePath( diff --git a/test/Riok.Mapperly.IntegrationTests/NestedMapperTest.cs b/test/Riok.Mapperly.IntegrationTests/NestedMapperTest.cs index 8aa70c81ac..f4d5c9cbd6 100644 --- a/test/Riok.Mapperly.IntegrationTests/NestedMapperTest.cs +++ b/test/Riok.Mapperly.IntegrationTests/NestedMapperTest.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using FluentAssertions; +using Riok.Mapperly.IntegrationTests.Helpers; using Riok.Mapperly.IntegrationTests.Mapper; using VerifyXunit; using Xunit; @@ -9,6 +10,7 @@ namespace Riok.Mapperly.IntegrationTests public class NestedMapperTest : BaseMapperTest { [Fact] + [VersionedSnapshot(Versions.NET6_0)] public Task SnapshotGeneratedSource() { var path = GetGeneratedMapperFilePath( diff --git a/test/Riok.Mapperly.IntegrationTests/NullableDisabledMapperTest.cs b/test/Riok.Mapperly.IntegrationTests/NullableDisabledMapperTest.cs new file mode 100644 index 0000000000..9d7659a2ca --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/NullableDisabledMapperTest.cs @@ -0,0 +1,33 @@ +using System.Threading.Tasks; +using Riok.Mapperly.IntegrationTests.Helpers; +using Riok.Mapperly.IntegrationTests.Mapper; +using VerifyXunit; +using Xunit; + +namespace Riok.Mapperly.IntegrationTests +{ + public class NullableDisabledMapperTest : BaseMapperTest + { + [Fact] + [VersionedSnapshot(Versions.NET6_0 | Versions.NET7_0)] + 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/Riok.Mapperly.IntegrationTests.csproj b/test/Riok.Mapperly.IntegrationTests/Riok.Mapperly.IntegrationTests.csproj index 3bf49567da..2233580b45 100644 --- a/test/Riok.Mapperly.IntegrationTests/Riok.Mapperly.IntegrationTests.csproj +++ b/test/Riok.Mapperly.IntegrationTests/Riok.Mapperly.IntegrationTests.csproj @@ -36,7 +36,7 @@ - + @@ -46,8 +46,8 @@ - - + + all runtime; build; native; contentfiles; analyzers diff --git a/test/Riok.Mapperly.IntegrationTests/UseExternalMapperTest.cs b/test/Riok.Mapperly.IntegrationTests/UseExternalMapperTest.cs index 7aeb644531..94b76614de 100644 --- a/test/Riok.Mapperly.IntegrationTests/UseExternalMapperTest.cs +++ b/test/Riok.Mapperly.IntegrationTests/UseExternalMapperTest.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using FluentAssertions; +using Riok.Mapperly.IntegrationTests.Helpers; using Riok.Mapperly.IntegrationTests.Mapper; using Riok.Mapperly.IntegrationTests.Models; using VerifyXunit; @@ -10,6 +11,7 @@ namespace Riok.Mapperly.IntegrationTests public class UseExternalMapperTest : BaseMapperTest { [Fact] + [VersionedSnapshot(Versions.NET6_0)] public Task SnapshotGeneratedSource() { var path = GetGeneratedMapperFilePath(nameof(UseExternalMapper)); diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/CircularReferenceMapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/CircularReferenceMapperTest.SnapshotGeneratedSource.verified.cs index 2b397a28a1..f43236bd52 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/CircularReferenceMapperTest.SnapshotGeneratedSource.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/CircularReferenceMapperTest.SnapshotGeneratedSource.verified.cs @@ -1,5 +1,5 @@ // -#nullable enable +#nullable enable annotations namespace Riok.Mapperly.IntegrationTests.Mapper { public static partial class CircularReferenceMapper diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/CircularReferenceMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/CircularReferenceMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs new file mode 100644 index 0000000000..2b397a28a1 --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/CircularReferenceMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs @@ -0,0 +1,35 @@ +// +#nullable enable +namespace Riok.Mapperly.IntegrationTests.Mapper +{ + public static partial class CircularReferenceMapper + { + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + public static partial global::Riok.Mapperly.IntegrationTests.Dto.CircularReferenceDto ToDto(global::Riok.Mapperly.IntegrationTests.Models.CircularReferenceObject obj) + { + return MapToCircularReferenceDto( + obj, + new global::Riok.Mapperly.Abstractions.ReferenceHandling.PreserveReferenceHandler() + ); + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private static global::Riok.Mapperly.IntegrationTests.Dto.CircularReferenceDto MapToCircularReferenceDto(global::Riok.Mapperly.IntegrationTests.Models.CircularReferenceObject source, global::Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + { + if (refHandler.TryGetReference(source, out var existingTargetReference)) + return existingTargetReference; + var target = new global::Riok.Mapperly.IntegrationTests.Dto.CircularReferenceDto(); + refHandler.SetReference(source, target); + target.Value = source.Value; + if (source.Parent != null) + { + target.Parent = MapToCircularReferenceDto(source.Parent, refHandler); + } + else + { + target.Parent = null; + } + return target; + } + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningMapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningMapperTest.SnapshotGeneratedSource.verified.cs index 3e20c70db4..0d63e079bc 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningMapperTest.SnapshotGeneratedSource.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningMapperTest.SnapshotGeneratedSource.verified.cs @@ -1,5 +1,5 @@ // -#nullable enable +#nullable enable annotations namespace Riok.Mapperly.IntegrationTests.Mapper { public static partial class DeepCloningMapper diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/DerivedMapperTest.SnapshotGeneratedSourceBaseMapper.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/DerivedMapperTest.SnapshotGeneratedSourceBaseMapper.verified.cs index 05c0dbba46..38f64c6738 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/DerivedMapperTest.SnapshotGeneratedSourceBaseMapper.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/DerivedMapperTest.SnapshotGeneratedSourceBaseMapper.verified.cs @@ -1,5 +1,5 @@ -// -#nullable enable +// +#nullable enable annotations namespace Riok.Mapperly.IntegrationTests.Mapper { public partial class BaseMapper diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/DerivedMapperTest.SnapshotGeneratedSourceBaseMapper_NET6_0.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/DerivedMapperTest.SnapshotGeneratedSourceBaseMapper_NET6_0.verified.cs new file mode 100644 index 0000000000..6ecb8a7b2d --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/DerivedMapperTest.SnapshotGeneratedSourceBaseMapper_NET6_0.verified.cs @@ -0,0 +1,19 @@ +// +#nullable enable +namespace Riok.Mapperly.IntegrationTests.Mapper +{ + public partial class BaseMapper + { + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + public virtual partial long IntToLong(int value) + { + return (long)value; + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + public partial short IntToShort(int value) + { + return (short)value; + } + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/DerivedMapperTest.SnapshotGeneratedSourceDerivedMapper.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/DerivedMapperTest.SnapshotGeneratedSourceDerivedMapper.verified.cs index 30f0829a5c..fa66d44a39 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/DerivedMapperTest.SnapshotGeneratedSourceDerivedMapper.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/DerivedMapperTest.SnapshotGeneratedSourceDerivedMapper.verified.cs @@ -1,5 +1,5 @@ -// -#nullable enable +// +#nullable enable annotations namespace Riok.Mapperly.IntegrationTests.Mapper { public partial class DerivedMapper diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/DerivedMapperTest.SnapshotGeneratedSourceDerivedMapper2.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/DerivedMapperTest.SnapshotGeneratedSourceDerivedMapper2.verified.cs index 3ff14d6857..3475c4e2cd 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/DerivedMapperTest.SnapshotGeneratedSourceDerivedMapper2.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/DerivedMapperTest.SnapshotGeneratedSourceDerivedMapper2.verified.cs @@ -1,5 +1,5 @@ -// -#nullable enable +// +#nullable enable annotations namespace Riok.Mapperly.IntegrationTests.Mapper { public partial class DerivedMapper2 diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/DerivedMapperTest.SnapshotGeneratedSourceDerivedMapper2_NET6_0.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/DerivedMapperTest.SnapshotGeneratedSourceDerivedMapper2_NET6_0.verified.cs new file mode 100644 index 0000000000..5553dee29d --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/DerivedMapperTest.SnapshotGeneratedSourceDerivedMapper2_NET6_0.verified.cs @@ -0,0 +1,19 @@ +// +#nullable enable +namespace Riok.Mapperly.IntegrationTests.Mapper +{ + public partial class DerivedMapper2 + { + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + public sealed override partial long IntToLong(int value) + { + return (long)value; + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + public new partial short IntToShort(int value) + { + return (short)value; + } + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/DerivedMapperTest.SnapshotGeneratedSourceDerivedMapper_NET6_0.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/DerivedMapperTest.SnapshotGeneratedSourceDerivedMapper_NET6_0.verified.cs new file mode 100644 index 0000000000..db4f4f0577 --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/DerivedMapperTest.SnapshotGeneratedSourceDerivedMapper_NET6_0.verified.cs @@ -0,0 +1,13 @@ +// +#nullable enable +namespace Riok.Mapperly.IntegrationTests.Mapper +{ + public partial class DerivedMapper + { + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + public override partial long IntToLong(int value) + { + return (long)value; + } + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperDefaultsTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperDefaultsTest.SnapshotGeneratedSource.verified.cs index ab51d0d79c..7b2033748e 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperDefaultsTest.SnapshotGeneratedSource.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperDefaultsTest.SnapshotGeneratedSource.verified.cs @@ -1,5 +1,5 @@ // -#nullable enable +#nullable enable annotations namespace Riok.Mapperly.IntegrationTests.Mapper { public static partial class EnumMapper diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperDefaultsTest.SnapshotGeneratedSource_NET6_0.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperDefaultsTest.SnapshotGeneratedSource_NET6_0.verified.cs new file mode 100644 index 0000000000..ab51d0d79c --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperDefaultsTest.SnapshotGeneratedSource_NET6_0.verified.cs @@ -0,0 +1,18 @@ +// +#nullable enable +namespace Riok.Mapperly.IntegrationTests.Mapper +{ + public static partial class EnumMapper + { + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + public static partial global::Riok.Mapperly.IntegrationTests.Mapper.Enum2 Map(global::Riok.Mapperly.IntegrationTests.Mapper.Enum1 e) + { + return e switch + { + global::Riok.Mapperly.IntegrationTests.Mapper.Enum1.Value1 => global::Riok.Mapperly.IntegrationTests.Mapper.Enum2.Value1, + global::Riok.Mapperly.IntegrationTests.Mapper.Enum1.Value2 => global::Riok.Mapperly.IntegrationTests.Mapper.Enum2.Value2, + _ => throw new System.ArgumentOutOfRangeException(nameof(e), e, "The value of enum Enum1 is not supported"), + }; + } + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.SnapshotGeneratedSource.verified.cs index 0af30f2565..13b9444850 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.SnapshotGeneratedSource.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.SnapshotGeneratedSource.verified.cs @@ -1,5 +1,5 @@ // -#nullable enable +#nullable enable annotations namespace Riok.Mapperly.IntegrationTests.Mapper { public partial class TestMapper diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/NestedMapperInterfaceTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/NestedMapperInterfaceTest.SnapshotGeneratedSource.verified.cs index 343d727084..defa301ec7 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/NestedMapperInterfaceTest.SnapshotGeneratedSource.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/NestedMapperInterfaceTest.SnapshotGeneratedSource.verified.cs @@ -1,5 +1,5 @@ // -#nullable enable +#nullable enable annotations namespace Riok.Mapperly.IntegrationTests.Mapper { public partial interface INestedTestMapper diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/NestedMapperInterfaceTest.SnapshotGeneratedSource_NET6_0.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/NestedMapperInterfaceTest.SnapshotGeneratedSource_NET6_0.verified.cs new file mode 100644 index 0000000000..343d727084 --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/NestedMapperInterfaceTest.SnapshotGeneratedSource_NET6_0.verified.cs @@ -0,0 +1,16 @@ +// +#nullable enable +namespace Riok.Mapperly.IntegrationTests.Mapper +{ + public partial interface INestedTestMapper + { + public static partial class NestedMapper + { + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + public static partial int ToInt(decimal value) + { + return (int)value; + } + } + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/NestedMapperRecordTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/NestedMapperRecordTest.SnapshotGeneratedSource.verified.cs index e51949ae5c..a5f6b00a9c 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/NestedMapperRecordTest.SnapshotGeneratedSource.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/NestedMapperRecordTest.SnapshotGeneratedSource.verified.cs @@ -1,5 +1,5 @@ // -#nullable enable +#nullable enable annotations namespace Riok.Mapperly.IntegrationTests.Mapper { public partial record NestedTestMapperRecord diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/NestedMapperRecordTest.SnapshotGeneratedSource_NET6_0.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/NestedMapperRecordTest.SnapshotGeneratedSource_NET6_0.verified.cs new file mode 100644 index 0000000000..e51949ae5c --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/NestedMapperRecordTest.SnapshotGeneratedSource_NET6_0.verified.cs @@ -0,0 +1,19 @@ +// +#nullable enable +namespace Riok.Mapperly.IntegrationTests.Mapper +{ + public partial record NestedTestMapperRecord + { + public partial record TestNesting + { + public static partial class NestedMapper + { + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + public static partial int ToInt(decimal value) + { + return (int)value; + } + } + } + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/NestedMapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/NestedMapperTest.SnapshotGeneratedSource.verified.cs index 347cbdffc2..0fe1db130f 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/NestedMapperTest.SnapshotGeneratedSource.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/NestedMapperTest.SnapshotGeneratedSource.verified.cs @@ -1,5 +1,5 @@ // -#nullable enable +#nullable enable annotations namespace Riok.Mapperly.IntegrationTests.Mapper { public static partial class NestedTestMapper diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/NestedMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/NestedMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs new file mode 100644 index 0000000000..347cbdffc2 --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/NestedMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs @@ -0,0 +1,19 @@ +// +#nullable enable +namespace Riok.Mapperly.IntegrationTests.Mapper +{ + public static partial class NestedTestMapper + { + public static partial class TestNesting + { + public static partial class NestedMapper + { + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + public static partial int ToInt(decimal value) + { + return (int)value; + } + } + } + } +} \ No newline at end of file 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..f9c3cbedde --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/NullableDisabledMapperTest.SnapshotGeneratedSource.verified.cs @@ -0,0 +1,19 @@ +// +#nullable enable annotations +namespace Riok.Mapperly.IntegrationTests.Mapper +{ + public static partial class NullableDisabledMapper + { + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + 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.IntegrationTests/_snapshots/NullableDisabledMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/NullableDisabledMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs new file mode 100644 index 0000000000..8244402f8c --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/NullableDisabledMapperTest.SnapshotGeneratedSource_NET6_0.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("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.IntegrationTests/_snapshots/NullableDisabledMapperTest.SnapshotGeneratedSource_NET7_0.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/NullableDisabledMapperTest.SnapshotGeneratedSource_NET7_0.verified.cs new file mode 100644 index 0000000000..184f684933 --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/NullableDisabledMapperTest.SnapshotGeneratedSource_NET7_0.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.IntegrationTests/_snapshots/ProjectionMapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.SnapshotGeneratedSource.verified.cs index bb54f8ea90..8ee9821eb9 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.SnapshotGeneratedSource.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.SnapshotGeneratedSource.verified.cs @@ -1,5 +1,5 @@ // -#nullable enable +#nullable enable annotations namespace Riok.Mapperly.IntegrationTests.Mapper { public static partial class ProjectionMapper @@ -67,7 +67,7 @@ public static partial class ProjectionMapper })), } ); -#nullable enable +#nullable enable annotations } [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] @@ -88,7 +88,7 @@ public static partial class ProjectionMapper BaseValue = ((global::Riok.Mapperly.IntegrationTests.Models.TestObjectProjectionTypeB)x).BaseValue, } : default) ); -#nullable enable +#nullable enable annotations } [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource.verified.cs index d4ccd0ed6d..bb027fb84c 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource.verified.cs @@ -1,5 +1,5 @@ // -#nullable enable +#nullable enable annotations namespace Riok.Mapperly.IntegrationTests.Mapper { public static partial class StaticTestMapper diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/UseExternalMapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/UseExternalMapperTest.SnapshotGeneratedSource.verified.cs index 3898d05a64..77c6b8057a 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/UseExternalMapperTest.SnapshotGeneratedSource.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/UseExternalMapperTest.SnapshotGeneratedSource.verified.cs @@ -1,5 +1,5 @@ -// -#nullable enable +// +#nullable enable annotations namespace Riok.Mapperly.IntegrationTests.Mapper { public static partial class UseExternalMapper diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/UseExternalMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/UseExternalMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs new file mode 100644 index 0000000000..8bea7436c2 --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/UseExternalMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs @@ -0,0 +1,15 @@ +// +#nullable enable +namespace Riok.Mapperly.IntegrationTests.Mapper +{ + public static partial class UseExternalMapper + { + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + public static partial global::Riok.Mapperly.IntegrationTests.Dto.IdObjectDto Map(global::Riok.Mapperly.IntegrationTests.Models.IdObject source) + { + var target = new global::Riok.Mapperly.IntegrationTests.Dto.IdObjectDto(); + target.IdValue = global::Riok.Mapperly.IntegrationTests.Mapper.UseExternalMapper.MyOtherMapper.MapInt(source.IdValue); + return target; + } + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/Helpers/GenericTypeCheckerTest.cs b/test/Riok.Mapperly.Tests/Helpers/GenericTypeCheckerTest.cs index 6c87f10bf7..e3dda84dc4 100644 --- a/test/Riok.Mapperly.Tests/Helpers/GenericTypeCheckerTest.cs +++ b/test/Riok.Mapperly.Tests/Helpers/GenericTypeCheckerTest.cs @@ -277,7 +277,13 @@ public class Mapper var methodNode = nodes.OfType().Single(x => x.Identifier.Text == "Test"); var model = compilation.GetSemanticModel(classNode.SyntaxTree); var mapperSymbol = model.GetDeclaredSymbol(classNode) ?? throw new NullReferenceException(); - var compilationContext = new CompilationContext(compilation, new WellKnownTypes(compilation), [], new FileNameBuilder()); + var compilationContext = new CompilationContext( + compilation, + LanguageVersion.Default, + new WellKnownTypes(compilation), + [], + new FileNameBuilder() + ); var symbolAccessor = new SymbolAccessor(compilationContext, mapperSymbol); var typeChecker = new GenericTypeChecker(symbolAccessor, compilationContext.Types); 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)