Skip to content

Commit

Permalink
feat: add MapDerivedType for existing target type mapping
Browse files Browse the repository at this point in the history
  • Loading branch information
TimothyMakkison committed Nov 23, 2023
1 parent 980ac3e commit 9673fbb
Show file tree
Hide file tree
Showing 13 changed files with 492 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Configuration;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Descriptors.Mappings.ExistingTarget;
using Riok.Mapperly.Diagnostics;
using Riok.Mapperly.Helpers;

Expand All @@ -19,24 +20,47 @@ public static class DerivedTypeMappingBuilder
: new DerivedTypeSwitchMapping(ctx.Source, ctx.Target, derivedTypeMappings);
}

public static IExistingTargetMapping? TryBuildExistingTargetMapping(MappingBuilderContext ctx)
{
var derivedTypeMappings = TryBuildExistingTargetContainedMappings(ctx);
return derivedTypeMappings == null ? null : new DerivedExistingTargetTypeSwitchMapping(ctx.Source, ctx.Target, derivedTypeMappings);
}

public static IReadOnlyCollection<INewInstanceMapping>? TryBuildContainedMappings(
MappingBuilderContext ctx,
bool duplicatedSourceTypesAllowed = false
)
{
return ctx.Configuration.DerivedTypes.Count == 0
? null
: BuildContainedMappings(ctx, ctx.Configuration.DerivedTypes, duplicatedSourceTypesAllowed);
: BuildContainedMappings(ctx, ctx.Configuration.DerivedTypes, ctx.FindOrBuildMapping, duplicatedSourceTypesAllowed);
}

private static IReadOnlyCollection<IExistingTargetMapping>? TryBuildExistingTargetContainedMappings(
MappingBuilderContext ctx,
bool duplicatedSourceTypesAllowed = false
)
{
return ctx.Configuration.DerivedTypes.Count == 0
? null
: BuildContainedMappings(
ctx,
ctx.Configuration.DerivedTypes,
(source, target, options, _) => ctx.FindOrBuildExistingTargetMapping(source, target, options),
duplicatedSourceTypesAllowed
);
}

