From e4f7e9c6598ff7897c902a525427de415ab35282 Mon Sep 17 00:00:00 2001 From: latonz Date: Sun, 21 Jul 2024 17:42:44 -0500 Subject: [PATCH] support additional mapping method parameters Co-authored-by: Qin Guan --- .../additional-mapping-parameters.mdx | 72 ++++ .../analyzer-diagnostics/index.mdx | 2 +- docs/docs/configuration/conversions.md | 2 +- docs/docs/configuration/ctor-mappings.md | 2 +- .../configuration/derived-type-mapping.md | 2 +- docs/docs/configuration/existing-target.mdx | 2 +- docs/docs/configuration/generic-mapping.md | 2 +- docs/docs/configuration/object-factories.md | 2 +- .../configuration/private-member-mapping.md | 2 +- .../configuration/queryable-projections.mdx | 2 +- docs/docs/configuration/reference-handling.md | 2 +- .../user-implemented-methods.mdx | 10 +- src/Riok.Mapperly/AnalyzerReleases.Shipped.md | 3 + .../Descriptors/DescriptorBuilder.cs | 1 + .../Descriptors/Enumerables/CollectionInfo.cs | 9 +- .../Enumerables/CollectionInfoBuilder.cs | 21 +- .../EnsureCapacity/EnsureCapacityBuilder.cs | 8 +- .../EnsureCapacity/EnsureCapacityMember.cs | 8 +- .../EnsureCapacityNonEnumerated.cs | 14 +- .../FormatProviders/FormatProviderBuilder.cs | 4 +- .../InlineExpressionMappingBuilderContext.cs | 2 +- .../Descriptors/MapperDescriptor.cs | 2 +- .../BuilderContext/IMembersBuilderContext.cs | 8 +- .../INewInstanceBuilderContext.cs | 2 +- .../BuilderContext/IgnoredMembersBuilder.cs | 12 +- .../MemberMappingDiagnosticReporter.cs | 9 + .../MembersContainerBuilderContext.cs | 27 +- .../MembersMappingBuilderContext.cs | 67 +++- .../BuilderContext/MembersMappingState.cs | 61 +++- .../MembersMappingStateBuilder.cs | 17 + .../BuilderContext/NestedMappingsContext.cs | 9 +- .../NewInstanceBuilderContext.cs | 2 +- .../NewInstanceContainerBuilderContext.cs | 2 +- .../NewValueTupleConstructorBuilderContext.cs | 21 +- .../NewValueTupleExpressionBuilderContext.cs | 21 +- .../EnumerableMappingBodyBuilder.cs | 36 +- .../MemberMappingBuilder.cs | 30 +- ...wInstanceObjectMemberMappingBodyBuilder.cs | 68 +--- .../NewValueTupleMappingBodyBuilder.cs | 3 +- .../ObjectMemberMappingBodyBuilder.cs | 38 +- .../MappingBodyBuilders/SourceValueBuilder.cs | 2 +- .../UserMethodMappingBodyBuilder.cs | 4 +- .../EnumerableMappingBuilder.cs | 2 +- .../Mappings/ArrayForEachMapping.cs | 7 +- .../ConstructorParameterMapping.cs | 14 +- .../MemberMappings/MemberAssignmentMapping.cs | 18 +- .../MemberExistingTargetMapping.cs | 6 +- .../MemberMappings/MemberMappingInfo.cs | 13 +- .../MemberNullAssignmentInitializerMapping.cs | 17 +- .../MemberNullDelegateAssignmentMapping.cs | 30 +- ...dMemberNullAssignmentInitializerMapping.cs | 22 +- .../SourceValue/MappedMemberSourceValue.cs | 14 +- .../NullMappedMemberSourceValue.cs | 28 +- .../UnsafeAccess/UnsafeFieldAccessor.cs | 43 --- .../UnsafeAccess/UnsafeGetPropertyAccessor.cs | 39 -- .../ValueTupleConstructorParameterMapping.cs | 12 +- .../Descriptors/Mappings/MethodMapping.cs | 15 +- .../UserDefinedExistingTargetMethodMapping.cs | 2 +- .../UserDefinedNewInstanceMethodMapping.cs | 34 +- ...stanceRuntimeTargetTypeParameterMapping.cs | 5 +- .../SimpleMappingBuilderContext.cs | 1 + .../Descriptors/SymbolAccessor.cs | 38 +- .../Descriptors/TypeMappingKey.cs | 13 +- .../UnsafeAccess/IUnsafeAccessor.cs | 6 +- .../UnsafeAccess/UnsafeAccessorContext.cs | 137 +++++++ .../UnsafeAccess/UnsafeFieldAccessor.cs | 55 +++ .../UnsafeAccess/UnsafeGetPropertyAccessor.cs | 49 +++ .../UnsafeAccess/UnsafeSetPropertyAccessor.cs | 31 +- .../Descriptors/UnsafeAccessorContext.cs | 91 ----- .../UserMappingMethodParameterExtractor.cs | 47 ++- .../Descriptors/UserMethodMappingExtractor.cs | 71 ++-- .../Diagnostics/DiagnosticDescriptors.cs | 20 ++ .../Syntax/SyntaxFactoryHelper.Invocation.cs | 14 +- .../Emit/Syntax/SyntaxFactoryHelper.String.cs | 9 +- .../Emit/Syntax/SyntaxFactoryHelper.cs | 5 + .../Emit/UnsafeAccessorEmitter.cs | 2 +- .../Helpers/EnumerableExtensions.cs | 6 +- .../Helpers/SymbolTypeEqualityComparer.cs | 1 - .../Symbols/ConstructorParameterMember.cs | 37 -- src/Riok.Mapperly/Symbols/FieldMember.cs | 41 --- src/Riok.Mapperly/Symbols/GetterMemberPath.cs | 123 ------- src/Riok.Mapperly/Symbols/IMappableMember.cs | 39 -- .../Symbols/MappingMethodParameters.cs | 7 +- .../Members/ConstructorParameterMember.cs | 39 ++ .../Symbols/{ => Members}/EmptyMemberPath.cs | 2 +- .../Symbols/Members/FieldMember.cs | 74 ++++ .../Symbols/Members/IMappableMember.cs | 51 +++ .../Symbols/Members/IMemberGetter.cs | 8 + .../Symbols/Members/IMemberSetter.cs | 10 + .../Symbols/{ => Members}/MappableMember.cs | 2 +- .../Symbols/{ => Members}/MemberPath.cs | 48 +-- .../Symbols/Members/MemberPathGetter.cs | 132 +++++++ .../Symbols/Members/MemberPathSetter.cs | 58 +++ .../{ => Members}/NonEmptyMemberPath.cs | 5 +- .../Symbols/Members/ParameterSourceMember.cs | 54 +++ .../Symbols/Members/PropertyMember.cs | 84 +++++ .../Symbols/Members/SourceMemberPath.cs | 3 + .../Symbols/Members/SourceMemberType.cs | 8 + .../Symbols/Members/SymbolMappableMember.cs | 28 ++ .../Symbols/MethodAccessorMember.cs | 45 --- src/Riok.Mapperly/Symbols/PropertyMember.cs | 48 --- ...untimeTargetTypeMappingMethodParameters.cs | 2 +- src/Riok.Mapperly/Symbols/SetterMemberPath.cs | 121 ------- .../Dto/AdditionalParametersDto.cs | 7 + .../Mapper/StaticTestMapper.cs | 2 + .../StaticMapperTest.cs | 8 + ...erTest.SnapshotGeneratedSource.verified.cs | 9 + ...SnapshotGeneratedSource_NET6_0.verified.cs | 9 + .../MapperGenerationResultAssertions.cs | 4 +- .../Mapping/ExtensionMethodTest.cs | 20 +- .../Mapping/GenericTest.cs | 25 ++ .../Mapping/ReferenceHandlingTest.cs | 16 + .../Mapping/RuntimeTargetTypeMappingTest.cs | 33 -- .../UserMethodAdditionalParametersTest.cs | 337 ++++++++++++++++++ .../Mapping/UserMethodTest.cs | 45 --- ...dditionalIntParameter#Mapper.g.verified.cs | 14 + ...lNullableIntParameter#Mapper.g.verified.cs | 17 + ...rsTest.ExistingTarget#Mapper.g.verified.cs | 12 + ...AndNotBeUsedAsDefault#Mapper.g.verified.cs | 39 ++ ...agnosticAndNotBeUsedAsDefault.verified.txt | 24 ++ ...mplicitDefaultMapping#Mapper.g.verified.cs | 39 ++ ...arkedAsImplicitDefaultMapping.verified.txt | 14 + ...oAdditionalParameters#Mapper.g.verified.cs | 17 + ...WithReferenceHandling#Mapper.g.verified.cs | 20 ++ ...dlingAsFirstParameter#Mapper.g.verified.cs | 19 + ...enceHandlingParameter#Mapper.g.verified.cs | 19 + 126 files changed, 2119 insertions(+), 1229 deletions(-) create mode 100644 docs/docs/configuration/additional-mapping-parameters.mdx delete mode 100644 src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/UnsafeAccess/UnsafeFieldAccessor.cs delete mode 100644 src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/UnsafeAccess/UnsafeGetPropertyAccessor.cs rename src/Riok.Mapperly/Descriptors/{Mappings/MemberMappings => }/UnsafeAccess/IUnsafeAccessor.cs (72%) create mode 100644 src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeAccessorContext.cs create mode 100644 src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeFieldAccessor.cs create mode 100644 src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeGetPropertyAccessor.cs rename src/Riok.Mapperly/Descriptors/{Mappings/MemberMappings => }/UnsafeAccess/UnsafeSetPropertyAccessor.cs (57%) delete mode 100644 src/Riok.Mapperly/Descriptors/UnsafeAccessorContext.cs delete mode 100644 src/Riok.Mapperly/Symbols/ConstructorParameterMember.cs delete mode 100644 src/Riok.Mapperly/Symbols/FieldMember.cs delete mode 100644 src/Riok.Mapperly/Symbols/GetterMemberPath.cs delete mode 100644 src/Riok.Mapperly/Symbols/IMappableMember.cs create mode 100644 src/Riok.Mapperly/Symbols/Members/ConstructorParameterMember.cs rename src/Riok.Mapperly/Symbols/{ => Members}/EmptyMemberPath.cs (91%) create mode 100644 src/Riok.Mapperly/Symbols/Members/FieldMember.cs create mode 100644 src/Riok.Mapperly/Symbols/Members/IMappableMember.cs create mode 100644 src/Riok.Mapperly/Symbols/Members/IMemberGetter.cs create mode 100644 src/Riok.Mapperly/Symbols/Members/IMemberSetter.cs rename src/Riok.Mapperly/Symbols/{ => Members}/MappableMember.cs (94%) rename src/Riok.Mapperly/Symbols/{ => Members}/MemberPath.cs (66%) create mode 100644 src/Riok.Mapperly/Symbols/Members/MemberPathGetter.cs create mode 100644 src/Riok.Mapperly/Symbols/Members/MemberPathSetter.cs rename src/Riok.Mapperly/Symbols/{ => Members}/NonEmptyMemberPath.cs (86%) create mode 100644 src/Riok.Mapperly/Symbols/Members/ParameterSourceMember.cs create mode 100644 src/Riok.Mapperly/Symbols/Members/PropertyMember.cs create mode 100644 src/Riok.Mapperly/Symbols/Members/SourceMemberPath.cs create mode 100644 src/Riok.Mapperly/Symbols/Members/SourceMemberType.cs create mode 100644 src/Riok.Mapperly/Symbols/Members/SymbolMappableMember.cs delete mode 100644 src/Riok.Mapperly/Symbols/MethodAccessorMember.cs delete mode 100644 src/Riok.Mapperly/Symbols/PropertyMember.cs delete mode 100644 src/Riok.Mapperly/Symbols/SetterMemberPath.cs create mode 100644 test/Riok.Mapperly.IntegrationTests/Dto/AdditionalParametersDto.cs create mode 100644 test/Riok.Mapperly.Tests/Mapping/UserMethodAdditionalParametersTest.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.AdditionalIntParameter#Mapper.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.AdditionalNullableIntParameter#Mapper.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.ExistingTarget#Mapper.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.ExplicitDefaultShouldDiagnosticAndNotBeUsedAsDefault#Mapper.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.ExplicitDefaultShouldDiagnosticAndNotBeUsedAsDefault.verified.txt create mode 100644 test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.ShouldNotBeMarkedAsImplicitDefaultMapping#Mapper.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.ShouldNotBeMarkedAsImplicitDefaultMapping.verified.txt create mode 100644 test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.TwoAdditionalParameters#Mapper.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.WithReferenceHandling#Mapper.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.WithReferenceHandlingAsFirstParameter#Mapper.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.WithReferenceHandlingParameter#Mapper.g.verified.cs diff --git a/docs/docs/configuration/additional-mapping-parameters.mdx b/docs/docs/configuration/additional-mapping-parameters.mdx new file mode 100644 index 0000000000..f5ad8445c4 --- /dev/null +++ b/docs/docs/configuration/additional-mapping-parameters.mdx @@ -0,0 +1,72 @@ +--- +sidebar_position: 6 +description: Additional mapping parameters +--- + +# Additional mapping parameters + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +A mapping method declaration can have additional parameters. +Each additional parameter is considered the same as a source member and matched by its case-insensitive name. +An additional mapping parameter has lower priority than a `MapProperty` mapping, +but higher than a by-name matched regular member mapping. + + + + ```csharp + [Mapper] + public partial class CarMapper + { + // highlight-start + public partial CarDto Map(Car source, string name); + // highlight-end + } + + public class Car + { + public string Brand { get; set; } = string.Empty; + public string Model { get; set; } = string.Empty; + } + + public class CarDto + { + public string Brand { get; set; } = string.Empty; + public string Model { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + } + ``` + + + + ```csharp + [Mapper] + public partial class CarMapper + { + // highlight-start + public partial CarDto Map(Car source, string name) + // highlight-end + { + var target = new CarDto(); + target.Brand = source.Brand; + target.Model = source.Model; + // highlight-start + target.Name = name; + // highlight-end + return target; + } + } + ``` + + + +:::info +Mappings with additional parameters do have some limitions: + +- The additional parameters are not passed to nested mappings. +- A mapping with additional mapping parameters cannot be the default mapping + (it is not used by Mapperly when encountering a nested mapping for the given types), + see also [default mapping methods](./user-implemented-methods.mdx##default-mapping-methods). +- Generic and runtime target type mappings do not support additional type parameters. + ::: diff --git a/docs/docs/configuration/analyzer-diagnostics/index.mdx b/docs/docs/configuration/analyzer-diagnostics/index.mdx index e0e19060cb..d5d7eac4f7 100644 --- a/docs/docs/configuration/analyzer-diagnostics/index.mdx +++ b/docs/docs/configuration/analyzer-diagnostics/index.mdx @@ -1,5 +1,5 @@ --- -sidebar_position: 15 +sidebar_position: 18 description: A list of all analyzer diagnostics used by Mapperly and how to configure them. --- diff --git a/docs/docs/configuration/conversions.md b/docs/docs/configuration/conversions.md index 6542b59bb0..bde8e45d92 100644 --- a/docs/docs/configuration/conversions.md +++ b/docs/docs/configuration/conversions.md @@ -1,5 +1,5 @@ --- -sidebar_position: 15 +sidebar_position: 17 description: A list of conversions supported by Mapperly --- diff --git a/docs/docs/configuration/ctor-mappings.md b/docs/docs/configuration/ctor-mappings.md index 1475053d92..e085b93465 100644 --- a/docs/docs/configuration/ctor-mappings.md +++ b/docs/docs/configuration/ctor-mappings.md @@ -1,5 +1,5 @@ --- -sidebar_position: 7 +sidebar_position: 8 description: Constructor mappings --- diff --git a/docs/docs/configuration/derived-type-mapping.md b/docs/docs/configuration/derived-type-mapping.md index b15aa0da2b..783a827361 100644 --- a/docs/docs/configuration/derived-type-mapping.md +++ b/docs/docs/configuration/derived-type-mapping.md @@ -1,5 +1,5 @@ --- -sidebar_position: 10 +sidebar_position: 11 description: Map derived types and interfaces --- diff --git a/docs/docs/configuration/existing-target.mdx b/docs/docs/configuration/existing-target.mdx index 27f6f5697c..810ea7b84a 100644 --- a/docs/docs/configuration/existing-target.mdx +++ b/docs/docs/configuration/existing-target.mdx @@ -1,5 +1,5 @@ --- -sidebar_position: 9 +sidebar_position: 10 description: Map to an existing target object --- diff --git a/docs/docs/configuration/generic-mapping.md b/docs/docs/configuration/generic-mapping.md index e6b1cb7cba..9213383086 100644 --- a/docs/docs/configuration/generic-mapping.md +++ b/docs/docs/configuration/generic-mapping.md @@ -1,5 +1,5 @@ --- -sidebar_position: 11 +sidebar_position: 12 description: Create a generic mapping method --- diff --git a/docs/docs/configuration/object-factories.md b/docs/docs/configuration/object-factories.md index 63a83be4d1..d4631f687b 100644 --- a/docs/docs/configuration/object-factories.md +++ b/docs/docs/configuration/object-factories.md @@ -1,5 +1,5 @@ --- -sidebar_position: 8 +sidebar_position: 9 description: Construct and resolve objects using object factories --- diff --git a/docs/docs/configuration/private-member-mapping.md b/docs/docs/configuration/private-member-mapping.md index 6995301c1b..1d456db6f7 100644 --- a/docs/docs/configuration/private-member-mapping.md +++ b/docs/docs/configuration/private-member-mapping.md @@ -1,5 +1,5 @@ --- -sidebar_position: 13 +sidebar_position: 14 description: Private member mapping --- diff --git a/docs/docs/configuration/queryable-projections.mdx b/docs/docs/configuration/queryable-projections.mdx index 52543c6174..b24eef90e2 100644 --- a/docs/docs/configuration/queryable-projections.mdx +++ b/docs/docs/configuration/queryable-projections.mdx @@ -1,5 +1,5 @@ --- -sidebar_position: 14 +sidebar_position: 16 description: Use queryable projections to map queryable objects and optimize ORM performance --- diff --git a/docs/docs/configuration/reference-handling.md b/docs/docs/configuration/reference-handling.md index 52fa7762f1..5118b2126e 100644 --- a/docs/docs/configuration/reference-handling.md +++ b/docs/docs/configuration/reference-handling.md @@ -1,5 +1,5 @@ --- -sidebar_position: 12 +sidebar_position: 13 description: Use reference handling to handle reference loops --- diff --git a/docs/docs/configuration/user-implemented-methods.mdx b/docs/docs/configuration/user-implemented-methods.mdx index 7fe4d58b37..60cc2633cc 100644 --- a/docs/docs/configuration/user-implemented-methods.mdx +++ b/docs/docs/configuration/user-implemented-methods.mdx @@ -1,5 +1,5 @@ --- -sidebar_position: 6 +sidebar_position: 7 description: Manually implement mappings --- @@ -27,7 +27,7 @@ The types of the user-implemented mapping method need to match the types to map If there are multiple user-implemented mapping methods suiting the given type-pair, by default, the first one is used. This can be customized by using [automatic user-implemented mapping method discovery](#automatic-user-implemented-mapping-method-discovery) -and [default user-implemented mapping method](#default-user-implemented-mapping-method). +and [default mapping method](#default-mapping-methods). ## Automatic user-implemented mapping method discovery @@ -82,11 +82,11 @@ public partial class CarMapper } ``` -## Default user-implemented mapping method +## Default mapping methods Whenever Mapperly will need a mapping for a given type-pair, -it will use the default user-implemented mapping. -A user-implemented mapping is considered the default mapping for a type-pair +it will use the default mapping. +A user-implemented or user-defined mapping is considered the default mapping for a type-pair if `Default = true` is set on the `UserMapping` attribute. If no user-implemented mapping with `Default = true` exists and `AutoUserMappings` is enabled, the first user-implemented mapping which has an unspecified `Default` value is used. diff --git a/src/Riok.Mapperly/AnalyzerReleases.Shipped.md b/src/Riok.Mapperly/AnalyzerReleases.Shipped.md index 5b23ebc129..a51d2e64c4 100644 --- a/src/Riok.Mapperly/AnalyzerReleases.Shipped.md +++ b/src/Riok.Mapperly/AnalyzerReleases.Shipped.md @@ -183,6 +183,8 @@ RMG012 | Mapper | Warning | Source member was not found for target member RMG020 | Mapper | Warning | Source member is not mapped to any target member RMG037 | Mapper | Warning | An enum member could not be found on the source enum RMG038 | Mapper | Warning | An enum member could not be found on the target enum +RMG081 | Mapper | Error | A mapping method with additional parameters cannot be a default mapping +RMG082 | Mapper | Warning | An additional mapping method parameter is not mapped ### Removed Rules @@ -192,3 +194,4 @@ RMG017 | Mapper | Warning | An init only member can have one configuration a RMG026 | Mapper | Info | Cannot map from indexed member RMG027 | Mapper | Warning | A constructor parameter can have one configuration at max RMG028 | Mapper | Warning | Constructor parameter cannot handle target paths + diff --git a/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs b/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs index 5c84c86078..5731e0e9b5 100644 --- a/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs @@ -8,6 +8,7 @@ using Riok.Mapperly.Descriptors.MappingBuilders; using Riok.Mapperly.Descriptors.Mappings.UserMappings; using Riok.Mapperly.Descriptors.ObjectFactories; +using Riok.Mapperly.Descriptors.UnsafeAccess; using Riok.Mapperly.Diagnostics; using Riok.Mapperly.Helpers; using Riok.Mapperly.Symbols; diff --git a/src/Riok.Mapperly/Descriptors/Enumerables/CollectionInfo.cs b/src/Riok.Mapperly/Descriptors/Enumerables/CollectionInfo.cs index 27023857f5..f3b9d3aedc 100644 --- a/src/Riok.Mapperly/Descriptors/Enumerables/CollectionInfo.cs +++ b/src/Riok.Mapperly/Descriptors/Enumerables/CollectionInfo.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis; +using Riok.Mapperly.Symbols.Members; namespace Riok.Mapperly.Descriptors.Enumerables; @@ -8,19 +9,17 @@ public record CollectionInfo( CollectionType CollectionType, CollectionType ImplementedTypes, ITypeSymbol EnumeratedType, - string? CountPropertyName, + IMappableMember? CountMember, bool HasImplicitCollectionAddMethod, bool IsImmutableCollectionType ) { public bool ImplementsIEnumerable => ImplementedTypes.HasFlag(CollectionType.IEnumerable); - public bool ImplementsDictionary = - ImplementedTypes.HasFlag(CollectionType.IDictionary) || ImplementedTypes.HasFlag(CollectionType.IReadOnlyDictionary); public bool IsArray => CollectionType is CollectionType.Array; public bool IsMemory => CollectionType is CollectionType.Memory or CollectionType.ReadOnlyMemory; public bool IsSpan => CollectionType is CollectionType.Span or CollectionType.ReadOnlySpan; - [MemberNotNullWhen(true, nameof(CountPropertyName))] - public bool CountIsKnown => CountPropertyName != null; + [MemberNotNullWhen(true, nameof(CountMember))] + public bool CountIsKnown => CountMember != null; } diff --git a/src/Riok.Mapperly/Descriptors/Enumerables/CollectionInfoBuilder.cs b/src/Riok.Mapperly/Descriptors/Enumerables/CollectionInfoBuilder.cs index f639df8e5c..8cc63fda71 100644 --- a/src/Riok.Mapperly/Descriptors/Enumerables/CollectionInfoBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/Enumerables/CollectionInfoBuilder.cs @@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis; using Riok.Mapperly.Helpers; +using Riok.Mapperly.Symbols.Members; namespace Riok.Mapperly.Descriptors.Enumerables; @@ -137,7 +138,7 @@ ITypeSymbol enumeratedType typeInfo, implementedTypes, symbolAccessor.UpgradeNullable(enumeratedType), - FindCountProperty(symbolAccessor, type, typeInfo), + FindCountMember(symbolAccessor, type, typeInfo), HasValidAddMethod(wellKnownTypes, type, typeInfo, implementedTypes), collectionTypeInfo?.Immutable == true ); @@ -218,7 +219,7 @@ or CollectionType.SortedSet return false; } - private static string? FindCountProperty(SymbolAccessor symbolAccessor, ITypeSymbol t, CollectionType typeInfo) + private static IMappableMember? FindCountMember(SymbolAccessor symbolAccessor, ITypeSymbol t, CollectionType typeInfo) { if (typeInfo is CollectionType.IEnumerable) return null; @@ -231,17 +232,15 @@ or CollectionType.ReadOnlySpan or CollectionType.Memory or CollectionType.ReadOnlyMemory ) - return "Length"; + { + return symbolAccessor.GetMappableMember(t, "Length"); + } if (typeInfo is not CollectionType.None) - return "Count"; - - var member = symbolAccessor - .GetAllAccessibleMappableMembers(t) - .FirstOrDefault(x => - x.Type.SpecialType == SpecialType.System_Int32 && x.Name is nameof(ICollection.Count) or nameof(Array.Length) - ); - return member?.Name; + return symbolAccessor.GetMappableMember(t, "Count"); + + var member = symbolAccessor.GetMappableMember(t, "Count") ?? symbolAccessor.GetMappableMember(t, "Length"); + return member?.Type.SpecialType == SpecialType.System_Int32 ? member : null; } private static CollectionTypeInfo? GetCollectionTypeInfo(WellKnownTypes types, ITypeSymbol type) diff --git a/src/Riok.Mapperly/Descriptors/Enumerables/EnsureCapacity/EnsureCapacityBuilder.cs b/src/Riok.Mapperly/Descriptors/Enumerables/EnsureCapacity/EnsureCapacityBuilder.cs index b5f74d5999..50e8a3b1f3 100644 --- a/src/Riok.Mapperly/Descriptors/Enumerables/EnsureCapacity/EnsureCapacityBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/Enumerables/EnsureCapacity/EnsureCapacityBuilder.cs @@ -24,7 +24,11 @@ public static class EnsureCapacityBuilder // if source count is known, create a simple EnsureCapacity statement if (source.CountIsKnown) - return new EnsureCapacityMember(target.CountPropertyName, source.CountPropertyName); + { + var targetCount = target.CountMember?.BuildGetter(ctx.UnsafeAccessorContext); + var sourceCount = source.CountMember.BuildGetter(ctx.UnsafeAccessorContext); + return new EnsureCapacityMember(targetCount, sourceCount); + } var nonEnumeratedCountMethod = ctx .Types.Get(typeof(Enumerable)) @@ -40,6 +44,6 @@ public static class EnsureCapacityBuilder return null; // if source does not have a count use GetNonEnumeratedCount, calling EnsureCapacity if count is available - return new EnsureCapacityNonEnumerated(target.CountPropertyName, nonEnumeratedCountMethod); + return new EnsureCapacityNonEnumerated(target.CountMember?.BuildGetter(ctx.UnsafeAccessorContext), nonEnumeratedCountMethod); } } diff --git a/src/Riok.Mapperly/Descriptors/Enumerables/EnsureCapacity/EnsureCapacityMember.cs b/src/Riok.Mapperly/Descriptors/Enumerables/EnsureCapacity/EnsureCapacityMember.cs index aac04c8a0f..7018a2016f 100644 --- a/src/Riok.Mapperly/Descriptors/Enumerables/EnsureCapacity/EnsureCapacityMember.cs +++ b/src/Riok.Mapperly/Descriptors/Enumerables/EnsureCapacity/EnsureCapacityMember.cs @@ -1,6 +1,6 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Riok.Mapperly.Descriptors.Mappings; -using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; +using Riok.Mapperly.Symbols.Members; namespace Riok.Mapperly.Descriptors.Enumerables.EnsureCapacity; @@ -12,15 +12,15 @@ namespace Riok.Mapperly.Descriptors.Enumerables.EnsureCapacity; /// target.EnsureCapacity(source.Length + target.Count); /// /// -public class EnsureCapacityMember(string? targetAccessor, string sourceAccessor) : EnsureCapacityInfo +public class EnsureCapacityMember(IMemberGetter? targetAccessor, IMemberGetter sourceAccessor) : EnsureCapacityInfo { public override StatementSyntax Build(TypeMappingBuildContext ctx, ExpressionSyntax target) { return EnsureCapacityStatement( ctx.SyntaxFactory, target, - MemberAccess(ctx.Source, sourceAccessor), - targetAccessor != null ? MemberAccess(target, targetAccessor) : null + sourceAccessor.BuildAccess(ctx.Source), + targetAccessor?.BuildAccess(target) ); } } diff --git a/src/Riok.Mapperly/Descriptors/Enumerables/EnsureCapacity/EnsureCapacityNonEnumerated.cs b/src/Riok.Mapperly/Descriptors/Enumerables/EnsureCapacity/EnsureCapacityNonEnumerated.cs index 11869d1c93..af99fdb716 100644 --- a/src/Riok.Mapperly/Descriptors/Enumerables/EnsureCapacity/EnsureCapacityNonEnumerated.cs +++ b/src/Riok.Mapperly/Descriptors/Enumerables/EnsureCapacity/EnsureCapacityNonEnumerated.cs @@ -1,7 +1,7 @@ using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Riok.Mapperly.Descriptors.Mappings; +using Riok.Mapperly.Symbols.Members; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; @@ -18,26 +18,24 @@ namespace Riok.Mapperly.Descriptors.Enumerables.EnsureCapacity; /// target.EnsureCapacity(sourceCount + target.Count); /// /// -public class EnsureCapacityNonEnumerated(string? targetAccessor, IMethodSymbol getNonEnumeratedMethod) : EnsureCapacityInfo +public class EnsureCapacityNonEnumerated(IMemberGetter? targetAccessor, IMethodSymbol getNonEnumeratedMethod) : EnsureCapacityInfo { private const string SourceCountVariableName = "sourceCount"; public override StatementSyntax Build(TypeMappingBuildContext ctx, ExpressionSyntax target) { - var targetCount = targetAccessor == null ? null : MemberAccess(target, targetAccessor); + var targetCount = targetAccessor?.BuildAccess(target); - var sourceCountIdentifier = Identifier(ctx.NameBuilder.New(SourceCountVariableName)); + var sourceCountName = ctx.NameBuilder.New(SourceCountVariableName); var enumerableArgument = Argument(ctx.Source); - - var outVarArgument = Argument(DeclarationExpression(VarIdentifier, SingleVariableDesignation(sourceCountIdentifier))) - .WithRefOrOutKeyword(TrailingSpacedToken(SyntaxKind.OutKeyword)); + var outVarArgument = OutVarArgument(sourceCountName); var getNonEnumeratedInvocation = StaticInvocation(getNonEnumeratedMethod, enumerableArgument, outVarArgument); var ensureCapacity = EnsureCapacityStatement( ctx.SyntaxFactory.AddIndentation(), target, - IdentifierName(sourceCountIdentifier), + IdentifierName(sourceCountName), targetCount ); return ctx.SyntaxFactory.If(getNonEnumeratedInvocation, ensureCapacity); diff --git a/src/Riok.Mapperly/Descriptors/FormatProviders/FormatProviderBuilder.cs b/src/Riok.Mapperly/Descriptors/FormatProviders/FormatProviderBuilder.cs index 763dddb104..494b859076 100644 --- a/src/Riok.Mapperly/Descriptors/FormatProviders/FormatProviderBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/FormatProviders/FormatProviderBuilder.cs @@ -2,7 +2,7 @@ using Riok.Mapperly.Abstractions; using Riok.Mapperly.Diagnostics; using Riok.Mapperly.Helpers; -using Riok.Mapperly.Symbols; +using Riok.Mapperly.Symbols.Members; namespace Riok.Mapperly.Descriptors.FormatProviders; @@ -33,7 +33,7 @@ public static FormatProviderCollection ExtractFormatProviders(SimpleMappingBuild if (memberSymbol == null) return null; - if (!memberSymbol.CanGet || symbol.IsStatic != isStatic || !memberSymbol.Type.Implements(ctx.Types.Get())) + if (!memberSymbol.CanGetDirectly || symbol.IsStatic != isStatic || !memberSymbol.Type.Implements(ctx.Types.Get())) { ctx.ReportDiagnostic(DiagnosticDescriptors.InvalidFormatProviderSignature, symbol, symbol.Name); return null; diff --git a/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs index 30c56ce893..a1daaf5c15 100644 --- a/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs @@ -127,7 +127,7 @@ conversionType is not MappingConversionType.EnumToString and not MappingConversi // for inline expression mappings. // This is not needed for regular mappings as these user defined method mappings // are directly built (with KeepUserSymbol) and called by the other mappings. - userMapping ??= (MappingBuilder.Find(mappingKey) as IUserMapping); + userMapping ??= MappingBuilder.Find(mappingKey) as IUserMapping; options &= ~MappingBuildingOptions.KeepUserSymbol; return BuildMapping(userMapping, mappingKey, options, diagnosticLocation); } diff --git a/src/Riok.Mapperly/Descriptors/MapperDescriptor.cs b/src/Riok.Mapperly/Descriptors/MapperDescriptor.cs index 97522d1580..a539996c0c 100644 --- a/src/Riok.Mapperly/Descriptors/MapperDescriptor.cs +++ b/src/Riok.Mapperly/Descriptors/MapperDescriptor.cs @@ -2,7 +2,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Riok.Mapperly.Descriptors.Mappings; -using Riok.Mapperly.Descriptors.Mappings.MemberMappings.UnsafeAccess; +using Riok.Mapperly.Descriptors.UnsafeAccess; using Riok.Mapperly.Helpers; using Riok.Mapperly.Symbols; diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/IMembersBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/IMembersBuilderContext.cs index 7200cc3eb7..2a10b78fd2 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/IMembersBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/IMembersBuilderContext.cs @@ -1,6 +1,6 @@ using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Descriptors.Mappings.MemberMappings; -using Riok.Mapperly.Symbols; +using Riok.Mapperly.Symbols.Members; namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; @@ -15,14 +15,16 @@ public interface IMembersBuilderContext MappingBuilderContext BuilderContext { get; } - void IgnoreMembers(string memberName); + void IgnoreMembers(IMappableMember member); - void SetMembersMapped(string memberName); + void SetMembersMapped(MemberMappingInfo members); void SetTargetMemberMapped(IMappableMember targetMember); void ConsumeMemberConfigs(MemberMappingInfo members); + void TryAddSourceMemberAlias(string alias, IMappableMember member); + IEnumerable EnumerateUnmappedTargetMembers(); IEnumerable EnumerateUnmappedOrConfiguredTargetMembers(); diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/INewInstanceBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/INewInstanceBuilderContext.cs index 677e67176b..a2b55112ae 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/INewInstanceBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/INewInstanceBuilderContext.cs @@ -2,7 +2,7 @@ using Microsoft.CodeAnalysis; using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Descriptors.Mappings.MemberMappings; -using Riok.Mapperly.Symbols; +using Riok.Mapperly.Symbols.Members; namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/IgnoredMembersBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/IgnoredMembersBuilder.cs index 0ad1fc9e4b..1633f173bd 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/IgnoredMembersBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/IgnoredMembersBuilder.cs @@ -87,10 +87,7 @@ private static IEnumerable GetIgnoredAtMemberMembers(MappingBuilderConte { var type = sourceTarget == MappingSourceTarget.Source ? ctx.Source : ctx.Target; - return ctx - .SymbolAccessor.GetAllAccessibleMappableMembers(type) - .Where(x => ctx.SymbolAccessor.HasAttribute(x.MemberSymbol)) - .Select(x => x.Name); + return ctx.SymbolAccessor.GetAllAccessibleMappableMembers(type).Where(x => x.IsIgnored).Select(x => x.Name); } private static IEnumerable GetIgnoredObsoleteMembers(MappingBuilderContext ctx, MappingSourceTarget sourceTarget) @@ -100,13 +97,10 @@ private static IEnumerable GetIgnoredObsoleteMembers(MappingBuilderConte sourceTarget == MappingSourceTarget.Source ? IgnoreObsoleteMembersStrategy.Source : IgnoreObsoleteMembersStrategy.Target; if (!obsoleteStrategy.HasFlag(strategy)) - return Enumerable.Empty(); + return []; var type = sourceTarget == MappingSourceTarget.Source ? ctx.Source : ctx.Target; - return ctx - .SymbolAccessor.GetAllAccessibleMappableMembers(type) - .Where(x => ctx.SymbolAccessor.HasAttribute(x.MemberSymbol)) - .Select(x => x.Name); + return ctx.SymbolAccessor.GetAllAccessibleMappableMembers(type).Where(x => x.IsObsolete).Select(x => x.Name); } } diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MemberMappingDiagnosticReporter.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MemberMappingDiagnosticReporter.cs index 4050ade7b0..42dd655332 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MemberMappingDiagnosticReporter.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MemberMappingDiagnosticReporter.cs @@ -10,6 +10,7 @@ public static void ReportDiagnostics(MappingBuilderContext ctx, MembersMappingSt AddUnusedTargetMembersDiagnostics(ctx, state); AddUnmappedSourceMembersDiagnostics(ctx, state); AddUnmappedTargetMembersDiagnostics(ctx, state); + AddUnmappedAdditionalSourceMembersDiagnostics(ctx, state); AddNoMemberMappedDiagnostic(ctx, state); } @@ -49,6 +50,14 @@ private static void AddUnmappedTargetMembersDiagnostics(MappingBuilderContext ct } } + private static void AddUnmappedAdditionalSourceMembersDiagnostics(MappingBuilderContext ctx, MembersMappingState state) + { + foreach (var name in state.UnmappedAdditionalSourceMemberNames) + { + ctx.ReportDiagnostic(DiagnosticDescriptors.AdditionalParameterNotMapped, name, ctx.UserMapping!.Method.Name); + } + } + private static void AddNoMemberMappedDiagnostic(MappingBuilderContext ctx, MembersMappingState state) { if (!state.HasMemberMapping) diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersContainerBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersContainerBuilderContext.cs index 29ed4823ea..a3c5412eed 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersContainerBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersContainerBuilderContext.cs @@ -1,7 +1,7 @@ using Riok.Mapperly.Descriptors.Mappings.MemberMappings; using Riok.Mapperly.Diagnostics; using Riok.Mapperly.Helpers; -using Riok.Mapperly.Symbols; +using Riok.Mapperly.Symbols.Members; namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; @@ -32,8 +32,8 @@ public void AddNullDelegateMemberAssignmentMapping(IMemberAssignmentMapping memb } var nullConditionSourcePath = new NonEmptyMemberPath( - memberMapping.MemberInfo.SourceMember.RootType, - memberMapping.MemberInfo.SourceMember.PathWithoutTrailingNonNullable().ToList() + memberMapping.MemberInfo.SourceMember.MemberPath.RootType, + memberMapping.MemberInfo.SourceMember.MemberPath.PathWithoutTrailingNonNullable().ToList() ); var container = GetOrCreateNullDelegateMappingForPath(nullConditionSourcePath); AddMemberAssignmentMapping(container, memberMapping); @@ -45,7 +45,8 @@ public void AddNullDelegateMemberAssignmentMapping(IMemberAssignmentMapping memb && memberMapping.MemberInfo.TargetMember.Member.Type.IsNullable() ) { - container.AddNullMemberAssignment(SetterMemberPath.Build(BuilderContext, memberMapping.MemberInfo.TargetMember)); + var targetMemberSetter = memberMapping.MemberInfo.TargetMember.BuildSetter(BuilderContext); + container.AddNullMemberAssignment(targetMemberSetter); } else if (BuilderContext.Configuration.Mapper.ThrowOnPropertyMappingNullMismatch) { @@ -76,18 +77,17 @@ private void AddNullMemberInitializers(IMemberAssignmentMappingContainer contain continue; } - var setterNullablePath = SetterMemberPath.Build(BuilderContext, nullablePath); - - if (setterNullablePath.IsMethod) + var nullablePathSetter = nullablePath.BuildSetter(BuilderContext); + if (!nullablePathSetter.SupportsCoalesceAssignment) { - var getterNullablePath = GetterMemberPath.Build(BuilderContext, nullablePath); + var nullablePathGetter = nullablePath.BuildGetter(BuilderContext); container.AddMemberMappingContainer( - new MethodMemberNullAssignmentInitializerMapping(setterNullablePath, getterNullablePath) + new MethodMemberNullAssignmentInitializerMapping(nullablePathSetter, nullablePathGetter) ); continue; } - container.AddMemberMappingContainer(new MemberNullAssignmentInitializerMapping(setterNullablePath)); + container.AddMemberMappingContainer(new MemberNullAssignmentInitializerMapping(nullablePathSetter)); } } @@ -118,11 +118,8 @@ out var parentMappingHolder needsNullSafeAccess = true; } - mapping = new MemberNullDelegateAssignmentMapping( - GetterMemberPath.Build(BuilderContext, nullConditionSourcePath), - parentMapping, - needsNullSafeAccess - ); + var nullConditionSourcePathGetter = nullConditionSourcePath.BuildGetter(BuilderContext); + mapping = new MemberNullDelegateAssignmentMapping(nullConditionSourcePathGetter, parentMapping, needsNullSafeAccess); _nullDelegateMappings[nullConditionSourcePath] = mapping; parentMapping.AddMemberMappingContainer(mapping); return mapping; diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingBuilderContext.cs index 58a84fa960..b816289dc2 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingBuilderContext.cs @@ -5,7 +5,7 @@ using Riok.Mapperly.Descriptors.Mappings.MemberMappings; using Riok.Mapperly.Diagnostics; using Riok.Mapperly.Helpers; -using Riok.Mapperly.Symbols; +using Riok.Mapperly.Symbols.Members; namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; @@ -34,14 +34,16 @@ public void AddDiagnostics() public IEnumerable EnumerateUnmappedOrConfiguredTargetMembers() => _state.EnumerateUnmappedOrConfiguredTargetMembers(); + public void TryAddSourceMemberAlias(string alias, IMappableMember member) => _state.TryAddSourceMemberAlias(alias, member); + public void SetTargetMemberMapped(IMappableMember targetMember) => _state.SetTargetMemberMapped(targetMember); protected void SetTargetMemberMapped(string targetMemberName, bool ignoreCase = false) => _state.SetTargetMemberMapped(targetMemberName, ignoreCase); - public void SetMembersMapped(string memberName) => _state.SetMembersMapped(memberName); + public void SetMembersMapped(MemberMappingInfo members) => _state.SetMembersMapped(members, false); - public void IgnoreMembers(string memberName) => _state.IgnoreMembers(memberName); + public void IgnoreMembers(IMappableMember member) => _state.IgnoreMembers(member); public void ConsumeMemberConfigs(MemberMappingInfo members) { @@ -127,16 +129,47 @@ protected bool TryGetMemberValueConfigs( protected virtual bool TryFindSourcePath( IReadOnlyList> pathCandidates, bool ignoreCase, - [NotNullWhen(true)] out MemberPath? sourceMemberPath + [NotNullWhen(true)] out SourceMemberPath? sourcePath ) { - return BuilderContext.SymbolAccessor.TryFindMemberPath( - Mapping.SourceType, - pathCandidates, - _state.IgnoredSourceMemberNames, - ignoreCase, - out sourceMemberPath - ); + // try to match in additional source members + if ( + BuilderContext.SymbolAccessor.TryFindMemberPath( + _state.AdditionalSourceMembers, + pathCandidates, + ignoreCase, + out var sourceMemberPath + ) + ) + { + sourcePath = new SourceMemberPath(sourceMemberPath, SourceMemberType.AdditionalMappingMethodParameter); + return true; + } + + // try to match in aliased source members + if (BuilderContext.SymbolAccessor.TryFindMemberPath(_state.AliasedSourceMembers, pathCandidates, ignoreCase, out sourceMemberPath)) + { + sourcePath = new SourceMemberPath(sourceMemberPath, SourceMemberType.MemberAlias); + return true; + } + + // match against source type members + if ( + BuilderContext.SymbolAccessor.TryFindMemberPath( + Mapping.SourceType, + pathCandidates, + _state.IgnoredSourceMemberNames, + ignoreCase, + out sourceMemberPath + ) + ) + { + sourcePath = new SourceMemberPath(sourceMemberPath, SourceMemberType.Member); + return true; + } + + sourcePath = null; + return false; } protected bool IsIgnoredSourceMember(string sourceMemberName) => _state.IgnoredSourceMemberNames.Contains(sourceMemberName); @@ -194,9 +227,9 @@ private IEnumerable ResolveMemberMappingInfo(IEnumerable cs.ToList()).ToList(); diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingState.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingState.cs index 7d3e885dfb..5b816a52b3 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingState.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingState.cs @@ -2,7 +2,7 @@ using Riok.Mapperly.Configuration; using Riok.Mapperly.Descriptors.Mappings.MemberMappings; using Riok.Mapperly.Helpers; -using Riok.Mapperly.Symbols; +using Riok.Mapperly.Symbols.Members; namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; @@ -11,6 +11,7 @@ namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; /// Contains discovered but unmapped members, ignored members, etc. /// /// Source member names which are not used in a member mapping yet. +/// Additional source member names (additional mapping method parameters) which are not used in a member mapping yet. /// Target member names which are not used in a member mapping yet. /// A dictionary with all members of the target with a case-insensitive key comparer. /// All known target members. @@ -19,7 +20,9 @@ namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; /// All ignored source members names. internal class MembersMappingState( HashSet unmappedSourceMemberNames, + HashSet unmappedAdditionalSourceMemberNames, HashSet unmappedTargetMemberNames, + IReadOnlyDictionary additionalSourceMembers, IReadOnlyDictionary targetMemberCaseMapping, Dictionary targetMembers, Dictionary> memberValueConfigsByRootTargetName, @@ -27,11 +30,18 @@ internal class MembersMappingState( HashSet ignoredSourceMemberNames ) { + private readonly Dictionary _aliasedSourceMembers = new(StringComparer.OrdinalIgnoreCase); + /// /// All source member names that are not used in a member mapping (yet). /// private readonly HashSet _unmappedSourceMemberNames = unmappedSourceMemberNames; + /// + /// All additional source member names (additional mapping method parameters) that are not used in a member mapping (yet). + /// + private readonly HashSet _unmappedAdditionalSourceMemberNames = unmappedAdditionalSourceMemberNames; + /// /// All target member names that are not used in a member mapping (yet). /// @@ -44,8 +54,16 @@ HashSet ignoredSourceMemberNames /// public bool HasMemberMapping { get; private set; } + /// public IEnumerable UnmappedSourceMemberNames => _unmappedSourceMemberNames; + /// + public IEnumerable UnmappedAdditionalSourceMemberNames => _unmappedAdditionalSourceMemberNames; + + public IReadOnlyDictionary AdditionalSourceMembers => additionalSourceMembers; + + public IReadOnlyDictionary AliasedSourceMembers => _aliasedSourceMembers; + public IEnumerable UnusedMemberConfigs => memberConfigsByRootTargetName.Values.SelectMany(x => x); public IEnumerable EnumerateUnmappedTargetMembers() => _unmappedTargetMemberNames.Select(x => targetMembers[x]); @@ -60,6 +78,8 @@ public IEnumerable EnumerateUnmappedOrConfiguredTargetMembers() .WhereNotNull(); } + public void TryAddSourceMemberAlias(string alias, IMappableMember member) => _aliasedSourceMembers.TryAdd(alias, member); + public void MappingAdded() => HasMemberMapping = true; public void MappingAdded(MemberMappingInfo info, bool ignoreTargetCasing) @@ -68,14 +88,15 @@ public void MappingAdded(MemberMappingInfo info, bool ignoreTargetCasing) SetMembersMapped(info, ignoreTargetCasing); } - public void IgnoreMembers(string memberName) + public void IgnoreMembers(IMappableMember member) { - SetMembersMapped(memberName); - ignoredSourceMemberNames.Add(memberName); + _unmappedSourceMemberNames.Remove(member.Name); + _unmappedTargetMemberNames.Remove(member.Name); + ignoredSourceMemberNames.Add(member.Name); - if (!HasMemberConfig(memberName)) + if (!HasMemberConfig(member.Name)) { - targetMembers.Remove(memberName); + targetMembers.Remove(member.Name); } } @@ -91,13 +112,7 @@ public void SetTargetMemberMapped(string targetName, bool ignoreCase = false) } } - public void SetMembersMapped(string memberName) - { - _unmappedSourceMemberNames.Remove(memberName); - _unmappedTargetMemberNames.Remove(memberName); - } - - private void SetMembersMapped(MemberMappingInfo info, bool ignoreTargetCasing) + public void SetMembersMapped(MemberMappingInfo info, bool ignoreTargetCasing) { SetTargetMemberMapped(info.TargetMember.Path[0].Name, ignoreTargetCasing); @@ -165,16 +180,24 @@ public bool TryGetMemberValueConfigs( return false; } - private void SetSourceMemberMapped(MemberPath sourcePath) + private void SetSourceMemberMapped(SourceMemberPath sourcePath) { - if (sourcePath.Path.FirstOrDefault() is { } sourceMember) - { - _unmappedSourceMemberNames.Remove(sourceMember.Name); - } - else + if (sourcePath.MemberPath.Path.FirstOrDefault() is not { } sourceMember) { // Assume all source members are used when the source object is used itself. _unmappedSourceMemberNames.Clear(); + return; + } + + switch (sourcePath.Type) + { + case SourceMemberType.Member + or SourceMemberType.MemberAlias: + _unmappedSourceMemberNames.Remove(sourceMember.Name); + break; + case SourceMemberType.AdditionalMappingMethodParameter: + _unmappedAdditionalSourceMemberNames.Remove(sourceMember.Name); + break; } } diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingStateBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingStateBuilder.cs index dc7b42ce6a..7ab9138e8e 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingStateBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingStateBuilder.cs @@ -3,6 +3,7 @@ using Riok.Mapperly.Diagnostics; using Riok.Mapperly.Helpers; using Riok.Mapperly.Symbols; +using Riok.Mapperly.Symbols.Members; namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; @@ -17,6 +18,8 @@ public static MembersMappingState Build(MappingBuilderContext ctx, IMapping mapp // build all members var unmappedSourceMemberNames = GetSourceMemberNames(ctx, mapping); + var additionalSourceMembers = GetAdditionalSourceMembers(ctx); + var unmappedAdditionalSourceMemberNames = new HashSet(additionalSourceMembers.Keys, StringComparer.Ordinal); var targetMembers = GetTargetMembers(ctx, mapping); // build ignored members @@ -37,7 +40,9 @@ public static MembersMappingState Build(MappingBuilderContext ctx, IMapping mapp var unmappedTargetMemberNames = targetMembers.Keys.ToHashSet(); return new MembersMappingState( unmappedSourceMemberNames, + unmappedAdditionalSourceMemberNames, unmappedTargetMemberNames, + additionalSourceMembers, targetMemberCaseMapping, targetMembers, memberValueConfigsByRootTargetName, @@ -46,6 +51,18 @@ public static MembersMappingState Build(MappingBuilderContext ctx, IMapping mapp ); } + private static IReadOnlyDictionary GetAdditionalSourceMembers(MappingBuilderContext ctx) + { + if (ctx.UserMapping is MethodMapping { AdditionalSourceParameters.Count: > 0 } methodMapping) + { + return methodMapping + .AdditionalSourceParameters.Select(p => new ParameterSourceMember(p)) + .ToDictionary(p => p.Name, p => p, StringComparer.OrdinalIgnoreCase); + } + + return new Dictionary(); + } + private static HashSet GetSourceMemberNames(MappingBuilderContext ctx, IMapping mapping) { return ctx.SymbolAccessor.GetAllAccessibleMappableMembers(mapping.SourceType).Select(x => x.Name).ToHashSet(); diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NestedMappingsContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NestedMappingsContext.cs index 7f48485013..f59a2dbe60 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NestedMappingsContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NestedMappingsContext.cs @@ -1,6 +1,6 @@ using System.Diagnostics.CodeAnalysis; using Riok.Mapperly.Diagnostics; -using Riok.Mapperly.Symbols; +using Riok.Mapperly.Symbols.Members; namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; @@ -47,7 +47,7 @@ private static List ResolveNestedMappings(MappingBuilderContext ctx) public bool TryFindNestedSourcePath( List> pathCandidates, bool ignoreCase, - [NotNullWhen(true)] out MemberPath? sourceMemberPath + [NotNullWhen(true)] out SourceMemberPath? sourceMemberPath ) { foreach (var nestedMemberPath in _paths) @@ -64,7 +64,7 @@ private bool TryFindNestedSourcePath( List> pathCandidates, bool ignoreCase, MemberPath nestedMemberPath, - [NotNullWhen(true)] out MemberPath? sourceMemberPath + [NotNullWhen(true)] out SourceMemberPath? sourceMemberPath ) { if ( @@ -78,7 +78,8 @@ out var nestedSourceMemberPath ) ) { - sourceMemberPath = new NonEmptyMemberPath(_context.Source, nestedMemberPath.Path.Concat(nestedSourceMemberPath.Path).ToList()); + var memberPath = new NonEmptyMemberPath(_context.Source, nestedMemberPath.Path.Concat(nestedSourceMemberPath.Path).ToList()); + sourceMemberPath = new SourceMemberPath(memberPath, SourceMemberType.Member); _unusedPaths.Remove(nestedMemberPath); return true; } diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewInstanceBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewInstanceBuilderContext.cs index 5778d489ce..51fad24210 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewInstanceBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewInstanceBuilderContext.cs @@ -3,7 +3,7 @@ using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Descriptors.Mappings.MemberMappings; using Riok.Mapperly.Diagnostics; -using Riok.Mapperly.Symbols; +using Riok.Mapperly.Symbols.Members; namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewInstanceContainerBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewInstanceContainerBuilderContext.cs index 10c695686c..e6f3afcd7b 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewInstanceContainerBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewInstanceContainerBuilderContext.cs @@ -3,7 +3,7 @@ using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Descriptors.Mappings.MemberMappings; using Riok.Mapperly.Diagnostics; -using Riok.Mapperly.Symbols; +using Riok.Mapperly.Symbols.Members; namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewValueTupleConstructorBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewValueTupleConstructorBuilderContext.cs index 1631a936f5..f820de36be 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewValueTupleConstructorBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewValueTupleConstructorBuilderContext.cs @@ -2,7 +2,7 @@ using Microsoft.CodeAnalysis; using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Descriptors.Mappings.MemberMappings; -using Riok.Mapperly.Symbols; +using Riok.Mapperly.Symbols.Members; namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; @@ -29,7 +29,8 @@ public NewValueTupleConstructorBuilderContext(MappingBuilderContext builderConte public bool TryMatchTupleElement(IFieldSymbol member, [NotNullWhen(true)] out MemberMappingInfo? memberInfo) { - if (TryMatchMember(new FieldMember(member, BuilderContext.SymbolAccessor), null, out memberInfo)) + var fieldMember = new FieldMember(member, BuilderContext.SymbolAccessor); + if (TryMatchMember(fieldMember, null, out memberInfo)) return true; if ( @@ -37,7 +38,8 @@ public bool TryMatchTupleElement(IFieldSymbol member, [NotNullWhen(true)] out Me && !string.Equals(member.CorrespondingTupleField.Name, member.Name, StringComparison.Ordinal) ) { - if (TryMatchMember(new FieldMember(member.CorrespondingTupleField, BuilderContext.SymbolAccessor), null, out memberInfo)) + var tupleFieldMember = new FieldMember(member.CorrespondingTupleField, BuilderContext.SymbolAccessor); + if (TryMatchMember(tupleFieldMember, null, out memberInfo)) return true; } @@ -54,7 +56,7 @@ public void AddTupleConstructorParameterMapping(ValueTupleConstructorParameterMa protected override bool TryFindSourcePath( IReadOnlyList> pathCandidates, bool ignoreCase, - [NotNullWhen(true)] out MemberPath? sourceMemberPath + [NotNullWhen(true)] out SourceMemberPath? sourceMemberPath ) { if (base.TryFindSourcePath(pathCandidates, ignoreCase, out sourceMemberPath)) @@ -68,7 +70,7 @@ protected override bool TryFindSourcePath( private bool TryFindSecondaryTupleSourceField( IReadOnlyList> pathCandidates, - [NotNullWhen(true)] out MemberPath? sourceMemberPath + [NotNullWhen(true)] out SourceMemberPath? sourcePath ) { foreach (var pathParts in pathCandidates) @@ -79,15 +81,14 @@ private bool TryFindSecondaryTupleSourceField( var name = pathParts[0]; if (_secondarySourceNames.TryGetValue(name, out var sourceField)) { - sourceMemberPath = new NonEmptyMemberPath( - Mapping.SourceType, - [new FieldMember(sourceField, BuilderContext.SymbolAccessor)] - ); + var sourceFieldMember = new FieldMember(sourceField, BuilderContext.SymbolAccessor); + var sourceMemberPath = new NonEmptyMemberPath(Mapping.SourceType, [sourceFieldMember]); + sourcePath = new SourceMemberPath(sourceMemberPath, SourceMemberType.Member); return true; } } - sourceMemberPath = null; + sourcePath = null; return false; } diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewValueTupleExpressionBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewValueTupleExpressionBuilderContext.cs index c87a4acd71..6f7369640b 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewValueTupleExpressionBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewValueTupleExpressionBuilderContext.cs @@ -2,7 +2,7 @@ using Microsoft.CodeAnalysis; using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Descriptors.Mappings.MemberMappings; -using Riok.Mapperly.Symbols; +using Riok.Mapperly.Symbols.Members; namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; @@ -29,7 +29,8 @@ public NewValueTupleExpressionBuilderContext(MappingBuilderContext builderContex public bool TryMatchTupleElement(IFieldSymbol member, [NotNullWhen(true)] out MemberMappingInfo? memberInfo) { - if (TryMatchMember(new FieldMember(member, BuilderContext.SymbolAccessor), null, out memberInfo)) + var fieldMember = new FieldMember(member, BuilderContext.SymbolAccessor); + if (TryMatchMember(fieldMember, null, out memberInfo)) return true; if ( @@ -37,7 +38,8 @@ public bool TryMatchTupleElement(IFieldSymbol member, [NotNullWhen(true)] out Me && !string.Equals(member.CorrespondingTupleField.Name, member.Name, StringComparison.Ordinal) ) { - if (TryMatchMember(new FieldMember(member.CorrespondingTupleField, BuilderContext.SymbolAccessor), null, out memberInfo)) + var tupleFieldMember = new FieldMember(member.CorrespondingTupleField, BuilderContext.SymbolAccessor); + if (TryMatchMember(tupleFieldMember, null, out memberInfo)) return true; } @@ -54,7 +56,7 @@ public void AddTupleConstructorParameterMapping(ValueTupleConstructorParameterMa protected override bool TryFindSourcePath( IReadOnlyList> pathCandidates, bool ignoreCase, - [NotNullWhen(true)] out MemberPath? sourceMemberPath + [NotNullWhen(true)] out SourceMemberPath? sourceMemberPath ) { if (base.TryFindSourcePath(pathCandidates, ignoreCase, out sourceMemberPath)) @@ -68,7 +70,7 @@ protected override bool TryFindSourcePath( private bool TryFindSecondaryTupleSourceField( IReadOnlyList> pathCandidates, - [NotNullWhen(true)] out MemberPath? sourceMemberPath + [NotNullWhen(true)] out SourceMemberPath? sourcePath ) { foreach (var pathParts in pathCandidates) @@ -79,15 +81,14 @@ private bool TryFindSecondaryTupleSourceField( var name = pathParts[0]; if (_secondarySourceNames.TryGetValue(name, out var sourceField)) { - sourceMemberPath = new NonEmptyMemberPath( - Mapping.SourceType, - [new FieldMember(sourceField, BuilderContext.SymbolAccessor)] - ); + var sourceFieldMember = new FieldMember(sourceField, BuilderContext.SymbolAccessor); + var sourceMemberPath = new NonEmptyMemberPath(Mapping.SourceType, [sourceFieldMember]); + sourcePath = new SourceMemberPath(sourceMemberPath, SourceMemberType.Member); return true; } } - sourceMemberPath = null; + sourcePath = null; return false; } diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/EnumerableMappingBodyBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/EnumerableMappingBodyBuilder.cs index bfed72dabe..d36cd06907 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/EnumerableMappingBodyBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/EnumerableMappingBodyBuilder.cs @@ -3,7 +3,6 @@ using Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Helpers; -using Riok.Mapperly.Symbols; namespace Riok.Mapperly.Descriptors.MappingBodyBuilders; @@ -56,7 +55,7 @@ private static void IgnoreSystemMembers(IMembersBuilderContext ctx, ITypeS foreach (var member in ctx.BuilderContext.SymbolAccessor.GetAllAccessibleMappableMembers(systemType)) { - ctx.IgnoreMembers(member.Name); + ctx.IgnoreMembers(member); } } @@ -64,41 +63,34 @@ private static void BuildConstructorMapping(INewInstanceBuilderContext(3, StringComparer.OrdinalIgnoreCase); - if ( - ctx.Mapping.CollectionInfos.Source.CountIsKnown - && ctx.BuilderContext.SymbolAccessor.TryFindMemberPath( - ctx.Mapping.SourceType, - [ctx.Mapping.CollectionInfos.Source.CountPropertyName], - out var sourceCountMemberPath - ) - ) + if (ctx.Mapping.CollectionInfos.Source.CountIsKnown) { - additionalCtorParameterMappings[nameof(List.Capacity)] = sourceCountMemberPath; - additionalCtorParameterMappings[nameof(List.Count)] = sourceCountMemberPath; - additionalCtorParameterMappings[nameof(Array.Length)] = sourceCountMemberPath; + ctx.TryAddSourceMemberAlias(nameof(List.Capacity), ctx.Mapping.CollectionInfos.Source.CountMember); + ctx.TryAddSourceMemberAlias(nameof(List.Count), ctx.Mapping.CollectionInfos.Source.CountMember); + ctx.TryAddSourceMemberAlias(nameof(Array.Length), ctx.Mapping.CollectionInfos.Source.CountMember); } // always prefer parameterized constructor for system collections (to map capacity correctly) var targetIsSystemType = ctx.Mapping.TargetType.IsArrayType() || ctx.Mapping.TargetType.IsInRootNamespace(SystemNamespaceName); + var ctorParamMappings = NewInstanceObjectMemberMappingBodyBuilder.BuildConstructorMapping(ctx, targetIsSystemType ? false : null); - var options = new NewInstanceObjectMemberMappingBodyBuilder.ConstructorMappingBuilderOptions( - additionalCtorParameterMappings, - PreferParameterlessConstructor: targetIsSystemType ? false : null - ); - var usedParameterValues = NewInstanceObjectMemberMappingBodyBuilder.BuildConstructorMapping(ctx, options); + var countIsMapped = + ctx.BuilderContext.CollectionInfos!.Source.CountIsKnown + && ctorParamMappings.Any(m => + Equals(m.MemberInfo.SourceMember?.MemberPath.Member, ctx.Mapping.CollectionInfos.Source.CountMember) + ); // if no additional parameter was used, // the count/capacity is not mapped, // try to build an EnsureCapacity statement. if ( - usedParameterValues.Count == 0 + !countIsMapped && EnsureCapacityBuilder.TryBuildEnsureCapacity(ctx.BuilderContext, ctx.Mapping.CollectionInfos) is { } ensureCapacity ) { - if (ctx.BuilderContext.CollectionInfos!.Source.CountIsKnown) + if (ctx.Mapping.CollectionInfos.Source.CountIsKnown) { - ctx.SetMembersMapped(ctx.BuilderContext.CollectionInfos.Source.CountPropertyName); + ctx.IgnoreMembers(ctx.Mapping.CollectionInfos.Source.CountMember); } ctx.Mapping.AddEnsureCapacity(ensureCapacity); diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MemberMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MemberMappingBuilder.cs index f60925fbe2..98fa7bb172 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MemberMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MemberMappingBuilder.cs @@ -6,7 +6,7 @@ using Riok.Mapperly.Descriptors.Mappings.MemberMappings.SourceValue; using Riok.Mapperly.Diagnostics; using Riok.Mapperly.Helpers; -using Riok.Mapperly.Symbols; +using Riok.Mapperly.Symbols.Members; namespace Riok.Mapperly.Descriptors.MappingBodyBuilders; @@ -35,9 +35,9 @@ public static bool TryBuildContainerAssignment( return false; } - var setterTargetPath = SetterMemberPath.Build(ctx.BuilderContext, memberInfo.TargetMember); + var targetPathSetter = memberInfo.TargetMember.BuildSetter(ctx.BuilderContext); requiresNullHandling = mappedSourceValue is MappedMemberSourceValue { RequiresSourceNullCheck: true }; - mapping = new MemberAssignmentMapping(setterTargetPath, mappedSourceValue, memberInfo); + mapping = new MemberAssignmentMapping(targetPathSetter, mappedSourceValue, memberInfo); return true; } @@ -53,8 +53,8 @@ public static bool TryBuildAssignment( return false; } - var setterTargetPath = SetterMemberPath.Build(ctx.BuilderContext, memberInfo.TargetMember); - mapping = new MemberAssignmentMapping(setterTargetPath, mappedSourceValue, memberInfo); + var targetMemberSetter = memberInfo.TargetMember.BuildSetter(ctx.BuilderContext); + mapping = new MemberAssignmentMapping(targetMemberSetter, mappedSourceValue, memberInfo); return true; } @@ -77,7 +77,7 @@ public static bool TryBuild( { ctx.BuilderContext.ReportDiagnostic( DiagnosticDescriptors.CouldNotMapMember, - sourceMember.ToDisplayString(), + sourceMember.MemberPath.ToDisplayString(), targetMember.ToDisplayString() ); sourceValue = null; @@ -86,17 +86,17 @@ public static bool TryBuild( if (codeStyle == CodeStyle.Statement) { - sourceValue = BuildBlockNullHandlingMapping(ctx, delegateMapping, sourceMember, targetMember); + sourceValue = BuildBlockNullHandlingMapping(ctx, delegateMapping, sourceMember.MemberPath, targetMember); return true; } - if (!ValidateLoopMapping(ctx, delegateMapping, sourceMember, targetMember)) + if (!ValidateLoopMapping(ctx, delegateMapping, sourceMember.MemberPath, targetMember)) { sourceValue = null; return false; } - sourceValue = BuildInlineNullHandlingMapping(ctx, delegateMapping, sourceMember, targetMember.MemberType); + sourceValue = BuildInlineNullHandlingMapping(ctx, delegateMapping, sourceMember.MemberPath, targetMember.MemberType); return true; } @@ -137,8 +137,6 @@ private static NullMappedMemberSourceValue BuildInlineNullHandlingMapping( ITypeSymbol targetMemberType ) { - var getterSourcePath = GetterMemberPath.Build(ctx.BuilderContext, sourcePath); - var nullFallback = NullFallbackValue.Default; if (!delegateMapping.SourceType.IsNullable() && sourcePath.IsAnyNullable()) { @@ -147,7 +145,7 @@ ITypeSymbol targetMemberType return new NullMappedMemberSourceValue( delegateMapping, - getterSourcePath, + sourcePath.BuildGetter(ctx.BuilderContext), targetMemberType, nullFallback, !ctx.BuilderContext.IsExpression @@ -161,12 +159,12 @@ private static ISourceValue BuildBlockNullHandlingMapping( NonEmptyMemberPath targetMember ) { - var getterSourcePath = GetterMemberPath.Build(ctx.BuilderContext, sourceMember); + var sourceGetter = sourceMember.BuildGetter(ctx.BuilderContext); // no member of the source path is nullable, no null handling needed if (!sourceMember.IsAnyNullable()) { - return new MappedMemberSourceValue(delegateMapping, getterSourcePath, false, true); + return new MappedMemberSourceValue(delegateMapping, sourceGetter, false, true); } // If null property assignments are allowed, @@ -178,12 +176,12 @@ NonEmptyMemberPath targetMember && (delegateMapping.SourceType.IsNullable() || delegateMapping.IsSynthetic && targetMember.Member.IsNullable) ) { - return new MappedMemberSourceValue(delegateMapping, getterSourcePath, true, false); + return new MappedMemberSourceValue(delegateMapping, sourceGetter, true, false); } // additional null condition check // (only map if the source is not null, else may throw depending on settings) // via RequiresNullCheck - return new MappedMemberSourceValue(delegateMapping, getterSourcePath, false, true); + return new MappedMemberSourceValue(delegateMapping, sourceGetter, false, true); } } diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewInstanceObjectMemberMappingBodyBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewInstanceObjectMemberMappingBodyBuilder.cs index c7ea93acf1..ee72e22edd 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewInstanceObjectMemberMappingBodyBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewInstanceObjectMemberMappingBodyBuilder.cs @@ -5,7 +5,7 @@ using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Descriptors.Mappings.MemberMappings; using Riok.Mapperly.Diagnostics; -using Riok.Mapperly.Symbols; +using Riok.Mapperly.Symbols.Members; namespace Riok.Mapperly.Descriptors.MappingBodyBuilders; @@ -14,11 +14,6 @@ namespace Riok.Mapperly.Descriptors.MappingBodyBuilders; /// public static class NewInstanceObjectMemberMappingBodyBuilder { - public record ConstructorMappingBuilderOptions( - IReadOnlyDictionary? AdditionalParameterSourceValues = null, - bool? PreferParameterlessConstructor = null - ); - public static void BuildMappingBody(MappingBuilderContext ctx, NewInstanceObjectMemberMapping mapping) { var mappingCtx = new NewInstanceBuilderContext(ctx, mapping); @@ -36,13 +31,9 @@ public static void BuildMappingBody(MappingBuilderContext ctx, NewInstanceObject mappingCtx.AddDiagnostics(); } - /// - /// Tries to build a constructor invocation. - /// - /// A containing all used . - public static HashSet BuildConstructorMapping( + public static IReadOnlyList BuildConstructorMapping( INewInstanceBuilderContext ctx, - ConstructorMappingBuilderOptions? options = null + bool? preferParameterlessConstructor = null ) { if (ctx.Mapping.TargetType is not INamedTypeSymbol namedTargetType) @@ -60,7 +51,7 @@ public static HashSet BuildConstructorMapping( .OrderByDescending(x => ctx.BuilderContext.SymbolAccessor.HasAttribute(x)) .ThenBy(x => ctx.BuilderContext.SymbolAccessor.HasAttribute(x)); - if (options?.PreferParameterlessConstructor ?? ctx.BuilderContext.Configuration.Mapper.PreferParameterlessConstructors) + if (preferParameterlessConstructor ?? ctx.BuilderContext.Configuration.Mapper.PreferParameterlessConstructors) { ctorCandidates = ctorCandidates.ThenByDescending(x => x.Parameters.Length == 0).ThenByDescending(x => x.Parameters.Length); } @@ -71,15 +62,7 @@ public static HashSet BuildConstructorMapping( foreach (var ctorCandidate in ctorCandidates) { - if ( - !TryBuildConstructorMapping( - ctx, - ctorCandidate, - options, - out var usedAdditionalParameterSourceValues, - out var constructorParameterMappings - ) - ) + if (!TryBuildConstructorMapping(ctx, ctorCandidate, out var constructorParameterMappings)) { if (ctx.BuilderContext.SymbolAccessor.HasAttribute(ctorCandidate)) { @@ -98,7 +81,7 @@ out var constructorParameterMappings ctx.AddConstructorParameterMapping(mapping); } - return usedAdditionalParameterSourceValues; + return constructorParameterMappings; } ctx.BuilderContext.ReportDiagnostic(DiagnosticDescriptors.NoConstructorFound, ctx.BuilderContext.Target); @@ -156,25 +139,17 @@ MemberMappingInfo memberInfo private static bool TryBuildConstructorMapping( INewInstanceBuilderContext ctx, IMethodSymbol ctor, - ConstructorMappingBuilderOptions? options, - [NotNullWhen(true)] out HashSet? usedAdditionalParameterSourceValues, [NotNullWhen(true)] out List? constructorParameterMappings ) { constructorParameterMappings = new List(); - usedAdditionalParameterSourceValues = new HashSet(); var skippedOptionalParam = false; foreach (var parameter in ctor.Parameters) { if ( - !TryMatchParameter( - ctx, - options?.AdditionalParameterSourceValues, - usedAdditionalParameterSourceValues, - parameter, - out var memberMappingInfo - ) || !SourceValueBuilder.TryBuildMappedSourceValue(ctx, memberMappingInfo, out var sourceValue) + !ctx.TryMatchParameter(parameter, out var memberMappingInfo) + || !SourceValueBuilder.TryBuildMappedSourceValue(ctx, memberMappingInfo, out var sourceValue) ) { // expressions do not allow skipping of optional parameters @@ -191,31 +166,4 @@ out var memberMappingInfo return true; } - - private static bool TryMatchParameter( - INewInstanceBuilderContext ctx, - IReadOnlyDictionary? additionalParameterMappings, - HashSet usedAdditionalParameterSourceValues, - IParameterSymbol parameter, - [NotNullWhen(true)] out MemberMappingInfo? memberInfo - ) - { - if (ctx.TryMatchParameter(parameter, out memberInfo)) - return true; - - if (additionalParameterMappings?.TryGetValue(parameter.Name, out var sourcePath) == true) - { - memberInfo = new MemberMappingInfo( - sourcePath, - new NonEmptyMemberPath( - ctx.Mapping.TargetType, - [new ConstructorParameterMember(parameter, ctx.BuilderContext.SymbolAccessor)] - ) - ); - usedAdditionalParameterSourceValues.Add(parameter.Name); - return true; - } - - return false; - } } diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewValueTupleMappingBodyBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewValueTupleMappingBodyBuilder.cs index 4812596556..300bb6740d 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewValueTupleMappingBodyBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewValueTupleMappingBodyBuilder.cs @@ -4,6 +4,7 @@ using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Descriptors.Mappings.MemberMappings; using Riok.Mapperly.Diagnostics; +using Riok.Mapperly.Symbols.Members; namespace Riok.Mapperly.Descriptors.MappingBodyBuilders; @@ -57,7 +58,7 @@ out List constructorParameterMappings foreach (var targetMember in targetMembers) { - var targetField = (IFieldSymbol)targetMember.MemberSymbol; + var targetField = ((FieldMember)targetMember).Symbol; if (!ctx.TryMatchTupleElement(targetField, out var memberMappingInfo)) { ctx.BuilderContext.ReportDiagnostic( diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/ObjectMemberMappingBodyBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/ObjectMemberMappingBodyBuilder.cs index 191077fb90..60cdaa92d5 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/ObjectMemberMappingBodyBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/ObjectMemberMappingBodyBuilder.cs @@ -3,7 +3,7 @@ using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Descriptors.Mappings.MemberMappings; using Riok.Mapperly.Diagnostics; -using Riok.Mapperly.Symbols; +using Riok.Mapperly.Symbols.Members; namespace Riok.Mapperly.Descriptors.MappingBodyBuilders; @@ -58,13 +58,7 @@ public static bool ValidateMappingSpecification( } // cannot access non public member in initializer - if ( - allowInitOnlyMember - && ( - !ctx.BuilderContext.SymbolAccessor.IsDirectlyAccessible(targetMemberPath.Member.MemberSymbol) - || !targetMemberPath.Member.CanSetDirectly - ) - ) + if (allowInitOnlyMember && !targetMemberPath.Member.CanSetDirectly) { ctx.BuilderContext.ReportDiagnostic( DiagnosticDescriptors.CannotMapToReadOnlyMember, @@ -78,10 +72,7 @@ public static bool ValidateMappingSpecification( // an expressions target member path is only accessible with unsafe access if ( targetMemberPath.ObjectPath.Any(p => !p.CanGet) - || ( - ctx.BuilderContext.IsExpression - && targetMemberPath.ObjectPath.Any(p => !ctx.BuilderContext.SymbolAccessor.IsDirectlyAccessible(p.MemberSymbol)) - ) + || (ctx.BuilderContext.IsExpression && targetMemberPath.ObjectPath.Any(p => !p.CanGetDirectly)) ) { ctx.BuilderContext.ReportDiagnostic( @@ -114,17 +105,14 @@ public static bool ValidateMappingSpecification( if ( sourceMemberPath != null && ( - sourceMemberPath.Path.Any(p => !p.CanGet) - || ( - ctx.BuilderContext.IsExpression - && sourceMemberPath.Path.Any(p => !ctx.BuilderContext.SymbolAccessor.IsDirectlyAccessible(p.MemberSymbol)) - ) + sourceMemberPath.MemberPath.Path.Any(p => !p.CanGet) + || (ctx.BuilderContext.IsExpression && sourceMemberPath.MemberPath.Path.Any(p => !p.CanGetDirectly)) ) ) { ctx.BuilderContext.ReportDiagnostic( DiagnosticDescriptors.CannotMapFromWriteOnlyMember, - sourceMemberPath.ToDisplayString(), + sourceMemberPath.MemberPath.ToDisplayString(), targetMemberPath.ToDisplayString() ); return false; @@ -209,7 +197,7 @@ MemberMappingInfo memberMappingInfo if ( targetMemberPath.Member is { CanSet: true, IsInitOnly: false } || !targetMemberPath.Path.All(op => op.CanGet) - || !sourceMemberPath.Path.All(op => op.CanGet) + || !sourceMemberPath.MemberPath.Path.All(op => op.CanGet) ) { return false; @@ -219,10 +207,14 @@ MemberMappingInfo memberMappingInfo if (existingTargetMapping == null) return false; - var getterSourcePath = GetterMemberPath.Build(ctx.BuilderContext, sourceMemberPath); - var getterTargetPath = GetterMemberPath.Build(ctx.BuilderContext, targetMemberPath); - - var memberMapping = new MemberExistingTargetMapping(existingTargetMapping, getterSourcePath, getterTargetPath, memberMappingInfo); + var sourceMemberGetter = sourceMemberPath.MemberPath.BuildGetter(ctx.BuilderContext); + var targetMemberGetter = targetMemberPath.BuildGetter(ctx.BuilderContext); + var memberMapping = new MemberExistingTargetMapping( + existingTargetMapping, + sourceMemberGetter, + targetMemberGetter, + memberMappingInfo + ); ctx.AddMemberAssignmentMapping(memberMapping); return true; } diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/SourceValueBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/SourceValueBuilder.cs index 8b51968512..ddd4290b43 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/SourceValueBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/SourceValueBuilder.cs @@ -54,7 +54,7 @@ private static bool TryBuildValue( { // always set the member mapped, // as other diagnostics are reported if the mapping fails to be built - ctx.SetMembersMapped(memberMappingInfo.TargetMember.Path[0].Name); + ctx.SetMembersMapped(memberMappingInfo); if (memberMappingInfo.ValueConfiguration!.Value != null) return TryBuildConstantSourceValue(ctx, memberMappingInfo, out sourceValue); diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/UserMethodMappingBodyBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/UserMethodMappingBodyBuilder.cs index 2ed9bdb82e..d258f41a68 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/UserMethodMappingBodyBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/UserMethodMappingBodyBuilder.cs @@ -29,9 +29,9 @@ public static void BuildMappingBody(MappingBuilderContext ctx, UserDefinedNewIns var options = MappingBuildingOptions.KeepUserSymbol; // the delegate mapping is not embedded - // and is therefore reusable + // and is therefore reusable if there are no additional parameters // if embedded, only the original mapping is callable by others - if (mapping.InternalReferenceHandlingEnabled) + if (mapping is { InternalReferenceHandlingEnabled: true, AdditionalSourceParameters.Count: 0 }) { options |= MappingBuildingOptions.MarkAsReusable; } diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumerableMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumerableMappingBuilder.cs index 4d22b21431..76c2a38b61 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumerableMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumerableMappingBuilder.cs @@ -250,7 +250,7 @@ private static INewInstanceMapping BuildArrayToArrayMapping(MappingBuilderContex targetType, elementMapping, elementMapping.TargetType, - sourceCollectionInfo.CountPropertyName! + sourceCollectionInfo.CountMember!.BuildGetter(ctx.UnsafeAccessorContext) ); } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/ArrayForEachMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/ArrayForEachMapping.cs index 19afac3521..f30091db8f 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/ArrayForEachMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/ArrayForEachMapping.cs @@ -1,6 +1,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Symbols.Members; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; @@ -15,7 +16,7 @@ public class ArrayForEachMapping( ITypeSymbol targetType, INewInstanceMapping elementMapping, ITypeSymbol targetArrayElementType, - string countPropertyName + IMemberGetter sourceCountAccessor ) : NewInstanceMethodMapping(sourceType, targetType) { private const string TargetVariableName = "target"; @@ -28,9 +29,7 @@ public override IEnumerable BuildBody(TypeMappingBuildContext c var loopCounterVariableName = ctx.NameBuilder.New(LoopCounterName); // var target = new T[source.Count]; - var sourceLengthArrayRank = ArrayRankSpecifier( - SingletonSeparatedList(MemberAccess(ctx.Source, countPropertyName)) - ); + var sourceLengthArrayRank = ArrayRankSpecifier(SingletonSeparatedList(sourceCountAccessor.BuildAccess(ctx.Source))); var targetInitializationValue = CreateArray( ArrayType(FullyQualifiedIdentifier(targetArrayElementType)).WithRankSpecifiers(SingletonList(sourceLengthArrayRank)) ); diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/ConstructorParameterMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/ConstructorParameterMapping.cs index 7afa58daad..42a0489061 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/ConstructorParameterMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/ConstructorParameterMapping.cs @@ -26,11 +26,6 @@ public ArgumentSyntax BuildArgument(TypeMappingBuildContext ctx) return _selfOrPreviousIsUnmappedOptional ? arg.WithNameColon(SpacedNameColon(_parameter.Name)) : arg; } - protected bool Equals(ConstructorParameterMapping other) => - _parameter.Equals(other._parameter, SymbolEqualityComparer.Default) - && _sourceValue.Equals(other._sourceValue) - && _selfOrPreviousIsUnmappedOptional == other._selfOrPreviousIsUnmappedOptional; - public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) @@ -42,7 +37,10 @@ public override bool Equals(object? obj) if (obj.GetType() != GetType()) return false; - return Equals((ConstructorParameterMapping)obj); + var other = (ConstructorParameterMapping)obj; + return _parameter.Equals(other._parameter, SymbolEqualityComparer.Default) + && _sourceValue.Equals(other._sourceValue) + && _selfOrPreviousIsUnmappedOptional == other._selfOrPreviousIsUnmappedOptional; } public override int GetHashCode() @@ -55,8 +53,4 @@ public override int GetHashCode() return hashCode; } } - - public static bool operator ==(ConstructorParameterMapping? left, ConstructorParameterMapping? right) => Equals(left, right); - - public static bool operator !=(ConstructorParameterMapping? left, ConstructorParameterMapping? right) => !Equals(left, right); } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberAssignmentMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberAssignmentMapping.cs index 9b5bf2681e..d19c996904 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberAssignmentMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberAssignmentMapping.cs @@ -1,7 +1,7 @@ using System.Diagnostics; using Microsoft.CodeAnalysis.CSharp.Syntax; using Riok.Mapperly.Descriptors.Mappings.MemberMappings.SourceValue; -using Riok.Mapperly.Symbols; +using Riok.Mapperly.Symbols.Members; namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings; @@ -10,13 +10,13 @@ namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings; /// (e.g. target.A = source.B or target.A = "fooBar") /// [DebuggerDisplay("MemberAssignmentMapping({_sourceValue} => {_targetPath})")] -public class MemberAssignmentMapping(SetterMemberPath targetPath, ISourceValue sourceValue, MemberMappingInfo memberInfo) +public class MemberAssignmentMapping(MemberPathSetter targetPath, ISourceValue sourceValue, MemberMappingInfo memberInfo) : IMemberAssignmentMapping { public MemberMappingInfo MemberInfo { get; } = memberInfo; private readonly ISourceValue _sourceValue = sourceValue; - private readonly SetterMemberPath _targetPath = targetPath; + private readonly MemberPathSetter _targetPath = targetPath; public IEnumerable Build(TypeMappingBuildContext ctx, ExpressionSyntax targetAccess) => ctx.SyntaxFactory.SingleStatement(BuildExpression(ctx, targetAccess)); @@ -40,17 +40,9 @@ public override bool Equals(object? obj) if (obj.GetType() != GetType()) return false; - return Equals((MemberAssignmentMapping)obj); + var other = (MemberAssignmentMapping)obj; + return _sourceValue.Equals(other._sourceValue) && _targetPath.Equals(other._targetPath); } public override int GetHashCode() => HashCode.Combine(_sourceValue, _targetPath); - - public static bool operator ==(MemberAssignmentMapping? left, MemberAssignmentMapping? right) => Equals(left, right); - - public static bool operator !=(MemberAssignmentMapping? left, MemberAssignmentMapping? right) => !Equals(left, right); - - protected bool Equals(MemberAssignmentMapping other) - { - return _sourceValue.Equals(other._sourceValue) && _targetPath.Equals(other._targetPath); - } } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberExistingTargetMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberExistingTargetMapping.cs index cf7e0fc827..35c2214b41 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberExistingTargetMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberExistingTargetMapping.cs @@ -1,6 +1,6 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Riok.Mapperly.Descriptors.Mappings.ExistingTarget; -using Riok.Mapperly.Symbols; +using Riok.Mapperly.Symbols.Members; namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings; @@ -9,8 +9,8 @@ namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings; /// public class MemberExistingTargetMapping( IExistingTargetMapping delegateMapping, - GetterMemberPath sourcePath, - GetterMemberPath targetPath, + MemberPathGetter sourcePath, + MemberPathGetter targetPath, MemberMappingInfo memberInfo ) : IMemberAssignmentMapping { diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberMappingInfo.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberMappingInfo.cs index 396abf6f9a..2db9e2bbf3 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberMappingInfo.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberMappingInfo.cs @@ -1,35 +1,36 @@ using System.Diagnostics; using Riok.Mapperly.Configuration; -using Riok.Mapperly.Symbols; +using Riok.Mapperly.Symbols.Members; namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings; [DebuggerDisplay("{DebuggerDisplay}")] public record MemberMappingInfo( - MemberPath? SourceMember, + SourceMemberPath? SourceMember, NonEmptyMemberPath TargetMember, MemberValueMappingConfiguration? ValueConfiguration = null, MemberMappingConfiguration? Configuration = null ) { - public MemberMappingInfo(MemberPath? sourceMember, NonEmptyMemberPath targetMember, MemberMappingConfiguration? configuration) + public MemberMappingInfo(SourceMemberPath? sourceMember, NonEmptyMemberPath targetMember, MemberMappingConfiguration? configuration) : this(sourceMember, targetMember, null, configuration) { } public MemberMappingInfo(NonEmptyMemberPath targetMember, MemberValueMappingConfiguration configuration) : this(null, targetMember, configuration) { } - private string DebuggerDisplay => $"{SourceMember?.FullName ?? ValueConfiguration?.DescribeValue()} => {TargetMember.FullName}"; + private string DebuggerDisplay => + $"{SourceMember?.MemberPath.FullName ?? ValueConfiguration?.DescribeValue()} => {TargetMember.FullName}"; public TypeMappingKey ToTypeMappingKey() { if (SourceMember == null) throw new InvalidOperationException($"{SourceMember} and {TargetMember} need to be set to create a {nameof(TypeMappingKey)}"); - return new TypeMappingKey(SourceMember.MemberType, TargetMember.MemberType, Configuration?.ToTypeMappingConfiguration()); + return new TypeMappingKey(SourceMember.MemberPath.MemberType, TargetMember.MemberType, Configuration?.ToTypeMappingConfiguration()); } public string DescribeSource() { - return SourceMember?.ToDisplayString(includeMemberType: false) ?? ValueConfiguration?.DescribeValue() ?? string.Empty; + return SourceMember?.MemberPath.ToDisplayString(includeMemberType: false) ?? ValueConfiguration?.DescribeValue() ?? string.Empty; } } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberNullAssignmentInitializerMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberNullAssignmentInitializerMapping.cs index 18c5243b3b..713d5a88f0 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberNullAssignmentInitializerMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberNullAssignmentInitializerMapping.cs @@ -1,6 +1,6 @@ using System.Diagnostics; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Riok.Mapperly.Symbols; +using Riok.Mapperly.Symbols.Members; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings; @@ -9,9 +9,9 @@ namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings; /// A member initializer which initializes null members to new objects. /// [DebuggerDisplay("MemberNullAssignmentInitializerMapping({_pathToInitialize} ??= new())")] -public class MemberNullAssignmentInitializerMapping(SetterMemberPath pathToInitialize) : MemberAssignmentMappingContainer +public class MemberNullAssignmentInitializerMapping(MemberPathSetter pathToInitialize) : MemberAssignmentMappingContainer { - private readonly SetterMemberPath _pathToInitialize = pathToInitialize; + private readonly MemberPathSetter _pathToInitialize = pathToInitialize; public override IEnumerable Build(TypeMappingBuildContext ctx, ExpressionSyntax targetAccess) { @@ -33,16 +33,9 @@ public override bool Equals(object? obj) if (obj.GetType() != GetType()) return false; - return Equals((MemberNullAssignmentInitializerMapping)obj); + var other = (MemberNullAssignmentInitializerMapping)obj; + return _pathToInitialize.Equals(other._pathToInitialize); } public override int GetHashCode() => _pathToInitialize.GetHashCode(); - - public static bool operator ==(MemberNullAssignmentInitializerMapping? left, MemberNullAssignmentInitializerMapping? right) => - Equals(left, right); - - public static bool operator !=(MemberNullAssignmentInitializerMapping? left, MemberNullAssignmentInitializerMapping? right) => - !Equals(left, right); - - protected bool Equals(MemberNullAssignmentInitializerMapping other) => _pathToInitialize.Equals(other._pathToInitialize); } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberNullDelegateAssignmentMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberNullDelegateAssignmentMapping.cs index 6d454e44df..3dcfc42506 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberNullDelegateAssignmentMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberNullDelegateAssignmentMapping.cs @@ -1,6 +1,6 @@ using System.Diagnostics; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Riok.Mapperly.Symbols; +using Riok.Mapperly.Symbols.Members; using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings; @@ -10,13 +10,13 @@ namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings; /// [DebuggerDisplay("MemberNullDelegateAssignmentMapping({_nullConditionalSourcePath} != null)")] public class MemberNullDelegateAssignmentMapping( - GetterMemberPath nullConditionalSourcePath, + MemberPathGetter nullConditionalSourcePath, IMemberAssignmentMappingContainer parent, bool needsNullSafeAccess ) : MemberAssignmentMappingContainer(parent) { - private readonly GetterMemberPath _nullConditionalSourcePath = nullConditionalSourcePath; - private readonly List _targetsToSetNull = new(); + private readonly MemberPathGetter _nullConditionalSourcePath = nullConditionalSourcePath; + private readonly List _targetsToSetNull = new(); private bool _throwOnSourcePathNull; public void ThrowOnSourcePathNull() @@ -30,7 +30,12 @@ public override IEnumerable Build(TypeMappingBuildContext ctx, // target.Value = Map(Source.Name); // else // throw ... - var sourceNullConditionalAccess = _nullConditionalSourcePath.BuildAccess(ctx.Source, false, needsNullSafeAccess, true); + var sourceNullConditionalAccess = _nullConditionalSourcePath.BuildAccess( + ctx.Source, + addValuePropertyOnNullable: false, + nullConditional: needsNullSafeAccess, + skipTrailingNonNullable: true + ); var condition = IsNotNull(sourceNullConditionalAccess); var conditionCtx = ctx.AddIndentation(); var trueClause = base.Build(conditionCtx, targetAccess); @@ -39,7 +44,7 @@ public override IEnumerable Build(TypeMappingBuildContext ctx, return new[] { ifExpression }; } - public void AddNullMemberAssignment(SetterMemberPath targetPath) => _targetsToSetNull.Add(targetPath); + public void AddNullMemberAssignment(MemberPathSetter targetPath) => _targetsToSetNull.Add(targetPath); public override bool Equals(object? obj) { @@ -58,18 +63,17 @@ public override bool Equals(object? obj) public override int GetHashCode() => _nullConditionalSourcePath.GetHashCode(); - public static bool operator ==(MemberNullDelegateAssignmentMapping? left, MemberNullDelegateAssignmentMapping? right) => - Equals(left, right); - - public static bool operator !=(MemberNullDelegateAssignmentMapping? left, MemberNullDelegateAssignmentMapping? right) => - !Equals(left, right); - private IEnumerable? BuildElseClause(TypeMappingBuildContext ctx, ExpressionSyntax targetAccess) { if (_throwOnSourcePathNull) { // throw new ArgumentNullException - var nameofSourceAccess = _nullConditionalSourcePath.BuildAccess(ctx.Source, false, false, true); + var nameofSourceAccess = _nullConditionalSourcePath.BuildAccess( + ctx.Source, + addValuePropertyOnNullable: false, + nullConditional: false, + skipTrailingNonNullable: true + ); return new[] { ctx.SyntaxFactory.ExpressionStatement(ThrowArgumentNullException(nameofSourceAccess)) }; } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MethodMemberNullAssignmentInitializerMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MethodMemberNullAssignmentInitializerMapping.cs index 2909461e7c..3de286b088 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MethodMemberNullAssignmentInitializerMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MethodMemberNullAssignmentInitializerMapping.cs @@ -2,7 +2,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Riok.Mapperly.Emit.Syntax; -using Riok.Mapperly.Symbols; +using Riok.Mapperly.Symbols.Members; namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings; @@ -10,10 +10,10 @@ namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings; /// A member initializer which initializes null members to new objects. /// [DebuggerDisplay("MemberNullAssignmentInitializerMapping({_targetPathToInitialize} ??= new())")] -public class MethodMemberNullAssignmentInitializerMapping(SetterMemberPath targetPathToInitialize, GetterMemberPath sourcePathToInitialize) +public class MethodMemberNullAssignmentInitializerMapping(MemberPathSetter targetPathToInitialize, MemberPathGetter sourcePathToInitialize) : MemberAssignmentMappingContainer { - private readonly SetterMemberPath _targetPathToInitialize = targetPathToInitialize; + private readonly MemberPathSetter _targetPathToInitialize = targetPathToInitialize; public override IEnumerable Build(TypeMappingBuildContext ctx, ExpressionSyntax targetAccess) { @@ -39,21 +39,9 @@ public override bool Equals(object? obj) if (obj.GetType() != GetType()) return false; - return Equals((MethodMemberNullAssignmentInitializerMapping)obj); + var other = (MethodMemberNullAssignmentInitializerMapping)obj; + return _targetPathToInitialize.Equals(other._targetPathToInitialize); } public override int GetHashCode() => _targetPathToInitialize.GetHashCode(); - - public static bool operator ==( - MethodMemberNullAssignmentInitializerMapping? left, - MethodMemberNullAssignmentInitializerMapping? right - ) => Equals(left, right); - - public static bool operator !=( - MethodMemberNullAssignmentInitializerMapping? left, - MethodMemberNullAssignmentInitializerMapping? right - ) => !Equals(left, right); - - protected bool Equals(MethodMemberNullAssignmentInitializerMapping other) => - _targetPathToInitialize.Equals(other._targetPathToInitialize); } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/MappedMemberSourceValue.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/MappedMemberSourceValue.cs index e62bc94a6a..5d89387251 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/MappedMemberSourceValue.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/MappedMemberSourceValue.cs @@ -1,5 +1,5 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; -using Riok.Mapperly.Symbols; +using Riok.Mapperly.Symbols.Members; namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings.SourceValue; @@ -9,16 +9,22 @@ namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings.SourceValue; /// public class MappedMemberSourceValue( INewInstanceMapping delegateMapping, - GetterMemberPath sourceGetter, + MemberPathGetter sourceMember, bool nullConditionalAccess, bool addValuePropertyOnNullable ) : ISourceValue { - public bool RequiresSourceNullCheck => !nullConditionalAccess && sourceGetter.MemberPath.IsAnyNullable(); + public bool RequiresSourceNullCheck => !nullConditionalAccess && sourceMember.MemberPath.IsAnyNullable(); public ExpressionSyntax Build(TypeMappingBuildContext ctx) { - ctx = ctx.WithSource(sourceGetter.BuildAccess(ctx.Source, addValuePropertyOnNullable, nullConditionalAccess)); + ctx = ctx.WithSource( + sourceMember.BuildAccess( + ctx.Source, + addValuePropertyOnNullable: addValuePropertyOnNullable, + nullConditional: nullConditionalAccess + ) + ); return delegateMapping.Build(ctx); } } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/NullMappedMemberSourceValue.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/NullMappedMemberSourceValue.cs index 2244b2b185..6c04d8adeb 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/NullMappedMemberSourceValue.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/NullMappedMemberSourceValue.cs @@ -2,7 +2,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Riok.Mapperly.Helpers; -using Riok.Mapperly.Symbols; +using Riok.Mapperly.Symbols.Members; using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings.SourceValue; @@ -14,7 +14,7 @@ namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings.SourceValue; [DebuggerDisplay("NullMappedMemberSourceValue({_sourceGetter}: {_delegateMapping})")] public class NullMappedMemberSourceValue( INewInstanceMapping delegateMapping, - GetterMemberPath sourceGetter, + MemberPathGetter sourceGetter, ITypeSymbol targetType, NullFallbackValue nullFallback, bool useNullConditionalAccess @@ -22,7 +22,7 @@ bool useNullConditionalAccess { private readonly INewInstanceMapping _delegateMapping = delegateMapping; private readonly NullFallbackValue _nullFallback = nullFallback; - private readonly GetterMemberPath _sourceGetter = sourceGetter; + private readonly MemberPathGetter _sourceGetter = sourceGetter; public ExpressionSyntax Build(TypeMappingBuildContext ctx) { @@ -51,21 +51,12 @@ public ExpressionSyntax Build(TypeMappingBuildContext ctx) : Coalesce(mapping, NullSubstitute(targetType, nameofSourceAccess, _nullFallback)); } - var notNullCondition = useNullConditionalAccess - ? IsNotNull(_sourceGetter.BuildAccess(ctx.Source, nullConditional: true, skipTrailingNonNullable: true)) - : _sourceGetter.MemberPath.BuildNonNullConditionWithoutConditionalAccess(ctx.Source)!; - var sourceMemberAccess = _sourceGetter.BuildAccess(ctx.Source, true); + var notNullCondition = _sourceGetter.BuildNotNullCondition(ctx.Source, useNullConditionalAccess)!; + var sourceMemberAccess = _sourceGetter.BuildAccess(ctx.Source, addValuePropertyOnNullable: true); ctx = ctx.WithSource(sourceMemberAccess); return Conditional(notNullCondition, _delegateMapping.Build(ctx), NullSubstitute(targetType, sourceMemberAccess, _nullFallback)); } - protected bool Equals(NullMappedMemberSourceValue other) - { - return _delegateMapping.Equals(other._delegateMapping) - && _nullFallback == other._nullFallback - && _sourceGetter.Equals(other._sourceGetter); - } - public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) @@ -77,12 +68,11 @@ public override bool Equals(object? obj) if (obj.GetType() != GetType()) return false; - return Equals((NullMappedMemberSourceValue)obj); + var other = (NullMappedMemberSourceValue)obj; + return _delegateMapping.Equals(other._delegateMapping) + && _nullFallback == other._nullFallback + && _sourceGetter.Equals(other._sourceGetter); } public override int GetHashCode() => HashCode.Combine(_delegateMapping, _nullFallback, _sourceGetter); - - public static bool operator ==(NullMappedMemberSourceValue? left, NullMappedMemberSourceValue? right) => Equals(left, right); - - public static bool operator !=(NullMappedMemberSourceValue? left, NullMappedMemberSourceValue? right) => !Equals(left, right); } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/UnsafeAccess/UnsafeFieldAccessor.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/UnsafeAccess/UnsafeFieldAccessor.cs deleted file mode 100644 index a15df2b851..0000000000 --- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/UnsafeAccess/UnsafeFieldAccessor.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Riok.Mapperly.Emit; -using Riok.Mapperly.Emit.Syntax; -using Riok.Mapperly.Helpers; -using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; -using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; - -namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings.UnsafeAccess; - -/// -/// Creates an extension method to access an objects non public field using .Net 8's UnsafeAccessor. -/// /// -/// [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_value")] -/// public extern static int GetValue(this global::MyClass source); -/// -/// -public class UnsafeFieldAccessor(IFieldSymbol value, string methodName) : IUnsafeAccessor -{ - private const string DefaultTargetParameterName = "target"; - - private readonly string _targetType = value.ContainingType.FullyQualifiedIdentifierName(); - private readonly string _result = value.Type.FullyQualifiedIdentifierName(); - private readonly string _memberName = value.Name; - - public string MethodName { get; } = methodName; - - public MethodDeclarationSyntax BuildMethod(SourceEmitterContext ctx) - { - var nameBuilder = ctx.NameBuilder.NewScope(); - var targetName = nameBuilder.New(DefaultTargetParameterName); - - var target = Parameter(_targetType, targetName, true); - - var parameters = ParameterList(CommaSeparatedList(target)); - var attributeList = ctx.SyntaxFactory.UnsafeAccessorAttributeList(UnsafeAccessorType.Field, _memberName); - var returnType = RefType(IdentifierName(_result).AddTrailingSpace()) - .WithRefKeyword(Token(TriviaList(), SyntaxKind.RefKeyword, TriviaList(Space))); - - return PublicStaticExternMethod(ctx, returnType, MethodName, parameters, attributeList); - } -} diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/UnsafeAccess/UnsafeGetPropertyAccessor.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/UnsafeAccess/UnsafeGetPropertyAccessor.cs deleted file mode 100644 index 77f5716aa3..0000000000 --- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/UnsafeAccess/UnsafeGetPropertyAccessor.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Riok.Mapperly.Emit; -using Riok.Mapperly.Emit.Syntax; -using Riok.Mapperly.Helpers; -using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; -using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; - -namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings.UnsafeAccess; - -/// -/// Creates an extension method to access an objects non public property using .Net 8's UnsafeAccessor. -/// /// -/// [UnsafeAccessor(UnsafeAccessorKind.Property, Name = "get_value")] -/// public extern static int GetValue(this global::MyClass source); -/// -/// -public class UnsafeGetPropertyAccessor(IPropertySymbol result, string methodName) : IUnsafeAccessor -{ - private const string DefaultSourceParameterName = "source"; - - private readonly string _result = result.Type.FullyQualifiedIdentifierName(); - private readonly string _sourceType = result.ContainingType.FullyQualifiedIdentifierName(); - private readonly string _memberName = result.Name; - - public string MethodName { get; } = methodName; - - public MethodDeclarationSyntax BuildMethod(SourceEmitterContext ctx) - { - var nameBuilder = ctx.NameBuilder.NewScope(); - var sourceName = nameBuilder.New(DefaultSourceParameterName); - - var source = Parameter(_sourceType, sourceName, true); - - var parameters = ParameterList(CommaSeparatedList(source)); - var attributeList = ctx.SyntaxFactory.UnsafeAccessorAttributeList(UnsafeAccessorType.Method, $"get_{_memberName}"); - return PublicStaticExternMethod(ctx, IdentifierName(_result).AddTrailingSpace(), MethodName, parameters, attributeList); - } -} diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/ValueTupleConstructorParameterMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/ValueTupleConstructorParameterMapping.cs index 879f0274b0..ed97b53e90 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/ValueTupleConstructorParameterMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/ValueTupleConstructorParameterMapping.cs @@ -34,9 +34,6 @@ public ArgumentSyntax BuildArgument(TypeMappingBuildContext ctx, bool emitFieldN : argument.WithNameColon(SpacedNameColon(Parameter.Name)); } - protected bool Equals(ValueTupleConstructorParameterMapping other) => - Parameter.Equals(other.Parameter, SymbolEqualityComparer.Default) && _sourceValue.Equals(other._sourceValue); - public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) @@ -48,7 +45,8 @@ public override bool Equals(object? obj) if (obj.GetType() != GetType()) return false; - return Equals((ValueTupleConstructorParameterMapping)obj); + var other = (ValueTupleConstructorParameterMapping)obj; + return Parameter.Equals(other.Parameter, SymbolEqualityComparer.Default) && _sourceValue.Equals(other._sourceValue); } public override int GetHashCode() @@ -60,10 +58,4 @@ public override int GetHashCode() return hashCode; } } - - public static bool operator ==(ValueTupleConstructorParameterMapping? left, ValueTupleConstructorParameterMapping? right) => - Equals(left, right); - - public static bool operator !=(ValueTupleConstructorParameterMapping? left, ValueTupleConstructorParameterMapping? right) => - !Equals(left, right); } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs index 2dae8a315f..ded1a2844e 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs @@ -32,6 +32,7 @@ public abstract class MethodMapping : ITypeMapping }; private readonly ITypeSymbol _returnType; + private readonly MethodDeclarationSyntax? _methodDeclarationSyntax; private string? _methodName; @@ -51,17 +52,17 @@ ITypeSymbol targetType { TargetType = targetType; SourceParameter = sourceParameter; + Method = method; IsExtensionMethod = method.IsExtensionMethod; ReferenceHandlerParameter = referenceHandlerParameter; - Method = method; - MethodDeclarationSyntax = Method?.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() as MethodDeclarationSyntax; + _methodDeclarationSyntax = method.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() as MethodDeclarationSyntax; _methodName = method.Name; _returnType = method.ReturnsVoid ? method.ReturnType : targetType; } - protected IMethodSymbol? Method { get; } + public IReadOnlyCollection AdditionalSourceParameters { get; init; } = []; - protected MethodDeclarationSyntax? MethodDeclarationSyntax { get; } + protected IMethodSymbol? Method { get; } protected bool IsExtensionMethod { get; } @@ -117,15 +118,15 @@ internal virtual void EnableReferenceHandling(INamedTypeSymbol iReferenceHandler } protected virtual ParameterListSyntax BuildParameterList() => - ParameterList(IsExtensionMethod, SourceParameter, ReferenceHandlerParameter); + ParameterList(IsExtensionMethod, [SourceParameter, ReferenceHandlerParameter, .. AdditionalSourceParameters]); private IEnumerable BuildModifiers(bool isStatic) { // if a syntax is referenced the code written by the user copy all modifiers, // otherwise only set private and optionally static - if (MethodDeclarationSyntax != null) + if (_methodDeclarationSyntax != null) { - return MethodDeclarationSyntax.Modifiers.Select(x => TrailingSpacedToken(x.Kind())); + return _methodDeclarationSyntax.Modifiers.Select(x => TrailingSpacedToken(x.Kind())); } return isStatic ? _privateStaticSyntaxToken : _privateSyntaxToken; diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetMethodMapping.cs index 2641d877d8..6a0d97ec43 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetMethodMapping.cs @@ -89,7 +89,7 @@ public override IEnumerable BuildBody(TypeMappingBuildContext c protected override ParameterListSyntax BuildParameterList() // needs to include the target parameter => - ParameterList(IsExtensionMethod, SourceParameter, TargetParameter, ReferenceHandlerParameter); + ParameterList(IsExtensionMethod, [SourceParameter, TargetParameter, ReferenceHandlerParameter, .. AdditionalSourceParameters]); internal override void EnableReferenceHandling(INamedTypeSymbol iReferenceHandlerType) { diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs index 3a03231dea..39cb5bc9de 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs @@ -17,55 +17,67 @@ public class UserDefinedNewInstanceMethodMapping( bool enableReferenceHandling ) : NewInstanceMethodMapping(method, sourceParameter, referenceHandlerParameter, targetType), INewInstanceUserMapping { + private INewInstanceMapping? _delegateMapping; + public new IMethodSymbol Method { get; } = method; public bool? Default { get; } = isDefault; public bool IsExternal => false; - public INewInstanceMapping? DelegateMapping { get; private set; } - /// /// The reference handling is enabled but is only internal to this method. /// No reference handler parameter is passed. /// public bool InternalReferenceHandlingEnabled => enableReferenceHandling && ReferenceHandlerParameter == null; - public void SetDelegateMapping(INewInstanceMapping mapping) => DelegateMapping = mapping; + public void SetDelegateMapping(INewInstanceMapping mapping) => _delegateMapping = mapping; public override ExpressionSyntax Build(TypeMappingBuildContext ctx) { - return InternalReferenceHandlingEnabled ? DelegateMapping?.Build(ctx) ?? base.Build(ctx) : base.Build(ctx); + return InternalReferenceHandlingEnabled ? _delegateMapping?.Build(ctx) ?? base.Build(ctx) : base.Build(ctx); } public override IEnumerable BuildBody(TypeMappingBuildContext ctx) { - if (DelegateMapping == null) + if (_delegateMapping == null) { return new[] { ctx.SyntaxFactory.ExpressionStatement(ctx.SyntaxFactory.ThrowMappingNotImplementedExceptionStatement()) }; } - // the generated mapping method is called with a new reference handler instance - // otherwise the generated method is embedded if (InternalReferenceHandlingEnabled) { // new RefHandler(); var createRefHandler = ctx.SyntaxFactory.CreateInstance(); + + // If additional parameters are used or it is explicitly set as non-default, the method is embedded + // as it cannot be reused by other mappings anyway (additional parameter mappings are never reused). + if ((Default == false || AdditionalSourceParameters.Count > 0) && _delegateMapping is MethodMapping delMethodMapping) + { + var refHandlerName = ctx.NameBuilder.New(DefaultReferenceHandlerParameterName); + + // var refHandler = new RefHandler(); + var declareRefHandler = ctx.SyntaxFactory.DeclareLocalVariable(refHandlerName, createRefHandler); + ctx = ctx.WithRefHandler(refHandlerName); + return delMethodMapping.BuildBody(ctx).Prepend(declareRefHandler); + } + + // the generated mapping method is called with a new reference handler instance ctx = ctx.WithRefHandler(createRefHandler); - return new[] { ctx.SyntaxFactory.Return(DelegateMapping.Build(ctx)) }; + return [ctx.SyntaxFactory.Return(_delegateMapping.Build(ctx))]; } - if (DelegateMapping is MethodMapping delegateMethodMapping) + if (_delegateMapping is MethodMapping delegateMethodMapping) return delegateMethodMapping.BuildBody(ctx); - return new[] { ctx.SyntaxFactory.Return(DelegateMapping.Build(ctx)) }; + return [ctx.SyntaxFactory.Return(_delegateMapping.Build(ctx))]; } internal override void EnableReferenceHandling(INamedTypeSymbol iReferenceHandlerType) { // the parameters of user defined methods should not be manipulated // if the user did not define a parameter a new reference handler is initialized - if (DelegateMapping is MethodMapping methodMapping) + if (_delegateMapping is MethodMapping methodMapping) { methodMapping.EnableReferenceHandling(iReferenceHandlerType); } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeParameterMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeParameterMapping.cs index c48a847bcb..a361c558fa 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeParameterMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeParameterMapping.cs @@ -29,7 +29,10 @@ ITypeSymbol objectType ) { protected override ParameterListSyntax BuildParameterList() => - ParameterList(IsExtensionMethod, SourceParameter, parameters.TargetType, ReferenceHandlerParameter); + ParameterList( + IsExtensionMethod, + [SourceParameter, parameters.TargetType, ReferenceHandlerParameter, .. AdditionalSourceParameters] + ); protected override ExpressionSyntax BuildTargetType() => IdentifierName(parameters.TargetType.Name); } diff --git a/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs index f90faf2476..d99e2b52fc 100644 --- a/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs @@ -2,6 +2,7 @@ using Riok.Mapperly.Abstractions; using Riok.Mapperly.Configuration; using Riok.Mapperly.Descriptors.MappingBuilders; +using Riok.Mapperly.Descriptors.UnsafeAccess; using Riok.Mapperly.Diagnostics; using Riok.Mapperly.Helpers; using Riok.Mapperly.Symbols; diff --git a/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs b/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs index 07799c5a2a..0960a06646 100644 --- a/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs +++ b/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs @@ -6,6 +6,7 @@ using Riok.Mapperly.Abstractions; using Riok.Mapperly.Helpers; using Riok.Mapperly.Symbols; +using Riok.Mapperly.Symbols.Members; namespace Riok.Mapperly.Descriptors; @@ -79,7 +80,7 @@ public bool CanAssign(ITypeSymbol sourceType, ITypeSymbol targetType) /// /// Upgrade the nullability of a symbol from to . - /// Does not upgrade the nullability of type parameters or array element types. + /// Value types are not upgraded. /// /// The symbol to upgrade. /// The upgraded symbol @@ -227,9 +228,35 @@ internal IReadOnlyCollection GetAllAccessibleMappableMembers(IT return members; } + internal bool TryFindMemberPath( + IReadOnlyDictionary members, + IEnumerable> pathCandidates, + bool ignoreCase, + [NotNullWhen(true)] out MemberPath? memberPath + ) + { + var foundPath = new List(); + foreach (var pathCandidate in pathCandidates) + { + if (!members.TryGetValue(pathCandidate[0], out var member)) + continue; + + foundPath.Clear(); + foundPath.Add(member); + if (pathCandidate.Count == 1 || TryFindPath(member.Type, pathCandidate.Skip(1), ignoreCase, foundPath)) + { + memberPath = new NonEmptyMemberPath(member.Type, foundPath); + return true; + } + } + + memberPath = null; + return false; + } + internal bool TryFindMemberPath( ITypeSymbol type, - IEnumerable> pathCandidates, + IEnumerable> pathCandidates, IReadOnlyCollection ignoredNames, bool ignoreCase, [NotNullWhen(true)] out MemberPath? memberPath @@ -238,11 +265,16 @@ internal bool TryFindMemberPath( var foundPath = new List(); foreach (var pathCandidate in pathCandidates) { + // fast path for exact case matches + if (ignoredNames.Contains(pathCandidate[0])) + continue; + // reuse List instead of allocating a new one foundPath.Clear(); if (!TryFindPath(type, pathCandidate, ignoreCase, foundPath)) continue; + // match again to respect ignoreCase parameter if (ignoredNames.Contains(foundPath[0].Name)) continue; @@ -283,7 +315,7 @@ private bool TryFindPath(ITypeSymbol type, IEnumerable path, bool ignore return true; } - private IMappableMember? GetMappableMember(ITypeSymbol symbol, string name, bool ignoreCase) + public IMappableMember? GetMappableMember(ITypeSymbol symbol, string name, bool ignoreCase = false) { var membersBySymbol = ignoreCase ? _allAccessibleMembersCaseInsensitive : _allAccessibleMembersCaseSensitive; diff --git a/src/Riok.Mapperly/Descriptors/TypeMappingKey.cs b/src/Riok.Mapperly/Descriptors/TypeMappingKey.cs index 8aa32bec26..b0062da3c8 100644 --- a/src/Riok.Mapperly/Descriptors/TypeMappingKey.cs +++ b/src/Riok.Mapperly/Descriptors/TypeMappingKey.cs @@ -26,10 +26,11 @@ public TypeMappingKey(ITypeMapping mapping, TypeMappingConfiguration? config = n public TypeMappingKey NonNullable() => new(Source.NonNullable(), Target.NonNullable(), Configuration); - private bool Equals(TypeMappingKey other) => - _comparer.Equals(Source, other.Source) && _comparer.Equals(Target, other.Target) && Configuration.Equals(other.Configuration); - - public override bool Equals(object? obj) => obj is TypeMappingKey other && Equals(other); + public override bool Equals(object? obj) => + obj is TypeMappingKey other + && _comparer.Equals(Source, other.Source) + && _comparer.Equals(Target, other.Target) + && Configuration.Equals(other.Configuration); public override int GetHashCode() { @@ -41,8 +42,4 @@ public override int GetHashCode() return hashCode; } } - - public static bool operator ==(TypeMappingKey left, TypeMappingKey right) => left.Equals(right); - - public static bool operator !=(TypeMappingKey left, TypeMappingKey right) => !left.Equals(right); } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/UnsafeAccess/IUnsafeAccessor.cs b/src/Riok.Mapperly/Descriptors/UnsafeAccess/IUnsafeAccessor.cs similarity index 72% rename from src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/UnsafeAccess/IUnsafeAccessor.cs rename to src/Riok.Mapperly/Descriptors/UnsafeAccess/IUnsafeAccessor.cs index b022aecc4c..e2e943d168 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/UnsafeAccess/IUnsafeAccessor.cs +++ b/src/Riok.Mapperly/Descriptors/UnsafeAccess/IUnsafeAccessor.cs @@ -1,7 +1,7 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Riok.Mapperly.Emit; -namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings.UnsafeAccess; +namespace Riok.Mapperly.Descriptors.UnsafeAccess; /// /// Represents a method accessor for inaccessible members. @@ -10,7 +10,5 @@ namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings.UnsafeAccess; /// public interface IUnsafeAccessor { - MethodDeclarationSyntax BuildMethod(SourceEmitterContext ctx); - - string MethodName { get; } + MethodDeclarationSyntax BuildAccessorMethod(SourceEmitterContext ctx); } diff --git a/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeAccessorContext.cs b/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeAccessorContext.cs new file mode 100644 index 0000000000..acfe6402db --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeAccessorContext.cs @@ -0,0 +1,137 @@ +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using Microsoft.CodeAnalysis; +using Riok.Mapperly.Helpers; +using Riok.Mapperly.Symbols.Members; + +namespace Riok.Mapperly.Descriptors.UnsafeAccess; + +public class UnsafeAccessorContext(UniqueNameBuilder nameBuilder, SymbolAccessor symbolAccessor) +{ + private readonly List _unsafeAccessors = new(); + private readonly Dictionary _unsafeAccessorsBySymbol = new(); + private readonly UniqueNameBuilder _nameBuilder = nameBuilder.NewScope(); + public IReadOnlyCollection UnsafeAccessors => _unsafeAccessors; + + public UnsafeSetPropertyAccessor GetOrBuildPropertySetter(PropertyMember member) + { + return GetOrBuild( + "Set", + UnsafeAccessorType.SetProperty, + member.Symbol, + static (m, methodName) => new UnsafeSetPropertyAccessor(m, methodName) + ); + } + + public UnsafeGetPropertyAccessor GetOrBuildPropertyGetter(PropertyMember member) + { + return GetOrBuild( + "Get", + UnsafeAccessorType.GetProperty, + member.Symbol, + static (m, methodName) => new UnsafeGetPropertyAccessor(m, methodName) + ); + } + + public UnsafeFieldAccessor GetOrBuildFieldGetter(FieldMember member) + { + return GetOrBuild( + "Get", + UnsafeAccessorType.GetField, + member.Symbol, + static (m, methodName) => new UnsafeFieldAccessor(m, methodName) + ); + } + + private TAccessor GetOrBuild( + string methodNamePrefix, + UnsafeAccessorType type, + TSymbol symbol, + Func factory + ) + where TAccessor : IUnsafeAccessor + where TSymbol : ISymbol + { + var key = new UnsafeAccessorKey(symbol, type); + if (TryGetAccessor(key, out var accessor)) + return accessor; + + var methodName = BuildMethodName(methodNamePrefix, symbol); + return CacheAccessor(key, factory(symbol, methodName)); + } + + private T CacheAccessor(UnsafeAccessorKey key, T accessor) + where T : IUnsafeAccessor + { + _unsafeAccessorsBySymbol.Add(key, accessor); + _unsafeAccessors.Add(accessor); + return accessor; + } + + private bool TryGetAccessor(UnsafeAccessorKey key, [NotNullWhen(true)] out T? accessor) + where T : IUnsafeAccessor + { + if (_unsafeAccessorsBySymbol.TryGetValue(key, out var acc)) + { + accessor = (T)acc; + return true; + } + + accessor = default; + return false; + } + + private string BuildMethodName(string prefix, ISymbol symbol) + { + var methodName = prefix + FormatAccessorName(symbol.Name); + return GetUniqueMethodName(symbol.ContainingType, methodName); + } + + private string GetUniqueMethodName(ITypeSymbol symbol, string name) + { + var memberNames = symbolAccessor.GetAllMembers(symbol).Select(x => x.Name); + return _nameBuilder.New(name, memberNames); + } + + /// + /// Strips the leading underscore and capitalise the first letter. + /// + /// Accessor name to be formatted. + /// Formatted accessor name. + private string FormatAccessorName(string name) + { + name = name.TrimStart('_'); + if (name.Length == 0) + return name; + + return char.ToUpper(name[0], CultureInfo.InvariantCulture) + name[1..]; + } + + private enum UnsafeAccessorType + { + GetProperty, + SetProperty, + GetField, + } + + private readonly struct UnsafeAccessorKey(ISymbol member, UnsafeAccessorType type) : IEquatable + { + private readonly ISymbol _member = member; + private readonly UnsafeAccessorType _type = type; + + public bool Equals(UnsafeAccessorKey other) => + _type == other._type && SymbolEqualityComparer.Default.Equals(_member, other._member); + + public override bool Equals(object? obj) => obj is UnsafeAccessorKey other && Equals(other); + + public override int GetHashCode() + { + unchecked + { + var hashCode = SymbolEqualityComparer.Default.GetHashCode(_member); + hashCode = (hashCode * 397) ^ (int)_type; + return hashCode; + } + } + } +} diff --git a/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeFieldAccessor.cs b/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeFieldAccessor.cs new file mode 100644 index 0000000000..d60082a37d --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeFieldAccessor.cs @@ -0,0 +1,55 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Emit; +using Riok.Mapperly.Emit.Syntax; +using Riok.Mapperly.Helpers; +using Riok.Mapperly.Symbols.Members; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; + +namespace Riok.Mapperly.Descriptors.UnsafeAccess; + +/// +/// Creates an extension method to access an objects non public field using .Net 8's UnsafeAccessor. +/// +/// [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_value")] +/// public extern static int GetValue(this global::MyClass source); +/// +/// +public class UnsafeFieldAccessor(IFieldSymbol symbol, string methodName) : IUnsafeAccessor, IMemberSetter, IMemberGetter +{ + private const string DefaultTargetParameterName = "target"; + + public bool SupportsCoalesceAssignment => true; + + public MethodDeclarationSyntax BuildAccessorMethod(SourceEmitterContext ctx) + { + var nameBuilder = ctx.NameBuilder.NewScope(); + var targetName = nameBuilder.New(DefaultTargetParameterName); + + var target = Parameter(symbol.ContainingType.FullyQualifiedIdentifierName(), targetName, true); + + var parameters = ParameterList(CommaSeparatedList(target)); + var attributeList = ctx.SyntaxFactory.UnsafeAccessorAttributeList(UnsafeAccessorType.Field, symbol.Name); + var returnType = RefType(IdentifierName(symbol.Type.FullyQualifiedIdentifierName()).AddTrailingSpace()) + .WithRefKeyword(Token(TriviaList(), SyntaxKind.RefKeyword, TriviaList(Space))); + + return PublicStaticExternMethod(ctx, returnType, methodName, parameters, attributeList); + } + + public ExpressionSyntax BuildAccess(ExpressionSyntax? baseAccess, bool nullConditional = false) + { + if (baseAccess == null) + throw new ArgumentNullException(nameof(baseAccess)); + + ExpressionSyntax method = nullConditional ? ConditionalAccess(baseAccess, methodName) : MemberAccess(baseAccess, methodName); + return Invocation(method); + } + + public ExpressionSyntax BuildAssignment(ExpressionSyntax? baseAccess, ExpressionSyntax valueToAssign, bool coalesceAssignment = false) + { + var access = BuildAccess(baseAccess); + return Assignment(access, valueToAssign, coalesceAssignment); + } +} diff --git a/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeGetPropertyAccessor.cs b/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeGetPropertyAccessor.cs new file mode 100644 index 0000000000..ad46fea538 --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeGetPropertyAccessor.cs @@ -0,0 +1,49 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Emit; +using Riok.Mapperly.Emit.Syntax; +using Riok.Mapperly.Helpers; +using Riok.Mapperly.Symbols.Members; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; + +namespace Riok.Mapperly.Descriptors.UnsafeAccess; + +/// +/// Creates an extension method to access an objects non public property using .Net 8's UnsafeAccessor. +/// +/// [UnsafeAccessor(UnsafeAccessorKind.Property, Name = "get_value")] +/// public extern static int GetValue(this global::MyClass source); +/// +/// +public class UnsafeGetPropertyAccessor(IPropertySymbol symbol, string methodName) : IUnsafeAccessor, IMemberGetter +{ + private const string DefaultSourceParameterName = "source"; + + public MethodDeclarationSyntax BuildAccessorMethod(SourceEmitterContext ctx) + { + var nameBuilder = ctx.NameBuilder.NewScope(); + var sourceName = nameBuilder.New(DefaultSourceParameterName); + + var source = Parameter(symbol.ContainingType.FullyQualifiedIdentifierName(), sourceName, true); + + var parameters = ParameterList(CommaSeparatedList(source)); + var attributeList = ctx.SyntaxFactory.UnsafeAccessorAttributeList(UnsafeAccessorType.Method, $"get_{symbol.Name}"); + return PublicStaticExternMethod( + ctx, + IdentifierName(symbol.Type.FullyQualifiedIdentifierName()).AddTrailingSpace(), + methodName, + parameters, + attributeList + ); + } + + public ExpressionSyntax BuildAccess(ExpressionSyntax? baseAccess, bool nullConditional = false) + { + if (baseAccess == null) + throw new ArgumentNullException(nameof(baseAccess)); + + ExpressionSyntax method = nullConditional ? ConditionalAccess(baseAccess, methodName) : MemberAccess(baseAccess, methodName); + return Invocation(method); + } +} diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/UnsafeAccess/UnsafeSetPropertyAccessor.cs b/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeSetPropertyAccessor.cs similarity index 57% rename from src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/UnsafeAccess/UnsafeSetPropertyAccessor.cs rename to src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeSetPropertyAccessor.cs index 01ac649f3b..3fd1c69d42 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/UnsafeAccess/UnsafeSetPropertyAccessor.cs +++ b/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeSetPropertyAccessor.cs @@ -4,47 +4,52 @@ using Riok.Mapperly.Emit; using Riok.Mapperly.Emit.Syntax; using Riok.Mapperly.Helpers; +using Riok.Mapperly.Symbols.Members; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; -namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings.UnsafeAccess; +namespace Riok.Mapperly.Descriptors.UnsafeAccess; /// /// Creates an extension method to set an objects non public property using .Net 8's UnsafeAccessor. -/// /// +/// /// [UnsafeAccessor(UnsafeAccessorKind.Property, Name = "set_value")] /// public extern static void SetValue(this global::MyClass source, int value); /// /// -public class UnsafeSetPropertyAccessor(IPropertySymbol value, string methodName) : IUnsafeAccessor +public class UnsafeSetPropertyAccessor(IPropertySymbol symbol, string methodName) : IUnsafeAccessor, IMemberSetter { private const string DefaultTargetParameterName = "target"; private const string DefaultValueParameterName = "value"; - private readonly string _targetType = value.ContainingType.FullyQualifiedIdentifierName(); - private readonly string _valueType = value.Type.FullyQualifiedIdentifierName(); - private readonly string _memberName = value.Name; + public bool SupportsCoalesceAssignment => false; - public string MethodName { get; } = methodName; - - public MethodDeclarationSyntax BuildMethod(SourceEmitterContext ctx) + public MethodDeclarationSyntax BuildAccessorMethod(SourceEmitterContext ctx) { var nameBuilder = ctx.NameBuilder.NewScope(); var targetName = nameBuilder.New(DefaultTargetParameterName); var valueName = nameBuilder.New(DefaultValueParameterName); - var target = Parameter(_targetType, targetName, true); - var targetValue = Parameter(_valueType, valueName); + var target = Parameter(symbol.ContainingType.FullyQualifiedIdentifierName(), targetName, true); + var targetValue = Parameter(symbol.Type.FullyQualifiedIdentifierName(), valueName); var parameters = ParameterList(CommaSeparatedList(target, targetValue)); - var attributeList = ctx.SyntaxFactory.UnsafeAccessorAttributeList(UnsafeAccessorType.Method, $"set_{_memberName}"); + var attributeList = ctx.SyntaxFactory.UnsafeAccessorAttributeList(UnsafeAccessorType.Method, $"set_{symbol.Name}"); return PublicStaticExternMethod( ctx, PredefinedType(Token(SyntaxKind.VoidKeyword)).AddTrailingSpace(), - MethodName, + methodName, parameters, attributeList ); } + + public ExpressionSyntax BuildAssignment(ExpressionSyntax? baseAccess, ExpressionSyntax valueToAssign, bool coalesceAssignment = false) + { + if (baseAccess == null) + throw new ArgumentNullException(nameof(baseAccess)); + + return Invocation(MemberAccess(baseAccess, methodName), valueToAssign); + } } diff --git a/src/Riok.Mapperly/Descriptors/UnsafeAccessorContext.cs b/src/Riok.Mapperly/Descriptors/UnsafeAccessorContext.cs deleted file mode 100644 index 3b87b3df12..0000000000 --- a/src/Riok.Mapperly/Descriptors/UnsafeAccessorContext.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System.Globalization; -using Microsoft.CodeAnalysis; -using Riok.Mapperly.Descriptors.Mappings.MemberMappings.UnsafeAccess; -using Riok.Mapperly.Helpers; - -namespace Riok.Mapperly.Descriptors; - -public class UnsafeAccessorContext(UniqueNameBuilder nameBuilder, SymbolAccessor symbolAccessor) -{ - private readonly UniqueNameBuilder _nameBuilder = nameBuilder.NewScope(); - private readonly Dictionary _unsafeAccessors = new(); - - public IReadOnlyCollection UnsafeAccessors => _unsafeAccessors.Values; - - public IUnsafeAccessor GetOrBuildAccessor(UnsafeAccessorType type, ISymbol symbol) - { - var key = new UnsafeAccessorKey(symbol, type); - if (_unsafeAccessors.TryGetValue(key, out var value)) - return value; - - var formatted = FormatAccessorName(symbol.Name); - - var defaultMethodName = type switch - { - UnsafeAccessorType.GetField => $"Get{formatted}", - UnsafeAccessorType.GetProperty => $"Get{formatted}", - UnsafeAccessorType.SetProperty => $"Set{formatted}", - _ => throw new ArgumentOutOfRangeException(nameof(type), type, $"Unknown {nameof(UnsafeAccessorType)}"), - }; - var methodName = GetValidMethodName(symbol.ContainingType, defaultMethodName); - - IUnsafeAccessor accessor = type switch - { - UnsafeAccessorType.GetField => new UnsafeFieldAccessor((IFieldSymbol)symbol, methodName), - UnsafeAccessorType.GetProperty => new UnsafeGetPropertyAccessor((IPropertySymbol)symbol, methodName), - UnsafeAccessorType.SetProperty => new UnsafeSetPropertyAccessor((IPropertySymbol)symbol, methodName), - _ => throw new ArgumentOutOfRangeException(nameof(type), type, $"Unknown {nameof(UnsafeAccessorType)}"), - }; - - _unsafeAccessors.Add(key, accessor); - return accessor; - } - - private string GetValidMethodName(ITypeSymbol symbol, string name) - { - var memberNames = symbolAccessor.GetAllMembers(symbol).Select(x => x.Name); - return _nameBuilder.New(name, memberNames); - } - - /// - /// Strips the leading underscore and capitalise the first letter. - /// - /// Accessor name to be formatted. - /// Formatted accessor name. - private string FormatAccessorName(string name) - { - name = name.TrimStart('_'); - if (name.Length == 0) - return name; - - return char.ToUpper(name[0], CultureInfo.InvariantCulture) + name[1..]; - } - - public enum UnsafeAccessorType - { - GetProperty, - SetProperty, - GetField, - } - - private readonly struct UnsafeAccessorKey(ISymbol member, UnsafeAccessorType type) : IEquatable - { - private readonly ISymbol _member = member; - private readonly UnsafeAccessorType _type = type; - - public bool Equals(UnsafeAccessorKey other) => - SymbolEqualityComparer.Default.Equals(_member, other._member) && _type == other._type; - - public override bool Equals(object? obj) => obj is UnsafeAccessorKey other && Equals(other); - - public override int GetHashCode() - { - unchecked - { - var hashCode = SymbolEqualityComparer.Default.GetHashCode(_member); - hashCode = (hashCode * 397) ^ (int)_type; - return hashCode; - } - } - } -} diff --git a/src/Riok.Mapperly/Descriptors/UserMappingMethodParameterExtractor.cs b/src/Riok.Mapperly/Descriptors/UserMappingMethodParameterExtractor.cs index 96d33d9ff5..034f5ea3b7 100644 --- a/src/Riok.Mapperly/Descriptors/UserMappingMethodParameterExtractor.cs +++ b/src/Riok.Mapperly/Descriptors/UserMappingMethodParameterExtractor.cs @@ -12,17 +12,12 @@ internal static class UserMappingMethodParameterExtractor public static bool BuildParameters( SimpleMappingBuilderContext ctx, IMethodSymbol method, + bool allowAdditionalParameters, [NotNullWhen(true)] out MappingMethodParameters? parameters ) { - // the source param is always required - var expectedParameterCount = 1; - var refHandlerParameter = FindReferenceHandlerParameter(ctx, method); - if (refHandlerParameter.HasValue) - { - expectedParameterCount++; - } + var refHandlerParameterOrdinal = refHandlerParameter?.Ordinal ?? -1; var sourceParameter = FindSourceParameter(ctx, method, refHandlerParameter); if (!sourceParameter.HasValue) @@ -31,23 +26,46 @@ public static bool BuildParameters( return false; } - var targetParameter = FindTargetParameter(ctx, method, sourceParameter.Value, refHandlerParameter); - // If the method returns void, a target parameter is required // if the method doesn't return void, a target parameter is not allowed. - if (method.ReturnsVoid == !targetParameter.HasValue) + MethodParameter? targetParameter = null; + if (method.ReturnsVoid) + { + targetParameter = FindTargetParameter(ctx, method, sourceParameter.Value, refHandlerParameter); + if (!targetParameter.HasValue) + { + parameters = null; + return false; + } + } + + var targetParameterOrdinal = targetParameter?.Ordinal ?? -1; + var additionalParameterSymbols = method + .Parameters.Where(p => + p.Ordinal != sourceParameter.Value.Ordinal && p.Ordinal != targetParameterOrdinal && p.Ordinal != refHandlerParameterOrdinal + ) + .ToList(); + if (!allowAdditionalParameters && additionalParameterSymbols.Count > 0) { parameters = null; return false; } - if (targetParameter.HasValue) + // additional parameters should not be attributed as target or ref handler + var hasInvalidAdditionalParameter = additionalParameterSymbols.Exists(p => + p.Type.TypeKind is TypeKind.TypeParameter or TypeKind.Error + || ctx.SymbolAccessor.HasAttribute(p) + || ctx.SymbolAccessor.HasAttribute(p) + ); + if (hasInvalidAdditionalParameter) { - expectedParameterCount++; + parameters = null; + return false; } - parameters = new MappingMethodParameters(sourceParameter.Value, targetParameter, refHandlerParameter); - return method.Parameters.Length == expectedParameterCount; + var additionalParameters = additionalParameterSymbols.Select(p => ctx.SymbolAccessor.WrapMethodParameter(p)).ToList(); + parameters = new MappingMethodParameters(sourceParameter.Value, targetParameter, refHandlerParameter, additionalParameters); + return true; } public static bool BuildRuntimeTargetTypeMappingParameters( @@ -64,6 +82,7 @@ public static bool BuildRuntimeTargetTypeMappingParameters( } // the source and target type param is always required + // and runtime target type mappings do not support additional parameters var expectedParameterCount = 2; var refHandlerParameter = FindReferenceHandlerParameter(ctx, method); diff --git a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs index 1447216dc5..20ac67874e 100644 --- a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs +++ b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs @@ -131,7 +131,7 @@ bool isExternal var userMappingConfig = GetUserMappingConfig(ctx, method, out var hasAttribute); var valid = !method.IsGenericMethod && (allowPartial || !method.IsPartialDefinition) && (!isStatic || method.IsStatic); - if (!valid || !UserMappingMethodParameterExtractor.BuildParameters(ctx, method, out var parameters)) + if (!valid || !UserMappingMethodParameterExtractor.BuildParameters(ctx, method, false, out var parameters)) { if (hasAttribute) { @@ -180,26 +180,10 @@ bool isExternal return null; } - if ( - !methodSymbol.IsGenericMethod - && UserMappingMethodParameterExtractor.BuildRuntimeTargetTypeMappingParameters( - ctx, - methodSymbol, - out var runtimeTargetTypeParams - ) - ) - { - return new UserDefinedNewInstanceRuntimeTargetTypeParameterMapping( - methodSymbol, - runtimeTargetTypeParams, - ctx.Configuration.Mapper.UseReferenceHandling, - ctx.SymbolAccessor.UpgradeNullable(methodSymbol.ReturnType), - GetTypeSwitchNullArm(methodSymbol, runtimeTargetTypeParams), - ctx.Compilation.ObjectType.WithNullableAnnotation(NullableAnnotation.NotAnnotated) - ); - } + if (TryBuildRuntimeTargetTypeMapping(ctx, methodSymbol) is { } userMapping) + return userMapping; - if (!UserMappingMethodParameterExtractor.BuildParameters(ctx, methodSymbol, out var parameters)) + if (!UserMappingMethodParameterExtractor.BuildParameters(ctx, methodSymbol, true, out var parameters)) { ctx.ReportDiagnostic(DiagnosticDescriptors.UnsupportedMappingMethodSignature, methodSymbol, methodSymbol.Name); return null; @@ -225,17 +209,58 @@ out var runtimeTargetTypeParams parameters.Target.Value, parameters.ReferenceHandler, ctx.Configuration.Mapper.UseReferenceHandling - ); + ) + { + AdditionalSourceParameters = parameters.AdditionalParameters, + }; } var userMappingConfig = GetUserMappingConfig(ctx, methodSymbol, out _); - return new UserDefinedNewInstanceMethodMapping( + if (userMappingConfig.Default == true && parameters.AdditionalParameters.Count > 0) + { + ctx.ReportDiagnostic( + DiagnosticDescriptors.MappingMethodWithAdditionalParametersCannotBeDefaultMapping, + methodSymbol, + methodSymbol.Name + ); + } + + var mapping = new UserDefinedNewInstanceMethodMapping( methodSymbol, - userMappingConfig.Default, + parameters.AdditionalParameters.Count == 0 ? userMappingConfig.Default : false, parameters.Source, parameters.ReferenceHandler, ctx.SymbolAccessor.UpgradeNullable(methodSymbol.ReturnType), ctx.Configuration.Mapper.UseReferenceHandling + ) + { + AdditionalSourceParameters = parameters.AdditionalParameters, + }; + return mapping; + } + + private static UserDefinedNewInstanceRuntimeTargetTypeParameterMapping? TryBuildRuntimeTargetTypeMapping( + SimpleMappingBuilderContext ctx, + IMethodSymbol methodSymbol + ) + { + if (methodSymbol.IsGenericMethod) + return null; + + if ( + !UserMappingMethodParameterExtractor.BuildRuntimeTargetTypeMappingParameters(ctx, methodSymbol, out var runtimeTargetTypeParams) + ) + { + return null; + } + + return new UserDefinedNewInstanceRuntimeTargetTypeParameterMapping( + methodSymbol, + runtimeTargetTypeParams, + ctx.Configuration.Mapper.UseReferenceHandling, + ctx.SymbolAccessor.UpgradeNullable(methodSymbol.ReturnType), + GetTypeSwitchNullArm(methodSymbol, runtimeTargetTypeParams), + ctx.Compilation.ObjectType.WithNullableAnnotation(NullableAnnotation.NotAnnotated) ); } diff --git a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs index b9f4823ec8..8e11b64d04 100644 --- a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs +++ b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs @@ -747,6 +747,26 @@ public static class DiagnosticDescriptors true ); + public static readonly DiagnosticDescriptor MappingMethodWithAdditionalParametersCannotBeDefaultMapping = + new( + "RMG081", + "A mapping method with additional parameters cannot be a default mapping", + "The mapping method {0} has additional parameters and therefore cannot be a default mapping", + DiagnosticCategories.Mapper, + DiagnosticSeverity.Error, + true + ); + + public static readonly DiagnosticDescriptor AdditionalParameterNotMapped = + new( + "RMG082", + "An additional mapping method parameter is not mapped", + "The additional mapping method parameter {0} of the method {1} is not mapped", + DiagnosticCategories.Mapper, + DiagnosticSeverity.Warning, + true + ); + private static string BuildHelpUri(string id) { #if ENV_NEXT diff --git a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Invocation.cs b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Invocation.cs index a0521f10c0..7f0f89eb0f 100644 --- a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Invocation.cs +++ b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Invocation.cs @@ -74,13 +74,7 @@ public static InvocationExpressionSyntax StaticInvocation(IMethodSymbol method, return InvocationExpression(methodAccess).WithArgumentList(ArgumentList(arguments)); } - public static TypeParameterListSyntax TypeParameterList(params ITypeParameterSymbol?[] parameters) - { - var typeParameters = parameters.WhereNotNull().OrderBy(x => x.Ordinal).Select(x => TypeParameter(x.Name)); - return SyntaxFactory.TypeParameterList(CommaSeparatedList(typeParameters)); - } - - public static ParameterListSyntax ParameterList(bool extensionMethod, params MethodParameter?[] parameters) + public static ParameterListSyntax ParameterList(bool extensionMethod, IEnumerable parameters) { var parameterSyntaxes = parameters .WhereNotNull() @@ -125,6 +119,12 @@ public static string StaticMethodString(IMethodSymbol method) return $"{qualifiedReceiverName}.{method.Name}"; } + public static ArgumentSyntax OutVarArgument(string name) + { + return Argument(DeclarationExpression(VarIdentifier, SingleVariableDesignation(Identifier(name)))) + .WithRefOrOutKeyword(TrailingSpacedToken(SyntaxKind.OutKeyword)); + } + private static ArgumentListSyntax ArgumentList(params ExpressionSyntax[] argSyntaxes) => SyntaxFactory.ArgumentList(CommaSeparatedList(argSyntaxes.Select(Argument))); diff --git a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.String.cs b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.String.cs index ccba9110c3..5766be286e 100644 --- a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.String.cs +++ b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.String.cs @@ -12,11 +12,8 @@ public partial struct SyntaxFactoryHelper { private static readonly IdentifierNameSyntax _nameofIdentifier = IdentifierName("nameof"); - private static readonly Regex FormattableStringPlaceholder = new Regex( - @"\{(?\d+)\}", - RegexOptions.Compiled, - TimeSpan.FromMilliseconds(100) - ); + private static readonly Regex _formattableStringPlaceholder = + new(@"\{(?\d+)\}", RegexOptions.Compiled, TimeSpan.FromMilliseconds(100)); public static InvocationExpressionSyntax NameOf(ExpressionSyntax expression) => Invocation(_nameofIdentifier, expression); @@ -25,7 +22,7 @@ public static IdentifierNameSyntax FullyQualifiedIdentifier(ITypeSymbol typeSymb public static InterpolatedStringExpressionSyntax InterpolatedString(FormattableString str) { - var matches = FormattableStringPlaceholder.Matches(str.Format); + var matches = _formattableStringPlaceholder.Matches(str.Format); var contents = new List(); var previousIndex = 0; foreach (Match match in matches) diff --git a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.cs b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.cs index 7455a81baa..a039a61d03 100644 --- a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.cs +++ b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.cs @@ -21,6 +21,11 @@ private SyntaxFactoryHelper(int indentation) public SyntaxFactoryHelper RemoveIndentation() => new(Indentation - 1); + public static AssignmentExpressionSyntax Assignment(ExpressionSyntax target, ExpressionSyntax source, bool coalesce) + { + return coalesce ? CoalesceAssignment(target, source) : Assignment(target, source); + } + public static AssignmentExpressionSyntax Assignment(ExpressionSyntax target, ExpressionSyntax source) { return AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, target, SpacedToken(SyntaxKind.EqualsToken), source); diff --git a/src/Riok.Mapperly/Emit/UnsafeAccessorEmitter.cs b/src/Riok.Mapperly/Emit/UnsafeAccessorEmitter.cs index 47009756a2..4be10c4469 100644 --- a/src/Riok.Mapperly/Emit/UnsafeAccessorEmitter.cs +++ b/src/Riok.Mapperly/Emit/UnsafeAccessorEmitter.cs @@ -38,7 +38,7 @@ CancellationToken cancellationToken foreach (var accessor in descriptor.UnsafeAccessors) { cancellationToken.ThrowIfCancellationRequested(); - yield return accessor.BuildMethod(ctx); + yield return accessor.BuildAccessorMethod(ctx); } } } diff --git a/src/Riok.Mapperly/Helpers/EnumerableExtensions.cs b/src/Riok.Mapperly/Helpers/EnumerableExtensions.cs index 3f5353ccfd..02dc21e172 100644 --- a/src/Riok.Mapperly/Helpers/EnumerableExtensions.cs +++ b/src/Riok.Mapperly/Helpers/EnumerableExtensions.cs @@ -34,8 +34,8 @@ public static IEnumerable SkipLast(this IEnumerable enumerable) public static TAccumulate AggregateWithPrevious( this IEnumerable source, - TAccumulate seed, - Func func + TAccumulate? seed, + Func func ) { var result = seed; @@ -46,6 +46,6 @@ public static TAccumulate AggregateWithPrevious( prev = element; } - return result; + return result ?? throw new InvalidOperationException("Aggregation was not initialized"); } } diff --git a/src/Riok.Mapperly/Helpers/SymbolTypeEqualityComparer.cs b/src/Riok.Mapperly/Helpers/SymbolTypeEqualityComparer.cs index 5f7fe5972f..94a511a9c9 100644 --- a/src/Riok.Mapperly/Helpers/SymbolTypeEqualityComparer.cs +++ b/src/Riok.Mapperly/Helpers/SymbolTypeEqualityComparer.cs @@ -4,7 +4,6 @@ namespace Riok.Mapperly.Helpers; internal static class SymbolTypeEqualityComparer { - public static readonly IEqualityComparer TypeParameterDefault = SymbolEqualityComparer.Default; public static readonly IEqualityComparer FieldDefault = SymbolEqualityComparer.Default; public static readonly IEqualityComparer MethodDefault = SymbolEqualityComparer.Default; } diff --git a/src/Riok.Mapperly/Symbols/ConstructorParameterMember.cs b/src/Riok.Mapperly/Symbols/ConstructorParameterMember.cs deleted file mode 100644 index 7451ba820c..0000000000 --- a/src/Riok.Mapperly/Symbols/ConstructorParameterMember.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Diagnostics; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Riok.Mapperly.Descriptors; -using Riok.Mapperly.Helpers; - -namespace Riok.Mapperly.Symbols; - -/// -/// A constructor parameter represented as a mappable member. -/// This is semantically not really a member, but it acts as a mapping target -/// and is therefore in terms of the mapping the same. -/// -[DebuggerDisplay("{Name}")] -public class ConstructorParameterMember(IParameterSymbol fieldSymbol, SymbolAccessor accessor) : IMappableMember -{ - private readonly IParameterSymbol _fieldSymbol = fieldSymbol; - - public string Name => _fieldSymbol.Name; - public ITypeSymbol Type { get; } = accessor.UpgradeNullable(fieldSymbol.Type); - public ISymbol MemberSymbol => _fieldSymbol; - public bool IsNullable => _fieldSymbol.NullableAnnotation.IsNullable(); - public bool IsIndexer => false; - public bool CanGet => false; - public bool CanSet => false; - public bool CanSetDirectly => false; - public bool IsInitOnly => true; - public bool IsRequired => !_fieldSymbol.IsOptional; - - public ExpressionSyntax BuildAccess(ExpressionSyntax source, bool nullConditional = false) => - throw new InvalidOperationException("Cannot access a constructor parameter"); - - public override bool Equals(object? obj) => - obj is ConstructorParameterMember other && SymbolEqualityComparer.IncludeNullability.Equals(_fieldSymbol, other._fieldSymbol); - - public override int GetHashCode() => SymbolEqualityComparer.IncludeNullability.GetHashCode(_fieldSymbol); -} diff --git a/src/Riok.Mapperly/Symbols/FieldMember.cs b/src/Riok.Mapperly/Symbols/FieldMember.cs deleted file mode 100644 index e6495a6c1e..0000000000 --- a/src/Riok.Mapperly/Symbols/FieldMember.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Diagnostics; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Riok.Mapperly.Descriptors; -using Riok.Mapperly.Helpers; -using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; - -namespace Riok.Mapperly.Symbols; - -[DebuggerDisplay("{Name}")] -public class FieldMember(IFieldSymbol fieldSymbol, SymbolAccessor symbolAccessor) : IMappableMember -{ - private readonly IFieldSymbol _fieldSymbol = fieldSymbol; - - public string Name => _fieldSymbol.Name; - public ITypeSymbol Type { get; } = symbolAccessor.UpgradeNullable(fieldSymbol.Type); - public ISymbol MemberSymbol => _fieldSymbol; - public bool IsNullable => Type.IsNullable(); - public bool IsIndexer => false; - public bool CanGet => true; - public bool CanSet => !_fieldSymbol.IsReadOnly; - public bool CanSetDirectly => true; - public bool IsInitOnly => false; - - public bool IsRequired -#if ROSLYN4_4_OR_GREATER - => _fieldSymbol.IsRequired; -#else - => false; -#endif - - public ExpressionSyntax BuildAccess(ExpressionSyntax source, bool nullConditional = false) - { - return nullConditional ? ConditionalAccess(source, Name) : MemberAccess(source, Name); - } - - public override bool Equals(object? obj) => - obj is FieldMember other && SymbolEqualityComparer.IncludeNullability.Equals(_fieldSymbol, other._fieldSymbol); - - public override int GetHashCode() => SymbolEqualityComparer.IncludeNullability.GetHashCode(_fieldSymbol); -} diff --git a/src/Riok.Mapperly/Symbols/GetterMemberPath.cs b/src/Riok.Mapperly/Symbols/GetterMemberPath.cs deleted file mode 100644 index bcd00343cb..0000000000 --- a/src/Riok.Mapperly/Symbols/GetterMemberPath.cs +++ /dev/null @@ -1,123 +0,0 @@ -using System.Diagnostics; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Riok.Mapperly.Descriptors; -using Riok.Mapperly.Emit.Syntax; -using Riok.Mapperly.Helpers; - -namespace Riok.Mapperly.Symbols; - -/// -/// Wraps a readable member. -/// Could be a directly accessible member -/// or one that is only accessible with an unsafe accessor method, . -/// -[DebuggerDisplay("{MemberPath}")] -public class GetterMemberPath : IEquatable -{ - private const string NullableValueProperty = "Value"; - - private GetterMemberPath(MemberPath memberPath) - { - MemberPath = memberPath; - } - - public MemberPath MemberPath { get; } - - public static GetterMemberPath Build(MappingBuilderContext ctx, MemberPath memberPath) - { - if (memberPath.Path.Count == 0) - { - return new GetterMemberPath(memberPath); - } - - var memberPathArray = memberPath.Path.Select(item => BuildMappableMember(ctx, item)).ToArray(); - return new GetterMemberPath(new NonEmptyMemberPath(memberPath.RootType, memberPathArray)); - } - - public static IEnumerable Build(MappingBuilderContext ctx, IEnumerable path) - { - return path.Select(item => BuildMappableMember(ctx, item)); - } - - private static IMappableMember BuildMappableMember(MappingBuilderContext ctx, IMappableMember item) - { - if (ctx.SymbolAccessor.IsDirectlyAccessible(item.MemberSymbol)) - { - return item; - } - - if (item.MemberSymbol.Kind == SymbolKind.Field) - { - var unsafeFieldAccessor = ctx.UnsafeAccessorContext.GetOrBuildAccessor( - UnsafeAccessorContext.UnsafeAccessorType.GetField, - (IFieldSymbol)item.MemberSymbol - ); - - return new MethodAccessorMember(item, unsafeFieldAccessor.MethodName); - } - - var unsafeGetAccessor = ctx.UnsafeAccessorContext.GetOrBuildAccessor( - UnsafeAccessorContext.UnsafeAccessorType.GetProperty, - (IPropertySymbol)item.MemberSymbol - ); - - return new MethodAccessorMember(item, unsafeGetAccessor.MethodName); - } - - public ExpressionSyntax BuildAccess( - ExpressionSyntax baseAccess, - bool addValuePropertyOnNullable = false, - bool nullConditional = false, - bool skipTrailingNonNullable = false - ) - { - var path = skipTrailingNonNullable ? MemberPath.PathWithoutTrailingNonNullable() : MemberPath.Path; - - if (nullConditional) - { - return path.AggregateWithPrevious( - baseAccess, - (expr, prevProp, prop) => prevProp?.IsNullable == true ? prop.BuildAccess(expr, true) : prop.BuildAccess(expr) - ); - } - - if (addValuePropertyOnNullable) - { - return path.Aggregate( - baseAccess, - (a, b) => - b.Type.IsNullableValueType() - ? SyntaxFactoryHelper.MemberAccess(b.BuildAccess(a), NullableValueProperty) - : b.BuildAccess(a) - ); - } - - return path.Aggregate(baseAccess, (a, b) => b.BuildAccess(a)); - } - - public bool Equals(GetterMemberPath other) => MemberPath.Equals(other.MemberPath); - - bool IEquatable.Equals(GetterMemberPath? other) => other is not null && Equals(other); - - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) - { - return false; - } - - if (ReferenceEquals(this, obj)) - { - return true; - } - - return obj is GetterMemberPath getterBuilder && Equals(getterBuilder); - } - - public override int GetHashCode() => MemberPath.GetHashCode(); - - public static bool operator ==(GetterMemberPath? left, GetterMemberPath? right) => Equals(left, right); - - public static bool operator !=(GetterMemberPath? left, GetterMemberPath? right) => !Equals(left, right); -} diff --git a/src/Riok.Mapperly/Symbols/IMappableMember.cs b/src/Riok.Mapperly/Symbols/IMappableMember.cs deleted file mode 100644 index bba749af1b..0000000000 --- a/src/Riok.Mapperly/Symbols/IMappableMember.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace Riok.Mapperly.Symbols; - -/// -/// A mappable member is a member of a class which can take part in a mapping. -/// (e.g., a field or a property). -/// -public interface IMappableMember -{ - string Name { get; } - - ITypeSymbol Type { get; } - - ISymbol MemberSymbol { get; } - - bool IsNullable { get; } - - bool IsIndexer { get; } - - bool CanGet { get; } - - /// - /// Whether the member can be modified using assignment or an unsafe accessor method. - /// - bool CanSet { get; } - - /// - /// Whether the member can be modified using simple assignment. - /// - bool CanSetDirectly { get; } - - bool IsInitOnly { get; } - - bool IsRequired { get; } - - ExpressionSyntax BuildAccess(ExpressionSyntax source, bool nullConditional = false); -} diff --git a/src/Riok.Mapperly/Symbols/MappingMethodParameters.cs b/src/Riok.Mapperly/Symbols/MappingMethodParameters.cs index 97f76dd3c6..2ad0cfce6b 100644 --- a/src/Riok.Mapperly/Symbols/MappingMethodParameters.cs +++ b/src/Riok.Mapperly/Symbols/MappingMethodParameters.cs @@ -3,4 +3,9 @@ namespace Riok.Mapperly.Symbols; /// /// Well-known mapping method parameters. /// -public record MappingMethodParameters(MethodParameter Source, MethodParameter? Target, MethodParameter? ReferenceHandler); +public record MappingMethodParameters( + MethodParameter Source, + MethodParameter? Target, + MethodParameter? ReferenceHandler, + IReadOnlyCollection AdditionalParameters +); diff --git a/src/Riok.Mapperly/Symbols/Members/ConstructorParameterMember.cs b/src/Riok.Mapperly/Symbols/Members/ConstructorParameterMember.cs new file mode 100644 index 0000000000..e30ee348d1 --- /dev/null +++ b/src/Riok.Mapperly/Symbols/Members/ConstructorParameterMember.cs @@ -0,0 +1,39 @@ +using System.Diagnostics; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Descriptors; +using Riok.Mapperly.Descriptors.UnsafeAccess; +using Riok.Mapperly.Helpers; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace Riok.Mapperly.Symbols.Members; + +/// +/// A constructor parameter represented as a mappable member. +/// This is semantically not really a member, but it acts as a mapping target +/// and is therefore in terms of the mapping the same. +/// +[DebuggerDisplay("{Name}")] +public class ConstructorParameterMember(IParameterSymbol symbol, SymbolAccessor accessor) + : SymbolMappableMember(symbol), + IMappableMember, + IMemberGetter +{ + public ITypeSymbol Type { get; } = accessor.UpgradeNullable(symbol.Type); + public bool IsNullable => Symbol.NullableAnnotation.IsNullable(); + public bool CanGet => false; + public bool CanGetDirectly => false; + public bool CanSet => false; + public bool CanSetDirectly => false; + public bool IsInitOnly => true; + public bool IsRequired => !Symbol.IsOptional; + public bool IsObsolete => false; + public bool IsIgnored => false; + + public IMemberGetter BuildGetter(UnsafeAccessorContext ctx) => this; + + public IMemberSetter BuildSetter(UnsafeAccessorContext ctx) => + throw new InvalidOperationException($"Cannot create a setter for {nameof(ParameterSourceMember)}"); + + public ExpressionSyntax BuildAccess(ExpressionSyntax? baseAccess, bool nullConditional = false) => IdentifierName(Name); +} diff --git a/src/Riok.Mapperly/Symbols/EmptyMemberPath.cs b/src/Riok.Mapperly/Symbols/Members/EmptyMemberPath.cs similarity index 91% rename from src/Riok.Mapperly/Symbols/EmptyMemberPath.cs rename to src/Riok.Mapperly/Symbols/Members/EmptyMemberPath.cs index a6546a58ea..34cd5feee3 100644 --- a/src/Riok.Mapperly/Symbols/EmptyMemberPath.cs +++ b/src/Riok.Mapperly/Symbols/Members/EmptyMemberPath.cs @@ -1,7 +1,7 @@ using System.Diagnostics; using Microsoft.CodeAnalysis; -namespace Riok.Mapperly.Symbols; +namespace Riok.Mapperly.Symbols.Members; [DebuggerDisplay("{RootType} (root)")] public class EmptyMemberPath(ITypeSymbol rootType) : MemberPath(rootType, []) diff --git a/src/Riok.Mapperly/Symbols/Members/FieldMember.cs b/src/Riok.Mapperly/Symbols/Members/FieldMember.cs new file mode 100644 index 0000000000..5bf619a3f3 --- /dev/null +++ b/src/Riok.Mapperly/Symbols/Members/FieldMember.cs @@ -0,0 +1,74 @@ +using System.Diagnostics; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Abstractions; +using Riok.Mapperly.Descriptors; +using Riok.Mapperly.Descriptors.UnsafeAccess; +using Riok.Mapperly.Helpers; +using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; + +namespace Riok.Mapperly.Symbols.Members; + +[DebuggerDisplay("{Name}")] +public class FieldMember(IFieldSymbol symbol, SymbolAccessor symbolAccessor) + : SymbolMappableMember(symbol), + IMappableMember, + IMemberGetter, + IMemberSetter +{ + public ITypeSymbol Type { get; } = symbolAccessor.UpgradeNullable(symbol.Type); + public bool IsNullable => Type.IsNullable(); + public bool CanGet => true; + public bool CanGetDirectly => symbolAccessor.IsDirectlyAccessible(Symbol); + public bool CanSet => !Symbol.IsReadOnly; + public bool CanSetDirectly => CanSet && symbolAccessor.IsDirectlyAccessible(Symbol); + public bool IsInitOnly => false; + + public bool IsRequired +#if ROSLYN4_4_OR_GREATER + => Symbol.IsRequired; +#else + => false; +#endif + + public bool IsObsolete => symbolAccessor.HasAttribute(Symbol); + public bool IsIgnored => symbolAccessor.HasAttribute(Symbol); + public bool SupportsCoalesceAssignment => true; + + public IMemberGetter BuildGetter(UnsafeAccessorContext ctx) + { + if (CanGetDirectly) + return this; + + if (!CanGet) + throw new InvalidOperationException($"Cannot build a getter for a property with {nameof(CanGet)} = false"); + + return ctx.GetOrBuildFieldGetter(this); + } + + public IMemberSetter BuildSetter(UnsafeAccessorContext ctx) + { + if (CanSetDirectly) + return this; + + if (!CanSet) + throw new InvalidOperationException($"Cannot build a setter for a property with {nameof(CanSet)} = false"); + + return ctx.GetOrBuildFieldGetter(this); + } + + public ExpressionSyntax BuildAssignment(ExpressionSyntax? baseAccess, ExpressionSyntax valueToAssign, bool coalesceAssignment = false) + { + var targetMemberRef = BuildAccess(baseAccess); + return Assignment(targetMemberRef, valueToAssign, coalesceAssignment); + } + + public ExpressionSyntax BuildAccess(ExpressionSyntax? baseAccess, bool nullConditional = false) + { + if (baseAccess == null) + return SyntaxFactory.IdentifierName(Name); + + return nullConditional ? ConditionalAccess(baseAccess, Name) : MemberAccess(baseAccess, Name); + } +} diff --git a/src/Riok.Mapperly/Symbols/Members/IMappableMember.cs b/src/Riok.Mapperly/Symbols/Members/IMappableMember.cs new file mode 100644 index 0000000000..c71c0e563a --- /dev/null +++ b/src/Riok.Mapperly/Symbols/Members/IMappableMember.cs @@ -0,0 +1,51 @@ +using Microsoft.CodeAnalysis; +using Riok.Mapperly.Descriptors.UnsafeAccess; + +namespace Riok.Mapperly.Symbols.Members; + +/// +/// A mappable member is a member of a class which can take part in a mapping. +/// (e.g., a field or a property). +/// +public interface IMappableMember +{ + string Name { get; } + + ITypeSymbol Type { get; } + + bool IsNullable { get; } + + /// + /// Whether the member can be read using direct access or an unsafe accessor method. + /// + bool CanGet { get; } + + /// + /// Whether the member can be read using simple assignment. + /// + bool CanGetDirectly { get; } + + /// + /// Whether the member can be modified using an assignment or an unsafe accessor method. + /// + bool CanSet { get; } + + /// + /// Whether the member can be modified using simple assignment. + /// + bool CanSetDirectly { get; } + + bool IsInitOnly { get; } + + bool IsRequired { get; } + + bool IsObsolete { get; } + + /// + /// Whether this member is attributed with . + /// + bool IsIgnored { get; } + + IMemberGetter BuildGetter(UnsafeAccessorContext ctx); + IMemberSetter BuildSetter(UnsafeAccessorContext ctx); +} diff --git a/src/Riok.Mapperly/Symbols/Members/IMemberGetter.cs b/src/Riok.Mapperly/Symbols/Members/IMemberGetter.cs new file mode 100644 index 0000000000..9a5ce17a86 --- /dev/null +++ b/src/Riok.Mapperly/Symbols/Members/IMemberGetter.cs @@ -0,0 +1,8 @@ +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Riok.Mapperly.Symbols.Members; + +public interface IMemberGetter +{ + ExpressionSyntax BuildAccess(ExpressionSyntax? baseAccess, bool nullConditional = false); +} diff --git a/src/Riok.Mapperly/Symbols/Members/IMemberSetter.cs b/src/Riok.Mapperly/Symbols/Members/IMemberSetter.cs new file mode 100644 index 0000000000..5c5e470265 --- /dev/null +++ b/src/Riok.Mapperly/Symbols/Members/IMemberSetter.cs @@ -0,0 +1,10 @@ +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Riok.Mapperly.Symbols.Members; + +public interface IMemberSetter +{ + bool SupportsCoalesceAssignment { get; } + + ExpressionSyntax BuildAssignment(ExpressionSyntax? baseAccess, ExpressionSyntax valueToAssign, bool coalesceAssignment = false); +} diff --git a/src/Riok.Mapperly/Symbols/MappableMember.cs b/src/Riok.Mapperly/Symbols/Members/MappableMember.cs similarity index 94% rename from src/Riok.Mapperly/Symbols/MappableMember.cs rename to src/Riok.Mapperly/Symbols/Members/MappableMember.cs index 227eff5111..e52c2b1bfa 100644 --- a/src/Riok.Mapperly/Symbols/MappableMember.cs +++ b/src/Riok.Mapperly/Symbols/Members/MappableMember.cs @@ -1,7 +1,7 @@ using Microsoft.CodeAnalysis; using Riok.Mapperly.Descriptors; -namespace Riok.Mapperly.Symbols; +namespace Riok.Mapperly.Symbols.Members; internal static class MappableMember { diff --git a/src/Riok.Mapperly/Symbols/MemberPath.cs b/src/Riok.Mapperly/Symbols/Members/MemberPath.cs similarity index 66% rename from src/Riok.Mapperly/Symbols/MemberPath.cs rename to src/Riok.Mapperly/Symbols/Members/MemberPath.cs index 094b2b1389..5fd1b09c09 100644 --- a/src/Riok.Mapperly/Symbols/MemberPath.cs +++ b/src/Riok.Mapperly/Symbols/Members/MemberPath.cs @@ -1,15 +1,15 @@ +using System.Diagnostics; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Descriptors; using Riok.Mapperly.Helpers; -using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; -using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; -namespace Riok.Mapperly.Symbols; +namespace Riok.Mapperly.Symbols.Members; /// /// Represents a (possibly empty) list of members to access a certain member. /// E.g. A.B.C /// +[DebuggerDisplay("{ToDebugString}")] public abstract class MemberPath(ITypeSymbol rootType, IReadOnlyList path) { protected const string MemberAccessSeparator = "."; @@ -67,35 +67,7 @@ public IEnumerable> ObjectPathNullableSubPaths() public bool IsAnyObjectPathNullable() => ObjectPath.Any(p => p.IsNullable); - /// - /// Builds a condition (the resulting expression evaluates to a boolean) - /// whether the path is non-null. - /// - /// The base access to access the member or null. - /// null if no part of the path is nullable or the condition which needs to be true, that the path cannot be null. - public ExpressionSyntax? BuildNonNullConditionWithoutConditionalAccess(ExpressionSyntax? baseAccess) - { - var nullablePath = PathWithoutTrailingNonNullable(); - ExpressionSyntax? condition = null; - var access = baseAccess; - if (access == null) - { - access = IdentifierName(nullablePath.First().Name); - nullablePath = nullablePath.Skip(1); - } - - foreach (var pathPart in nullablePath) - { - access = MemberAccess(access, pathPart.Name); - - if (!pathPart.IsNullable) - continue; - - condition = And(condition, IsNotNull(access)); - } - - return condition; - } + public MemberPathGetter BuildGetter(SimpleMappingBuilderContext ctx) => MemberPathGetter.Build(ctx, this); public static MemberPath Create(ITypeSymbol rootType, IReadOnlyList path) { @@ -118,7 +90,8 @@ public override bool Equals(object? obj) if (obj.GetType() != GetType()) return false; - return Equals((MemberPath)obj); + var other = (MemberPath)obj; + return RootType.Equals(other.RootType, SymbolEqualityComparer.IncludeNullability) && Path.SequenceEqual(other.Path); } public override int GetHashCode() @@ -132,12 +105,7 @@ public override int GetHashCode() return hc; } - public static bool operator ==(MemberPath? left, MemberPath? right) => Equals(left, right); - - public static bool operator !=(MemberPath? left, MemberPath? right) => !Equals(left, right); - - private bool Equals(MemberPath other) => - RootType.Equals(other.RootType, SymbolEqualityComparer.IncludeNullability) && Path.SequenceEqual(other.Path); + public string ToDebugString() => ToDisplayString(); public abstract string ToDisplayString(bool includeRootType = true, bool includeMemberType = true); } diff --git a/src/Riok.Mapperly/Symbols/Members/MemberPathGetter.cs b/src/Riok.Mapperly/Symbols/Members/MemberPathGetter.cs new file mode 100644 index 0000000000..8451d3f9b4 --- /dev/null +++ b/src/Riok.Mapperly/Symbols/Members/MemberPathGetter.cs @@ -0,0 +1,132 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Descriptors; +using Riok.Mapperly.Helpers; +using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; +using MemberGetterPair = (Riok.Mapperly.Symbols.Members.IMappableMember Member, Riok.Mapperly.Symbols.Members.IMemberGetter Getter); + +namespace Riok.Mapperly.Symbols.Members; + +/// +/// A getter for a . +/// +[DebuggerDisplay("{MemberPath}")] +public class MemberPathGetter +{ + private const string NullableValueProperty = nameof(Nullable.Value); + + public MemberPath MemberPath { get; } + + private readonly IReadOnlyCollection _path; + + private MemberPathGetter(MemberPath memberPath, IReadOnlyCollection path) + { + _path = path; + MemberPath = memberPath; + } + + public static MemberPathGetter Build(SimpleMappingBuilderContext ctx, MemberPath path) + { + var getterPath = path.Path.Select(x => (x, x.BuildGetter(ctx.UnsafeAccessorContext))).ToList(); + return new MemberPathGetter(path, getterPath); + } + + [return: NotNullIfNotNull(nameof(baseAccess))] + public ExpressionSyntax? BuildAccess( + ExpressionSyntax? baseAccess, + bool addValuePropertyOnNullable = false, + bool nullConditional = false, + bool skipTrailingNonNullable = false + ) + { + var path = skipTrailingNonNullable ? PathWithoutTrailingNonNullable() : _path; + return BuildAccess(baseAccess, path, addValuePropertyOnNullable, nullConditional); + } + + [return: NotNullIfNotNull(nameof(baseAccess))] + private ExpressionSyntax? BuildAccess( + ExpressionSyntax? baseAccess, + IEnumerable path, + bool addValuePropertyOnNullable = false, + bool nullConditional = false + ) + { + if (nullConditional) + { + return path.AggregateWithPrevious( + baseAccess, + (expr, prevProp, prop) => prop.Getter.BuildAccess(expr, prevProp.Member?.IsNullable == true) + ); + } + + if (addValuePropertyOnNullable) + { + return path.Aggregate( + baseAccess, + (a, b) => + b.Member.Type.IsNullableValueType() + ? MemberAccess(b.Getter.BuildAccess(a), NullableValueProperty) + : b.Getter.BuildAccess(a) + ); + } + + return path.Aggregate(baseAccess, (a, b) => b.Getter.BuildAccess(a)); + } + + /// + /// Builds a condition (the resulting expression evaluates to a boolean) + /// whether the path is non-null. + /// + /// The base access to access the member or null. + /// Whether null conditional member access can be used. + /// null if no part of the path is nullable or the condition which needs to be true, + /// that the path cannot be null. + public ExpressionSyntax? BuildNotNullCondition(ExpressionSyntax baseAccess, bool useNullConditionalAccess) + { + return useNullConditionalAccess ? BuildNonNullCondition(baseAccess) : BuildNonNullConditionWithoutConditionalAccess(baseAccess); + } + + private BinaryExpressionSyntax BuildNonNullCondition(ExpressionSyntax baseAccess) + { + return IsNotNull(BuildAccess(baseAccess, nullConditional: true, skipTrailingNonNullable: true)); + } + + private ExpressionSyntax? BuildNonNullConditionWithoutConditionalAccess(ExpressionSyntax baseAccess) + { + var nullablePath = PathWithoutTrailingNonNullable(); + var access = baseAccess; + var conditions = new List(); + foreach (var pathPart in nullablePath) + { + access = pathPart.Getter.BuildAccess(access); + + if (!pathPart.Member.IsNullable) + continue; + + conditions.Add(IsNotNull(access)); + } + + return conditions.Count == 0 ? null : And(conditions); + } + + private IEnumerable PathWithoutTrailingNonNullable() => + _path.Reverse().SkipWhile(x => !x.Member.IsNullable).Reverse(); + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + return false; + + if (ReferenceEquals(this, obj)) + return true; + + if (obj.GetType() != GetType()) + return false; + + var other = (MemberPathGetter)obj; + return MemberPath.Equals(other.MemberPath); + } + + public override int GetHashCode() => MemberPath.GetHashCode(); +} diff --git a/src/Riok.Mapperly/Symbols/Members/MemberPathSetter.cs b/src/Riok.Mapperly/Symbols/Members/MemberPathSetter.cs new file mode 100644 index 0000000000..580fecb182 --- /dev/null +++ b/src/Riok.Mapperly/Symbols/Members/MemberPathSetter.cs @@ -0,0 +1,58 @@ +using System.Diagnostics; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Descriptors; + +namespace Riok.Mapperly.Symbols.Members; + +/// +/// A setter for a . +/// +[DebuggerDisplay("{_memberPath}")] +public class MemberPathSetter +{ + private readonly NonEmptyMemberPath _memberPath; + private readonly MemberPathGetter _baseAccessGetter; + private readonly IMemberSetter _memberSetter; + + private MemberPathSetter(NonEmptyMemberPath memberPath, MemberPathGetter baseAccessGetter, IMemberSetter memberSetter) + { + _memberPath = memberPath; + _baseAccessGetter = baseAccessGetter; + _memberSetter = memberSetter; + } + + public bool SupportsCoalesceAssignment => _memberSetter.SupportsCoalesceAssignment; + + public static MemberPathSetter Build(SimpleMappingBuilderContext ctx, NonEmptyMemberPath path) + { + Debug.Assert(path.Member.CanSet); + + var objectPath = MemberPath.Create(path.RootType, path.ObjectPath.ToList()); + var objectGetter = objectPath.BuildGetter(ctx); + var memberSetter = path.Member.BuildSetter(ctx.UnsafeAccessorContext); + return new MemberPathSetter(path, objectGetter, memberSetter); + } + + public ExpressionSyntax BuildAssignment(ExpressionSyntax? baseAccess, ExpressionSyntax valueToAssign, bool coalesceAssignment = false) + { + baseAccess = _baseAccessGetter.BuildAccess(baseAccess); + return _memberSetter.BuildAssignment(baseAccess, valueToAssign, coalesceAssignment); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + return false; + + if (ReferenceEquals(this, obj)) + return true; + + if (obj.GetType() != GetType()) + return false; + + var other = (MemberPathSetter)obj; + return _memberPath.Equals(other._memberPath); + } + + public override int GetHashCode() => _memberPath.GetHashCode(); +} diff --git a/src/Riok.Mapperly/Symbols/NonEmptyMemberPath.cs b/src/Riok.Mapperly/Symbols/Members/NonEmptyMemberPath.cs similarity index 86% rename from src/Riok.Mapperly/Symbols/NonEmptyMemberPath.cs rename to src/Riok.Mapperly/Symbols/Members/NonEmptyMemberPath.cs index ee42f42f4b..0f28a8b67e 100644 --- a/src/Riok.Mapperly/Symbols/NonEmptyMemberPath.cs +++ b/src/Riok.Mapperly/Symbols/Members/NonEmptyMemberPath.cs @@ -1,7 +1,8 @@ using System.Diagnostics; using Microsoft.CodeAnalysis; +using Riok.Mapperly.Descriptors; -namespace Riok.Mapperly.Symbols; +namespace Riok.Mapperly.Symbols.Members; [DebuggerDisplay("{FullName}")] public class NonEmptyMemberPath : MemberPath @@ -24,6 +25,8 @@ public NonEmptyMemberPath(ITypeSymbol rootType, IReadOnlyList p public override ITypeSymbol MemberType => IsAnyNullable() ? Member.Type.WithNullableAnnotation(NullableAnnotation.Annotated) : Member.Type; + public MemberPathSetter BuildSetter(SimpleMappingBuilderContext ctx) => MemberPathSetter.Build(ctx, this); + public override string ToDisplayString(bool includeRootType = true, bool includeMemberType = true) { var ofType = includeMemberType ? $" of type {Member.Type.ToDisplayString()}" : null; diff --git a/src/Riok.Mapperly/Symbols/Members/ParameterSourceMember.cs b/src/Riok.Mapperly/Symbols/Members/ParameterSourceMember.cs new file mode 100644 index 0000000000..0cd71d02d6 --- /dev/null +++ b/src/Riok.Mapperly/Symbols/Members/ParameterSourceMember.cs @@ -0,0 +1,54 @@ +using System.Diagnostics; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Descriptors.UnsafeAccess; +using Riok.Mapperly.Helpers; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace Riok.Mapperly.Symbols.Members; + +/// +/// A mapping method parameter represented as a mappable member. +/// This is semantically not really a member, but it acts as an additional mapping source member +/// and is therefore in terms of the mapping the same. +/// +[DebuggerDisplay("{Name}")] +public class ParameterSourceMember(MethodParameter parameter) : IMappableMember, IMemberGetter +{ + public string Name => parameter.Name; + public ITypeSymbol Type => parameter.Type; + public bool IsNullable => parameter.Type.IsNullable(); + public bool CanGet => true; + public bool CanGetDirectly => true; + public bool CanSet => false; + public bool CanSetDirectly => false; + public bool IsInitOnly => false; + public bool IsRequired => false; + public bool IsObsolete => false; + public bool IsIgnored => false; + + public IMemberGetter BuildGetter(UnsafeAccessorContext ctx) => this; + + public IMemberSetter BuildSetter(UnsafeAccessorContext ctx) => + throw new InvalidOperationException($"Cannot create a setter for {nameof(ParameterSourceMember)}"); + + public ExpressionSyntax BuildAccess(ExpressionSyntax? baseAccess, bool nullConditional = false) => IdentifierName(Name); + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + return false; + + if (ReferenceEquals(this, obj)) + return true; + + if (obj.GetType() != GetType()) + return false; + + var other = (ParameterSourceMember)obj; + return string.Equals(Name, other.Name, StringComparison.Ordinal) + && SymbolEqualityComparer.IncludeNullability.Equals(Type, other.Type); + } + + public override int GetHashCode() => StringComparer.Ordinal.GetHashCode(Name); +} diff --git a/src/Riok.Mapperly/Symbols/Members/PropertyMember.cs b/src/Riok.Mapperly/Symbols/Members/PropertyMember.cs new file mode 100644 index 0000000000..282603f76b --- /dev/null +++ b/src/Riok.Mapperly/Symbols/Members/PropertyMember.cs @@ -0,0 +1,84 @@ +using System.Diagnostics; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Abstractions; +using Riok.Mapperly.Descriptors; +using Riok.Mapperly.Descriptors.UnsafeAccess; +using Riok.Mapperly.Helpers; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; + +namespace Riok.Mapperly.Symbols.Members; + +[DebuggerDisplay("{Name}")] +public class PropertyMember(IPropertySymbol symbol, SymbolAccessor symbolAccessor) + : SymbolMappableMember(symbol), + IMappableMember, + IMemberSetter, + IMemberGetter +{ + public ITypeSymbol Type { get; } = symbolAccessor.UpgradeNullable(symbol.Type); + public bool IsNullable => Type.IsNullable(); + + public bool CanGet => !Symbol.IsWriteOnly && (Symbol.GetMethod == null || symbolAccessor.IsAccessible(Symbol.GetMethod)); + + public bool CanGetDirectly => + !Symbol.IsWriteOnly && (Symbol.GetMethod == null || symbolAccessor.IsDirectlyAccessible(Symbol.GetMethod)); + + public bool CanSet => !Symbol.IsReadOnly && (Symbol.SetMethod == null || symbolAccessor.IsAccessible(Symbol.SetMethod)); + + public bool CanSetDirectly => !Symbol.IsReadOnly && (Symbol.SetMethod == null || symbolAccessor.IsDirectlyAccessible(Symbol.SetMethod)); + + public bool IsInitOnly => Symbol.SetMethod?.IsInitOnly == true; + + public bool IsRequired +#if ROSLYN4_4_OR_GREATER + => Symbol.IsRequired; +#else + => false; +#endif + + public bool IsObsolete => symbolAccessor.HasAttribute(Symbol); + public bool IsIgnored => symbolAccessor.HasAttribute(Symbol); + + public bool SupportsCoalesceAssignment => CanSetDirectly; + + public IMemberGetter BuildGetter(UnsafeAccessorContext ctx) + { + if (CanGetDirectly) + return this; + + if (!CanGet) + throw new InvalidOperationException($"Cannot build a getter for a property with {nameof(CanGet)} = false"); + + return ctx.GetOrBuildPropertyGetter(this); + } + + public IMemberSetter BuildSetter(UnsafeAccessorContext ctx) + { + if (CanSetDirectly) + return this; + + if (!CanSet) + throw new InvalidOperationException($"Cannot build a setter for a property with {nameof(CanSet)} = false"); + + return ctx.GetOrBuildPropertySetter(this); + } + + public ExpressionSyntax BuildAssignment(ExpressionSyntax? baseAccess, ExpressionSyntax valueToAssign, bool coalesceAssignment = false) + { + Debug.Assert(CanSetDirectly); + ExpressionSyntax targetMember = baseAccess == null ? IdentifierName(Name) : MemberAccess(baseAccess, Name); + + return Assignment(targetMember, valueToAssign, coalesceAssignment); + } + + public ExpressionSyntax BuildAccess(ExpressionSyntax? baseAccess, bool nullConditional = false) + { + Debug.Assert(CanGetDirectly); + if (baseAccess == null) + return IdentifierName(Name); + + return nullConditional ? ConditionalAccess(baseAccess, Name) : MemberAccess(baseAccess, Name); + } +} diff --git a/src/Riok.Mapperly/Symbols/Members/SourceMemberPath.cs b/src/Riok.Mapperly/Symbols/Members/SourceMemberPath.cs new file mode 100644 index 0000000000..6941303bfc --- /dev/null +++ b/src/Riok.Mapperly/Symbols/Members/SourceMemberPath.cs @@ -0,0 +1,3 @@ +namespace Riok.Mapperly.Symbols.Members; + +public record SourceMemberPath(MemberPath MemberPath, SourceMemberType Type); diff --git a/src/Riok.Mapperly/Symbols/Members/SourceMemberType.cs b/src/Riok.Mapperly/Symbols/Members/SourceMemberType.cs new file mode 100644 index 0000000000..94b0af9a9d --- /dev/null +++ b/src/Riok.Mapperly/Symbols/Members/SourceMemberType.cs @@ -0,0 +1,8 @@ +namespace Riok.Mapperly.Symbols.Members; + +public enum SourceMemberType +{ + Member, + MemberAlias, + AdditionalMappingMethodParameter, +} diff --git a/src/Riok.Mapperly/Symbols/Members/SymbolMappableMember.cs b/src/Riok.Mapperly/Symbols/Members/SymbolMappableMember.cs new file mode 100644 index 0000000000..f67405d07f --- /dev/null +++ b/src/Riok.Mapperly/Symbols/Members/SymbolMappableMember.cs @@ -0,0 +1,28 @@ +using Microsoft.CodeAnalysis; + +namespace Riok.Mapperly.Symbols.Members; + +public abstract class SymbolMappableMember(T symbol) + where T : ISymbol +{ + public T Symbol { get; } = symbol; + + public string Name => Symbol.Name; + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + return false; + + if (ReferenceEquals(this, obj)) + return true; + + if (obj.GetType() != GetType()) + return false; + + var other = (SymbolMappableMember)obj; + return SymbolEqualityComparer.IncludeNullability.Equals(Symbol, other.Symbol); + } + + public override int GetHashCode() => SymbolEqualityComparer.IncludeNullability.GetHashCode(Symbol); +} diff --git a/src/Riok.Mapperly/Symbols/MethodAccessorMember.cs b/src/Riok.Mapperly/Symbols/MethodAccessorMember.cs deleted file mode 100644 index 23fcfeb322..0000000000 --- a/src/Riok.Mapperly/Symbols/MethodAccessorMember.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Diagnostics; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; - -namespace Riok.Mapperly.Symbols; - -[DebuggerDisplay("Accessor {Name}")] -public class MethodAccessorMember(IMappableMember mappableMember, string methodName, bool methodRequiresParameter = false) : IMappableMember -{ - /// - /// This member requires invocation with a parameter. - /// - private readonly bool _methodRequiresParameter = methodRequiresParameter; - - public string Name => mappableMember.Name; - public ITypeSymbol Type => mappableMember.Type; - public ISymbol MemberSymbol => mappableMember.MemberSymbol; - public bool IsNullable => mappableMember.IsNullable; - public bool IsIndexer => mappableMember.IsIndexer; - public bool CanGet => mappableMember.CanGet; - public bool CanSet => mappableMember.CanSet; - public bool CanSetDirectly => mappableMember.CanSetDirectly; - public bool IsInitOnly => mappableMember.IsInitOnly; - public bool IsRequired => mappableMember.IsRequired; - - public ExpressionSyntax BuildAccess(ExpressionSyntax source, bool nullConditional = false) - { - if (_methodRequiresParameter) - { - // the receiver of the resulting ExpressionSyntax will add an invocation call with a parameter - // src?.SetValue or src.SetValue - return nullConditional ? ConditionalAccess(source, methodName) : MemberAccess(source, methodName); - } - - // src?.GetValue() or src.GetValue() - return nullConditional ? Invocation(ConditionalAccess(source, methodName)) : Invocation(MemberAccess(source, methodName)); - } - - public override bool Equals(object? obj) => - obj is MethodAccessorMember other - && SymbolEqualityComparer.IncludeNullability.Equals(mappableMember.MemberSymbol, other.MemberSymbol); - - public override int GetHashCode() => SymbolEqualityComparer.IncludeNullability.GetHashCode(mappableMember.MemberSymbol); -} diff --git a/src/Riok.Mapperly/Symbols/PropertyMember.cs b/src/Riok.Mapperly/Symbols/PropertyMember.cs deleted file mode 100644 index 349785464b..0000000000 --- a/src/Riok.Mapperly/Symbols/PropertyMember.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Diagnostics; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Riok.Mapperly.Descriptors; -using Riok.Mapperly.Helpers; -using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; - -namespace Riok.Mapperly.Symbols; - -[DebuggerDisplay("{Name}")] -internal class PropertyMember(IPropertySymbol propertySymbol, SymbolAccessor symbolAccessor) : IMappableMember -{ - private readonly IPropertySymbol _propertySymbol = propertySymbol; - - public string Name => _propertySymbol.Name; - public ITypeSymbol Type { get; } = symbolAccessor.UpgradeNullable(propertySymbol.Type); - - public ISymbol MemberSymbol => _propertySymbol; - public bool IsNullable => Type.IsNullable(); - public bool IsIndexer => _propertySymbol.IsIndexer; - public bool CanGet => - !_propertySymbol.IsWriteOnly && (_propertySymbol.GetMethod == null || symbolAccessor.IsAccessible(_propertySymbol.GetMethod)); - public bool CanSet => - !_propertySymbol.IsReadOnly && (_propertySymbol.SetMethod == null || symbolAccessor.IsAccessible(_propertySymbol.SetMethod)); - - public bool CanSetDirectly => - !_propertySymbol.IsReadOnly - && (_propertySymbol.SetMethod == null || symbolAccessor.IsDirectlyAccessible(_propertySymbol.SetMethod)); - - public bool IsInitOnly => _propertySymbol.SetMethod?.IsInitOnly == true; - - public bool IsRequired -#if ROSLYN4_4_OR_GREATER - => _propertySymbol.IsRequired; -#else - => false; -#endif - - public ExpressionSyntax BuildAccess(ExpressionSyntax source, bool nullConditional = false) - { - return nullConditional ? ConditionalAccess(source, Name) : MemberAccess(source, Name); - } - - public override bool Equals(object? obj) => - obj is PropertyMember other && SymbolEqualityComparer.IncludeNullability.Equals(_propertySymbol, other._propertySymbol); - - public override int GetHashCode() => SymbolEqualityComparer.IncludeNullability.GetHashCode(_propertySymbol); -} diff --git a/src/Riok.Mapperly/Symbols/RuntimeTargetTypeMappingMethodParameters.cs b/src/Riok.Mapperly/Symbols/RuntimeTargetTypeMappingMethodParameters.cs index ab80cecde5..66fc9b9b8a 100644 --- a/src/Riok.Mapperly/Symbols/RuntimeTargetTypeMappingMethodParameters.cs +++ b/src/Riok.Mapperly/Symbols/RuntimeTargetTypeMappingMethodParameters.cs @@ -9,4 +9,4 @@ public record RuntimeTargetTypeMappingMethodParameters( MethodParameter Source, MethodParameter TargetType, MethodParameter? ReferenceHandler -) : MappingMethodParameters(Source, null, ReferenceHandler); +) : MappingMethodParameters(Source, null, ReferenceHandler, []); diff --git a/src/Riok.Mapperly/Symbols/SetterMemberPath.cs b/src/Riok.Mapperly/Symbols/SetterMemberPath.cs deleted file mode 100644 index 9cf3bffec3..0000000000 --- a/src/Riok.Mapperly/Symbols/SetterMemberPath.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System.Diagnostics; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Riok.Mapperly.Descriptors; -using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; - -namespace Riok.Mapperly.Symbols; - -/// -/// Wraps a writeable member. -/// Could be a directly accessible member -/// or one that is only accessible with an unsafe accessor method, . -/// -[DebuggerDisplay("{MemberPath}")] -public class SetterMemberPath : IEquatable -{ - private SetterMemberPath(NonEmptyMemberPath memberPath, bool isMethod) - { - MemberPath = memberPath; - IsMethod = isMethod; - } - - /// - /// Indicates whether this setter is an UnsafeAccessor for a property, i.e. target.SetValue(source.Value); - /// False for standard properties, fields and UnsafeAccessor fields. - /// - public bool IsMethod { get; } - - public NonEmptyMemberPath MemberPath { get; } - - public static SetterMemberPath Build(MappingBuilderContext ctx, NonEmptyMemberPath memberPath) - { - // object path is the same as a getter - var setterPath = GetterMemberPath.Build(ctx, memberPath.ObjectPath).ToList(); - // build the final member in the path and add it to the setter path - var (member, isMethod) = BuildMemberSetter(ctx, memberPath.Member); - setterPath.Add(member); - - return new SetterMemberPath(new NonEmptyMemberPath(memberPath.RootType, setterPath), isMethod); - } - - private static (IMappableMember, bool) BuildMemberSetter(MappingBuilderContext ctx, IMappableMember member) - { - if (ctx.SymbolAccessor.IsDirectlyAccessible(member.MemberSymbol) && member.CanSetDirectly) - return (member, false); - - if (member.MemberSymbol.Kind == SymbolKind.Field) - { - var unsafeFieldAccessor = ctx.UnsafeAccessorContext.GetOrBuildAccessor( - UnsafeAccessorContext.UnsafeAccessorType.GetField, - (IFieldSymbol)member.MemberSymbol - ); - - return (new MethodAccessorMember(member, unsafeFieldAccessor.MethodName), false); - } - - var unsafeGetAccessor = ctx.UnsafeAccessorContext.GetOrBuildAccessor( - UnsafeAccessorContext.UnsafeAccessorType.SetProperty, - (IPropertySymbol)member.MemberSymbol - ); - - return (new MethodAccessorMember(member, unsafeGetAccessor.MethodName, methodRequiresParameter: true), true); - } - - public ExpressionSyntax BuildAssignment(ExpressionSyntax? baseAccess, ExpressionSyntax valueToAssign, bool coalesceAssignment = false) - { - IEnumerable path = MemberPath.Path; - - if (baseAccess == null) - { - baseAccess = SyntaxFactory.IdentifierName(MemberPath.Path[0].Name); - path = path.Skip(1); - } - - var memberPath = path.Aggregate(baseAccess, (a, b) => b.BuildAccess(a)); - - if (coalesceAssignment) - { - // cannot use coalesce assignment within a setter method invocation. - Debug.Assert(!IsMethod); - - // target.Value ??= mappedValue; - return CoalesceAssignment(memberPath, valueToAssign); - } - - if (IsMethod) - { - // target.SetValue(source.Value); - return Invocation(memberPath, valueToAssign); - } - - // target.Value = source.Value; - return Assignment(memberPath, valueToAssign); - } - - public bool Equals(SetterMemberPath other) => IsMethod == other.IsMethod && MemberPath.Equals(other.MemberPath); - - bool IEquatable.Equals(SetterMemberPath? other) => other is not null && Equals(other); - - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) - { - return false; - } - - if (ReferenceEquals(this, obj)) - { - return true; - } - - return obj is SetterMemberPath setterBuilder && Equals(setterBuilder); - } - - public override int GetHashCode() => HashCode.Combine(IsMethod, MemberPath); - - public static bool operator ==(SetterMemberPath? left, SetterMemberPath? right) => Equals(left, right); - - public static bool operator !=(SetterMemberPath? left, SetterMemberPath? right) => !Equals(left, right); -} diff --git a/test/Riok.Mapperly.IntegrationTests/Dto/AdditionalParametersDto.cs b/test/Riok.Mapperly.IntegrationTests/Dto/AdditionalParametersDto.cs new file mode 100644 index 0000000000..6956a5fbca --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/Dto/AdditionalParametersDto.cs @@ -0,0 +1,7 @@ +namespace Riok.Mapperly.IntegrationTests.Dto +{ + public class AdditionalParametersDto : IdObjectDto + { + public int ValueFromParameter { get; set; } + } +} diff --git a/test/Riok.Mapperly.IntegrationTests/Mapper/StaticTestMapper.cs b/test/Riok.Mapperly.IntegrationTests/Mapper/StaticTestMapper.cs index bdcdc1e4c6..d19e008c3a 100644 --- a/test/Riok.Mapperly.IntegrationTests/Mapper/StaticTestMapper.cs +++ b/test/Riok.Mapperly.IntegrationTests/Mapper/StaticTestMapper.cs @@ -145,5 +145,7 @@ public static void MapExistingList(List src, List dst) public static partial ConstantValuesDto MapConstantValues(ConstantValuesObject source); private static int IntValueBuilder() => 2; + + public static partial AdditionalParametersDto MapWithAdditionalParameter(IdObject source, int valueFromParameter); } } diff --git a/test/Riok.Mapperly.IntegrationTests/StaticMapperTest.cs b/test/Riok.Mapperly.IntegrationTests/StaticMapperTest.cs index aafa86b96e..d51f9ecff6 100644 --- a/test/Riok.Mapperly.IntegrationTests/StaticMapperTest.cs +++ b/test/Riok.Mapperly.IntegrationTests/StaticMapperTest.cs @@ -87,5 +87,13 @@ public void RunMappingIdTargetFirstShouldWork() StaticTestMapper.MapIdTargetFirst(model, new IdObjectDto { IdValue = 20 }); model.IdValue.Should().Be(20); } + + [Fact] + public void MapWithAdditionalParameterShouldWork() + { + var dto = StaticTestMapper.MapWithAdditionalParameter(new IdObject { IdValue = 1 }, 2); + dto.IdValue.Should().Be(1); + dto.ValueFromParameter.Should().Be(2); + } } } diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource.verified.cs index 434d0b0bb0..4e89086b66 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource.verified.cs @@ -698,6 +698,15 @@ public static partial void MapToDerivedExisting(global::Riok.Mapperly.Integratio return target; } + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + public static partial global::Riok.Mapperly.IntegrationTests.Dto.AdditionalParametersDto MapWithAdditionalParameter(global::Riok.Mapperly.IntegrationTests.Models.IdObject source, int valueFromParameter) + { + var target = new global::Riok.Mapperly.IntegrationTests.Dto.AdditionalParametersDto(); + target.ValueFromParameter = DirectInt(valueFromParameter); + target.IdValue = DirectInt(source.IdValue); + return target; + } + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] private static global::Riok.Mapperly.IntegrationTests.Dto.TestObjectNestedDto MapToTestObjectNestedDto(global::Riok.Mapperly.IntegrationTests.Models.TestObjectNested source) { diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs index 5492effc04..ed077de2ea 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs @@ -707,6 +707,15 @@ public static partial void MapToDerivedExisting(global::Riok.Mapperly.Integratio return target; } + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + public static partial global::Riok.Mapperly.IntegrationTests.Dto.AdditionalParametersDto MapWithAdditionalParameter(global::Riok.Mapperly.IntegrationTests.Models.IdObject source, int valueFromParameter) + { + var target = new global::Riok.Mapperly.IntegrationTests.Dto.AdditionalParametersDto(); + target.ValueFromParameter = DirectInt(valueFromParameter); + target.IdValue = DirectInt(source.IdValue); + return target; + } + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] private static global::Riok.Mapperly.IntegrationTests.Dto.TestObjectNestedDto MapToTestObjectNestedDto(global::Riok.Mapperly.IntegrationTests.Models.TestObjectNested source) { diff --git a/test/Riok.Mapperly.Tests/MapperGenerationResultAssertions.cs b/test/Riok.Mapperly.Tests/MapperGenerationResultAssertions.cs index 5e31f5fcff..0e208241f6 100644 --- a/test/Riok.Mapperly.Tests/MapperGenerationResultAssertions.cs +++ b/test/Riok.Mapperly.Tests/MapperGenerationResultAssertions.cs @@ -27,7 +27,7 @@ public MapperGenerationResultAssertions HaveAssertedAllDiagnostics() if (_notAssertedDiagnostics.Count == 0) return this; - var assertions = _notAssertedDiagnostics.GroupBy(x => x.Descriptor.Id).Select(x => BuildDiagnosticAssertions(x)); + var assertions = _notAssertedDiagnostics.GroupBy(x => x.Descriptor.Id).Select(BuildDiagnosticAssertions); Assert.Fail( $""" {_notAssertedDiagnostics.Count} not asserted diagnostics found. @@ -46,7 +46,7 @@ private string BuildDiagnosticAssertions(IGrouping diagnosti .Where(x => x.Value is DiagnosticDescriptor && ((DiagnosticDescriptor)x.Value!).Id.Equals(diagnosticGroup.Key)) .Select(x => x.Name) .Single(); - var diagnosticDescriptorAccess = $"{nameof(DiagnosticDescriptors)}.{diagnosticDescriptorFieldName}"; + var diagnosticDescriptorAccess = $"{typeof(DiagnosticDescriptors).FullName}.{diagnosticDescriptorFieldName}"; if (!diagnosticGroup.Skip(1).Any()) { return $"{nameof(HaveDiagnostic)}({diagnosticDescriptorAccess}, \"{diagnosticGroup.First().GetMessage()}\")"; diff --git a/test/Riok.Mapperly.Tests/Mapping/ExtensionMethodTest.cs b/test/Riok.Mapperly.Tests/Mapping/ExtensionMethodTest.cs index 31478ee5ce..9a2d63206d 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ExtensionMethodTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ExtensionMethodTest.cs @@ -1,4 +1,6 @@ -namespace Riok.Mapperly.Tests.Mapping; +using Riok.Mapperly.Diagnostics; + +namespace Riok.Mapperly.Tests.Mapping; public class ExtensionMethodTest { @@ -37,4 +39,20 @@ public Task ExtensionExistingTargetAsFirstParamShouldWork() return TestHelper.VerifyGenerator(source); } + + [Fact] + public void ExtensionExistingTargetDuplicatedParamShouldDiagnostic() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "static partial void MapToB([MappingTarget] this B target, A source, [MappingTarget] B anotherTarget);", + "class A { public int Value { get; set; } }", + "class B { public int Value { get; set; } }" + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic(DiagnosticDescriptors.UnsupportedMappingMethodSignature, "MapToB has an unsupported mapping method signature") + .HaveAssertedAllDiagnostics(); + } } diff --git a/test/Riok.Mapperly.Tests/Mapping/GenericTest.cs b/test/Riok.Mapperly.Tests/Mapping/GenericTest.cs index d4d50727b1..f3be015db2 100644 --- a/test/Riok.Mapperly.Tests/Mapping/GenericTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/GenericTest.cs @@ -1,3 +1,5 @@ +using Riok.Mapperly.Diagnostics; + namespace Riok.Mapperly.Tests.Mapping; public class GenericTest @@ -723,4 +725,27 @@ public Task WithGenericSourceAndTargetAndUnboundGenericShouldDiagnostic() ); return TestHelper.VerifyGenerator(source); } + + [Fact] + public void AdditionalParameterIsGenericShouldDiagnostic() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + private partial TTarget Map(TSource source, TSource otherParam); + + private partial B MapToB(A source); + private partial D MapToD(C source); + """, + "record struct A(string Value);", + "record struct B(string Value);", + "record C(string Value1);", + "record D(string Value1);" + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic(DiagnosticDescriptors.UnsupportedMappingMethodSignature, "Map has an unsupported mapping method signature") + .HaveAssertedAllDiagnostics(); + } } diff --git a/test/Riok.Mapperly.Tests/Mapping/ReferenceHandlingTest.cs b/test/Riok.Mapperly.Tests/Mapping/ReferenceHandlingTest.cs index dceac3aa85..352cba347b 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ReferenceHandlingTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ReferenceHandlingTest.cs @@ -321,4 +321,20 @@ public void ReferenceHandlerParameterIsAlsoMappingTargetParameterShouldDiagnosti ) .HaveAssertedAllDiagnostics(); } + + [Fact] + public void DuplicatedReferenceHandlerParameterShouldDiagnostic() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "public partial B Map(A source, [ReferenceHandler] IReferenceHandler refHandler, [ReferenceHandler] IReferenceHandler refHandler1);", + TestSourceBuilderOptions.WithReferenceHandling, + "record A;", + "record b;" + ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic(DiagnosticDescriptors.UnsupportedMappingMethodSignature, "Map has an unsupported mapping method signature") + .HaveAssertedAllDiagnostics(); + } } diff --git a/test/Riok.Mapperly.Tests/Mapping/RuntimeTargetTypeMappingTest.cs b/test/Riok.Mapperly.Tests/Mapping/RuntimeTargetTypeMappingTest.cs index 6274488384..f3ffd13095 100644 --- a/test/Riok.Mapperly.Tests/Mapping/RuntimeTargetTypeMappingTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/RuntimeTargetTypeMappingTest.cs @@ -1,5 +1,3 @@ -using Riok.Mapperly.Diagnostics; - namespace Riok.Mapperly.Tests.Mapping; public class RuntimeTargetTypeMappingTest @@ -272,37 +270,6 @@ public void WithGenericUserImplementedMethodShouldBeIgnored() ); } - [Fact] - public void InvalidSignatureAdditionalParameterShouldDiagnostic() - { - var source = TestSourceBuilder.MapperWithBodyAndTypes( - "partial object Map(A a, Type targetType, string format);", - "class A { public string StringValue { get; set; } }" - ); - - TestHelper - .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) - .Should() - .HaveDiagnostic(DiagnosticDescriptors.UnsupportedMappingMethodSignature) - .HaveAssertedAllDiagnostics(); - } - - [Fact] - public void InvalidSignatureWithReferenceHandlerAdditionalParameterShouldDiagnostic() - { - var source = TestSourceBuilder.MapperWithBodyAndTypes( - "partial object Map(A a, Type targetType, [ReferenceHandler] IReferenceHandler refHanlder, string format);", - TestSourceBuilderOptions.WithReferenceHandling, - "class A { public string StringValue { get; set; } }" - ); - - TestHelper - .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) - .Should() - .HaveDiagnostic(DiagnosticDescriptors.UnsupportedMappingMethodSignature) - .HaveAssertedAllDiagnostics(); - } - [Fact] public Task WithReferenceHandlingEnabled() { diff --git a/test/Riok.Mapperly.Tests/Mapping/UserMethodAdditionalParametersTest.cs b/test/Riok.Mapperly.Tests/Mapping/UserMethodAdditionalParametersTest.cs new file mode 100644 index 0000000000..92cb8ed0a0 --- /dev/null +++ b/test/Riok.Mapperly.Tests/Mapping/UserMethodAdditionalParametersTest.cs @@ -0,0 +1,337 @@ +using Riok.Mapperly.Diagnostics; + +namespace Riok.Mapperly.Tests.Mapping; + +public class UserMethodAdditionalParametersTest +{ + [Fact] + public Task AdditionalIntParameter() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "partial B Map(A src, int value);", + "class A { public string StringValue { get; set; } }", + "class B { public string StringValue { get; set; } public string Value { get; set; } }" + ); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task AdditionalNullableIntParameter() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "partial B Map(A src, int? value);", + "class A { public string StringValue { get; set; } }", + "class B { public string StringValue { get; set; } public string Value { get; set; } }" + ); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public void AdditionalInitParameter() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "partial B Map(A src, int value);", + "class A { public string StringValue { get; set; } }", + "class B { public string StringValue { get; set; } public string Value { get; init; } }" + ); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveMapMethodBody( + """ + var target = new global::B() + { + Value = value.ToString(), + }; + target.StringValue = src.StringValue; + return target; + """ + ); + } + + [Fact] + public void AdditionalInitNullableIntParameter() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "partial B Map(A src, int? value);", + "class A { public string StringValue { get; set; } }", + "class B { public string StringValue { get; set; } public string Value { get; init; } }" + ); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveMapMethodBody( + """ + var target = new global::B() + { + Value = value != null ? value.Value.ToString() : throw new System.ArgumentNullException(nameof(value.Value)), + }; + target.StringValue = src.StringValue; + return target; + """ + ); + } + + [Fact] + public Task TwoAdditionalParameters() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "partial B Map(A src, int value, int id);", + "class A { public string StringValue { get; set; } }", + "class B { public string StringValue { get; set; } public string Value { get; init; } public int Id { get; init; } }" + ); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public void PreferParameterOverSourceMember() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "partial B Map(A src, int value);", + "class A { public int Value { get; set; } }", + "class B { public int Value { get; init; } }" + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic( + DiagnosticDescriptors.SourceMemberNotMapped, + "The member Value on the mapping source type A is not mapped to any member on the mapping target type B" + ) + .HaveAssertedAllDiagnostics() + .HaveMapMethodBody( + """ + var target = new global::B() + { + Value = value, + }; + return target; + """ + ); + } + + [Fact] + public void ClassFlattening() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "partial B Map(A src, C nested);", + "class A { public string StringValue { get; set; } }", + "class B { public string StringValue { get; set; } public string NestedValue { get; init; } }", + "class C { public int Value { get; init; } }" + ); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveMapMethodBody( + """ + var target = new global::B() + { + NestedValue = nested.Value.ToString(), + }; + target.StringValue = src.StringValue; + return target; + """ + ); + } + + [Fact] + public void NullableClassFlattening() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "partial B Map(A src, C? nested);", + "class A { public string StringValue { get; set; } }", + "class B { public string StringValue { get; set; } public string NestedValue { get; init; } }", + "class C { public int Value { get; init; } }" + ); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveMapMethodBody( + """ + var target = new global::B() + { + NestedValue = nested != null ? nested.Value.ToString() : throw new System.ArgumentNullException(nameof(nested.Value)), + }; + target.StringValue = src.StringValue; + return target; + """ + ); + } + + [Fact] + public void NullableClassToNullableFlattening() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "partial B Map(A src, C? nested);", + "class A { public string StringValue { get; set; } }", + "class B { public string StringValue { get; set; } public int? NestedValue { get; init; } }", + "class C { public int Value { get; init; } }" + ); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveMapMethodBody( + """ + var target = new global::B() + { + NestedValue = nested?.Value, + }; + target.StringValue = src.StringValue; + return target; + """ + ); + } + + [Fact] + public Task WithReferenceHandling() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "partial B Map(A src, int value);", + TestSourceBuilderOptions.WithReferenceHandling, + "class A { public string StringValue { get; set; } }", + "class B { public string StringValue { get; set; } public string Value { get; init; } }" + ); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task WithReferenceHandlingParameter() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "partial B Map(A src, int value, [ReferenceHandler] IReferenceHandler refHandler);", + TestSourceBuilderOptions.WithReferenceHandling, + "class A { public string StringValue { get; set; } }", + "class B { public string StringValue { get; set; } public string Value { get; init; } }" + ); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task WithReferenceHandlingAsFirstParameter() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "partial B Map([ReferenceHandler] IReferenceHandler refHandler, A src, int value);", + TestSourceBuilderOptions.WithReferenceHandling, + "class A { public string StringValue { get; set; } }", + "class B { public string StringValue { get; set; } public string Value { get; init; } }" + ); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task ExplicitDefaultShouldDiagnosticAndNotBeUsedAsDefault() + { + // D.Value => E.Value should not use the Map method + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [UserMapping(Default = true)] + partial B Map(A src, int value); + partial E MapNested(D src); + """, + "class A { public string StringValue { get; set; } }", + "class B { public string StringValue { get; set; } public string Value { get; init; } }", + "class D { public A? Value { get; set; }}", + "class E { public B? Value { get; set; } }" + ); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task ShouldNotBeMarkedAsImplicitDefaultMapping() + { + // D.Value => E.Value should not use the Map method + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + partial B Map(A src, int value); + partial E MapNested(D src); + """, + "class A { public string StringValue { get; set; } }", + "class B { public string StringValue { get; set; } public string Value { get; init; } }", + "class D { public A? Value { get; set; }}", + "class E { public B? Value { get; set; } }" + ); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public void UnusedParameterShouldDiagnostic() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "partial B Map(A src, int value2);", + "class A { public int Value { get; set; } }", + "class B { public int Value { get; init; } }" + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic( + DiagnosticDescriptors.AdditionalParameterNotMapped, + "The additional mapping method parameter value2 of the method Map is not mapped" + ) + .HaveAssertedAllDiagnostics() + .HaveMapMethodBody( + """ + var target = new global::B() + { + Value = src.Value, + }; + return target; + """ + ); + } + + [Fact] + public void UnusedSourceMemberSameNameAsParameterShouldDiagnostic() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "partial B Map(A src, int value);", + "class A { public int value { get; set; } }", + "class B { public int value { get; init; } }" + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic( + DiagnosticDescriptors.SourceMemberNotMapped, + "The member value on the mapping source type A is not mapped to any member on the mapping target type B" + ) + .HaveAssertedAllDiagnostics() + .HaveMapMethodBody( + """ + var target = new global::B() + { + value = value, + }; + return target; + """ + ); + } + + [Fact] + public Task ExistingTarget() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "partial void Map(A src, B target, int value);", + "class A { public string StringValue { get; set; } }", + "class B { public string StringValue { get; set; } public string Value { get; set; } }" + ); + + return TestHelper.VerifyGenerator(source); + } +} diff --git a/test/Riok.Mapperly.Tests/Mapping/UserMethodTest.cs b/test/Riok.Mapperly.Tests/Mapping/UserMethodTest.cs index 6b96416db7..e32261c298 100644 --- a/test/Riok.Mapperly.Tests/Mapping/UserMethodTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/UserMethodTest.cs @@ -165,51 +165,6 @@ public void WithSameNamesShouldGenerateUniqueMethodNames() TestHelper.GenerateMapper(source).Should().HaveOnlyMethods("MapToB", "MapToB1"); } - [Fact] - public void InvalidSignatureReturnTypeAdditionalParameterShouldDiagnostic() - { - var source = TestSourceBuilder.MapperWithBodyAndTypes("partial string ToString(T source, string format);"); - - TestHelper - .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) - .Should() - .HaveDiagnostic(DiagnosticDescriptors.UnsupportedMappingMethodSignature, "ToString has an unsupported mapping method signature") - .HaveAssertedAllDiagnostics(); - } - - [Fact] - public void InvalidSignatureAdditionalParameterShouldDiagnostic() - { - var source = TestSourceBuilder.MapperWithBodyAndTypes( - "partial void Map(A a, B b, string format);", - "class A { public string StringValue { get; set; } }", - "class B { public string StringValue { get; set; } }" - ); - - TestHelper - .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) - .Should() - .HaveDiagnostic(DiagnosticDescriptors.UnsupportedMappingMethodSignature, "Map has an unsupported mapping method signature") - .HaveAssertedAllDiagnostics(); - } - - [Fact] - public void InvalidSignatureAdditionalParameterWithReferenceHandlingShouldDiagnostic() - { - var source = TestSourceBuilder.MapperWithBodyAndTypes( - "partial void Map(A a, B b, [ReferenceHandler] IReferenceHandler refHandler, string format);", - TestSourceBuilderOptions.WithReferenceHandling, - "class A { public string StringValue { get; set; } }", - "class B { public string StringValue { get; set; } }" - ); - - TestHelper - .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) - .Should() - .HaveDiagnostic(DiagnosticDescriptors.UnsupportedMappingMethodSignature) - .HaveAssertedAllDiagnostics(); - } - [Fact] public void InvalidSignatureAsyncShouldDiagnostic() { diff --git a/test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.AdditionalIntParameter#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.AdditionalIntParameter#Mapper.g.verified.cs new file mode 100644 index 0000000000..0c7245e8c7 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.AdditionalIntParameter#Mapper.g.verified.cs @@ -0,0 +1,14 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + partial global::B Map(global::A src, int value) + { + var target = new global::B(); + target.StringValue = src.StringValue; + target.Value = value.ToString(); + return target; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.AdditionalNullableIntParameter#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.AdditionalNullableIntParameter#Mapper.g.verified.cs new file mode 100644 index 0000000000..4fbe6747f7 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.AdditionalNullableIntParameter#Mapper.g.verified.cs @@ -0,0 +1,17 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + partial global::B Map(global::A src, int? value) + { + var target = new global::B(); + if (value != null) + { + target.Value = value.Value.ToString(); + } + target.StringValue = src.StringValue; + return target; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.ExistingTarget#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.ExistingTarget#Mapper.g.verified.cs new file mode 100644 index 0000000000..82a64bd0ad --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.ExistingTarget#Mapper.g.verified.cs @@ -0,0 +1,12 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + partial void Map(global::A src, global::B target, int value) + { + target.StringValue = src.StringValue; + target.Value = value.ToString(); + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.ExplicitDefaultShouldDiagnosticAndNotBeUsedAsDefault#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.ExplicitDefaultShouldDiagnosticAndNotBeUsedAsDefault#Mapper.g.verified.cs new file mode 100644 index 0000000000..5fd5dc04a3 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.ExplicitDefaultShouldDiagnosticAndNotBeUsedAsDefault#Mapper.g.verified.cs @@ -0,0 +1,39 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + partial global::B Map(global::A src, int value) + { + var target = new global::B() + { + Value = value.ToString(), + }; + target.StringValue = src.StringValue; + return target; + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + partial global::E MapNested(global::D src) + { + var target = new global::E(); + if (src.Value != null) + { + target.Value = MapToB(src.Value); + } + else + { + target.Value = null; + } + return target; + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private global::B MapToB(global::A source) + { + var target = new global::B(); + target.StringValue = source.StringValue; + return target; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.ExplicitDefaultShouldDiagnosticAndNotBeUsedAsDefault.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.ExplicitDefaultShouldDiagnosticAndNotBeUsedAsDefault.verified.txt new file mode 100644 index 0000000000..2b3eb1b661 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.ExplicitDefaultShouldDiagnosticAndNotBeUsedAsDefault.verified.txt @@ -0,0 +1,24 @@ +{ + Diagnostics: [ + { + Id: RMG081, + Title: A mapping method with additional parameters cannot be a default mapping, + Severity: Error, + WarningLevel: 0, + Location: : (11,4)-(12,32), + MessageFormat: The mapping method {0} has additional parameters and therefore cannot be a default mapping, + Message: The mapping method Map has additional parameters and therefore cannot be a default mapping, + Category: Mapper + }, + { + Id: RMG012, + Title: Source member was not found for target member, + Severity: Warning, + WarningLevel: 1, + Location: : (13,0)-(13,27), + MessageFormat: The member {0} on the mapping target type {1} was not found on the mapping source type {2}, + Message: The member Value on the mapping target type B was not found on the mapping source type A, + Category: Mapper + } + ] +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.ShouldNotBeMarkedAsImplicitDefaultMapping#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.ShouldNotBeMarkedAsImplicitDefaultMapping#Mapper.g.verified.cs new file mode 100644 index 0000000000..5fd5dc04a3 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.ShouldNotBeMarkedAsImplicitDefaultMapping#Mapper.g.verified.cs @@ -0,0 +1,39 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + partial global::B Map(global::A src, int value) + { + var target = new global::B() + { + Value = value.ToString(), + }; + target.StringValue = src.StringValue; + return target; + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + partial global::E MapNested(global::D src) + { + var target = new global::E(); + if (src.Value != null) + { + target.Value = MapToB(src.Value); + } + else + { + target.Value = null; + } + return target; + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private global::B MapToB(global::A source) + { + var target = new global::B(); + target.StringValue = source.StringValue; + return target; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.ShouldNotBeMarkedAsImplicitDefaultMapping.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.ShouldNotBeMarkedAsImplicitDefaultMapping.verified.txt new file mode 100644 index 0000000000..ca55107bb2 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.ShouldNotBeMarkedAsImplicitDefaultMapping.verified.txt @@ -0,0 +1,14 @@ +{ + Diagnostics: [ + { + Id: RMG012, + Title: Source member was not found for target member, + Severity: Warning, + WarningLevel: 1, + Location: : (12,0)-(12,27), + MessageFormat: The member {0} on the mapping target type {1} was not found on the mapping source type {2}, + Message: The member Value on the mapping target type B was not found on the mapping source type A, + Category: Mapper + } + ] +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.TwoAdditionalParameters#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.TwoAdditionalParameters#Mapper.g.verified.cs new file mode 100644 index 0000000000..d8c9531507 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.TwoAdditionalParameters#Mapper.g.verified.cs @@ -0,0 +1,17 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + partial global::B Map(global::A src, int value, int id) + { + var target = new global::B() + { + Value = value.ToString(), + Id = id, + }; + target.StringValue = src.StringValue; + return target; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.WithReferenceHandling#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.WithReferenceHandling#Mapper.g.verified.cs new file mode 100644 index 0000000000..99744b0f59 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.WithReferenceHandling#Mapper.g.verified.cs @@ -0,0 +1,20 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + partial global::B Map(global::A src, int value) + { + var refHandler = new global::Riok.Mapperly.Abstractions.ReferenceHandling.PreserveReferenceHandler(); + if (refHandler.TryGetReference(src, out var existingTargetReference)) + return existingTargetReference; + var target = new global::B() + { + Value = value.ToString(), + }; + refHandler.SetReference(src, target); + target.StringValue = src.StringValue; + return target; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.WithReferenceHandlingAsFirstParameter#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.WithReferenceHandlingAsFirstParameter#Mapper.g.verified.cs new file mode 100644 index 0000000000..1b0176f5d9 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.WithReferenceHandlingAsFirstParameter#Mapper.g.verified.cs @@ -0,0 +1,19 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + partial global::B Map(global::Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler, global::A src, int value) + { + if (refHandler.TryGetReference(src, out var existingTargetReference)) + return existingTargetReference; + var target = new global::B() + { + Value = value.ToString(), + }; + refHandler.SetReference(src, target); + target.StringValue = src.StringValue; + return target; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.WithReferenceHandlingParameter#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.WithReferenceHandlingParameter#Mapper.g.verified.cs new file mode 100644 index 0000000000..4e40938732 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParametersTest.WithReferenceHandlingParameter#Mapper.g.verified.cs @@ -0,0 +1,19 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + partial global::B Map(global::A src, int value, global::Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + { + if (refHandler.TryGetReference(src, out var existingTargetReference)) + return existingTargetReference; + var target = new global::B() + { + Value = value.ToString(), + }; + refHandler.SetReference(src, target); + target.StringValue = src.StringValue; + return target; + } +} \ No newline at end of file