diff --git a/src/Riok.Mapperly/AnalyzerReleases.Shipped.md b/src/Riok.Mapperly/AnalyzerReleases.Shipped.md index 44006c8c6e..e52b60b3c9 100644 --- a/src/Riok.Mapperly/AnalyzerReleases.Shipped.md +++ b/src/Riok.Mapperly/AnalyzerReleases.Shipped.md @@ -153,3 +153,4 @@ RMG065 | Mapper | Warning | Cannot configure an object mapping on a queryabl RMG066 | Mapper | Warning | No members are mapped in an object mapping RMG067 | Mapper | Error | Invalid usage of the MapPropertyAttribute RMG068 | Mapper | Info | Cannot inline user implemented queryable expression mapping +RMG069 | Mapper | Warning | Runtime target type or generic type mapping does not match any mappings diff --git a/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs b/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs index f8e195ceb4..70b6d808e3 100644 --- a/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs @@ -57,6 +57,7 @@ MapperConfiguration defaultMapperConfiguration compilationContext, configurationReader, _symbolAccessor, + new GenericTypeChecker(_symbolAccessor, _types), attributeAccessor, _unsafeAccessorContext, _diagnostics, diff --git a/src/Riok.Mapperly/Descriptors/Enumerables/CollectionInfoBuilder.cs b/src/Riok.Mapperly/Descriptors/Enumerables/CollectionInfoBuilder.cs index 12c0507874..80db777de8 100644 --- a/src/Riok.Mapperly/Descriptors/Enumerables/CollectionInfoBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/Enumerables/CollectionInfoBuilder.cs @@ -375,7 +375,7 @@ static CollectionType IterateImplementedTypes(ITypeSymbol type, WellKnownTypes t if (typeInfo.GetTypeSymbol(types) is not { } typeSymbol) continue; - if (type.ImplementsGeneric(typeSymbol, out _)) + if (type.ExtendsOrImplementsGeneric(typeSymbol, out _)) { implementedCollectionTypes |= typeInfo.CollectionType; } diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/RuntimeTargetTypeMappingBodyBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/RuntimeTargetTypeMappingBodyBuilder.cs index c7bc8dc93c..12d8be3f07 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/RuntimeTargetTypeMappingBodyBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/RuntimeTargetTypeMappingBodyBuilder.cs @@ -1,7 +1,7 @@ -using Microsoft.CodeAnalysis; using Riok.Mapperly.Descriptors.MappingBuilders; using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Descriptors.Mappings.UserMappings; +using Riok.Mapperly.Diagnostics; using Riok.Mapperly.Helpers; using Riok.Mapperly.Symbols; @@ -16,26 +16,16 @@ public static void BuildMappingBody(MappingBuilderContext ctx, UserDefinedNewIns // as non-nullables are also assignable to nullables. var mappings = GetUserMappingCandidates(ctx) .Where(x => - DoesTypesSatisfySubstitutionPrinciples(mapping, ctx.SymbolAccessor, x.SourceType.NonNullable(), x.TargetType) - && mapping.TypeParameters.DoesTypesSatisfyTypeParameterConstraints( - ctx.SymbolAccessor, - x.SourceType.NonNullable(), - x.TargetType - ) + ctx.GenericTypeChecker.InferAndCheckTypes( + mapping.Method.TypeParameters, + (mapping.SourceType, x.SourceType.NonNullable()), + (mapping.TargetType, x.TargetType) + ).Success ); BuildMappingBody(ctx, mapping, mappings); } - private static bool DoesTypesSatisfySubstitutionPrinciples( - IMapping mapping, - SymbolAccessor symbolAccessor, - ITypeSymbol sourceType, - ITypeSymbol targetType - ) => - (mapping.SourceType.TypeKind == TypeKind.TypeParameter || symbolAccessor.HasImplicitConversion(sourceType, mapping.SourceType)) - && (mapping.TargetType.TypeKind == TypeKind.TypeParameter || symbolAccessor.HasImplicitConversion(targetType, mapping.TargetType)); - public static void BuildMappingBody(MappingBuilderContext ctx, UserDefinedNewInstanceRuntimeTargetTypeMapping mapping) { // source nulls are filtered out by the type switch arms, @@ -43,15 +33,15 @@ public static void BuildMappingBody(MappingBuilderContext ctx, UserDefinedNewIns // as non-nullables are also assignable to nullables. var mappings = GetUserMappingCandidates(ctx) .Where(x => - ctx.SymbolAccessor.HasImplicitConversion(x.SourceType.NonNullable(), mapping.SourceType) - && ctx.SymbolAccessor.HasImplicitConversion(x.TargetType, mapping.TargetType) + ctx.SymbolAccessor.CanAssign(x.SourceType.NonNullable(), mapping.SourceType) + && ctx.SymbolAccessor.CanAssign(x.TargetType, mapping.TargetType) ); BuildMappingBody(ctx, mapping, mappings); } - private static IEnumerable GetUserMappingCandidates(MappingBuilderContext ctx) => - ctx.UserMappings.Where(x => x is not UserDefinedNewInstanceRuntimeTargetTypeMapping); + private static IEnumerable GetUserMappingCandidates(MappingBuilderContext ctx) => + ctx.UserMappings.Where(x => x is not UserDefinedNewInstanceRuntimeTargetTypeMapping).OfType(); private static void BuildMappingBody( MappingBuilderContext ctx, @@ -78,7 +68,14 @@ IEnumerable childMappings .ThenBy(x => x.TargetType.IsNullable()) .GroupBy(x => new TypeMappingKey(x, includeNullability: false)) .Select(x => x.First()) - .Select(x => new RuntimeTargetTypeMapping(x, ctx.Compilation.HasImplicitConversion(x.TargetType, ctx.Target))); + .Select(x => new RuntimeTargetTypeMapping(x, ctx.Compilation.HasImplicitConversion(x.TargetType, ctx.Target))) + .ToList(); + + if (runtimeTargetTypeMappings.Count == 0) + { + ctx.ReportDiagnostic(DiagnosticDescriptors.RuntimeTargetTypeMappingNoContentMappings); + } + mapping.AddMappings(runtimeTargetTypeMappings); } } diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs index 50e3b5a970..29d079b682 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs @@ -61,7 +61,7 @@ bool ignoreDerivedTypes public DictionaryInfos? DictionaryInfos => _dictionaryInfos ??= DictionaryInfoBuilder.Build(Types, CollectionInfos); - protected IMethodSymbol? UserSymbol { get; } + public IMethodSymbol? UserSymbol { get; } public bool HasUserSymbol => UserSymbol != null; diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/DerivedTypeMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/DerivedTypeMappingBuilder.cs index 1402868695..2870f55bee 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/DerivedTypeMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/DerivedTypeMappingBuilder.cs @@ -61,33 +61,34 @@ bool duplicatedSourceTypesAllowed { var derivedTypeMappingSourceTypes = new HashSet(SymbolEqualityComparer.Default); var derivedTypeMappings = new List(configs.Count); - Func isAssignableToSource = ctx.Source is ITypeParameterSymbol sourceTypeParameter - ? t => ctx.SymbolAccessor.DoesTypeSatisfyTypeParameterConstraints(sourceTypeParameter, t) - : t => ctx.SymbolAccessor.HasImplicitConversion(t, ctx.Source); - Func isAssignableToTarget = ctx.Target is ITypeParameterSymbol targetTypeParameter - ? t => ctx.SymbolAccessor.DoesTypeSatisfyTypeParameterConstraints(targetTypeParameter, t) - : t => ctx.SymbolAccessor.HasImplicitConversion(t, ctx.Target); foreach (var config in configs) { // set types non-nullable as they can never be null when type-switching. var sourceType = config.SourceType.NonNullable(); + var targetType = config.TargetType.NonNullable(); if (!duplicatedSourceTypesAllowed && !derivedTypeMappingSourceTypes.Add(sourceType)) { ctx.ReportDiagnostic(DiagnosticDescriptors.DerivedSourceTypeDuplicated, sourceType); continue; } - if (!isAssignableToSource(sourceType)) + var typeCheckerResult = ctx.GenericTypeChecker.InferAndCheckTypes( + ctx.UserSymbol!.TypeParameters, + (ctx.Source, sourceType), + (ctx.Target, targetType) + ); + if (!typeCheckerResult.Success) { - ctx.ReportDiagnostic(DiagnosticDescriptors.DerivedSourceTypeIsNotAssignableToParameterType, sourceType, ctx.Source); - continue; - } + if (ReferenceEquals(sourceType, typeCheckerResult.FailedArgument)) + { + ctx.ReportDiagnostic(DiagnosticDescriptors.DerivedSourceTypeIsNotAssignableToParameterType, sourceType, ctx.Source); + } + else + { + ctx.ReportDiagnostic(DiagnosticDescriptors.DerivedTargetTypeIsNotAssignableToReturnType, targetType, ctx.Target); + } - var targetType = config.TargetType.NonNullable(); - if (!isAssignableToTarget(targetType)) - { - ctx.ReportDiagnostic(DiagnosticDescriptors.DerivedTargetTypeIsNotAssignableToReturnType, targetType, ctx.Target); continue; } diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumerableMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumerableMappingBuilder.cs index 68665bff22..1f83e80a28 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumerableMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumerableMappingBuilder.cs @@ -324,7 +324,7 @@ INewInstanceMapping elementMapping if (!hasObjectFactory) { sourceCollectionInfo = BuildCollectionTypeForICollection(ctx, sourceCollectionInfo); - ctx.ObjectFactories.TryFindObjectFactory(sourceCollectionInfo.Type, ctx.Target, out objectFactory); + ctx.ObjectFactories.TryFindObjectFactory(ctx.Source, ctx.Target, out objectFactory); var existingMapping = ctx.BuildDelegatedMapping(sourceCollectionInfo.Type, ctx.Target); if (existingMapping != null) return existingMapping; diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs index 4794796644..2dae8a315f 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs @@ -32,7 +32,6 @@ public abstract class MethodMapping : ITypeMapping }; private readonly ITypeSymbol _returnType; - private readonly IMethodSymbol? _partialMethodDefinition; private string? _methodName; @@ -54,11 +53,16 @@ ITypeSymbol targetType SourceParameter = sourceParameter; IsExtensionMethod = method.IsExtensionMethod; ReferenceHandlerParameter = referenceHandlerParameter; - _partialMethodDefinition = method; + Method = method; + MethodDeclarationSyntax = Method?.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() as MethodDeclarationSyntax; _methodName = method.Name; _returnType = method.ReturnsVoid ? method.ReturnType : targetType; } + protected IMethodSymbol? Method { get; } + + protected MethodDeclarationSyntax? MethodDeclarationSyntax { get; } + protected bool IsExtensionMethod { get; } protected string MethodName => _methodName ?? throw new InvalidOperationException(); @@ -117,11 +121,11 @@ internal virtual void EnableReferenceHandling(INamedTypeSymbol iReferenceHandler private IEnumerable BuildModifiers(bool isStatic) { - // if a syntax is referenced it is the implementation part of partial method definition - // then copy all modifiers otherwise only set private and optionally static - if (_partialMethodDefinition?.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() is MethodDeclarationSyntax syntax) + // if a syntax is referenced the code written by the user copy all modifiers, + // otherwise only set private and optionally static + if (MethodDeclarationSyntax != null) { - return syntax.Modifiers.Select(x => TrailingSpacedToken(x.Kind())); + return MethodDeclarationSyntax.Modifiers.Select(x => TrailingSpacedToken(x.Kind())); } return isStatic ? _privateStaticSyntaxToken : _privateSyntaxToken; diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetMethodMapping.cs index faa94842ee..2641d877d8 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetMethodMapping.cs @@ -22,7 +22,7 @@ bool enableReferenceHandling { private IExistingTargetMapping? _delegateMapping; - public IMethodSymbol Method { get; } = method; + public new IMethodSymbol Method { get; } = method; public bool? Default => false; diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceGenericTypeMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceGenericTypeMapping.cs index ec99c8542b..2fe8547ed2 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceGenericTypeMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceGenericTypeMapping.cs @@ -16,11 +16,10 @@ namespace Riok.Mapperly.Descriptors.Mappings.UserMappings; /// public class UserDefinedNewInstanceGenericTypeMapping( IMethodSymbol method, - GenericMappingTypeParameters typeParameters, MappingMethodParameters parameters, ITypeSymbol targetType, bool enableReferenceHandling, - NullFallbackValue nullArm, + NullFallbackValue? nullArm, ITypeSymbol objectType ) : UserDefinedNewInstanceRuntimeTargetTypeMapping( @@ -33,16 +32,16 @@ ITypeSymbol objectType objectType ) { - public GenericMappingTypeParameters TypeParameters { get; } = typeParameters; - - public override MethodDeclarationSyntax BuildMethod(SourceEmitterContext ctx) => - base.BuildMethod(ctx).WithTypeParameterList(TypeParameterList(TypeParameters.SourceType, TypeParameters.TargetType)); + public override MethodDeclarationSyntax BuildMethod(SourceEmitterContext ctx) + { + var methodSyntax = (MethodDeclarationSyntax)Method.DeclaringSyntaxReferences.First().GetSyntax(); + return base.BuildMethod(ctx).WithTypeParameterList(methodSyntax.TypeParameterList); + } protected override ExpressionSyntax BuildTargetType() { - // typeof(TTarget) or typeof() - var targetTypeName = TypeParameters.TargetType ?? TargetType; - return TypeOfExpression(FullyQualifiedIdentifier(targetTypeName.NonNullable())); + // typeof() + return TypeOfExpression(FullyQualifiedIdentifier(Method.ReturnType.NonNullable())); } protected override ExpressionSyntax? BuildSwitchArmWhenClause(ExpressionSyntax targetType, RuntimeTargetTypeMapping mapping) diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs index 8dbcc7426b..3a03231dea 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs @@ -17,7 +17,7 @@ public class UserDefinedNewInstanceMethodMapping( bool enableReferenceHandling ) : NewInstanceMethodMapping(method, sourceParameter, referenceHandlerParameter, targetType), INewInstanceUserMapping { - public IMethodSymbol Method { get; } = method; + public new IMethodSymbol Method { get; } = method; public bool? Default { get; } = isDefault; diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeMapping.cs index 1259ec0fa1..bc53f0bcc5 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeMapping.cs @@ -19,7 +19,7 @@ public abstract class UserDefinedNewInstanceRuntimeTargetTypeMapping( MethodParameter? referenceHandlerParameter, ITypeSymbol targetType, bool enableReferenceHandling, - NullFallbackValue nullArm, + NullFallbackValue? nullArm, ITypeSymbol objectType ) : NewInstanceMethodMapping(method, sourceParameter, referenceHandlerParameter, targetType), INewInstanceUserMapping { @@ -28,7 +28,7 @@ ITypeSymbol objectType private readonly List _mappings = new(); - public IMethodSymbol Method { get; } = method; + public new IMethodSymbol Method { get; } = method; /// /// Always false, as this cannot be called by other mappings, @@ -77,7 +77,11 @@ public override IEnumerable BuildBody(TypeMappingBuildContext c var arms = _mappings.Select(x => BuildSwitchArm(typeArmContext, typeArmVariableName, x, targetTypeExpr)); // null => default / throw - arms = arms.Append(SwitchArm(ConstantPattern(NullLiteral()), NullSubstitute(TargetType, ctx.Source, nullArm))); + if (nullArm.HasValue) + { + arms = arms.Append(SwitchArm(ConstantPattern(NullLiteral()), NullSubstitute(TargetType, ctx.Source, nullArm.Value))); + } + arms = arms.Append(fallbackArm); var switchExpression = ctx.SyntaxFactory.Switch(ctx.Source, arms); yield return ctx.SyntaxFactory.Return(switchExpression); @@ -85,11 +89,11 @@ public override IEnumerable BuildBody(TypeMappingBuildContext c protected abstract ExpressionSyntax BuildTargetType(); - protected virtual ExpressionSyntax? BuildSwitchArmWhenClause(ExpressionSyntax targetType, RuntimeTargetTypeMapping mapping) + protected virtual ExpressionSyntax? BuildSwitchArmWhenClause(ExpressionSyntax runtimeTargetType, RuntimeTargetTypeMapping mapping) { // targetType.IsAssignableFrom(typeof(ADto)) return Invocation( - MemberAccess(targetType, IsAssignableFromMethodName), + MemberAccess(runtimeTargetType, IsAssignableFromMethodName), TypeOfExpression(FullyQualifiedIdentifier(mapping.Mapping.TargetType.NonNullable())) ); } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeParameterMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeParameterMapping.cs index 2eae8a5663..c48a847bcb 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeParameterMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeParameterMapping.cs @@ -15,7 +15,7 @@ public class UserDefinedNewInstanceRuntimeTargetTypeParameterMapping( RuntimeTargetTypeMappingMethodParameters parameters, bool enableReferenceHandling, ITypeSymbol targetType, - NullFallbackValue nullArm, + NullFallbackValue? nullArm, ITypeSymbol objectType ) : UserDefinedNewInstanceRuntimeTargetTypeMapping( diff --git a/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericSourceObjectFactory.cs b/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericSourceObjectFactory.cs index 0baefdd81b..a86e366525 100644 --- a/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericSourceObjectFactory.cs +++ b/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericSourceObjectFactory.cs @@ -1,5 +1,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Helpers; using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; namespace Riok.Mapperly.Descriptors.ObjectFactories; @@ -9,12 +10,13 @@ namespace Riok.Mapperly.Descriptors.ObjectFactories; /// with a named return type and one type parameter which is also the only parameter of the method. /// Example signature: TypeToCreate Create<S>(S source); /// -public class GenericSourceObjectFactory(SymbolAccessor symbolAccessor, IMethodSymbol method) : ObjectFactory(symbolAccessor, method) +public class GenericSourceObjectFactory(GenericTypeChecker typeChecker, SymbolAccessor symbolAccessor, IMethodSymbol method) + : ObjectFactory(symbolAccessor, method) { public override bool CanCreateType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate) => SymbolEqualityComparer.Default.Equals(Method.ReturnType, targetTypeToCreate) - && SymbolAccessor.DoesTypeSatisfyTypeParameterConstraints(Method.TypeParameters[0], sourceType); + && typeChecker.CheckTypes((Method.TypeParameters[0], sourceType)); protected override ExpressionSyntax BuildCreateType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate, ExpressionSyntax source) => - GenericInvocation(Method.Name, new[] { NonNullableIdentifier(sourceType) }, source); + GenericInvocation(Method.Name, [NonNullableIdentifier(sourceType)], source); } diff --git a/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericSourceTargetObjectFactory.cs b/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericSourceTargetObjectFactory.cs index 616312c61c..1b541f9567 100644 --- a/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericSourceTargetObjectFactory.cs +++ b/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericSourceTargetObjectFactory.cs @@ -1,17 +1,24 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Helpers; using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; namespace Riok.Mapperly.Descriptors.ObjectFactories; -public class GenericSourceTargetObjectFactory(SymbolAccessor symbolAccessor, IMethodSymbol method, int sourceTypeParameterIndex) - : ObjectFactory(symbolAccessor, method) +public class GenericSourceTargetObjectFactory( + GenericTypeChecker typeChecker, + SymbolAccessor symbolAccessor, + IMethodSymbol method, + int sourceTypeParameterIndex +) : ObjectFactory(symbolAccessor, method) { private readonly int _targetTypeParameterIndex = (sourceTypeParameterIndex + 1) % 2; public override bool CanCreateType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate) => - SymbolAccessor.DoesTypeSatisfyTypeParameterConstraints(Method.TypeParameters[sourceTypeParameterIndex], sourceType) - && SymbolAccessor.DoesTypeSatisfyTypeParameterConstraints(Method.TypeParameters[_targetTypeParameterIndex], targetTypeToCreate); + typeChecker.CheckTypes( + (Method.TypeParameters[sourceTypeParameterIndex], sourceType), + (Method.TypeParameters[_targetTypeParameterIndex], targetTypeToCreate) + ); protected override ExpressionSyntax BuildCreateType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate, ExpressionSyntax source) { diff --git a/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericTargetObjectFactory.cs b/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericTargetObjectFactory.cs index c3ed60de5a..29a9d82c81 100644 --- a/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericTargetObjectFactory.cs +++ b/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericTargetObjectFactory.cs @@ -1,5 +1,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Helpers; using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; namespace Riok.Mapperly.Descriptors.ObjectFactories; @@ -9,10 +10,11 @@ namespace Riok.Mapperly.Descriptors.ObjectFactories; /// without any parameters but a single type parameter which is also the return type. /// Example signature: T Create<T>(); /// -public class GenericTargetObjectFactory(SymbolAccessor symbolAccessor, IMethodSymbol method) : ObjectFactory(symbolAccessor, method) +public class GenericTargetObjectFactory(GenericTypeChecker typeChecker, SymbolAccessor symbolAccessor, IMethodSymbol method) + : ObjectFactory(symbolAccessor, method) { public override bool CanCreateType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate) => - SymbolAccessor.DoesTypeSatisfyTypeParameterConstraints(Method.TypeParameters[0], targetTypeToCreate); + typeChecker.CheckTypes((Method.TypeParameters[0], targetTypeToCreate)); protected override ExpressionSyntax BuildCreateType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate, ExpressionSyntax source) => GenericInvocation(Method.Name, new[] { NonNullableIdentifier(targetTypeToCreate) }); diff --git a/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericTargetObjectFactoryWithSource.cs b/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericTargetObjectFactoryWithSource.cs index 91f671b852..3da7353c59 100644 --- a/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericTargetObjectFactoryWithSource.cs +++ b/src/Riok.Mapperly/Descriptors/ObjectFactories/GenericTargetObjectFactoryWithSource.cs @@ -1,5 +1,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Helpers; using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; namespace Riok.Mapperly.Descriptors.ObjectFactories; @@ -9,8 +10,8 @@ namespace Riok.Mapperly.Descriptors.ObjectFactories; /// with a single parameter (which is the source object and is a named type) and a single type parameter which is also the return type. /// Example signature: T Create<T>(SourceType source); /// -public class GenericTargetObjectFactoryWithSource(SymbolAccessor symbolAccessor, IMethodSymbol method) - : GenericTargetObjectFactory(symbolAccessor, method) +public class GenericTargetObjectFactoryWithSource(GenericTypeChecker typeChecker, SymbolAccessor symbolAccessor, IMethodSymbol method) + : GenericTargetObjectFactory(typeChecker, symbolAccessor, method) { public override bool CanCreateType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate) => base.CanCreateType(sourceType, targetTypeToCreate) && SymbolEqualityComparer.Default.Equals(Method.Parameters[0].Type, sourceType); diff --git a/src/Riok.Mapperly/Descriptors/ObjectFactories/ObjectFactory.cs b/src/Riok.Mapperly/Descriptors/ObjectFactories/ObjectFactory.cs index affaef5509..31a536a4d3 100644 --- a/src/Riok.Mapperly/Descriptors/ObjectFactories/ObjectFactory.cs +++ b/src/Riok.Mapperly/Descriptors/ObjectFactories/ObjectFactory.cs @@ -10,8 +10,6 @@ namespace Riok.Mapperly.Descriptors.ObjectFactories; /// public abstract class ObjectFactory(SymbolAccessor symbolAccessor, IMethodSymbol method) { - protected SymbolAccessor SymbolAccessor { get; } = symbolAccessor; - protected IMethodSymbol Method { get; } = method; public ExpressionSyntax CreateType(ITypeSymbol sourceType, ITypeSymbol targetTypeToCreate, ExpressionSyntax source) => @@ -35,7 +33,7 @@ private ExpressionSyntax HandleNull(ExpressionSyntax expression, ITypeSymbol typ if (!Method.ReturnType.IsNullable()) return expression; - ExpressionSyntax nullFallback = SymbolAccessor.HasDirectlyAccessibleParameterlessConstructor(typeToCreate) + ExpressionSyntax nullFallback = symbolAccessor.HasDirectlyAccessibleParameterlessConstructor(typeToCreate) ? CreateInstance(typeToCreate) : ThrowNullReferenceException($"The object factory {Method.Name} returned null"); diff --git a/src/Riok.Mapperly/Descriptors/ObjectFactories/ObjectFactoryBuilder.cs b/src/Riok.Mapperly/Descriptors/ObjectFactories/ObjectFactoryBuilder.cs index a4fa8b4a0c..e33b9cbe74 100644 --- a/src/Riok.Mapperly/Descriptors/ObjectFactories/ObjectFactoryBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/ObjectFactories/ObjectFactoryBuilder.cs @@ -77,12 +77,12 @@ public static ObjectFactoryCollection ExtractObjectFactories(SimpleMappingBuilde if (returnTypeIsGeneric) { return hasSourceParameter - ? new GenericTargetObjectFactoryWithSource(ctx.SymbolAccessor, methodSymbol) - : new GenericTargetObjectFactory(ctx.SymbolAccessor, methodSymbol); + ? new GenericTargetObjectFactoryWithSource(ctx.GenericTypeChecker, ctx.SymbolAccessor, methodSymbol) + : new GenericTargetObjectFactory(ctx.GenericTypeChecker, ctx.SymbolAccessor, methodSymbol); } if (hasSourceParameter) - return new GenericSourceObjectFactory(ctx.SymbolAccessor, methodSymbol); + return new GenericSourceObjectFactory(ctx.GenericTypeChecker, ctx.SymbolAccessor, methodSymbol); ctx.ReportDiagnostic(DiagnosticDescriptors.InvalidObjectFactorySignature, methodSymbol, methodSymbol.Name); return null; @@ -110,6 +110,6 @@ public static ObjectFactoryCollection ExtractObjectFactories(SimpleMappingBuilde return null; } - return new GenericSourceTargetObjectFactory(ctx.SymbolAccessor, methodSymbol, sourceParameterIndex); + return new GenericSourceTargetObjectFactory(ctx.GenericTypeChecker, ctx.SymbolAccessor, methodSymbol, sourceParameterIndex); } } diff --git a/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs index 3e613cbc06..e5f2f8af65 100644 --- a/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs @@ -15,6 +15,7 @@ public class SimpleMappingBuilderContext( CompilationContext compilationContext, MapperConfigurationReader configurationReader, SymbolAccessor symbolAccessor, + GenericTypeChecker genericTypeChecker, AttributeDataAccessor attributeAccessor, UnsafeAccessorContext unsafeAccessorContext, DiagnosticCollection diagnostics, @@ -34,6 +35,7 @@ protected SimpleMappingBuilderContext(SimpleMappingBuilderContext ctx, Location? ctx._compilationContext, ctx._configurationReader, ctx.SymbolAccessor, + ctx.GenericTypeChecker, ctx.AttributeAccessor, ctx.UnsafeAccessorContext, ctx._diagnostics, @@ -51,6 +53,8 @@ protected SimpleMappingBuilderContext(SimpleMappingBuilderContext ctx, Location? public SymbolAccessor SymbolAccessor { get; } = symbolAccessor; + public GenericTypeChecker GenericTypeChecker { get; } = genericTypeChecker; + public AttributeDataAccessor AttributeAccessor { get; } = attributeAccessor; public UnsafeAccessorContext UnsafeAccessorContext { get; } = unsafeAccessorContext; diff --git a/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs b/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs index 3d52e83fd7..66c8c5a067 100644 --- a/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs +++ b/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs @@ -53,27 +53,19 @@ public bool IsAccessible(ISymbol symbol) public bool HasImplicitConversion(ITypeSymbol source, ITypeSymbol destination) => Compilation.ClassifyConversion(source, destination).IsImplicit && (destination.IsNullable() || !source.IsNullable()); - public bool DoesTypeSatisfyTypeParameterConstraints(ITypeParameterSymbol typeParameter, ITypeSymbol type) + /// + /// Returns true when a conversion form the + /// to the is possible with a conversion + /// of type identity, boxing or implicit and compatible nullability. + /// + /// The source type. + /// The target type. + /// Whether the assignment is valid + public bool CanAssign(ITypeSymbol sourceType, ITypeSymbol targetType) { - if (typeParameter.HasConstructorConstraint && !HasDirectlyAccessibleParameterlessConstructor(type)) - return false; - - if (!typeParameter.IsNullable() && type.IsNullable()) - return false; - - if (typeParameter.HasValueTypeConstraint && !type.IsValueType) - return false; - - if (typeParameter.HasReferenceTypeConstraint && !type.IsReferenceType) - return false; - - foreach (var constraintType in typeParameter.ConstraintTypes) - { - if (!Compilation.ClassifyConversion(type, UpgradeNullable(constraintType)).IsImplicit) - return false; - } - - return true; + var conversion = Compilation.ClassifyConversion(sourceType, targetType); + return (conversion.IsIdentity || conversion.IsBoxing || conversion.IsImplicit) + && (targetType.IsNullable() || !sourceType.IsNullable()); } public MethodParameter? WrapOptionalMethodParameter(IParameterSymbol? symbol) @@ -123,12 +115,8 @@ internal bool TryUpgradeNullable(ITypeSymbol symbol, [NotNullWhen(true)] out ITy break; case IArrayTypeSymbol { ElementType.IsValueType: false, ElementNullableAnnotation: NullableAnnotation.None } arrayTypeSymbol: - upgradedSymbol = compilationContext - .Compilation.CreateArrayTypeSymbol( - UpgradeNullable(arrayTypeSymbol.ElementType), - arrayTypeSymbol.Rank, - NullableAnnotation.Annotated - ) + upgradedSymbol = Compilation + .CreateArrayTypeSymbol(UpgradeNullable(arrayTypeSymbol.ElementType), arrayTypeSymbol.Rank, NullableAnnotation.Annotated) .WithNullableAnnotation(NullableAnnotation.Annotated); break; diff --git a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs index 13282c4dd0..221442b74d 100644 --- a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs +++ b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis; using Riok.Mapperly.Abstractions; @@ -187,7 +188,7 @@ bool isExternal runtimeTargetTypeParams, ctx.Configuration.Mapper.UseReferenceHandling, ctx.SymbolAccessor.UpgradeNullable(methodSymbol.ReturnType), - GetTypeSwitchNullArm(methodSymbol, runtimeTargetTypeParams, null), + GetTypeSwitchNullArm(methodSymbol, runtimeTargetTypeParams), ctx.Compilation.ObjectType.WithNullableAnnotation(NullableAnnotation.NotAnnotated) ); } @@ -198,25 +199,18 @@ bool isExternal return null; } - if (BuildGenericTypeParameters(methodSymbol, parameters, out var typeParameters)) + if (methodSymbol.IsGenericMethod) { return new UserDefinedNewInstanceGenericTypeMapping( methodSymbol, - typeParameters.Value, parameters, ctx.SymbolAccessor.UpgradeNullable(methodSymbol.ReturnType), ctx.Configuration.Mapper.UseReferenceHandling, - GetTypeSwitchNullArm(methodSymbol, parameters, typeParameters), + GetTypeSwitchNullArm(methodSymbol, parameters), ctx.Compilation.ObjectType.WithNullableAnnotation(NullableAnnotation.NotAnnotated) ); } - if (methodSymbol.IsGenericMethod) - { - ctx.ReportDiagnostic(DiagnosticDescriptors.UnsupportedMappingMethodSignature, methodSymbol, methodSymbol.Name); - return null; - } - if (parameters.Target.HasValue) { return new UserDefinedExistingTargetMethodMapping( @@ -239,45 +233,6 @@ bool isExternal ); } - private static bool BuildGenericTypeParameters( - IMethodSymbol methodSymbol, - MappingMethodParameters parameters, - [NotNullWhen(true)] out GenericMappingTypeParameters? typeParameters - ) - { - if (!methodSymbol.IsGenericMethod) - { - typeParameters = null; - return false; - } - - var targetType = parameters.Target?.Type ?? methodSymbol.ReturnType; - var targetTypeParameter = methodSymbol.TypeParameters.FirstOrDefault(x => SymbolEqualityComparer.Default.Equals(x, targetType)); - var sourceTypeParameter = methodSymbol.TypeParameters.FirstOrDefault(x => - SymbolEqualityComparer.Default.Equals(x, parameters.Source.Type) - ); - - var expectedTypeParametersCount = 0; - if (targetTypeParameter != null) - { - expectedTypeParametersCount++; - } - - if (sourceTypeParameter != null && !SymbolEqualityComparer.Default.Equals(sourceTypeParameter, targetTypeParameter)) - { - expectedTypeParametersCount++; - } - - if (methodSymbol.TypeParameters.Length != expectedTypeParametersCount) - { - typeParameters = null; - return false; - } - - typeParameters = new GenericMappingTypeParameters(sourceTypeParameter, targetTypeParameter); - return true; - } - private static bool BuildRuntimeTargetTypeMappingParameters( SimpleMappingBuilderContext ctx, IMethodSymbol method, @@ -418,14 +373,24 @@ bool isExternal return refHandlerParameter; } - private static NullFallbackValue GetTypeSwitchNullArm( - IMethodSymbol method, - MappingMethodParameters parameters, - GenericMappingTypeParameters? typeParameters - ) + private static NullFallbackValue? GetTypeSwitchNullArm(IMethodSymbol method, MappingMethodParameters parameters) { - var targetCanBeNull = typeParameters?.TargetNullable ?? parameters.Target?.Type.IsNullable() ?? method.ReturnType.IsNullable(); - return targetCanBeNull ? NullFallbackValue.Default : NullFallbackValue.ThrowArgumentNullException; + // target is always the return type for runtime target mappings + Debug.Assert(parameters.Target == null); + var targetType = method.ReturnType; + var sourceType = parameters.Source.Type; + + // no polymorphism for extension methods... + // for type parameters: + // for the target type we assume a non-nullable by default + // for the source type we assume a nullable by default + var targetCanBeNull = targetType is ITypeParameterSymbol tpsTarget ? tpsTarget.IsNullable() ?? false : targetType.IsNullable(); + var sourceCanBeNull = sourceType is ITypeParameterSymbol tpsSource ? tpsSource.IsNullable() ?? true : sourceType.IsNullable(); + return !sourceCanBeNull + ? null + : targetCanBeNull + ? NullFallbackValue.Default + : NullFallbackValue.ThrowArgumentNullException; } private static UserMappingConfiguration GetUserMappingConfig( diff --git a/src/Riok.Mapperly/Descriptors/WellKnownTypes.cs b/src/Riok.Mapperly/Descriptors/WellKnownTypes.cs index 859efef9a6..bbb26ca5cd 100644 --- a/src/Riok.Mapperly/Descriptors/WellKnownTypes.cs +++ b/src/Riok.Mapperly/Descriptors/WellKnownTypes.cs @@ -15,6 +15,9 @@ public class WellKnownTypes(Compilation compilation) public ITypeSymbol GetArrayType(ITypeSymbol type) => compilation.CreateArrayTypeSymbol(type, elementNullableAnnotation: type.NullableAnnotation).NonNullable(); + public ITypeSymbol GetArrayType(ITypeSymbol elementType, int rank, NullableAnnotation elementNullableAnnotation) => + compilation.CreateArrayTypeSymbol(elementType, rank, elementNullableAnnotation); + public INamedTypeSymbol Get() => Get(typeof(T)); public INamedTypeSymbol Get(Type type) diff --git a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs index a790c1f908..79a2fbe3e6 100644 --- a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs +++ b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs @@ -667,6 +667,16 @@ public static class DiagnosticDescriptors true ); + public static readonly DiagnosticDescriptor RuntimeTargetTypeMappingNoContentMappings = + new( + "RMG069", + "Runtime target type or generic type mapping does not match any mappings", + "Runtime target type or generic type mapping does not match any mappings", + DiagnosticCategories.Mapper, + DiagnosticSeverity.Warning, + true + ); + private static string BuildHelpUri(string id) { #if ENV_NEXT diff --git a/src/Riok.Mapperly/Helpers/GenericTypeChecker.cs b/src/Riok.Mapperly/Helpers/GenericTypeChecker.cs new file mode 100644 index 0000000000..83b130c904 --- /dev/null +++ b/src/Riok.Mapperly/Helpers/GenericTypeChecker.cs @@ -0,0 +1,210 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Riok.Mapperly.Descriptors; + +namespace Riok.Mapperly.Helpers; + +/// +/// Type checker to check generic type parameters. +/// +public class GenericTypeChecker(SymbolAccessor accessor, WellKnownTypes types) +{ + /// + /// Tries to infer the actual types for the type parameters + /// and checks whether these types conform to the type parameter constraints. + /// + /// The type parameters. + /// The parameters and the argument for each parameter. + /// The result of the type checking. + public GenericTypeCheckerResult InferAndCheckTypes( + IEnumerable typeParameters, + params (ITypeSymbol Parameter, ITypeSymbol Argument)[] parameterArguments + ) + { + var state = new InferState(typeParameters); + return InferAndCheckTypes(state, parameterArguments); + } + + /// + /// Checks whether the given types can be assigned to the type parameters. + /// + /// Type parameters with a type for each parameter which should be assigned to the type parameter. + /// Whether the types can be bound to the type parameters. + public bool CheckTypes(params (ITypeParameterSymbol, ITypeSymbol)[] typeArguments) + { + var state = new InferState(typeArguments.Select(x => x.Item1)); + var parameterArguments = typeArguments.Select<(ITypeParameterSymbol, ITypeSymbol), (ITypeSymbol, ITypeSymbol)>(x => + (x.Item1, x.Item2) + ); + return InferAndCheckTypes(state, parameterArguments).Success; + } + + private GenericTypeCheckerResult InferAndCheckTypes( + InferState state, + IEnumerable<(ITypeSymbol Parameter, ITypeSymbol Argument)> parameterArguments + ) + { + var idx = 0; + foreach (var (param, arg) in parameterArguments) + { + var inferredParamType = InferAndCheckTypes(state, param, arg); + if (inferredParamType == null || !accessor.CanAssign(arg, inferredParamType)) + return GenericTypeCheckerResult.Failure(state.InferredTypes, idx, param, arg); + + idx++; + } + + return state.AllTypeParametersInferred + ? GenericTypeCheckerResult.Successful(state.InferredTypes) + : GenericTypeCheckerResult.Failure(state.InferredTypes); + } + + private ITypeSymbol? InferAndCheckTypes(InferState state, ITypeSymbol param, ITypeSymbol arg) + { + return param switch + { + ITypeParameterSymbol typeParam => InferAndCheckTypes(state, typeParam, arg), + IArrayTypeSymbol paramArray => InferAndCheckTypes(state, paramArray, arg), + INamedTypeSymbol { IsGenericType: true } paramNamedType => InferAndCheckTypes(state, paramNamedType, arg), + _ => param, + }; + } + + private ITypeSymbol? InferAndCheckTypes(InferState state, ITypeParameterSymbol typeParameter, ITypeSymbol arg) + { + if (state.IsTypeParameterInferred(typeParameter, out var boundType)) + { + if (!SymbolEqualityComparer.Default.Equals(boundType, arg)) + return null; + + if (arg.NullableAnnotation == boundType.NullableAnnotation) + return boundType; + + if (typeParameter.IsNullable() != false || !arg.IsNullable()) + return arg; + + return null; + } + + state.SetInferredType(typeParameter, arg); + if (typeParameter.HasConstructorConstraint && !accessor.HasDirectlyAccessibleParameterlessConstructor(arg)) + return null; + + if (typeParameter.IsNullable() == false && arg.NullableAnnotation == NullableAnnotation.Annotated) + return null; + + if (typeParameter.HasValueTypeConstraint && !arg.IsValueType) + return null; + + if (typeParameter.HasReferenceTypeConstraint && !arg.IsReferenceType) + return null; + + foreach (var constraintType in typeParameter.ConstraintTypes) + { + var inferredType = InferAndCheckTypes(state, constraintType, arg); + if (inferredType == null) + return null; + + if (!accessor.CanAssign(arg, inferredType)) + return null; + } + + return arg; + } + + private ITypeSymbol? InferAndCheckTypes(InferState state, INamedTypeSymbol param, ITypeSymbol arg) + { + if (!arg.ExtendsOrImplementsGeneric(param.OriginalDefinition, out var genericTypedArgument)) + return null; + + var inferredParamTypeArgs = new ITypeSymbol[param.TypeArguments.Length]; + var inferredParamsHasChanges = false; + for (var i = 0; i < param.TypeArguments.Length; i++) + { + var paramTypeArg = param.TypeArguments[i]; + var argTypeArg = genericTypedArgument.TypeArguments[i]; + var inferredParamTypeArg = InferAndCheckTypes(state, paramTypeArg, argTypeArg); + if (inferredParamTypeArg == null) + return null; + + inferredParamTypeArgs[i] = inferredParamTypeArg; + inferredParamsHasChanges |= !ReferenceEquals(inferredParamTypeArg, paramTypeArg); + } + + return inferredParamsHasChanges ? param.OriginalDefinition.Construct(inferredParamTypeArgs) : param; + } + + private ITypeSymbol? InferAndCheckTypes(InferState state, IArrayTypeSymbol param, ITypeSymbol arg) + { + if (arg is not IArrayTypeSymbol argArray) + return null; + + var elementType = InferAndCheckTypes( + state, + param.ElementType.WithNullableAnnotation(param.ElementNullableAnnotation), + argArray.ElementType.WithNullableAnnotation(argArray.ElementNullableAnnotation) + ); + if (elementType == null) + return null; + + if (ReferenceEquals(elementType, param.ElementType)) + return param; + + return types.GetArrayType(elementType, argArray.Rank, argArray.ElementNullableAnnotation); + } + + private readonly struct InferState + { + private readonly HashSet _unboundTypeParameters; + private readonly Dictionary _inferredTypes; + + public bool AllTypeParametersInferred => _unboundTypeParameters.Count == 0; + + public IReadOnlyDictionary InferredTypes => _inferredTypes; + + internal InferState(IEnumerable typeParameters) + { + _unboundTypeParameters = new(typeParameters, SymbolEqualityComparer.Default); + _inferredTypes = new(SymbolEqualityComparer.Default); + } + + internal bool IsTypeParameterInferred(ITypeParameterSymbol typeParameter, [NotNullWhen(true)] out ITypeSymbol? o) => + _inferredTypes.TryGetValue(typeParameter, out o); + + internal void SetInferredType(ITypeParameterSymbol typeParameter, ITypeSymbol argumentType) + { + _unboundTypeParameters.Remove(typeParameter); + _inferredTypes.Add(typeParameter, argumentType); + } + } + + /// + /// The result of a type check. + /// + /// Whether the type check succeeded. + /// The inferred type for each type parameter. + /// The index on which the type check/inferrence failed (-1 if is true) + /// The parameter on which the type check/inferrence failed (null if is true) + /// The argument on which the type check/inferrence failed (null if is true) + public readonly record struct GenericTypeCheckerResult( + bool Success, + IReadOnlyDictionary InferredTypes, + int FailedIndex = -1, + ITypeSymbol? FailedParameter = null, + ITypeSymbol? FailedArgument = null + ) + { + internal static GenericTypeCheckerResult Successful(IReadOnlyDictionary inferredTypes) => + new(true, inferredTypes); + + internal static GenericTypeCheckerResult Failure( + IReadOnlyDictionary inferredTypes, + int failedIndex = -1, + ITypeSymbol? failedParameter = null, + ITypeSymbol? failedArgument = null + ) + { + return new(false, inferredTypes, failedIndex, failedParameter, failedArgument); + } + } +} diff --git a/src/Riok.Mapperly/Helpers/NullableSymbolExtensions.cs b/src/Riok.Mapperly/Helpers/NullableSymbolExtensions.cs index b991798b1e..ad1111ce05 100644 --- a/src/Riok.Mapperly/Helpers/NullableSymbolExtensions.cs +++ b/src/Riok.Mapperly/Helpers/NullableSymbolExtensions.cs @@ -63,19 +63,37 @@ internal static ITypeSymbol NonNullable(this ITypeSymbol symbol) /// Whether or not the is nullable. /// /// The type parameter. - /// A boolean indicating whether null can be used to satisfy the type parameter constraints. - internal static bool IsNullable(this ITypeParameterSymbol typeParameter) + /// A boolean indicating whether null can be used to satisfy the type parameter constraints. Null means unspecified. + internal static bool? IsNullable(this ITypeParameterSymbol typeParameter) { - if (typeParameter.NullableAnnotation == NullableAnnotation.Annotated) + if (typeParameter.NullableAnnotation != NullableAnnotation.NotAnnotated) return true; if (typeParameter.HasNotNullConstraint || typeParameter.HasValueTypeConstraint || typeParameter.HasUnmanagedTypeConstraint) return false; - if (typeParameter.ConstraintTypes.Length > 0 && typeParameter.ConstraintTypes.All(t => t.NullableAnnotation.IsNullable())) - return true; + bool? fallback = null; + + if (typeParameter.HasReferenceTypeConstraint) + { + if (!typeParameter.ReferenceTypeConstraintNullableAnnotation.IsNullable()) + return false; + + fallback = true; + } + + if (typeParameter.ConstraintTypes.Length > 0) + { + foreach (var constraint in typeParameter.ConstraintTypes) + { + if (!constraint.IsNullable()) + return false; + + fallback = true; + } + } - return typeParameter.HasReferenceTypeConstraint && typeParameter.ReferenceTypeConstraintNullableAnnotation.IsNullable(); + return fallback; } internal static bool IsNullableUpgraded(this ITypeSymbol symbol) diff --git a/src/Riok.Mapperly/Helpers/SymbolExtensions.cs b/src/Riok.Mapperly/Helpers/SymbolExtensions.cs index 4518b791b7..ce5c2ada33 100644 --- a/src/Riok.Mapperly/Helpers/SymbolExtensions.cs +++ b/src/Riok.Mapperly/Helpers/SymbolExtensions.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis; @@ -82,12 +83,53 @@ internal static bool Implements(this ITypeSymbol t, INamedTypeSymbol interfaceSy || t.AllInterfaces.Any(x => SymbolEqualityComparer.Default.Equals(x, interfaceSymbol)); } + internal static bool ExtendsOrImplementsGeneric( + this ITypeSymbol t, + INamedTypeSymbol genericSymbol, + [NotNullWhen(true)] out INamedTypeSymbol? typedGenericSymbol + ) + { + return genericSymbol.TypeKind == TypeKind.Interface + ? t.ImplementsGeneric(genericSymbol, out typedGenericSymbol) + : t.ExtendsGeneric(genericSymbol, out typedGenericSymbol); + } + + internal static bool ExtendsGeneric( + this ITypeSymbol t, + INamedTypeSymbol genericSymbol, + [NotNullWhen(true)] out INamedTypeSymbol? typedGenericSymbol + ) + { + Debug.Assert(genericSymbol.IsGenericType); + + if (SymbolEqualityComparer.Default.Equals(t.OriginalDefinition, genericSymbol)) + { + typedGenericSymbol = (INamedTypeSymbol)t; + return true; + } + + for (var baseType = t.BaseType; baseType != null; baseType = baseType.BaseType) + { + if (!SymbolEqualityComparer.Default.Equals(baseType.OriginalDefinition, genericSymbol)) + continue; + + typedGenericSymbol = baseType; + return true; + } + + typedGenericSymbol = null; + return false; + } + internal static bool ImplementsGeneric( this ITypeSymbol t, INamedTypeSymbol genericInterfaceSymbol, [NotNullWhen(true)] out INamedTypeSymbol? typedInterface ) { + Debug.Assert(genericInterfaceSymbol.IsGenericType); + Debug.Assert(genericInterfaceSymbol.TypeKind == TypeKind.Interface); + if (SymbolEqualityComparer.Default.Equals(t.OriginalDefinition, genericInterfaceSymbol)) { typedInterface = (INamedTypeSymbol)t; @@ -115,6 +157,9 @@ internal static bool Implements(this ITypeSymbol t, INamedTypeSymbol interfaceSy out bool isExplicit ) { + Debug.Assert(genericInterfaceSymbol.IsGenericType); + Debug.Assert(genericInterfaceSymbol.TypeKind == TypeKind.Interface); + if (SymbolEqualityComparer.Default.Equals(t.OriginalDefinition, genericInterfaceSymbol)) { typedInterface = (INamedTypeSymbol)t; diff --git a/src/Riok.Mapperly/Helpers/SymbolTypeEqualityComparer.cs b/src/Riok.Mapperly/Helpers/SymbolTypeEqualityComparer.cs index 94a511a9c9..5f7fe5972f 100644 --- a/src/Riok.Mapperly/Helpers/SymbolTypeEqualityComparer.cs +++ b/src/Riok.Mapperly/Helpers/SymbolTypeEqualityComparer.cs @@ -4,6 +4,7 @@ namespace Riok.Mapperly.Helpers; internal static class SymbolTypeEqualityComparer { + public static readonly IEqualityComparer TypeParameterDefault = SymbolEqualityComparer.Default; public static readonly IEqualityComparer FieldDefault = SymbolEqualityComparer.Default; public static readonly IEqualityComparer MethodDefault = SymbolEqualityComparer.Default; } diff --git a/src/Riok.Mapperly/Symbols/GenericMappingTypeParameters.cs b/src/Riok.Mapperly/Symbols/GenericMappingTypeParameters.cs deleted file mode 100644 index 74b97a533f..0000000000 --- a/src/Riok.Mapperly/Symbols/GenericMappingTypeParameters.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Microsoft.CodeAnalysis; -using Riok.Mapperly.Descriptors; -using Riok.Mapperly.Helpers; - -namespace Riok.Mapperly.Symbols; - -/// -/// Method -/// -public readonly struct GenericMappingTypeParameters(ITypeParameterSymbol? sourceType, ITypeParameterSymbol? targetType) -{ - public ITypeParameterSymbol? SourceType { get; } = sourceType; - - public bool? SourceNullable { get; } = sourceType?.IsNullable(); - - public ITypeParameterSymbol? TargetType { get; } = targetType; - - public bool? TargetNullable { get; } = targetType?.IsNullable(); - - public bool DoesTypesSatisfyTypeParameterConstraints(SymbolAccessor symbolAccessor, ITypeSymbol sourceType, ITypeSymbol targetType) - { - return (SourceType == null || symbolAccessor.DoesTypeSatisfyTypeParameterConstraints(SourceType, sourceType)) - && (TargetType == null || symbolAccessor.DoesTypeSatisfyTypeParameterConstraints(TargetType, targetType)); - } -} diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource.verified.cs index e6597cc9c1..9317534dde 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource.verified.cs @@ -523,7 +523,6 @@ public static partial class StaticTestMapper global::Riok.Mapperly.IntegrationTests.Dto.TestObjectDto x when targetType.IsAssignableFrom(typeof(global::Riok.Mapperly.IntegrationTests.Models.TestObject)) => MapFromDto(x), global::System.Collections.Generic.IEnumerable x when targetType.IsAssignableFrom(typeof(global::System.Collections.Generic.IEnumerable)) => MapAllDtos(x), object x when targetType.IsAssignableFrom(typeof(object)) => DerivedTypes(x), - null => throw new System.ArgumentNullException(nameof(source)), _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {targetType} as there is no known type mapping", nameof(source)), }; } diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs index 5655aca0fc..8a79be878e 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs @@ -532,7 +532,6 @@ public static partial class StaticTestMapper global::Riok.Mapperly.IntegrationTests.Dto.TestObjectDto x when targetType.IsAssignableFrom(typeof(global::Riok.Mapperly.IntegrationTests.Models.TestObject)) => MapFromDto(x), global::System.Collections.Generic.IEnumerable x when targetType.IsAssignableFrom(typeof(global::System.Collections.Generic.IEnumerable)) => MapAllDtos(x), object x when targetType.IsAssignableFrom(typeof(object)) => DerivedTypes(x), - null => throw new System.ArgumentNullException(nameof(source)), _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {targetType} as there is no known type mapping", nameof(source)), }; } diff --git a/test/Riok.Mapperly.Tests/Helpers/GenericTypeCheckerTest.cs b/test/Riok.Mapperly.Tests/Helpers/GenericTypeCheckerTest.cs new file mode 100644 index 0000000000..6f77063c6e --- /dev/null +++ b/test/Riok.Mapperly.Tests/Helpers/GenericTypeCheckerTest.cs @@ -0,0 +1,327 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Descriptors; +using Riok.Mapperly.Helpers; +using Riok.Mapperly.Symbols; + +namespace Riok.Mapperly.Tests.Helpers; + +public class GenericTypeCheckerTest +{ + [Fact] + public void DirectTypeParameters() + { + var result = InferAndCheckTypes( + "B Test(A source)", + """ + class Source; + class Target; + """ + ); + AssertSuccessResult(result, ("A", "Source"), ("B", "Target")); + } + + [Fact] + public void NestedInterfaceTypeParameters() + { + var result = InferAndCheckTypes( + "IEnumerable Test(IEnumerable source)", + """ + class Source : IEnumerable; + class Target : IEnumerable; + """ + ); + AssertSuccessResult(result, ("A", "Int32"), ("B", "String")); + } + + [Fact] + public void AdditionalNonInferrableTypeParameter() + { + var result = InferAndCheckTypes( + "B Test(A source)", + """ + class Source; + class Target; + """ + ); + result.Success.Should().BeFalse(); + result.FailedArgument.Should().BeNull(); + } + + [Fact] + public void BindDifferentTypesToTheSameTypeParameter() + { + var result = InferAndCheckTypes( + "void Test(A source, B target, B target2)", + """ + class Source; + class Target; + class Target2; + """, + buildParameterAndArguments: (m, c) => + + [ + (m.Parameters[0].Type, c.GetTypeByMetadataName("Source")!.WithNullableAnnotation(NullableAnnotation.NotAnnotated)), + (m.Parameters[1].Type, c.GetTypeByMetadataName("Target")!.WithNullableAnnotation(NullableAnnotation.NotAnnotated)), + (m.Parameters[2].Type, c.GetTypeByMetadataName("Target2")!.WithNullableAnnotation(NullableAnnotation.NotAnnotated)), + ] + ); + result.Success.Should().BeFalse(); + result.FailedIndex.Should().Be(2); + result.FailedArgument.Should().NotBeNull(); + result.FailedArgument!.Name.Should().Be("Target2"); + result.FailedParameter.Should().NotBeNull(); + result.FailedParameter!.Name.Should().Be("B"); + } + + [Fact] + public void NestedBaseTypeParameters() + { + var result = InferAndCheckTypes( + "BaseClass Test(A source)", + """ + class Source; + class Target : BaseClass; + class BaseClass; + """ + ); + AssertSuccessResult(result, ("A", "Source")); + } + + [Fact] + public void TwoParameters() + { + var result = InferAndCheckTypes( + "void Test(A source, B target)", + """ + class Source; + class Target; + """ + ); + AssertSuccessResult(result, ("A", "Source"), ("B", "Target")); + } + + [Fact] + public void ArrayParameter() + { + var result = InferAndCheckTypes( + "void Test(A[] sources, B[] targets)", + """ + class Source; + class Target; + """, + (s, c) => + c.CreateArrayTypeSymbol(s, 1, NullableAnnotation.NotAnnotated).WithNullableAnnotation(NullableAnnotation.NotAnnotated), + (s, c) => c.CreateArrayTypeSymbol(s, 1, NullableAnnotation.NotAnnotated).WithNullableAnnotation(NullableAnnotation.NotAnnotated) + ); + AssertSuccessResult(result, ("A", "Source"), ("B", "Target")); + } + + [Fact] + public void ArrayElementTypeNullMismatch() + { + var result = InferAndCheckTypes( + "void Test(A[] sources, B[] targets) where A : notnull", + """ + class Source; + class Target; + """, + (s, c) => c.CreateArrayTypeSymbol(s, 1, NullableAnnotation.Annotated).WithNullableAnnotation(NullableAnnotation.NotAnnotated), + (s, c) => c.CreateArrayTypeSymbol(s, 1, NullableAnnotation.NotAnnotated).WithNullableAnnotation(NullableAnnotation.NotAnnotated) + ); + result.Success.Should().BeFalse(); + result.FailedIndex.Should().Be(0); + } + + [Fact] + public void NonArrayAtArrayParameter() + { + var result = InferAndCheckTypes( + "IEnumerable Test(A[] source)", + """ + class Source : IEnumerable; + class Target : IEnumerable; + """ + ); + AssertFailureResult(result, "Source"); + } + + [Fact] + public void GenericParameter() + { + var result = InferAndCheckTypes( + "void Test(IEnumerable sources, IReadOnlyCollection targets)", + """ + class Source; + class Target; + """, + (s, c) => + c.CreateArrayTypeSymbol(s, 1, NullableAnnotation.NotAnnotated).WithNullableAnnotation(NullableAnnotation.NotAnnotated), + (s, c) => c.CreateArrayTypeSymbol(s, 1, NullableAnnotation.NotAnnotated).WithNullableAnnotation(NullableAnnotation.NotAnnotated) + ); + AssertSuccessResult(result, ("A", "Source"), ("B", "Target")); + } + + [Theory] + [InlineData(false, "new()", "class Source { private Source() {} }")] + [InlineData(true, "new()", "class Source;")] + [InlineData(false, "struct", "class Source;")] + [InlineData(true, "struct", "struct Source;")] + [InlineData(false, "class", "struct Source;")] + [InlineData(true, "class", "class Source;")] + [InlineData(false, "class", "class Source;", NullableAnnotation.Annotated)] + [InlineData(true, "class?", "class Source;", NullableAnnotation.Annotated)] + [InlineData(true, "class?", "class Source;", NullableAnnotation.None)] + [InlineData(true, "class?", "class Source;")] + [InlineData(true, "notnull", "class Source;")] + [InlineData(true, "notnull", "struct Source;")] + [InlineData(false, "notnull", "class Source;", NullableAnnotation.Annotated)] + [InlineData(true, "notnull", "class Source;", NullableAnnotation.None)] + [InlineData(true, "BaseClass", "class Source : BaseClass; class BaseClass;")] + [InlineData(true, "BaseClass", "class Source : BaseClass; class BaseClass;", NullableAnnotation.None, NullableContextOptions.Disable)] + [InlineData(false, "BaseClass", "class Source; class BaseClass;")] + [InlineData(true, "BaseClass?", "class Source : BaseClass; class BaseClass;")] + [InlineData(true, "BaseClass?", "class Source : BaseClass; class BaseClass;", NullableAnnotation.Annotated)] + [InlineData(true, "BaseClass?", "class Source : BaseClass; class BaseClass;", NullableAnnotation.None)] + [InlineData(true, "IBase", "class Source : IBase; interface IBase;")] + [InlineData(true, "IBase", "class Source : IBase; interface IBase;", NullableAnnotation.None, NullableContextOptions.Disable)] + [InlineData(false, "IBase", "class Source; interface IBase;")] + [InlineData(true, "IBase?", "class Source : IBase; interface IBase;")] + [InlineData(true, "IBase?", "class Source : IBase; interface IBase;", NullableAnnotation.Annotated)] + [InlineData(true, "IBase?", "class Source : IBase; interface IBase;", NullableAnnotation.None)] + [InlineData(true, "IDtoProvider", "class Source : IDtoProvider; interface IDtoProvider;")] + [InlineData(true, "IDtoProvider?", "class Source : IDtoProvider; interface IDtoProvider;")] + [InlineData(true, "IDtoProvider?", "class Source : IDtoProvider; interface IDtoProvider;", NullableAnnotation.Annotated)] + [InlineData(true, "IDtoProvider", "class Source : IDtoProvider; interface IDtoProvider;", NullableAnnotation.None)] + [InlineData(true, "IDtoProvider?", "class Source : IDtoProvider; interface IDtoProvider;", NullableAnnotation.None)] + [InlineData( + true, + "IDtoProvider", + "class Source : IDtoProvider; interface IDtoProvider;", + NullableAnnotation.None, + NullableContextOptions.Disable + )] + [InlineData(false, "IDtoProvider", "class Source : IDtoProvider; interface IDtoProvider;")] + [InlineData(true, "IDtoProvider", "class Source : IDtoProvider; interface IDtoProvider;")] + [InlineData(true, "DtoProvider", "class Source : DtoProvider; class DtoProvider;")] + [InlineData( + true, + "DtoProvider", + "class Source : DtoProvider; class DtoProvider;", + NullableAnnotation.None, + NullableContextOptions.Disable + )] + [InlineData(false, "DtoProvider", "class Source : DtoProvider; class DtoProvider;")] + [InlineData(true, "DtoProvider", "class Source : DtoProvider; class DtoProvider;")] + [InlineData(true, "DtoProvider", "class Source : DtoProvider; class DtoProvider;", NullableAnnotation.None)] + public void TypeConstraints( + bool valid, + [StringSyntax(StringSyntax.CSharp)] string typeConstraints, + [StringSyntax(StringSyntax.CSharp)] string types, + NullableAnnotation sourceNullableAnnotation = NullableAnnotation.NotAnnotated, + NullableContextOptions nullableContextOptions = NullableContextOptions.Enable + ) + { + var result = InferAndCheckTypes( + $"Target Test(T source) where T : {typeConstraints}", + $$""" + {{types}} + class Target; + """, + (x, _) => x.WithNullableAnnotation(sourceNullableAnnotation), + nullableOptions: nullableContextOptions + ); + + if (valid) + { + AssertSuccessResult(result, ("T", "Source")); + } + else + { + AssertFailureResult(result, "Source"); + } + } + + private static GenericTypeChecker.GenericTypeCheckerResult InferAndCheckTypes( + [StringSyntax(StringSyntax.CSharp)] string methodSignature, + [StringSyntax(StringSyntax.CSharp)] string types, + Func? sourceTypeModifier = null, + Func? targetTypeModifier = null, + Func? buildParameterAndArguments = null, + NullableContextOptions nullableOptions = NullableContextOptions.Enable + ) + { + sourceTypeModifier ??= (x, _) => x.WithNullableAnnotation(NullableAnnotation.NotAnnotated); + targetTypeModifier ??= (x, _) => x.WithNullableAnnotation(NullableAnnotation.NotAnnotated); + + var source = TestSourceBuilder.CSharp( + $$""" + using System; + using System.Linq; + using System.Collections.Generic; + + public class Mapper + { + {{methodSignature}} + => throw new Exception(); + } + + {{types}} + """ + ); + var compilation = TestHelper.BuildCompilation(source, TestHelperOptions.Default with { NullableOption = nullableOptions, }); + var nodes = compilation.SyntaxTrees.Single().GetRoot().DescendantNodes(); + var classNode = nodes.OfType().Single(x => x.Identifier.Text == "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 symbolAccessor = new SymbolAccessor(compilationContext, mapperSymbol); + var typeChecker = new GenericTypeChecker(symbolAccessor, compilationContext.Types); + + var methodSymbol = model.GetDeclaredSymbol(methodNode) ?? throw new NullReferenceException(); + var parametersAndArguments = buildParameterAndArguments?.Invoke(methodSymbol, compilation); + if (parametersAndArguments == null) + { + var sourceSymbol = sourceTypeModifier( + compilation.GetTypeByMetadataName("Source") ?? throw new NullReferenceException(), + compilation + ); + var targetSymbol = targetTypeModifier( + compilation.GetTypeByMetadataName("Target") ?? throw new NullReferenceException(), + compilation + ); + + var targetMethodTypeSymbol = methodSymbol.ReturnsVoid ? methodSymbol.Parameters[1].Type : methodSymbol.ReturnType; + parametersAndArguments = [(methodSymbol.Parameters[0].Type, sourceSymbol), (targetMethodTypeSymbol, targetSymbol),]; + } + + return typeChecker.InferAndCheckTypes(methodSymbol.TypeParameters, parametersAndArguments); + } + + private static void AssertFailureResult(GenericTypeChecker.GenericTypeCheckerResult result, string failedArgumentName) + { + result.Success.Should().BeFalse(); + result.FailedArgument.Should().NotBeNull(); + result.FailedArgument!.Name.Should().Be(failedArgumentName); + } + + private static void AssertSuccessResult( + GenericTypeChecker.GenericTypeCheckerResult result, + params (string TypeParameterName, string TypeName)[] inferredTypeNames + ) + { + result.Success.Should().BeTrue(); + result.FailedArgument.Should().BeNull(); + + var inferredTypeNamesDict = inferredTypeNames.ToDictionary(x => x.TypeParameterName, x => x.TypeName); + + foreach (var (typeParameter, inferredType) in result.InferredTypes) + { + inferredTypeNamesDict.Remove(typeParameter.Name, out var typeName).Should().BeTrue(); + inferredType.Name.Should().Be(typeName); + } + } +} diff --git a/test/Riok.Mapperly.Tests/Mapping/GenericDerivedTypeTest.cs b/test/Riok.Mapperly.Tests/Mapping/GenericDerivedTypeTest.cs index 37c2c6b2b1..d344a3f350 100644 --- a/test/Riok.Mapperly.Tests/Mapping/GenericDerivedTypeTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/GenericDerivedTypeTest.cs @@ -60,7 +60,6 @@ public void GenericWithDerivedTypesOnSameMethod() { global::A x when typeof(TTarget).IsAssignableFrom(typeof(global::B)) => (TTarget)(object)MapToB(x), global::C x when typeof(TTarget).IsAssignableFrom(typeof(global::D)) => (TTarget)(object)MapToD(x), - null => throw new System.ArgumentNullException(nameof(source)), _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(TTarget)} as there is no known type mapping", nameof(source)), }; """ @@ -95,7 +94,6 @@ public void GenericWithDerivedTypesInvalidConstraint() return source switch { global::A x when typeof(TTarget).IsAssignableFrom(typeof(global::B)) => (TTarget)(object)MapToB(x), - null => throw new System.ArgumentNullException(nameof(source)), _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(TTarget)} as there is no known type mapping", nameof(source)), }; """ diff --git a/test/Riok.Mapperly.Tests/Mapping/GenericTest.cs b/test/Riok.Mapperly.Tests/Mapping/GenericTest.cs index ea4691fe4f..85a41e1a3c 100644 --- a/test/Riok.Mapperly.Tests/Mapping/GenericTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/GenericTest.cs @@ -20,6 +20,80 @@ public Task WithGenericSourceAndTarget() return TestHelper.VerifyGenerator(source); } + [Fact] + public Task WithGenericSourceAndTargetInNullableDisabledContext() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + private partial TTarget Map(TSource source); + + private partial B MapToB(A source); + private partial D MapToD(C source); + """, + "record struct A(string Value);", + "record struct B(string Value);", + "record C(string Value1);", + "record D(string Value1);" + ); + return TestHelper.VerifyGenerator(source, TestHelperOptions.DisabledNullable); + } + + [Fact] + public Task WithNestedGenericSourceAndTarget() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + private partial IEnumerable Map(IEnumerable source); + + private partial IEnumerable MapToB(IEnumerable source); + private partial List MapToD(IReadOnlyCollection source); + """, + "record struct A(string Value);", + "record struct B(string Value);", + "record C(string Value1);", + "record D(string Value1);" + ); + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task WithQueryableGenericSourceAndTarget() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + private partial IQueryable Map(IQueryable source); + + private partial IQueryable MapToB(IQueryable source); + private partial IQueryable MapToD(IQueryable source); + """, + "record A(string Value);", + "record B(string Value);", + "record C(string Value1);", + "record D(string Value1);" + ); + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task WithTypeConstrainedQueryableGenericSourceAndTarget() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + private partial TTarget Map(TSource source) + where TSource : IQueryable + where TTarget : IQueryable; + + private partial IQueryable MapToB(IQueryable source); + private partial IQueryable MapToD(IQueryable source); + """, + "record A(string Value);", + "record B(string Value);", + "record C(string Value1);", + "record D(string Value1);" + ); + return TestHelper.VerifyGenerator(source); + } + [Fact] public void WithGenericSource() { @@ -108,7 +182,6 @@ public void WithGenericSourceTypeNotNullConstraint() { global::A x => MapToB(x), global::C x => MapToD(x), - null => throw new System.ArgumentNullException(nameof(source)), _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(object)} as there is no known type mapping", nameof(source)), }; """ @@ -139,7 +212,6 @@ public void WithGenericSourceTypeValueTypeConstraint() return source switch { global::A x => MapToB(x), - null => throw new System.ArgumentNullException(nameof(source)), _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(object)} as there is no known type mapping", nameof(source)), }; """ @@ -170,7 +242,6 @@ public void WithGenericSourceTypeUnmanagedConstraint() return source switch { global::A x => MapToB(x), - null => throw new System.ArgumentNullException(nameof(source)), _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(object)} as there is no known type mapping", nameof(source)), }; """ @@ -201,7 +272,6 @@ public void WithGenericSourceTypeReferenceTypeConstraint() return source switch { global::C x => MapToD(x), - null => throw new System.ArgumentNullException(nameof(source)), _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(object)} as there is no known type mapping", nameof(source)), }; """ @@ -361,7 +431,6 @@ public void WithGenericTarget() { global::A x when typeof(TTarget).IsAssignableFrom(typeof(global::B)) => (TTarget)(object)MapToB(x), global::C x when typeof(TTarget).IsAssignableFrom(typeof(global::D)) => (TTarget)(object)MapToD(x), - null => throw new System.ArgumentNullException(nameof(source)), _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(TTarget)} as there is no known type mapping", nameof(source)), }; """ @@ -392,7 +461,6 @@ public void WithGenericTargetTypeConstraints() return source switch { global::C x when typeof(TTarget).IsAssignableFrom(typeof(global::D)) => (TTarget)(object)MapToD(x), - null => throw new System.ArgumentNullException(nameof(source)), _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(TTarget)} as there is no known type mapping", nameof(source)), }; """ @@ -427,7 +495,6 @@ public void WithGenericTargetSpecificSource() { global::A x when typeof(TTarget).IsAssignableFrom(typeof(global::C)) => (TTarget)(object)MapToC(x), global::B x when typeof(TTarget).IsAssignableFrom(typeof(global::D)) => (TTarget)(object)MapToD(x), - null => throw new System.ArgumentNullException(nameof(source)), _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(TTarget)} as there is no known type mapping", nameof(source)), }; """ @@ -459,7 +526,6 @@ public void WithGenericSourceAndTargetTypeConstraints() return source switch { global::C x when typeof(TTarget).IsAssignableFrom(typeof(global::D)) => (TTarget)(object)MapToD(x), - null => throw new System.ArgumentNullException(nameof(source)), _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(TTarget)} as there is no known type mapping", nameof(source)), }; """ @@ -536,4 +602,22 @@ public Task WithGenericSourceAndTargetAndEnabledReferenceHandlingAndParameter() ); return TestHelper.VerifyGenerator(source); } + + [Fact] + public Task WithGenericSourceAndTargetAndUnboundGenericShouldDiagnostic() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + private partial TTarget Map(TSource source); + + private partial B MapToB(A source); + private partial D MapToD(C source); + """, + "record struct A(string Value);", + "record struct B(string Value);", + "record C(string Value1);", + "record D(string Value1);" + ); + return TestHelper.VerifyGenerator(source); + } } diff --git a/test/Riok.Mapperly.Tests/Mapping/ObjectFactoryTest.cs b/test/Riok.Mapperly.Tests/Mapping/ObjectFactoryTest.cs index f9d4029934..effc47c768 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ObjectFactoryTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ObjectFactoryTest.cs @@ -8,7 +8,10 @@ public class ObjectFactoryTest public void ShouldUseSimpleObjectFactory() { var source = TestSourceBuilder.MapperWithBodyAndTypes( - "[ObjectFactory] B CreateB() => new B();" + "partial B Map(A a);", + """ + [ObjectFactory] B CreateB() => new B(); + partial B Map(A a); + """, "class A { public string StringValue { get; set; } }", "class B { public string StringValue { get; set; } }" ); @@ -82,7 +85,10 @@ public void ShouldUseSimpleObjectFactoryWithSource() public void ShouldUseGenericTargetObjectFactory() { var source = TestSourceBuilder.MapperWithBodyAndTypes( - "[ObjectFactory] T Create() where T : new() => new T();" + "partial B Map(A a);", + """ + [ObjectFactory] T Create() where T : new() => new T(); + partial B Map(A a); + """, "class A { public string StringValue { get; set; } }", "class B { public string StringValue { get; set; } }" ); @@ -103,7 +109,10 @@ public void ShouldUseGenericTargetObjectFactory() public void ShouldUseGenericTargetObjectFactoryWithSource() { var source = TestSourceBuilder.MapperWithBodyAndTypes( - "[ObjectFactory] T Create(A source) where T : new() => new T();" + "partial B Map(A a);", + """ + [ObjectFactory] T Create(A source) where T : new() => new T(); + partial B Map(A a); + """, "class A { public string StringValue { get; set; } }", "class B { public string StringValue { get; set; } }" ); @@ -124,7 +133,10 @@ public void ShouldUseGenericTargetObjectFactoryWithSource() public void ShouldUseGenericSourceTargetObjectFactoryTargetFirst() { var source = TestSourceBuilder.MapperWithBodyAndTypes( - "[ObjectFactory] T Create(S source) where T : new() => new T();" + "partial B Map(A a);", + """ + [ObjectFactory] T Create(S source) where T : new() => new T(); + partial B Map(A a); + """, "class A { public string StringValue { get; set; } }", "class B { public string StringValue { get; set; } }" ); @@ -145,7 +157,10 @@ public void ShouldUseGenericSourceTargetObjectFactoryTargetFirst() public void ShouldUseGenericSourceTargetObjectFactorySourceFirst() { var source = TestSourceBuilder.MapperWithBodyAndTypes( - "[ObjectFactory] T Create(S source) where T : new() => new T();" + "partial B Map(A a);", + """ + [ObjectFactory] T Create(S source) where T : new() => new T(); + partial B Map(A a); + """, "class A { public string StringValue { get; set; } }", "class B { public string StringValue { get; set; } }" ); @@ -166,7 +181,10 @@ public void ShouldUseGenericSourceTargetObjectFactorySourceFirst() public void ShouldUseGenericSourceObjectFactory() { var source = TestSourceBuilder.MapperWithBodyAndTypes( - "[ObjectFactory] B Create(S source) => new B();" + "partial B Map(A a);", + """ + [ObjectFactory] B Create(S source) => new B(); + partial B Map(A a); + """, "class A { public string StringValue { get; set; } }", "class B { public string StringValue { get; set; } }" ); @@ -211,10 +229,13 @@ public void ShouldUseFirstMatchingObjectFactory() public void ShouldUseGenericObjectFactoryWithTypeConstraint() { var source = TestSourceBuilder.MapperWithBodyAndTypes( - "[ObjectFactory] private T CreateA() where T : new(), A => new T();" - + "[ObjectFactory] private T CreateStruct() where T : new(), struct => new T();" - + "[ObjectFactory] private T CreateB() where T : new(), B => new T();" - + "partial B Map(A a);", + """ + [ObjectFactory] private T CreateA() where T : new(), A => new T(); + [ObjectFactory] private T CreateStruct() where T : new(), struct => new T(); + [ObjectFactory] private T CreateB() where T : new(), B => new T(); + + partial B Map(A a); + """, "class A { public string StringValue { get; set; } }", "class B { public string StringValue { get; set; } }" ); @@ -235,7 +256,10 @@ public void ShouldUseGenericObjectFactoryWithTypeConstraint() public void ShouldUseSimpleObjectFactoryIfTypeIsNullable() { var source = TestSourceBuilder.MapperWithBodyAndTypes( - "[ObjectFactory] B CreateB() => new B();" + "partial B Map(A a);", + """ + [ObjectFactory] B CreateB() => new B(); + partial B Map(A a); + """, "class A { public string StringValue { get; set; } }", "#nullable disable\nclass B { public string StringValue { get; set; } }\n#nullable restore" ); @@ -256,7 +280,10 @@ public void ShouldUseSimpleObjectFactoryIfTypeIsNullable() public void ShouldUseSimpleObjectFactoryAndCreateObjectIfNullIsReturned() { var source = TestSourceBuilder.MapperWithBodyAndTypes( - "[ObjectFactory] B? CreateB() => null;" + "partial B Map(A a);", + """ + [ObjectFactory] B? CreateB() => null; + partial B Map(A a); + """, "class A { public string StringValue { get; set; } }", "class B { public string StringValue { get; set; } }" ); @@ -277,7 +304,10 @@ public void ShouldUseSimpleObjectFactoryAndCreateObjectIfNullIsReturned() public void ShouldUseSimpleObjectFactoryAndThrowIfNoParameterlessCtor() { var source = TestSourceBuilder.MapperWithBodyAndTypes( - "[ObjectFactory] B? CreateB() => null;" + "partial B Map(A a);", + """ + [ObjectFactory] B? CreateB() => null; + partial B Map(A a); + """, "class A { public string StringValue { get; set; } }", "class B { private B() {} public string StringValue { get; set; } }" ); @@ -298,7 +328,10 @@ public void ShouldUseSimpleObjectFactoryAndThrowIfNoParameterlessCtor() public void ShouldUseGenericObjectFactoryAndCreateObjectIfNullIsReturned() { var source = TestSourceBuilder.MapperWithBodyAndTypes( - "[ObjectFactory] T? Create() where T : notnull => null;" + "partial B Map(A a);", + """ + [ObjectFactory] T? Create() where T : notnull => null; + partial B Map(A a); + """, "class A { public string StringValue { get; set; } }", "class B { public string StringValue { get; set; } }" ); @@ -319,7 +352,10 @@ public void ShouldUseGenericObjectFactoryAndCreateObjectIfNullIsReturned() public void ShouldUseGenericObjectFactoryAndThrowIfNoParameterlessCtor() { var source = TestSourceBuilder.MapperWithBodyAndTypes( - "[ObjectFactory] T? Create() where T : notnull => null;" + "partial B Map(A a);", + """ + [ObjectFactory] T? Create() where T : notnull => null; + partial B Map(A a); + """, "class A { public string StringValue { get; set; } }", "class B { private B() {} public string StringValue { get; set; } }" ); @@ -337,26 +373,42 @@ public void ShouldUseGenericObjectFactoryAndThrowIfNoParameterlessCtor() } [Fact] - public void InvalidSignatureAsync() + public void ShouldUseObjectFactoryWithRecursiveTypeParameter() { var source = TestSourceBuilder.MapperWithBodyAndTypes( - "[ObjectFactory] async Task Create() where T : new() => Task.FromResult(new T());" + "partial B Map(A a);", - "class A { public string StringValue { get; set; } }", - "class B { public string StringValue { get; set; } }" + """ + [ObjectFactory] + private B Create(TSource source) + where TSource : Base + => new B(); + + partial B Map(A source); + """, + "class Base;", + "class A : Base { public string StringValue { get; set; } }", + "class B { private B() {} public string StringValue { get; set; } }" ); TestHelper - .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .GenerateMapper(source) .Should() - .HaveDiagnostic(DiagnosticDescriptors.InvalidObjectFactorySignature) - .HaveAssertedAllDiagnostics(); + .HaveSingleMethodBody( + """ + var target = Create(source); + target.StringValue = source.StringValue; + return target; + """ + ); } [Fact] - public void InvalidSignatureParameters() + public void InvalidSignatureAsync() { var source = TestSourceBuilder.MapperWithBodyAndTypes( - "[ObjectFactory] B CreateB(int i, int j) => new B();" + "partial B Map(A a);", + """ + [ObjectFactory] async Task Create() where T : new() => Task.FromResult(new T()); + partial B Map(A a); + """, "class A { public string StringValue { get; set; } }", "class B { public string StringValue { get; set; } }" ); @@ -369,10 +421,13 @@ public void InvalidSignatureParameters() } [Fact] - public void InvalidSignatureVoid() + public void InvalidSignatureParameters() { var source = TestSourceBuilder.MapperWithBodyAndTypes( - "[ObjectFactory] void CreateB() => 0" + "partial B Map(A a);", + """ + [ObjectFactory] B CreateB(int i, int j) => new B(); + partial B Map(A a); + """, "class A { public string StringValue { get; set; } }", "class B { public string StringValue { get; set; } }" ); @@ -385,10 +440,13 @@ public void InvalidSignatureVoid() } [Fact] - public void InvalidSignatureMultipleTypeParameters() + public void InvalidSignatureVoid() { var source = TestSourceBuilder.MapperWithBodyAndTypes( - "[ObjectFactory] T Create() where T : new() => new T();" + "partial B Map(A a);", + """ + [ObjectFactory] void CreateB() {} + partial B Map(A a); + """, "class A { public string StringValue { get; set; } }", "class B { public string StringValue { get; set; } }" ); @@ -401,10 +459,13 @@ public void InvalidSignatureMultipleTypeParameters() } [Fact] - public void InvalidSignatureTypeParameterNotReturnType() + public void InvalidSignatureMultipleTypeParameters() { var source = TestSourceBuilder.MapperWithBodyAndTypes( - "[ObjectFactory] B CreateB() => new B();" + "partial B Map(A a);", + """ + [ObjectFactory] T Create() where T : new() => new T(); + partial B Map(A a); + """, "class A { public string StringValue { get; set; } }", "class B { public string StringValue { get; set; } }" ); @@ -417,10 +478,13 @@ public void InvalidSignatureTypeParameterNotReturnType() } [Fact] - public void InvalidSignatureTooManyTypeParameters() + public void InvalidSignatureTypeParameterNotReturnType() { var source = TestSourceBuilder.MapperWithBodyAndTypes( - "[ObjectFactory] T CreateB(S source) where T : new() => new T();" + "partial B Map(A a);", + """ + [ObjectFactory] B CreateB() => new B(); + partial B Map(A a); + """, "class A { public string StringValue { get; set; } }", "class B { public string StringValue { get; set; } }" ); @@ -433,10 +497,13 @@ public void InvalidSignatureTooManyTypeParameters() } [Fact] - public void InvalidSignatureTooManyTypeParametersSourceNotTypeParameter() + public void InvalidSignatureTooManyTypeParameters() { var source = TestSourceBuilder.MapperWithBodyAndTypes( - "[ObjectFactory] T CreateB(A source) where T : new() => new T();" + "partial B Map(A a);", + """ + [ObjectFactory] T CreateB(S source) where T : new() => new T(); + partial B Map(A a); + """, "class A { public string StringValue { get; set; } }", "class B { public string StringValue { get; set; } }" ); @@ -449,10 +516,13 @@ public void InvalidSignatureTooManyTypeParametersSourceNotTypeParameter() } [Fact] - public void InvalidSignatureTooManyTypeParametersTargetNotTypeParameter() + public void InvalidSignatureTooManyTypeParametersSourceNotTypeParameter() { var source = TestSourceBuilder.MapperWithBodyAndTypes( - "[ObjectFactory] B CreateB(S source) => new B();" + "partial B Map(A a);", + """ + [ObjectFactory] T CreateB(A source) where T : new() => new T(); + partial B Map(A a); + """, "class A { public string StringValue { get; set; } }", "class B { public string StringValue { get; set; } }" ); @@ -465,10 +535,13 @@ public void InvalidSignatureTooManyTypeParametersTargetNotTypeParameter() } [Fact] - public void InvalidSignatureGenericSourceTargetSameTypeParameter() + public void InvalidSignatureTooManyTypeParametersTargetNotTypeParameter() { var source = TestSourceBuilder.MapperWithBodyAndTypes( - "[ObjectFactory] T CreateB(T source) where T : new() => new T();" + "partial B Map(A a);", + """ + [ObjectFactory] B CreateB(S source) => new B(); + partial B Map(A a); + """, "class A { public string StringValue { get; set; } }", "class B { public string StringValue { get; set; } }" ); diff --git a/test/Riok.Mapperly.Tests/Mapping/RuntimeTargetTypeMappingTest.cs b/test/Riok.Mapperly.Tests/Mapping/RuntimeTargetTypeMappingTest.cs index f13983698e..6274488384 100644 --- a/test/Riok.Mapperly.Tests/Mapping/RuntimeTargetTypeMappingTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/RuntimeTargetTypeMappingTest.cs @@ -25,6 +25,25 @@ public Task WithNullableObjectSourceAndTargetTypeShouldIncludeNullables() return TestHelper.VerifyGenerator(source); } + [Fact] + public Task WithGenericSourceAndTarget() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + public partial IQueryable Map(IQueryable source); + + private partial IQueryable ProjectToB(IQueryable q); + private partial IQueryable ProjectToD(IQueryable q); + """, + "class A { public string Value { get; set; } }", + "class B { public string Value { get; set; } }", + "class C { public string Value2 { get; set; } }", + "class D { public string Value2 { get; set; } }" + ); + + return TestHelper.VerifyGenerator(source); + } + [Fact] public void WithNonNullableReturnTypeShouldOnlyIncludeNonNullableMappings() { @@ -49,7 +68,6 @@ public void WithNonNullableReturnTypeShouldOnlyIncludeNonNullableMappings() return source switch { global::A x when targetType.IsAssignableFrom(typeof(global::B)) => MapToB(x), - null => throw new System.ArgumentNullException(nameof(source)), _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {targetType} as there is no known type mapping", nameof(source)), }; """ @@ -83,7 +101,6 @@ public void WithSubsetSourceTypeAndObjectTargetTypeShouldWork() return source switch { global::A x when targetType.IsAssignableFrom(typeof(global::B)) => MapToB(x), - null => throw new System.ArgumentNullException(nameof(source)), _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {targetType} as there is no known type mapping", nameof(source)), }; """ @@ -119,7 +136,6 @@ public void WithTypeHierarchyShouldPreferMostSpecificMapping() global::B x when targetType.IsAssignableFrom(typeof(global::C)) => MapBToC(x), global::Base2 x when targetType.IsAssignableFrom(typeof(global::C)) => MapB2ToC(x), global::Base1 x when targetType.IsAssignableFrom(typeof(global::C)) => MapB1ToC(x), - null => throw new System.ArgumentNullException(nameof(source)), _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {targetType} as there is no known type mapping", nameof(source)), }; """ @@ -152,7 +168,6 @@ public void WithDerivedTypesShouldUseBaseType() return source switch { global::Base x when targetType.IsAssignableFrom(typeof(global::BaseDto)) => MapDerivedTypes(x), - null => throw new System.ArgumentNullException(nameof(source)), _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {targetType} as there is no known type mapping", nameof(source)), }; """ @@ -188,7 +203,6 @@ public void WithDerivedTypesOnSameMethodAndDuplicatedSourceTypeShouldIncludeAll( global::A x when targetType.IsAssignableFrom(typeof(global::D)) => MapToD(x), global::C x when targetType.IsAssignableFrom(typeof(global::B)) => MapToB1(x), global::C x when targetType.IsAssignableFrom(typeof(global::D)) => MapToD1(x), - null => throw new System.ArgumentNullException(nameof(source)), _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {targetType} as there is no known type mapping", nameof(source)), }; """ @@ -221,7 +235,6 @@ public void WithUserImplementedMethodsShouldBeIncluded() { global::A x when targetType.IsAssignableFrom(typeof(global::B)) => MapToB(x), global::B x when targetType.IsAssignableFrom(typeof(global::D)) => MapToD(x), - null => throw new System.ArgumentNullException(nameof(source)), _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {targetType} as there is no known type mapping", nameof(source)), }; """ @@ -253,7 +266,6 @@ public void WithGenericUserImplementedMethodShouldBeIgnored() return source switch { global::A x when targetType.IsAssignableFrom(typeof(global::B)) => MapToB(x), - null => throw new System.ArgumentNullException(nameof(source)), _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {targetType} as there is no known type mapping", nameof(source)), }; """ diff --git a/test/Riok.Mapperly.Tests/Mapping/UserMethodTest.cs b/test/Riok.Mapperly.Tests/Mapping/UserMethodTest.cs index 1dcd546764..07fb0bf326 100644 --- a/test/Riok.Mapperly.Tests/Mapping/UserMethodTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/UserMethodTest.cs @@ -226,18 +226,6 @@ public void InvalidSignatureAsyncShouldDiagnostic() .HaveAssertedAllDiagnostics(); } - [Fact] - public void InvalidSignatureGenericShouldDiagnostic() - { - var source = TestSourceBuilder.MapperWithBodyAndTypes("partial TTarget Map(TSource source);"); - - TestHelper - .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) - .Should() - .HaveDiagnostic(DiagnosticDescriptors.UnsupportedMappingMethodSignature, "Map has an unsupported mapping method signature") - .HaveAssertedAllDiagnostics(); - } - [Fact] public Task WithClassBaseTypeShouldWork() { diff --git a/test/Riok.Mapperly.Tests/TestHelper.cs b/test/Riok.Mapperly.Tests/TestHelper.cs index ae3dc25f27..f900ec59ba 100644 --- a/test/Riok.Mapperly.Tests/TestHelper.cs +++ b/test/Riok.Mapperly.Tests/TestHelper.cs @@ -77,16 +77,20 @@ public static GeneratorDriver GenerateTracked(Compilation compilation) return driver.RunGenerators(compilation); } + public static Compilation BuildCompilation([StringSyntax(StringSyntax.CSharp)] string source, TestHelperOptions? options) + { + options ??= TestHelperOptions.Default; + var syntaxTree = CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(options.LanguageVersion)); + return BuildCompilation(options.AssemblyName, options.NullableOption, true, syntaxTree); + } + private static GeneratorDriver Generate( - string source, + [StringSyntax(StringSyntax.CSharp)] string source, TestHelperOptions? options, IReadOnlyCollection? additionalAssemblies = null ) { - options ??= TestHelperOptions.Default; - - var syntaxTree = CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(options.LanguageVersion)); - var compilation = BuildCompilation(options.AssemblyName, options.NullableOption, true, syntaxTree); + var compilation = BuildCompilation(source, options); if (additionalAssemblies != null) { compilation = compilation.AddReferences(additionalAssemblies.Select(x => x.MetadataReference)); diff --git a/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithGenericSourceAndTargetAndUnboundGenericShouldDiagnostic#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithGenericSourceAndTargetAndUnboundGenericShouldDiagnostic#Mapper.g.verified.cs new file mode 100644 index 0000000000..504b364197 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithGenericSourceAndTargetAndUnboundGenericShouldDiagnostic#Mapper.g.verified.cs @@ -0,0 +1,30 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial TTarget Map(TSource source) + { + return source switch + { + null => throw new System.ArgumentNullException(nameof(source)), + _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(TTarget)} as there is no known type mapping", nameof(source)), + }; + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial global::B MapToB(global::A source) + { + var target = new global::B(); + target.Value = source.Value; + return target; + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial global::D MapToD(global::C source) + { + var target = new global::D(source.Value1); + return target; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithGenericSourceAndTargetAndUnboundGenericShouldDiagnostic.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithGenericSourceAndTargetAndUnboundGenericShouldDiagnostic.verified.txt new file mode 100644 index 0000000000..8f014fbe6f --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithGenericSourceAndTargetAndUnboundGenericShouldDiagnostic.verified.txt @@ -0,0 +1,14 @@ +{ + Diagnostics: [ + { + Id: RMG069, + Title: Runtime target type or generic type mapping does not match any mappings, + Severity: Warning, + WarningLevel: 1, + Location: : (11,4)-(11,76), + MessageFormat: Runtime target type or generic type mapping does not match any mappings, + Message: Runtime target type or generic type mapping does not match any mappings, + Category: Mapper + } + ] +} \ No newline at end of file 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 new file mode 100644 index 0000000000..cda75d7b2d --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithGenericSourceAndTargetInNullableDisabledContext#Mapper.g.verified.cs @@ -0,0 +1,34 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial TTarget? Map(TSource? source) + { + return source switch + { + global::A x when typeof(TTarget).IsAssignableFrom(typeof(global::B)) => (TTarget?)(object)MapToB(x), + global::C x when typeof(TTarget).IsAssignableFrom(typeof(global::D)) => (TTarget?)(object)MapToD(x), + null => default, + _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(TTarget)} as there is no known type mapping", nameof(source)), + }; + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial global::B MapToB(global::A source) + { + var target = new global::B(); + target.Value = source.Value; + return target; + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial global::D? MapToD(global::C? source) + { + if (source == null) + return default; + var target = new global::D(source.Value1); + return target; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithNestedGenericSourceAndTarget#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithNestedGenericSourceAndTarget#Mapper.g.verified.cs new file mode 100644 index 0000000000..a22da19270 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithNestedGenericSourceAndTarget#Mapper.g.verified.cs @@ -0,0 +1,48 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial global::System.Collections.Generic.IEnumerable Map(global::System.Collections.Generic.IEnumerable source) + { + return source switch + { + global::System.Collections.Generic.IReadOnlyCollection x when typeof(global::System.Collections.Generic.IEnumerable).IsAssignableFrom(typeof(global::System.Collections.Generic.List)) => (global::System.Collections.Generic.IEnumerable)(object)MapToD(x), + global::System.Collections.Generic.IEnumerable x when typeof(global::System.Collections.Generic.IEnumerable).IsAssignableFrom(typeof(global::System.Collections.Generic.IEnumerable)) => (global::System.Collections.Generic.IEnumerable)(object)MapToB(x), + _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(global::System.Collections.Generic.IEnumerable)} as there is no known type mapping", nameof(source)), + }; + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial global::System.Collections.Generic.IEnumerable MapToB(global::System.Collections.Generic.IEnumerable source) + { + return global::System.Linq.Enumerable.Select(source, x => MapToB1(x)); + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial global::System.Collections.Generic.List MapToD(global::System.Collections.Generic.IReadOnlyCollection source) + { + var target = new global::System.Collections.Generic.List(source.Count); + foreach (var item in source) + { + target.Add(MapToD1(item)); + } + return target; + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private global::B MapToB1(global::A source) + { + var target = new global::B(); + target.Value = source.Value; + return target; + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private global::D MapToD1(global::C source) + { + var target = new global::D(source.Value1); + return target; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithQueryableGenericSourceAndTarget#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithQueryableGenericSourceAndTarget#Mapper.g.verified.cs new file mode 100644 index 0000000000..a2c6b5570c --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithQueryableGenericSourceAndTarget#Mapper.g.verified.cs @@ -0,0 +1,32 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable source) + { + return source switch + { + global::System.Linq.IQueryable x when typeof(global::System.Linq.IQueryable).IsAssignableFrom(typeof(global::System.Linq.IQueryable)) => (global::System.Linq.IQueryable)(object)MapToB(x), + global::System.Linq.IQueryable x when typeof(global::System.Linq.IQueryable).IsAssignableFrom(typeof(global::System.Linq.IQueryable)) => (global::System.Linq.IQueryable)(object)MapToD(x), + _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(global::System.Linq.IQueryable)} as there is no known type mapping", nameof(source)), + }; + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial global::System.Linq.IQueryable MapToB(global::System.Linq.IQueryable source) + { +#nullable disable + return System.Linq.Queryable.Select(source, x => new global::B(x.Value)); +#nullable enable + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial global::System.Linq.IQueryable MapToD(global::System.Linq.IQueryable source) + { +#nullable disable + return System.Linq.Queryable.Select(source, x => new global::D(x.Value1)); +#nullable enable + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithTypeConstrainedQueryableGenericSourceAndTarget#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithTypeConstrainedQueryableGenericSourceAndTarget#Mapper.g.verified.cs new file mode 100644 index 0000000000..0e23b99293 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithTypeConstrainedQueryableGenericSourceAndTarget#Mapper.g.verified.cs @@ -0,0 +1,32 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial TTarget Map(TSource source) + { + return source switch + { + global::System.Linq.IQueryable x when typeof(TTarget).IsAssignableFrom(typeof(global::System.Linq.IQueryable)) => (TTarget)(object)MapToB(x), + global::System.Linq.IQueryable x when typeof(TTarget).IsAssignableFrom(typeof(global::System.Linq.IQueryable)) => (TTarget)(object)MapToD(x), + _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(TTarget)} as there is no known type mapping", nameof(source)), + }; + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial global::System.Linq.IQueryable MapToB(global::System.Linq.IQueryable source) + { +#nullable disable + return System.Linq.Queryable.Select(source, x => new global::B(x.Value)); +#nullable enable + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial global::System.Linq.IQueryable MapToD(global::System.Linq.IQueryable source) + { +#nullable disable + return System.Linq.Queryable.Select(source, x => new global::D(x.Value1)); +#nullable enable + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.RuntimeTargetTypeShouldWork#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.RuntimeTargetTypeShouldWork#Mapper.g.verified.cs index 162c80b255..03d69af73a 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.RuntimeTargetTypeShouldWork#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.RuntimeTargetTypeShouldWork#Mapper.g.verified.cs @@ -10,7 +10,6 @@ public partial class Mapper return source switch { global::A x when destinationType.IsAssignableFrom(typeof(global::B)) => MapToB(x, refHandler), - null => throw new System.ArgumentNullException(nameof(source)), _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {destinationType} as there is no known type mapping", nameof(source)), }; } diff --git a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.RuntimeTargetTypeWithReferenceHandlingShouldWork#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.RuntimeTargetTypeWithReferenceHandlingShouldWork#Mapper.g.verified.cs index 9c04903f18..3a71b926df 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.RuntimeTargetTypeWithReferenceHandlingShouldWork#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.RuntimeTargetTypeWithReferenceHandlingShouldWork#Mapper.g.verified.cs @@ -9,7 +9,6 @@ public partial class Mapper return source switch { global::A x when destinationType.IsAssignableFrom(typeof(global::B)) => MapToB(x, refHandler), - null => throw new System.ArgumentNullException(nameof(source)), _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {destinationType} as there is no known type mapping", nameof(source)), }; } diff --git a/test/Riok.Mapperly.Tests/_snapshots/RuntimeTargetTypeMappingTest.WithGenericSourceAndTarget#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/RuntimeTargetTypeMappingTest.WithGenericSourceAndTarget#Mapper.g.verified.cs new file mode 100644 index 0000000000..339a9f95ab --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/RuntimeTargetTypeMappingTest.WithGenericSourceAndTarget#Mapper.g.verified.cs @@ -0,0 +1,38 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + public partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable source) + { + return source switch + { + global::System.Linq.IQueryable x when typeof(global::System.Linq.IQueryable).IsAssignableFrom(typeof(global::System.Linq.IQueryable)) => (global::System.Linq.IQueryable)(object)ProjectToB(x), + global::System.Linq.IQueryable x when typeof(global::System.Linq.IQueryable).IsAssignableFrom(typeof(global::System.Linq.IQueryable)) => (global::System.Linq.IQueryable)(object)ProjectToD(x), + _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(global::System.Linq.IQueryable)} as there is no known type mapping", nameof(source)), + }; + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial global::System.Linq.IQueryable ProjectToB(global::System.Linq.IQueryable q) + { +#nullable disable + return System.Linq.Queryable.Select(q, x => new global::B() + { + Value = x.Value, + }); +#nullable enable + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial global::System.Linq.IQueryable ProjectToD(global::System.Linq.IQueryable q) + { +#nullable disable + return System.Linq.Queryable.Select(q, x => new global::D() + { + Value2 = x.Value2, + }); +#nullable enable + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/RuntimeTargetTypeMappingTest.WithReferenceHandlerParameter#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/RuntimeTargetTypeMappingTest.WithReferenceHandlerParameter#Mapper.g.verified.cs index 95c9e0b135..107100239c 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/RuntimeTargetTypeMappingTest.WithReferenceHandlerParameter#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/RuntimeTargetTypeMappingTest.WithReferenceHandlerParameter#Mapper.g.verified.cs @@ -10,7 +10,6 @@ public partial class Mapper { global::A x when targetType.IsAssignableFrom(typeof(global::B)) => MapToB(x, refHandler), global::C x when targetType.IsAssignableFrom(typeof(global::D)) => MapToD(x, refHandler), - null => throw new System.ArgumentNullException(nameof(source)), _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {targetType} as there is no known type mapping", nameof(source)), }; } diff --git a/test/Riok.Mapperly.Tests/_snapshots/RuntimeTargetTypeMappingTest.WithReferenceHandlingEnabled#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/RuntimeTargetTypeMappingTest.WithReferenceHandlingEnabled#Mapper.g.verified.cs index 32a82b97bf..d6e76cc652 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/RuntimeTargetTypeMappingTest.WithReferenceHandlingEnabled#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/RuntimeTargetTypeMappingTest.WithReferenceHandlingEnabled#Mapper.g.verified.cs @@ -11,7 +11,6 @@ public partial class Mapper { global::A x when targetType.IsAssignableFrom(typeof(global::B)) => MapToB1(x, refHandler), global::C x when targetType.IsAssignableFrom(typeof(global::D)) => MapToD1(x, refHandler), - null => throw new System.ArgumentNullException(nameof(source)), _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {targetType} as there is no known type mapping", nameof(source)), }; }