private static IReadOnlyCollection<INewInstanceMapping> BuildContainedMappings(
private static IReadOnlyCollection<TMapping> BuildContainedMappings<TMapping>(
MappingBuilderContext ctx,
IReadOnlyCollection<DerivedTypeMappingConfiguration> configs,
Func<ITypeSymbol, ITypeSymbol, MappingBuildingOptions, Location?, TMapping?> mappingFunction,
bool duplicatedSourceTypesAllowed
)
where TMapping : ITypeMapping
{
var derivedTypeMappingSourceTypes = new HashSet<ITypeSymbol>(SymbolEqualityComparer.Default);
var derivedTypeMappings = new List<INewInstanceMapping>(configs.Count);
var derivedTypeMappings = new List<TMapping>(configs.Count);
Func<ITypeSymbol, bool> isAssignableToSource = ctx.Source is ITypeParameterSymbol sourceTypeParameter
? t => ctx.SymbolAccessor.DoesTypeSatisfyTypeParameterConstraints(sourceTypeParameter, t, ctx.Source.NullableAnnotation)
: t => ctx.SymbolAccessor.HasImplicitConversion(t, ctx.Source);
Expand Down Expand Up @@ -67,7 +91,7 @@ bool duplicatedSourceTypesAllowed
continue;
}

var mapping = ctx.FindOrBuildMapping(
var mapping = mappingFunction(
sourceType,
targetType,
MappingBuildingOptions.KeepUserSymbol | MappingBuildingOptions.MarkAsReusable | MappingBuildingOptions.ClearDerivedTypes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public class ExistingTargetMappingBuilder(MappingCollection mappings)
private static readonly IReadOnlyCollection<BuildExistingTargetMapping> _builders = new BuildExistingTargetMapping[]
{
NullableMappingBuilder.TryBuildExistingTargetMapping,
DerivedTypeMappingBuilder.TryBuildExistingTargetMapping,
DictionaryMappingBuilder.TryBuildExistingTargetMapping,
SpanMappingBuilder.TryBuildExistingTargetMapping,
MemoryMappingBuilder.TryBuildExistingTargetMapping,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Riok.Mapperly.Descriptors.Mappings.ExistingTarget;
using Riok.Mapperly.Emit.Syntax;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper;

namespace Riok.Mapperly.Descriptors.Mappings;

/// <summary>
/// A derived type mapping maps one base type or interface to another
/// by implementing a switch statement over known types and performs the provided mapping for each type.
/// </summary>
public class DerivedExistingTargetTypeSwitchMapping (ITypeSymbol sourceType,
ITypeSymbol targetType,
IReadOnlyCollection<IExistingTargetMapping> existingTargetTypeMappings)
: ExistingTargetMapping(sourceType, targetType)
{
private const string SourceName = "source";
private const string TargetName = "target";
private const string GetTypeMethodName = nameof(GetType);

public override IEnumerable<StatementSyntax> Build(TypeMappingBuildContext ctx, ExpressionSyntax target)
{
var sourceExpression = TupleExpression(CommaSeparatedList(Argument(ctx.Source), Argument(target)));
var caseSections = existingTargetTypeMappings.Select(x => BuildSwitchSection(ctx, x));
var defaultSection = BuildDefaultSwitchSection(ctx, target);

yield return ctx.SyntaxFactory.SwitchStatement(sourceExpression, caseSections, defaultSection).AddLeadingLineFeed(ctx.SyntaxFactory.Indentation);
}

private SwitchSectionSyntax BuildSwitchSection(TypeMappingBuildContext ctx, IExistingTargetMapping mapping)
{
var (sectionCtx, sourceVariableName) = ctx.WithNewScopedSource(IdentifierName, SourceName);
var targetVariableName = sectionCtx.NameBuilder.New(TargetName);
sectionCtx = sectionCtx.AddIndentation();

// (A source, B target)
var positionalTypeMatch = PositionalPatternClause(
CommaSeparatedList(
Subpattern(DeclarationPattern(mapping.SourceType, sourceVariableName)),
Subpattern(DeclarationPattern(mapping.TargetType, targetVariableName))
)
);
var pattern = RecursivePattern().WithPositionalPatternClause(positionalTypeMatch);

// case (A source, B target):
var caseLabel = CasePatternSwitchLabel(pattern).AddLeadingLineFeed(sectionCtx.SyntaxFactory.Indentation);

// break;
var statementContext = sectionCtx.AddIndentation();
var breakStatement = BreakStatement().AddLeadingLineFeed(statementContext.SyntaxFactory.Indentation);
var target = IdentifierName(targetVariableName);
var statements = mapping.Build(statementContext, target).Append(breakStatement);

return SwitchSection(caseLabel, statements);
}

private SwitchSectionSyntax BuildDefaultSwitchSection(TypeMappingBuildContext ctx, ExpressionSyntax target)
{
// default:
var sectionCtx = ctx.SyntaxFactory.AddIndentation();
var defaultCaseLabel = DefaultSwitchLabel().AddLeadingLineFeed(sectionCtx.Indentation);

// throw new ArgumentException(msg, nameof(ctx.Source)),
var sourceType = Invocation(MemberAccess(ctx.Source, GetTypeMethodName));
var targetType = Invocation(MemberAccess(target, GetTypeMethodName));
var statementContext = sectionCtx.AddIndentation();
var throwExpression = ThrowArgumentExpression(
InterpolatedString($"Cannot map {sourceType} to {targetType} as there is no known derived type mapping"),
ctx.Source
)
.AddLeadingLineFeed(statementContext.Indentation);

var statements = new StatementSyntax[] { ExpressionStatement(throwExpression) };

return SwitchSection(defaultCaseLabel, statements);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,13 @@ SyntaxFactoryHelper syntaxFactory
/// </summary>
/// <param name="sourceBuilder">A function to build the source access for the new context.</param>
/// <returns>The new context and the scoped name of the source.</returns>
public (TypeMappingBuildContext Context, string SourceName) WithNewScopedSource(Func<string, ExpressionSyntax> sourceBuilder)
public (TypeMappingBuildContext Context, string SourceName) WithNewScopedSource(
Func<string, ExpressionSyntax> sourceBuilder,
string sourceName = DefaultSourceName
)
{
var scopedNameBuilder = NameBuilder.NewScope();
var scopedSourceName = scopedNameBuilder.New(DefaultSourceName);
var scopedSourceName = scopedNameBuilder.New(sourceName);
var ctx = new TypeMappingBuildContext(sourceBuilder(scopedSourceName), ReferenceHandler, scopedNameBuilder, SyntaxFactory);
return (ctx, scopedSourceName);
}
Expand Down
7 changes: 7 additions & 0 deletions src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Pattern.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Riok.Mapperly.Helpers;
Expand All @@ -16,6 +17,12 @@ public static PatternSyntax OrPattern(IEnumerable<ExpressionSyntax?> values) =>
public static IsPatternExpressionSyntax IsPattern(ExpressionSyntax expression, PatternSyntax pattern) =>
IsPatternExpression(expression, SpacedToken(SyntaxKind.IsKeyword), pattern);

public static DeclarationPatternSyntax DeclarationPattern(ITypeSymbol type, string designation) =>
SyntaxFactory.DeclarationPattern(
FullyQualifiedIdentifier(type).AddTrailingSpace(),
SingleVariableDesignation(Identifier(designation))
);

private static BinaryPatternSyntax BinaryPattern(SyntaxKind kind, PatternSyntax left, PatternSyntax right)
{
var binaryPattern = SyntaxFactory.BinaryPattern(kind, left, right);
Expand Down
24 changes: 24 additions & 0 deletions src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Switch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,28 @@ public static SwitchExpressionArmSyntax SwitchArm(PatternSyntax pattern, Express
}

public static WhenClauseSyntax SwitchWhen(ExpressionSyntax condition) => WhenClause(SpacedToken(SyntaxKind.WhenKeyword), condition);

public SwitchStatementSyntax SwitchStatement(
ExpressionSyntax governingExpression,
IEnumerable<SwitchSectionSyntax> sections,
SwitchSectionSyntax defaultSection
)
{
return SyntaxFactory.SwitchStatement(
default,
TrailingSpacedToken(SyntaxKind.SwitchKeyword),
Token(SyntaxKind.None),
governingExpression,
Token(SyntaxKind.None),
LeadingLineFeedToken(SyntaxKind.OpenBraceToken),
List(sections.Append(defaultSection)),
LeadingLineFeedToken(SyntaxKind.CloseBraceToken)
);
}

public static SwitchSectionSyntax SwitchSection(SwitchLabelSyntax labelSyntax, IEnumerable<StatementSyntax> statements) =>
SyntaxFactory.SwitchSection().WithLabels(SingletonList(labelSyntax)).WithStatements(List(statements));

public static CasePatternSwitchLabelSyntax CasePatternSwitchLabel(PatternSyntax pattern) =>
SyntaxFactory.CasePatternSwitchLabel(TrailingSpacedToken(SyntaxKind.CaseKeyword), pattern, null, Token(SyntaxKind.ColonToken));
}
Loading

0 comments on commit 9673fbb

Please sign in to comment.