diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs index 9b5911227f..c13ead6284 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs @@ -16,8 +16,6 @@ namespace Riok.Mapperly.Descriptors; public class MappingBuilderContext : SimpleMappingBuilderContext { private readonly FormatProviderCollection _formatProviders; - private CollectionInfos? _collectionInfos; - private DictionaryInfos? _dictionaryInfos; public MappingBuilderContext( SimpleMappingBuilderContext parentCtx, @@ -57,9 +55,9 @@ bool ignoreDerivedTypes public ITypeSymbol Target => MappingKey.Target; - public CollectionInfos? CollectionInfos => _collectionInfos ??= CollectionInfoBuilder.Build(Types, SymbolAccessor, Source, Target); + public CollectionInfos? CollectionInfos => field ??= CollectionInfoBuilder.Build(Types, SymbolAccessor, Source, Target); - public DictionaryInfos? DictionaryInfos => _dictionaryInfos ??= DictionaryInfoBuilder.Build(Types, CollectionInfos); + public DictionaryInfos? DictionaryInfos => field ??= DictionaryInfoBuilder.Build(Types, CollectionInfos); public IUserMapping? UserMapping { get; } @@ -162,7 +160,7 @@ bool ignoreDerivedTypes /// /// Finds or builds a mapping (). /// Before a new mapping is built existing mappings are tried to be found by the following priorities: - /// 1. exact match + /// 1. user mapping with exact type match /// 2. ignoring the nullability of the source and the target (needs to be handled by the caller of this method) /// If no mapping can be found a new mapping is built with the source and the target as non-nullables. /// @@ -176,7 +174,7 @@ bool ignoreDerivedTypes Location? diagnosticLocation = null ) { - if (FindMapping(key) is { } mapping) + if (FindMapping(key) is INewInstanceUserMapping mapping) return mapping; // if a user mapping is referenced diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/NullMappedMemberSourceValue.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/NullMappedMemberSourceValue.cs index 6c04d8adeb..c5c7cef44f 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/NullMappedMemberSourceValue.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/NullMappedMemberSourceValue.cs @@ -26,9 +26,17 @@ bool useNullConditionalAccess public ExpressionSyntax Build(TypeMappingBuildContext ctx) { - // the source type of the delegate mapping is nullable or the source path is not nullable - // build mapping with null conditional access - if (_delegateMapping.SourceType.IsNullable() || !_sourceGetter.MemberPath.IsAnyNullable()) + // if the source is not nullable, return it directly. + if (!_sourceGetter.MemberPath.IsAnyNullable()) + { + ctx = ctx.WithSource(_sourceGetter.BuildAccess(ctx.Source)); + return _delegateMapping.Build(ctx); + } + + // if null conditional is allowed and the delegate mapping allows null source types, + // use null conditional access. + // => Map(source?.Value) + if (_delegateMapping.SourceType.IsNullable() && useNullConditionalAccess) { ctx = ctx.WithSource(_sourceGetter.BuildAccess(ctx.Source, nullConditional: true)); return _delegateMapping.Build(ctx); diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedInlinedExpressionMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedInlinedExpressionMapping.cs index e5a5d24756..695202a02d 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedInlinedExpressionMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedInlinedExpressionMapping.cs @@ -17,8 +17,12 @@ public class UserImplementedInlinedExpressionMapping( ParameterSyntax sourceParameter, IReadOnlyDictionary mappingInvocations, ExpressionSyntax mappingBody -) : NewInstanceMapping(userMapping.SourceType, userMapping.TargetType), INewInstanceMapping +) : NewInstanceMapping(userMapping.SourceType, userMapping.TargetType), INewInstanceUserMapping { + public IMethodSymbol Method => userMapping.Method; + public bool? Default => userMapping.Default; + public bool IsExternal => userMapping.IsExternal; + public override ExpressionSyntax Build(TypeMappingBuildContext ctx) { var body = InlineUserMappings(ctx, mappingBody); diff --git a/src/Riok.Mapperly/Symbols/Members/MemberPath.cs b/src/Riok.Mapperly/Symbols/Members/MemberPath.cs index 5f8662bd3b..daf03f76db 100644 --- a/src/Riok.Mapperly/Symbols/Members/MemberPath.cs +++ b/src/Riok.Mapperly/Symbols/Members/MemberPath.cs @@ -10,7 +10,7 @@ namespace Riok.Mapperly.Symbols.Members; /// Represents a (possibly empty) list of members to access a certain member. /// E.g. A.B.C /// -[DebuggerDisplay("{ToDebugString}")] +[DebuggerDisplay("{ToDebugString()}")] public abstract class MemberPath(ITypeSymbol rootType, IReadOnlyList path) { protected const string MemberAccessSeparator = "."; diff --git a/src/Riok.Mapperly/Symbols/Members/NonEmptyMemberPath.cs b/src/Riok.Mapperly/Symbols/Members/NonEmptyMemberPath.cs index 528ec72b61..c88b98419a 100644 --- a/src/Riok.Mapperly/Symbols/Members/NonEmptyMemberPath.cs +++ b/src/Riok.Mapperly/Symbols/Members/NonEmptyMemberPath.cs @@ -1,10 +1,8 @@ -using System.Diagnostics; using Microsoft.CodeAnalysis; using Riok.Mapperly.Descriptors; namespace Riok.Mapperly.Symbols.Members; -[DebuggerDisplay("{FullName}")] public class NonEmptyMemberPath : MemberPath { public NonEmptyMemberPath(ITypeSymbol rootType, IReadOnlyList path) diff --git a/src/Riok.Mapperly/Symbols/Members/SourceMemberPath.cs b/src/Riok.Mapperly/Symbols/Members/SourceMemberPath.cs index 6941303bfc..d1002526dc 100644 --- a/src/Riok.Mapperly/Symbols/Members/SourceMemberPath.cs +++ b/src/Riok.Mapperly/Symbols/Members/SourceMemberPath.cs @@ -1,3 +1,6 @@ +using System.Diagnostics; + namespace Riok.Mapperly.Symbols.Members; +[DebuggerDisplay("{MemberPath} ({Type})")] public record SourceMemberPath(MemberPath MemberPath, SourceMemberType Type); diff --git a/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionNullableTest.cs b/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionNullableTest.cs index adfba4e959..71e118291c 100644 --- a/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionNullableTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionNullableTest.cs @@ -112,6 +112,26 @@ public Task ClassToClassNullableSourcePathAutoFlatten() return TestHelper.VerifyGenerator(source); } + [Fact] + public Task NestedPropertyWithDeepCloneable() + { + // see https://github.com/riok/mapperly/issues/1710 + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + public partial System.Linq.IQueryable Map(System.Linq.IQueryable source); + + [MapNestedProperties("Nested")] + public partial B MapConfig(A source); + """, + TestSourceBuilderOptions.WithDeepCloning, + "class A { public C Nested { get; set; } }", + "class B { public string[] Value0 { get; set; } public string Value { get; set; } }", + "class C { public string[] Value0 { get; set; } public string Value { get; set; } }" + ); + + return TestHelper.VerifyGenerator(source, TestHelperOptions.DisabledNullable); + } + [Fact] public Task ClassToClassNullableSourcePathAutoFlattenString() { diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullableTest.NestedPropertyWithDeepCloneable#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullableTest.NestedPropertyWithDeepCloneable#Mapper.g.verified.cs new file mode 100644 index 0000000000..3bd8523ec4 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullableTest.NestedPropertyWithDeepCloneable#Mapper.g.verified.cs @@ -0,0 +1,53 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + public partial global::System.Linq.IQueryable? Map(global::System.Linq.IQueryable? source) + { + if (source == null) + return default; +#nullable disable + return System.Linq.Queryable.Select( + source, + x => new global::B() + { + Value0 = x.Nested != null && x.Nested.Value0 != null ? global::System.Linq.Enumerable.ToArray( + global::System.Linq.Enumerable.Select(x.Nested.Value0, x1 => x1 == null ? default : x1) + ) : default, + Value = x.Nested != null && x.Nested.Value != null ? x.Nested.Value : default, + } + ); +#nullable enable + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + public partial global::B? MapConfig(global::A? source) + { + if (source == null) + return default; + var target = new global::B(); + if (source.Nested?.Value0 != null) + { + target.Value0 = MapToStringArray(source.Nested.Value0); + } + else + { + target.Value0 = null; + } + target.Value = source.Nested?.Value; + return target; + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private string?[] MapToStringArray(string?[] source) + { + var target = new string?[source.Length]; + for (var i = 0; i < source.Length; i++) + { + target[i] = source[i] == null ? default : source[i]!; + } + return target; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.TwoQueryableMappingsWithNamedUsedMappingsAndAmbiguousName.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.TwoQueryableMappingsWithNamedUsedMappingsAndAmbiguousName.verified.txt index 9adf3dc393..b85411a512 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.TwoQueryableMappingsWithNamedUsedMappingsAndAmbiguousName.verified.txt +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.TwoQueryableMappingsWithNamedUsedMappingsAndAmbiguousName.verified.txt @@ -37,6 +37,25 @@ DefaultSeverity: Error, IsEnabledByDefault: true } + }, + { + Location: /* + +[MapProperty(nameof(A.StringValue1), nameof(B.StringValue1), Use = nameof(ModifyString)] + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +[MapProperty(nameof(A.StringValue2), nameof(B.StringValue2), Use = nameof(ModifyString2)] +*/ + : (18,1)-(18,87), + Message: The referenced mapping name ModifyString is ambiguous, use a unique name, + Severity: Error, + Descriptor: { + Id: RMG062, + Title: The referenced mapping name is ambiguous, + MessageFormat: The referenced mapping name {0} is ambiguous, use a unique name, + Category: Mapper, + DefaultSeverity: Error, + IsEnabledByDefault: true + } } ] } \ No newline at end of file