Skip to content

Commit

Permalink
chore: separate mapping body builders (#225)
Browse files Browse the repository at this point in the history
  • Loading branch information
latonz authored Jan 12, 2023
1 parent 82c7844 commit 9f4f69b
Show file tree
Hide file tree
Showing 8 changed files with 473 additions and 449 deletions.
7 changes: 4 additions & 3 deletions src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Configuration;
using Riok.Mapperly.Descriptors.MappingBodyBuilder;
using Riok.Mapperly.Descriptors.MappingBuilder;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Descriptors.ObjectFactories;
Expand Down Expand Up @@ -183,13 +184,13 @@ private void BuildMappingBodies()
switch (typeMapping)
{
case NewInstanceObjectPropertyMapping mapping:
NewInstanceObjectPropertyMappingBuilder.BuildMappingBody(ctx, mapping);
NewInstanceObjectPropertyMappingBodyBuilder.BuildMappingBody(ctx, mapping);
break;
case ObjectPropertyMapping mapping:
ObjectPropertyMappingBuilder.BuildMappingBody(ctx, mapping);
ObjectPropertyMappingBodyBuilder.BuildMappingBody(ctx, mapping);
break;
case UserDefinedNewInstanceMethodMapping mapping:
UserMethodMappingBuilder.BuildMappingBody(ctx, mapping);
UserMethodMappingBodyBuilder.BuildMappingBody(ctx, mapping);
break;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Descriptors.MappingBuilder;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Descriptors.Mappings.PropertyMappings;
using Riok.Mapperly.Diagnostics;
using Riok.Mapperly.Helpers;

namespace Riok.Mapperly.Descriptors.MappingBodyBuilder;

public static class NewInstanceObjectPropertyMappingBodyBuilder
{
public static void BuildMappingBody(MappingBuilderContext ctx, NewInstanceObjectPropertyMapping mapping)
{
var mappingCtx = new NewInstanceMappingBuilderContext(ctx, mapping);

// map constructor
if (TryBuildConstructorMapping(mappingCtx, out var mappedTargetPropertyNames))
{
mappingCtx.TargetProperties.RemoveRange(mappedTargetPropertyNames);
}
else
{
ctx.ReportDiagnostic(DiagnosticDescriptors.NoConstructorFound, ctx.Target);
}

BuildInitOnlyPropertyMappings(mappingCtx);
ObjectPropertyMappingBodyBuilder.BuildMappingBody(mappingCtx);
}

private static void BuildInitOnlyPropertyMappings(NewInstanceMappingBuilderContext ctx)
{
var initOnlyTargetProperties = ctx.TargetProperties.Values.Where(x => x.IsInitOnly() || x.IsRequired()).ToArray();
foreach (var targetProperty in initOnlyTargetProperties)
{
ctx.TargetProperties.Remove(targetProperty.Name);

if (ctx.PropertyConfigsByRootTargetName.Remove(targetProperty.Name, out var propertyConfigs))
{
BuildInitPropertyMapping(ctx, targetProperty, propertyConfigs);
continue;
}

if (!PropertyPath.TryFind(
ctx.Mapping.SourceType,
MemberPathCandidateBuilder.BuildMemberPathCandidates(targetProperty.Name),
ctx.IgnoredSourcePropertyNames,
out var sourcePropertyPath))
{
ctx.BuilderContext.ReportDiagnostic(
targetProperty.IsRequired()
? DiagnosticDescriptors.RequiredPropertyNotMapped
: DiagnosticDescriptors.MappingSourcePropertyNotFound,
targetProperty.Name,
ctx.Mapping.SourceType);
continue;
}

BuildInitPropertyMapping(ctx, targetProperty, sourcePropertyPath);
}
}

private static void BuildInitPropertyMapping(
NewInstanceMappingBuilderContext ctx,
IPropertySymbol targetProperty,
IReadOnlyCollection<MapPropertyAttribute> propertyConfigs)
{
// add configured mapping
// target paths are not supported (yet), only target properties
if (propertyConfigs.Count > 1)
{
ctx.BuilderContext.ReportDiagnostic(
DiagnosticDescriptors.MultipleConfigurationsForInitOnlyProperty,
targetProperty.Type,
targetProperty.Name);
}

var propertyConfig = propertyConfigs.First();
if (propertyConfig.Target.Count > 1)
{
ctx.BuilderContext.ReportDiagnostic(
DiagnosticDescriptors.InitOnlyPropertyDoesNotSupportPaths,
targetProperty.Type,
string.Join(".", propertyConfig.Target));
return;
}

if (!PropertyPath.TryFind(
ctx.Mapping.SourceType,
propertyConfig.Source,
out var sourcePropertyPath))
{
ctx.BuilderContext.ReportDiagnostic(
DiagnosticDescriptors.MappingSourcePropertyNotFound,
targetProperty.Name,
ctx.Mapping.SourceType);
return;
}

BuildInitPropertyMapping(ctx, targetProperty, sourcePropertyPath);
}

private static void BuildInitPropertyMapping(
NewInstanceMappingBuilderContext ctx,
IPropertySymbol targetProperty,
PropertyPath sourcePath)
{
var targetPath = new PropertyPath(new[] { targetProperty });
if (!ObjectPropertyMappingBodyBuilder.ValidateMappingSpecification(ctx, sourcePath, targetPath, true))
return;

var delegateMapping = ctx.BuilderContext.FindMapping(sourcePath.MemberType, targetProperty.Type)
?? ctx.BuilderContext.FindOrBuildMapping(sourcePath.MemberType.NonNullable(), targetProperty.Type);

if (delegateMapping == null)
{
ctx.BuilderContext.ReportDiagnostic(
DiagnosticDescriptors.CouldNotMapProperty,
ctx.Mapping.SourceType,
sourcePath.FullName,
sourcePath.Member.Type,
ctx.Mapping.TargetType,
targetPath.FullName,
targetPath.Member.Type);
return;
}

var propertyMapping = new NullPropertyMapping(
delegateMapping,
sourcePath,
ctx.BuilderContext.GetNullFallbackValue(targetProperty.Type));
var propertyAssignmentMapping = new PropertyAssignmentMapping(
targetPath,
propertyMapping);
ctx.AddInitPropertyMapping(propertyAssignmentMapping);
}

private static bool TryBuildConstructorMapping(
NewInstanceMappingBuilderContext ctx,
[NotNullWhen(true)] out ISet<string>? mappedTargetPropertyNames)
{
if (ctx.Mapping.TargetType is not INamedTypeSymbol namedTargetType)
{
mappedTargetPropertyNames = null;
return false;
}

// attributed ctor is prio 1
// parameterless ctor is prio 2
// then by descending parameter count
// ctors annotated with [Obsolete] are considered last unless they have a MapperConstructor attribute set
var ctorCandidates = namedTargetType.Constructors
.Where(ctor => ctor.IsAccessible())
.OrderByDescending(x => x.HasAttribute(ctx.BuilderContext.Types.MapperConstructorAttribute))
.ThenBy(x => x.HasAttribute(ctx.BuilderContext.Types.ObsoleteAttribute))
.ThenByDescending(x => x.Parameters.Length == 0)
.ThenByDescending(x => x.Parameters.Length);
foreach (var ctorCandidate in ctorCandidates)
{
if (!TryBuildConstructorMapping(
ctx,
ctorCandidate,
out mappedTargetPropertyNames,
out var constructorParameterMappings))
{
if (ctorCandidate.HasAttribute(ctx.BuilderContext.Types.MapperConstructorAttribute))
{
ctx.BuilderContext.ReportDiagnostic(
DiagnosticDescriptors.CannotMapToConfiguredConstructor,
ctx.Mapping.SourceType,
ctorCandidate);
}

continue;
}

foreach (var constructorParameterMapping in constructorParameterMappings)
{
ctx.AddConstructorParameterMapping(constructorParameterMapping);
}

return true;
}

mappedTargetPropertyNames = null;
return false;
}

private static bool TryBuildConstructorMapping(
NewInstanceMappingBuilderContext ctx,
IMethodSymbol ctor,
[NotNullWhen(true)] out ISet<string>? mappedTargetPropertyNames,
[NotNullWhen(true)] out ISet<ConstructorParameterMapping>? constructorParameterMappings)
{
constructorParameterMappings = new HashSet<ConstructorParameterMapping>();
mappedTargetPropertyNames = new HashSet<string>();
var skippedOptionalParam = false;
foreach (var parameter in ctor.Parameters)
{
if (!PropertyPath.TryFind(
ctx.Mapping.SourceType,
MemberPathCandidateBuilder.BuildMemberPathCandidates(parameter.Name),
ctx.IgnoredSourcePropertyNames,
StringComparer.OrdinalIgnoreCase,
out var sourcePath))
{
if (!parameter.IsOptional)
return false;

skippedOptionalParam = true;
continue;
}

// nullability is handled inside the property mapping
var paramType = parameter.Type.WithNullableAnnotation(parameter.NullableAnnotation);
var delegateMapping = ctx.BuilderContext.FindMapping(sourcePath.MemberType, paramType)
?? ctx.BuilderContext.FindOrBuildMapping(sourcePath.Member.Type.NonNullable(), paramType);
if (delegateMapping == null)
{
if (!parameter.IsOptional)
return false;

skippedOptionalParam = true;
continue;
}

var propertyMapping = new NullPropertyMapping(delegateMapping, sourcePath, ctx.BuilderContext.GetNullFallbackValue(paramType));
var ctorMapping = new ConstructorParameterMapping(parameter, propertyMapping, skippedOptionalParam);
constructorParameterMappings.Add(ctorMapping);
mappedTargetPropertyNames.Add(parameter.Name);
}

return true;
}
}
Loading

0 comments on commit 9f4f69b

Please sign in to comment.