diff --git a/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs b/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs index 35c7e58dd1..f230ac0996 100644 --- a/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs @@ -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; @@ -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; } } diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilder/NewInstanceObjectPropertyMappingBodyBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilder/NewInstanceObjectPropertyMappingBodyBuilder.cs new file mode 100644 index 0000000000..12c27f3224 --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilder/NewInstanceObjectPropertyMappingBodyBuilder.cs @@ -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 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? 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? mappedTargetPropertyNames, + [NotNullWhen(true)] out ISet? constructorParameterMappings) + { + constructorParameterMappings = new HashSet(); + mappedTargetPropertyNames = new HashSet(); + 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; + } +} diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilder/ObjectPropertyMappingBodyBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilder/ObjectPropertyMappingBodyBuilder.cs new file mode 100644 index 0000000000..89dec0d4e7 --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilder/ObjectPropertyMappingBodyBuilder.cs @@ -0,0 +1,207 @@ +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 ObjectPropertyMappingBodyBuilder +{ + public static void BuildMappingBody(MappingBuilderContext ctx, ObjectPropertyMapping mapping) + { + var mappingCtx = new ObjectPropertyMappingBuilderContext(ctx, mapping); + BuildMappingBody(mappingCtx); + } + + public static void BuildMappingBody(ObjectPropertyMappingBuilderContext ctx) + { + var propertyNameComparer = ctx.BuilderContext.MapperConfiguration.PropertyNameMappingStrategy == + PropertyNameMappingStrategy.CaseSensitive + ? StringComparer.Ordinal + : StringComparer.OrdinalIgnoreCase; + + foreach (var targetProperty in ctx.TargetProperties.Values) + { + if (ctx.PropertyConfigsByRootTargetName.Remove(targetProperty.Name, out var propertyConfigs)) + { + // add all configured mappings + // order by target path count to map less nested items first (otherwise they would overwrite all others) + // eg. target.A = source.B should be mapped before target.A.Id = source.B.Id + foreach (var config in propertyConfigs.OrderBy(x => x.Target.Count)) + { + BuildPropertyAssignmentMapping(ctx, config); + } + + continue; + } + + if (!PropertyPath.TryFind( + ctx.Mapping.SourceType, + MemberPathCandidateBuilder.BuildMemberPathCandidates(targetProperty.Name), + ctx.IgnoredSourcePropertyNames, + propertyNameComparer, + out var sourcePropertyPath)) + { + ctx.BuilderContext.ReportDiagnostic( + DiagnosticDescriptors.MappingSourcePropertyNotFound, + targetProperty.Name, + ctx.Mapping.SourceType); + continue; + } + + BuildPropertyAssignmentMapping(ctx, sourcePropertyPath, new PropertyPath(new[] { targetProperty })); + } + + ctx.AddDiagnostics(); + } + + private static void BuildPropertyAssignmentMapping(ObjectPropertyMappingBuilderContext ctx, MapPropertyAttribute config) + { + if (!PropertyPath.TryFind(ctx.Mapping.TargetType, config.Target, out var targetPropertyPath)) + { + ctx.BuilderContext.ReportDiagnostic( + DiagnosticDescriptors.ConfiguredMappingTargetPropertyNotFound, + string.Join(PropertyPath.PropertyAccessSeparator, config.Target), + ctx.Mapping.TargetType); + return; + } + + if (!PropertyPath.TryFind(ctx.Mapping.SourceType, config.Source, out var sourcePropertyPath)) + { + ctx.BuilderContext.ReportDiagnostic( + DiagnosticDescriptors.ConfiguredMappingSourcePropertyNotFound, + string.Join(PropertyPath.PropertyAccessSeparator, config.Source), + ctx.Mapping.SourceType); + return; + } + + BuildPropertyAssignmentMapping(ctx, sourcePropertyPath, targetPropertyPath); + } + + public static bool ValidateMappingSpecification( + ObjectPropertyMappingBuilderContext ctx, + PropertyPath sourcePropertyPath, + PropertyPath targetPropertyPath, + bool allowInitOnlyMember = false) + { + // the target property path is readonly or not accessible + if (targetPropertyPath.Member.IsReadOnly || targetPropertyPath.Member.SetMethod?.IsAccessible() != true) + { + ctx.BuilderContext.ReportDiagnostic( + DiagnosticDescriptors.CannotMapToReadOnlyProperty, + ctx.Mapping.SourceType, + sourcePropertyPath.FullName, + sourcePropertyPath.Member.Type, + ctx.Mapping.TargetType, + targetPropertyPath.FullName, + targetPropertyPath.Member.Type); + return false; + } + + // a target property path part is write only or not accessible + if (targetPropertyPath.ObjectPath.Any(p => p.IsWriteOnly || p.GetMethod?.IsAccessible() != true)) + { + ctx.BuilderContext.ReportDiagnostic( + DiagnosticDescriptors.CannotMapToWriteOnlyPropertyPath, + ctx.Mapping.SourceType, + sourcePropertyPath.FullName, + sourcePropertyPath.Member.Type, + ctx.Mapping.TargetType, + targetPropertyPath.FullName, + targetPropertyPath.Member.Type); + return false; + } + + // a target property path part is init only + var noInitOnlyPath = allowInitOnlyMember ? targetPropertyPath.ObjectPath : targetPropertyPath.Path; + if (noInitOnlyPath.Any(p => p.IsInitOnly())) + { + ctx.BuilderContext.ReportDiagnostic( + DiagnosticDescriptors.CannotMapToInitOnlyPropertyPath, + ctx.Mapping.SourceType, + sourcePropertyPath.FullName, + sourcePropertyPath.Member.Type, + ctx.Mapping.TargetType, + targetPropertyPath.FullName, + targetPropertyPath.Member.Type); + return false; + } + + // a source property path is write only or not accessible + if (sourcePropertyPath.Path.Any(p => p.IsWriteOnly || p.GetMethod?.IsAccessible() != true)) + { + ctx.BuilderContext.ReportDiagnostic( + DiagnosticDescriptors.CannotMapFromWriteOnlyProperty, + ctx.Mapping.SourceType, + sourcePropertyPath.FullName, + sourcePropertyPath.Member.Type, + ctx.Mapping.TargetType, + targetPropertyPath.FullName, + targetPropertyPath.Member.Type); + return false; + } + + return true; + } + + private static void BuildPropertyAssignmentMapping( + ObjectPropertyMappingBuilderContext ctx, + PropertyPath sourcePropertyPath, + PropertyPath targetPropertyPath) + { + if (!ValidateMappingSpecification(ctx, sourcePropertyPath, targetPropertyPath)) + return; + + // nullability is handled inside the property mapping + var delegateMapping = ctx.BuilderContext.FindMapping(sourcePropertyPath.Member.Type, targetPropertyPath.Member.Type) + ?? ctx.BuilderContext.FindOrBuildMapping(sourcePropertyPath.Member.Type.NonNullable(), + targetPropertyPath.Member.Type.NonNullable()); + + // couldn't build the mapping + if (delegateMapping == null) + { + ctx.BuilderContext.ReportDiagnostic( + DiagnosticDescriptors.CouldNotMapProperty, + ctx.Mapping.SourceType, + sourcePropertyPath.FullName, + sourcePropertyPath.Member.Type, + ctx.Mapping.TargetType, + targetPropertyPath.FullName, + targetPropertyPath.Member.Type); + return; + } + + // no member of the source path is nullable, no null handling needed + if (!sourcePropertyPath.IsAnyNullable()) + { + var propertyMapping = new PropertyMapping( + delegateMapping, + sourcePropertyPath, + false, + true); + ctx.AddPropertyAssignmentMapping(new PropertyAssignmentMapping(targetPropertyPath, propertyMapping)); + return; + } + + // the source is nullable, or the mapping is a direct assignment and the target allows nulls + // access the source in a null save matter (via ?.) but no other special handling required. + if (delegateMapping.SourceType.IsNullable() || delegateMapping.IsSynthetic && targetPropertyPath.Member.IsNullable()) + { + var propertyMapping = new PropertyMapping( + delegateMapping, + sourcePropertyPath, + true, + false); + ctx.AddPropertyAssignmentMapping(new PropertyAssignmentMapping(targetPropertyPath, propertyMapping)); + return; + } + + // additional null condition check + // (only map if source is not null, else may throw depending on settings) + ctx.AddNullDelegatePropertyAssignmentMapping(new PropertyAssignmentMapping( + targetPropertyPath, + new PropertyMapping(delegateMapping, sourcePropertyPath, false, true))); + } +} diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilder/UserMethodMappingBodyBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilder/UserMethodMappingBodyBuilder.cs new file mode 100644 index 0000000000..f5cf80e47a --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilder/UserMethodMappingBodyBuilder.cs @@ -0,0 +1,24 @@ +using Riok.Mapperly.Descriptors.Mappings; +using Riok.Mapperly.Diagnostics; + +namespace Riok.Mapperly.Descriptors.MappingBodyBuilder; + +public static class UserMethodMappingBodyBuilder +{ + public static void BuildMappingBody(MappingBuilderContext ctx, UserDefinedNewInstanceMethodMapping mapping) + { + var delegateMapping = mapping.CallableByOtherMappings + ? ctx.BuildDelegateMapping(mapping.SourceType, mapping.TargetType) + : ctx.BuildMappingWithUserSymbol(mapping.SourceType, mapping.TargetType); + if (delegateMapping != null) + { + mapping.SetDelegateMapping(delegateMapping); + return; + } + + ctx.ReportDiagnostic( + DiagnosticDescriptors.CouldNotCreateMapping, + mapping.SourceType, + mapping.TargetType); + } +} diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/DictionaryMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/DictionaryMappingBuilder.cs index ee9acc4a08..36b68bde60 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilder/DictionaryMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/DictionaryMappingBuilder.cs @@ -7,7 +7,7 @@ namespace Riok.Mapperly.Descriptors.MappingBuilder; public static class DictionaryMappingBuilder { - private static readonly string _countPropertyName = "Count"; + private const string CountPropertyName = nameof(IDictionary.Count); public static TypeMapping? TryBuildMapping(MappingBuilderContext ctx) { @@ -29,7 +29,7 @@ public static class DictionaryMappingBuilder // The constructed type should be Dictionary<,> if (IsDictionaryType(ctx, ctx.Target)) { - var sourceHasCount = ctx.Source.GetAllMembers(_countPropertyName) + var sourceHasCount = ctx.Source.GetAllMembers(CountPropertyName) .OfType() .Any(x => !x.IsStatic && !x.IsIndexer && !x.IsWriteOnly && x.Type.SpecialType == SpecialType.System_Int32); diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/NewInstanceObjectPropertyMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/NewInstanceObjectPropertyMappingBuilder.cs index 8bf53bd9a7..8520d445ac 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilder/NewInstanceObjectPropertyMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/NewInstanceObjectPropertyMappingBuilder.cs @@ -1,9 +1,5 @@ -using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis; -using Riok.Mapperly.Abstractions; using Riok.Mapperly.Descriptors.Mappings; -using Riok.Mapperly.Descriptors.Mappings.PropertyMappings; -using Riok.Mapperly.Diagnostics; using Riok.Mapperly.Helpers; namespace Riok.Mapperly.Descriptors.MappingBuilder; @@ -26,227 +22,4 @@ public static class NewInstanceObjectPropertyMappingBuilder return new NewInstanceObjectPropertyMapping(ctx.Source, ctx.Target.NonNullable(), ctx.MapperConfiguration.UseReferenceHandling); } - - 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); - ObjectPropertyMappingBuilder.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 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 (!ObjectPropertyMappingBuilder.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? 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? mappedTargetPropertyNames, - [NotNullWhen(true)] out ISet? constructorParameterMappings) - { - constructorParameterMappings = new HashSet(); - mappedTargetPropertyNames = new HashSet(); - 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; - } } diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/ObjectPropertyMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/ObjectPropertyMappingBuilder.cs index 9b944d8999..fa95f5974f 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilder/ObjectPropertyMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/ObjectPropertyMappingBuilder.cs @@ -1,206 +1,6 @@ -using Riok.Mapperly.Abstractions; -using Riok.Mapperly.Descriptors.Mappings; -using Riok.Mapperly.Descriptors.Mappings.PropertyMappings; -using Riok.Mapperly.Diagnostics; -using Riok.Mapperly.Helpers; - namespace Riok.Mapperly.Descriptors.MappingBuilder; public static class ObjectPropertyMappingBuilder { - public static void BuildMappingBody(MappingBuilderContext ctx, ObjectPropertyMapping mapping) - { - var mappingCtx = new ObjectPropertyMappingBuilderContext(ctx, mapping); - BuildMappingBody(mappingCtx); - } - - public static void BuildMappingBody(ObjectPropertyMappingBuilderContext ctx) - { - var propertyNameComparer = ctx.BuilderContext.MapperConfiguration.PropertyNameMappingStrategy == - PropertyNameMappingStrategy.CaseSensitive - ? StringComparer.Ordinal - : StringComparer.OrdinalIgnoreCase; - - foreach (var targetProperty in ctx.TargetProperties.Values) - { - if (ctx.PropertyConfigsByRootTargetName.Remove(targetProperty.Name, out var propertyConfigs)) - { - // add all configured mappings - // order by target path count to map less nested items first (otherwise they would overwrite all others) - // eg. target.A = source.B should be mapped before target.A.Id = source.B.Id - foreach (var config in propertyConfigs.OrderBy(x => x.Target.Count)) - { - BuildPropertyAssignmentMapping(ctx, config); - } - - continue; - } - - if (!PropertyPath.TryFind( - ctx.Mapping.SourceType, - MemberPathCandidateBuilder.BuildMemberPathCandidates(targetProperty.Name), - ctx.IgnoredSourcePropertyNames, - propertyNameComparer, - out var sourcePropertyPath)) - { - ctx.BuilderContext.ReportDiagnostic( - DiagnosticDescriptors.MappingSourcePropertyNotFound, - targetProperty.Name, - ctx.Mapping.SourceType); - continue; - } - - BuildPropertyAssignmentMapping(ctx, sourcePropertyPath, new PropertyPath(new[] { targetProperty })); - } - - ctx.AddDiagnostics(); - } - - private static void BuildPropertyAssignmentMapping(ObjectPropertyMappingBuilderContext ctx, MapPropertyAttribute config) - { - if (!PropertyPath.TryFind(ctx.Mapping.TargetType, config.Target, out var targetPropertyPath)) - { - ctx.BuilderContext.ReportDiagnostic( - DiagnosticDescriptors.ConfiguredMappingTargetPropertyNotFound, - string.Join(PropertyPath.PropertyAccessSeparator, config.Target), - ctx.Mapping.TargetType); - return; - } - - if (!PropertyPath.TryFind(ctx.Mapping.SourceType, config.Source, out var sourcePropertyPath)) - { - ctx.BuilderContext.ReportDiagnostic( - DiagnosticDescriptors.ConfiguredMappingSourcePropertyNotFound, - string.Join(PropertyPath.PropertyAccessSeparator, config.Source), - ctx.Mapping.SourceType); - return; - } - - BuildPropertyAssignmentMapping(ctx, sourcePropertyPath, targetPropertyPath); - } - - public static bool ValidateMappingSpecification( - ObjectPropertyMappingBuilderContext ctx, - PropertyPath sourcePropertyPath, - PropertyPath targetPropertyPath, - bool allowInitOnlyMember = false) - { - // the target property path is readonly or not accessible - if (targetPropertyPath.Member.IsReadOnly || targetPropertyPath.Member.SetMethod?.IsAccessible() != true) - { - ctx.BuilderContext.ReportDiagnostic( - DiagnosticDescriptors.CannotMapToReadOnlyProperty, - ctx.Mapping.SourceType, - sourcePropertyPath.FullName, - sourcePropertyPath.Member.Type, - ctx.Mapping.TargetType, - targetPropertyPath.FullName, - targetPropertyPath.Member.Type); - return false; - } - - // a target property path part is write only or not accessible - if (targetPropertyPath.ObjectPath.Any(p => p.IsWriteOnly || p.GetMethod?.IsAccessible() != true)) - { - ctx.BuilderContext.ReportDiagnostic( - DiagnosticDescriptors.CannotMapToWriteOnlyPropertyPath, - ctx.Mapping.SourceType, - sourcePropertyPath.FullName, - sourcePropertyPath.Member.Type, - ctx.Mapping.TargetType, - targetPropertyPath.FullName, - targetPropertyPath.Member.Type); - return false; - } - - // a target property path part is init only - var noInitOnlyPath = allowInitOnlyMember ? targetPropertyPath.ObjectPath : targetPropertyPath.Path; - if (noInitOnlyPath.Any(p => p.IsInitOnly())) - { - ctx.BuilderContext.ReportDiagnostic( - DiagnosticDescriptors.CannotMapToInitOnlyPropertyPath, - ctx.Mapping.SourceType, - sourcePropertyPath.FullName, - sourcePropertyPath.Member.Type, - ctx.Mapping.TargetType, - targetPropertyPath.FullName, - targetPropertyPath.Member.Type); - return false; - } - - // a source property path is write only or not accessible - if (sourcePropertyPath.Path.Any(p => p.IsWriteOnly || p.GetMethod?.IsAccessible() != true)) - { - ctx.BuilderContext.ReportDiagnostic( - DiagnosticDescriptors.CannotMapFromWriteOnlyProperty, - ctx.Mapping.SourceType, - sourcePropertyPath.FullName, - sourcePropertyPath.Member.Type, - ctx.Mapping.TargetType, - targetPropertyPath.FullName, - targetPropertyPath.Member.Type); - return false; - } - - return true; - } - - private static void BuildPropertyAssignmentMapping( - ObjectPropertyMappingBuilderContext ctx, - PropertyPath sourcePropertyPath, - PropertyPath targetPropertyPath) - { - if (!ValidateMappingSpecification(ctx, sourcePropertyPath, targetPropertyPath)) - return; - - // nullability is handled inside the property mapping - var delegateMapping = ctx.BuilderContext.FindMapping(sourcePropertyPath.Member.Type, targetPropertyPath.Member.Type) - ?? ctx.BuilderContext.FindOrBuildMapping(sourcePropertyPath.Member.Type.NonNullable(), - targetPropertyPath.Member.Type.NonNullable()); - - // couldn't build the mapping - if (delegateMapping == null) - { - ctx.BuilderContext.ReportDiagnostic( - DiagnosticDescriptors.CouldNotMapProperty, - ctx.Mapping.SourceType, - sourcePropertyPath.FullName, - sourcePropertyPath.Member.Type, - ctx.Mapping.TargetType, - targetPropertyPath.FullName, - targetPropertyPath.Member.Type); - return; - } - - // no member of the source path is nullable, no null handling needed - if (!sourcePropertyPath.IsAnyNullable()) - { - var propertyMapping = new PropertyMapping( - delegateMapping, - sourcePropertyPath, - false, - true); - ctx.AddPropertyAssignmentMapping(new PropertyAssignmentMapping(targetPropertyPath, propertyMapping)); - return; - } - - // the source is nullable, or the mapping is a direct assignment and the target allows nulls - // access the source in a null save matter (via ?.) but no other special handling required. - if (delegateMapping.SourceType.IsNullable() || delegateMapping.IsSynthetic && targetPropertyPath.Member.IsNullable()) - { - var propertyMapping = new PropertyMapping( - delegateMapping, - sourcePropertyPath, - true, - false); - ctx.AddPropertyAssignmentMapping(new PropertyAssignmentMapping(targetPropertyPath, propertyMapping)); - return; - } - // additional null condition check - // (only map if source is not null, else may throw depending on settings) - ctx.AddNullDelegatePropertyAssignmentMapping(new PropertyAssignmentMapping( - targetPropertyPath, - new PropertyMapping(delegateMapping, sourcePropertyPath, false, true))); - } } diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/UserMethodMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/UserMethodMappingBuilder.cs index a4d0cc1b41..7bb1c1255a 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilder/UserMethodMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/UserMethodMappingBuilder.cs @@ -38,23 +38,6 @@ public static IEnumerable ExtractUserMappings( } } - public static void BuildMappingBody(MappingBuilderContext ctx, UserDefinedNewInstanceMethodMapping mapping) - { - var delegateMapping = mapping.CallableByOtherMappings - ? ctx.BuildDelegateMapping(mapping.SourceType, mapping.TargetType) - : ctx.BuildMappingWithUserSymbol(mapping.SourceType, mapping.TargetType); - if (delegateMapping != null) - { - mapping.SetDelegateMapping(delegateMapping); - return; - } - - ctx.ReportDiagnostic( - DiagnosticDescriptors.CouldNotCreateMapping, - mapping.SourceType, - mapping.TargetType); - } - private static IEnumerable ExtractMethods(ITypeSymbol mapperSymbol) => mapperSymbol.GetMembers().OfType();