Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support nested generic type parameters for generic mappings #1199

Merged
merged 1 commit into from Mar 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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