Skip to content

Commit 1e78844

Browse files
authored
feat: rework generic type matching (#1199)
* 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
1 parent f08b7b4 commit 1e78844

File tree

49 files changed

+1214
-270
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1214
-270
lines changed

src/Riok.Mapperly/AnalyzerReleases.Shipped.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,3 +153,4 @@ RMG065 | Mapper | Warning | Cannot configure an object mapping on a queryabl
153153
RMG066 | Mapper | Warning | No members are mapped in an object mapping
154154
RMG067 | Mapper | Error | Invalid usage of the MapPropertyAttribute
155155
RMG068 | Mapper | Info | Cannot inline user implemented queryable expression mapping
156+
RMG069 | Mapper | Warning | Runtime target type or generic type mapping does not match any mappings

src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ MapperConfiguration defaultMapperConfiguration
5757
compilationContext,
5858
configurationReader,
5959
_symbolAccessor,
60+
new GenericTypeChecker(_symbolAccessor, _types),
6061
attributeAccessor,
6162
_unsafeAccessorContext,
6263
_diagnostics,

src/Riok.Mapperly/Descriptors/Enumerables/CollectionInfoBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@ static CollectionType IterateImplementedTypes(ITypeSymbol type, WellKnownTypes t
375375
if (typeInfo.GetTypeSymbol(types) is not { } typeSymbol)
376376
continue;
377377

378-
if (type.ImplementsGeneric(typeSymbol, out _))
378+
if (type.ExtendsOrImplementsGeneric(typeSymbol, out _))
379379
{
380380
implementedCollectionTypes |= typeInfo.CollectionType;
381381
}

src/Riok.Mapperly/Descriptors/MappingBodyBuilders/RuntimeTargetTypeMappingBodyBuilder.cs

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
using Microsoft.CodeAnalysis;
21
using Riok.Mapperly.Descriptors.MappingBuilders;
32
using Riok.Mapperly.Descriptors.Mappings;
43
using Riok.Mapperly.Descriptors.Mappings.UserMappings;
4+
using Riok.Mapperly.Diagnostics;
55
using Riok.Mapperly.Helpers;
66
using Riok.Mapperly.Symbols;
77

@@ -16,42 +16,32 @@ public static void BuildMappingBody(MappingBuilderContext ctx, UserDefinedNewIns
1616
// as non-nullables are also assignable to nullables.
1717
var mappings = GetUserMappingCandidates(ctx)
1818
.Where(x =>
19-
DoesTypesSatisfySubstitutionPrinciples(mapping, ctx.SymbolAccessor, x.SourceType.NonNullable(), x.TargetType)
20-
&& mapping.TypeParameters.DoesTypesSatisfyTypeParameterConstraints(
21-
ctx.SymbolAccessor,
22-
x.SourceType.NonNullable(),
23-
x.TargetType
24-
)
19+
ctx.GenericTypeChecker.InferAndCheckTypes(
20+
mapping.Method.TypeParameters,
21+
(mapping.SourceType, x.SourceType.NonNullable()),
22+
(mapping.TargetType, x.TargetType)
23+
).Success
2524
);
2625

2726
BuildMappingBody(ctx, mapping, mappings);
2827
}
2928

30-
private static bool DoesTypesSatisfySubstitutionPrinciples(
31-
IMapping mapping,
32-
SymbolAccessor symbolAccessor,
33-
ITypeSymbol sourceType,
34-
ITypeSymbol targetType
35-
) =>
36-
(mapping.SourceType.TypeKind == TypeKind.TypeParameter || symbolAccessor.HasImplicitConversion(sourceType, mapping.SourceType))
37-
&& (mapping.TargetType.TypeKind == TypeKind.TypeParameter || symbolAccessor.HasImplicitConversion(targetType, mapping.TargetType));
38-
3929
public static void BuildMappingBody(MappingBuilderContext ctx, UserDefinedNewInstanceRuntimeTargetTypeMapping mapping)
4030
{
4131
// source nulls are filtered out by the type switch arms,
4232
// therefore set source type always to nun-nullable
4333
// as non-nullables are also assignable to nullables.
4434
var mappings = GetUserMappingCandidates(ctx)
4535
.Where(x =>
46-
ctx.SymbolAccessor.HasImplicitConversion(x.SourceType.NonNullable(), mapping.SourceType)
47-
&& ctx.SymbolAccessor.HasImplicitConversion(x.TargetType, mapping.TargetType)
36+
ctx.SymbolAccessor.CanAssign(x.SourceType.NonNullable(), mapping.SourceType)
37+
&& ctx.SymbolAccessor.CanAssign(x.TargetType, mapping.TargetType)
4838
);
4939

5040
BuildMappingBody(ctx, mapping, mappings);
5141
}
5242

53-
private static IEnumerable<ITypeMapping> GetUserMappingCandidates(MappingBuilderContext ctx) =>
54-
ctx.UserMappings.Where(x => x is not UserDefinedNewInstanceRuntimeTargetTypeMapping);
43+
private static IEnumerable<INewInstanceUserMapping> GetUserMappingCandidates(MappingBuilderContext ctx) =>
44+
ctx.UserMappings.Where(x => x is not UserDefinedNewInstanceRuntimeTargetTypeMapping).OfType<INewInstanceUserMapping>();
5545

5646
private static void BuildMappingBody(
5747
MappingBuilderContext ctx,
@@ -78,7 +68,14 @@ IEnumerable<ITypeMapping> childMappings
7868
.ThenBy(x => x.TargetType.IsNullable())
7969
.GroupBy(x => new TypeMappingKey(x, includeNullability: false))
8070
.Select(x => x.First())
81-
.Select(x => new RuntimeTargetTypeMapping(x, ctx.Compilation.HasImplicitConversion(x.TargetType, ctx.Target)));
71+
.Select(x => new RuntimeTargetTypeMapping(x, ctx.Compilation.HasImplicitConversion(x.TargetType, ctx.Target)))
72+
.ToList();
73+
74+
if (runtimeTargetTypeMappings.Count == 0)
75+
{
76+
ctx.ReportDiagnostic(DiagnosticDescriptors.RuntimeTargetTypeMappingNoContentMappings);
77+
}
78+
8279
mapping.AddMappings(runtimeTargetTypeMappings);
8380
}
8481
}

src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ bool ignoreDerivedTypes
6161

6262
public DictionaryInfos? DictionaryInfos => _dictionaryInfos ??= DictionaryInfoBuilder.Build(Types, CollectionInfos);
6363

64-
protected IMethodSymbol? UserSymbol { get; }
64+
public IMethodSymbol? UserSymbol { get; }
6565

6666
public bool HasUserSymbol => UserSymbol != null;
6767

src/Riok.Mapperly/Descriptors/MappingBuilders/DerivedTypeMappingBuilder.cs

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -61,33 +61,34 @@ bool duplicatedSourceTypesAllowed
6161
{
6262
var derivedTypeMappingSourceTypes = new HashSet<ITypeSymbol>(SymbolEqualityComparer.Default);
6363
var derivedTypeMappings = new List<TMapping>(configs.Count);
64-
Func<ITypeSymbol, bool> isAssignableToSource = ctx.Source is ITypeParameterSymbol sourceTypeParameter
65-
? t => ctx.SymbolAccessor.DoesTypeSatisfyTypeParameterConstraints(sourceTypeParameter, t)
66-
: t => ctx.SymbolAccessor.HasImplicitConversion(t, ctx.Source);
67-
Func<ITypeSymbol, bool> isAssignableToTarget = ctx.Target is ITypeParameterSymbol targetTypeParameter
68-
? t => ctx.SymbolAccessor.DoesTypeSatisfyTypeParameterConstraints(targetTypeParameter, t)
69-
: t => ctx.SymbolAccessor.HasImplicitConversion(t, ctx.Target);
7064

7165
foreach (var config in configs)
7266
{
7367
// set types non-nullable as they can never be null when type-switching.
7468
var sourceType = config.SourceType.NonNullable();
69+
var targetType = config.TargetType.NonNullable();
7570
if (!duplicatedSourceTypesAllowed && !derivedTypeMappingSourceTypes.Add(sourceType))
7671
{
7772
ctx.ReportDiagnostic(DiagnosticDescriptors.DerivedSourceTypeDuplicated, sourceType);
7873
continue;
7974
}
8075

81-
if (!isAssignableToSource(sourceType))
76+
var typeCheckerResult = ctx.GenericTypeChecker.InferAndCheckTypes(
77+
ctx.UserSymbol!.TypeParameters,
78+
(ctx.Source, sourceType),
79+
(ctx.Target, targetType)
80+
);
81+
if (!typeCheckerResult.Success)
8282
{
83-
ctx.ReportDiagnostic(DiagnosticDescriptors.DerivedSourceTypeIsNotAssignableToParameterType, sourceType, ctx.Source);
84-
continue;
85-
}
83+
if (ReferenceEquals(sourceType, typeCheckerResult.FailedArgument))
84+
{
85+
ctx.ReportDiagnostic(DiagnosticDescriptors.DerivedSourceTypeIsNotAssignableToParameterType, sourceType, ctx.Source);
86+
}
87+
else
88+
{
89+
ctx.ReportDiagnostic(DiagnosticDescriptors.DerivedTargetTypeIsNotAssignableToReturnType, targetType, ctx.Target);
90+
}
8691

87-
var targetType = config.TargetType.NonNullable();
88-
if (!isAssignableToTarget(targetType))
89-
{
90-
ctx.ReportDiagnostic(DiagnosticDescriptors.DerivedTargetTypeIsNotAssignableToReturnType, targetType, ctx.Target);
9192
continue;
9293
}
9394

src/Riok.Mapperly/Descriptors/MappingBuilders/EnumerableMappingBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ INewInstanceMapping elementMapping
324324
if (!hasObjectFactory)
325325
{
326326
sourceCollectionInfo = BuildCollectionTypeForICollection(ctx, sourceCollectionInfo);
327-
ctx.ObjectFactories.TryFindObjectFactory(sourceCollectionInfo.Type, ctx.Target, out objectFactory);
327+
ctx.ObjectFactories.TryFindObjectFactory(ctx.Source, ctx.Target, out objectFactory);
328328
var existingMapping = ctx.BuildDelegatedMapping(sourceCollectionInfo.Type, ctx.Target);
329329
if (existingMapping != null)
330330
return existingMapping;

src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ public abstract class MethodMapping : ITypeMapping
3232
};
3333

3434
private readonly ITypeSymbol _returnType;
35-
private readonly IMethodSymbol? _partialMethodDefinition;
3635

3736
private string? _methodName;
3837

@@ -54,11 +53,16 @@ ITypeSymbol targetType
5453
SourceParameter = sourceParameter;
5554
IsExtensionMethod = method.IsExtensionMethod;
5655
ReferenceHandlerParameter = referenceHandlerParameter;
57-
_partialMethodDefinition = method;
56+
Method = method;
57+
MethodDeclarationSyntax = Method?.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() as MethodDeclarationSyntax;
5858
_methodName = method.Name;
5959
_returnType = method.ReturnsVoid ? method.ReturnType : targetType;
6060
}
6161

62+
protected IMethodSymbol? Method { get; }
63+
64+
protected MethodDeclarationSyntax? MethodDeclarationSyntax { get; }
65+
6266
protected bool IsExtensionMethod { get; }
6367

6468
protected string MethodName => _methodName ?? throw new InvalidOperationException();
@@ -117,11 +121,11 @@ protected virtual ParameterListSyntax BuildParameterList() =>
117121

118122
private IEnumerable<SyntaxToken> BuildModifiers(bool isStatic)
119123
{
120-
// if a syntax is referenced it is the implementation part of partial method definition
121-
// then copy all modifiers otherwise only set private and optionally static
122-
if (_partialMethodDefinition?.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() is MethodDeclarationSyntax syntax)
124+
// if a syntax is referenced the code written by the user copy all modifiers,
125+
// otherwise only set private and optionally static
126+
if (MethodDeclarationSyntax != null)
123127
{
124-
return syntax.Modifiers.Select(x => TrailingSpacedToken(x.Kind()));
128+
return MethodDeclarationSyntax.Modifiers.Select(x => TrailingSpacedToken(x.Kind()));
125129
}
126130

127131
return isStatic ? _privateStaticSyntaxToken : _privateSyntaxToken;

src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetMethodMapping.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ bool enableReferenceHandling
2222
{
2323
private IExistingTargetMapping? _delegateMapping;
2424

25-
public IMethodSymbol Method { get; } = method;
25+
public new IMethodSymbol Method { get; } = method;
2626

2727
public bool? Default => false;
2828

src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceGenericTypeMapping.cs

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,10 @@ namespace Riok.Mapperly.Descriptors.Mappings.UserMappings;
1616
/// </summary>
1717
public class UserDefinedNewInstanceGenericTypeMapping(
1818
IMethodSymbol method,
19-
GenericMappingTypeParameters typeParameters,
2019
MappingMethodParameters parameters,
2120
ITypeSymbol targetType,
2221
bool enableReferenceHandling,
23-
NullFallbackValue nullArm,
22+
NullFallbackValue? nullArm,
2423
ITypeSymbol objectType
2524
)
2625
: UserDefinedNewInstanceRuntimeTargetTypeMapping(
@@ -33,16 +32,16 @@ ITypeSymbol objectType
3332
objectType
3433
)
3534
{
36-
public GenericMappingTypeParameters TypeParameters { get; } = typeParameters;
37-
38-
public override MethodDeclarationSyntax BuildMethod(SourceEmitterContext ctx) =>
39-
base.BuildMethod(ctx).WithTypeParameterList(TypeParameterList(TypeParameters.SourceType, TypeParameters.TargetType));
35+
public override MethodDeclarationSyntax BuildMethod(SourceEmitterContext ctx)
36+
{
37+
var methodSyntax = (MethodDeclarationSyntax)Method.DeclaringSyntaxReferences.First().GetSyntax();
38+
return base.BuildMethod(ctx).WithTypeParameterList(methodSyntax.TypeParameterList);
39+
}
4040

4141
protected override ExpressionSyntax BuildTargetType()
4242
{
43-
// typeof(TTarget) or typeof(<ReturnType>)
44-
var targetTypeName = TypeParameters.TargetType ?? TargetType;
45-
return TypeOfExpression(FullyQualifiedIdentifier(targetTypeName.NonNullable()));
43+
// typeof(<ReturnType>)
44+
return TypeOfExpression(FullyQualifiedIdentifier(Method.ReturnType.NonNullable()));
4645
}
4746

4847
protected override ExpressionSyntax? BuildSwitchArmWhenClause(ExpressionSyntax targetType, RuntimeTargetTypeMapping mapping)

0 commit comments

Comments
 (0)