Skip to content

Commit

Permalink
feat: rework generic type matching (#1199)
Browse files Browse the repository at this point in the history
* support more type parameter constructs for generic mappings such as nested generic types
* emit a new diagnostic (RMG069, warning) for runtime target type mappings which do not match any mapping
* add null arm to runtime target type mappings only if source type is nullable
  • Loading branch information
latonz committed Mar 27, 2024
1 parent f08b7b4 commit 1e78844
Show file tree
Hide file tree
Showing 49 changed files with 1,214 additions and 270 deletions.
1 change: 1 addition & 0 deletions src/Riok.Mapperly/AnalyzerReleases.Shipped.md
Expand Up @@ -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
1 change: 1 addition & 0 deletions src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs
Expand Up @@ -57,6 +57,7 @@ MapperConfiguration defaultMapperConfiguration
compilationContext,
configurationReader,
_symbolAccessor,
new GenericTypeChecker(_symbolAccessor, _types),
attributeAccessor,
_unsafeAccessorContext,
_diagnostics,
Expand Down
Expand Up @@ -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;
}
Expand Down
@@ -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;

Expand All @@ -16,42 +16,32 @@ 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,
// therefore set source type always to nun-nullable
// 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<ITypeMapping> GetUserMappingCandidates(MappingBuilderContext ctx) =>
ctx.UserMappings.Where(x => x is not UserDefinedNewInstanceRuntimeTargetTypeMapping);
private static IEnumerable<INewInstanceUserMapping> GetUserMappingCandidates(MappingBuilderContext ctx) =>
ctx.UserMappings.Where(x => x is not UserDefinedNewInstanceRuntimeTargetTypeMapping).OfType<INewInstanceUserMapping>();

private static void BuildMappingBody(
MappingBuilderContext ctx,
Expand All @@ -78,7 +68,14 @@ IEnumerable<ITypeMapping> 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);
}
}
2 changes: 1 addition & 1 deletion src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs
Expand Up @@ -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;

Expand Down
Expand Up @@ -61,33 +61,34 @@ bool duplicatedSourceTypesAllowed
{
var derivedTypeMappingSourceTypes = new HashSet<ITypeSymbol>(SymbolEqualityComparer.Default);
var derivedTypeMappings = new List<TMapping>(configs.Count);
Func<ITypeSymbol, bool> isAssignableToSource = ctx.Source is ITypeParameterSymbol sourceTypeParameter
? t => ctx.SymbolAccessor.DoesTypeSatisfyTypeParameterConstraints(sourceTypeParameter, t)
: t => ctx.SymbolAccessor.HasImplicitConversion(t, ctx.Source);
Func<ITypeSymbol, bool> 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;
}

Expand Down
Expand Up @@ -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;
Expand Down
16 changes: 10 additions & 6 deletions src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs
Expand Up @@ -32,7 +32,6 @@ public abstract class MethodMapping : ITypeMapping
};

private readonly ITypeSymbol _returnType;
private readonly IMethodSymbol? _partialMethodDefinition;

private string? _methodName;

Expand All @@ -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();
Expand Down Expand Up @@ -117,11 +121,11 @@ internal virtual void EnableReferenceHandling(INamedTypeSymbol iReferenceHandler

private IEnumerable<SyntaxToken> 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;
Expand Down
Expand Up @@ -22,7 +22,7 @@ bool enableReferenceHandling
{
private IExistingTargetMapping? _delegateMapping;

public IMethodSymbol Method { get; } = method;
public new IMethodSymbol Method { get; } = method;

public bool? Default => false;

Expand Down
Expand Up @@ -16,11 +16,10 @@ namespace Riok.Mapperly.Descriptors.Mappings.UserMappings;
/// </summary>
public class UserDefinedNewInstanceGenericTypeMapping(
IMethodSymbol method,
GenericMappingTypeParameters typeParameters,
MappingMethodParameters parameters,
ITypeSymbol targetType,
bool enableReferenceHandling,
NullFallbackValue nullArm,
NullFallbackValue? nullArm,
ITypeSymbol objectType
)
: UserDefinedNewInstanceRuntimeTargetTypeMapping(
Expand All @@ -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(<ReturnType>)
var targetTypeName = TypeParameters.TargetType ?? TargetType;
return TypeOfExpression(FullyQualifiedIdentifier(targetTypeName.NonNullable()));
// typeof(<ReturnType>)
return TypeOfExpression(FullyQualifiedIdentifier(Method.ReturnType.NonNullable()));
}

protected override ExpressionSyntax? BuildSwitchArmWhenClause(ExpressionSyntax targetType, RuntimeTargetTypeMapping mapping)
Expand Down
Expand Up @@ -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;

Expand Down
Expand Up @@ -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
{
Expand All @@ -28,7 +28,7 @@ ITypeSymbol objectType

private readonly List<RuntimeTargetTypeMapping> _mappings = new();

public IMethodSymbol Method { get; } = method;
public new IMethodSymbol Method { get; } = method;

/// <summary>
/// Always false, as this cannot be called by other mappings,
Expand Down Expand Up @@ -77,19 +77,23 @@ public override IEnumerable<StatementSyntax> 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);
}

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()))
);
}
Expand Down
Expand Up @@ -15,7 +15,7 @@ public class UserDefinedNewInstanceRuntimeTargetTypeParameterMapping(
RuntimeTargetTypeMappingMethodParameters parameters,
bool enableReferenceHandling,
ITypeSymbol targetType,
NullFallbackValue nullArm,
NullFallbackValue? nullArm,
ITypeSymbol objectType
)
: UserDefinedNewInstanceRuntimeTargetTypeMapping(
Expand Down
@@ -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;
Expand All @@ -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: <c>TypeToCreate Create&lt;S&gt;(S source);</c>
/// </summary>
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);
}
@@ -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)
{
Expand Down
@@ -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;
Expand All @@ -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: <c>T Create&lt;T&gt;();</c>
/// </summary>
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) });
Expand Down

0 comments on commit 1e78844

Please sign in to comment.