diff --git a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs index c93e11d5090..d5b1e63f94d 100644 --- a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs +++ b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs @@ -106,4 +106,9 @@ public class MapperAttribute : Attribute /// When false, accessible constructors are ordered in descending order by their parameter count. /// public bool PreferParameterlessConstructors { get; set; } = true; + + /// + /// Defines the maximum recursion depth that an IQueryable mapping will use. + /// + public int MaxRecursionDepth { get; set; } = 8; } diff --git a/src/Riok.Mapperly.Abstractions/MapperMaxRecursionDepthAttribute.cs b/src/Riok.Mapperly.Abstractions/MapperMaxRecursionDepthAttribute.cs new file mode 100644 index 00000000000..ceb419e9cc3 --- /dev/null +++ b/src/Riok.Mapperly.Abstractions/MapperMaxRecursionDepthAttribute.cs @@ -0,0 +1,22 @@ +namespace Riok.Mapperly.Abstractions; + +/// +/// Defines the maximum recursion depth that an IQueryable mapping will use. +/// +[AttributeUsage(AttributeTargets.Method)] +public sealed class MapperMaxRecursionDepthAttribute : Attribute +{ + /// + /// Defines the maximum recursion depth that an IQueryable mapping will use. + /// + /// The maximum recursion depth used when mapping IQueryable members. + public MapperMaxRecursionDepthAttribute(int maxRecursionDepth) + { + MaxRecursionDepth = maxRecursionDepth; + } + + /// + /// The maximum recursion depth used when mapping IQueryable members. + /// + public int MaxRecursionDepth { get; } +} diff --git a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt index b40cc119902..4482358b6d0 100644 --- a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt +++ b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt @@ -143,3 +143,8 @@ Riok.Mapperly.Abstractions.ReferenceHandling.PreserveReferenceHandler.SetReferen Riok.Mapperly.Abstractions.ReferenceHandling.PreserveReferenceHandler.TryGetReference(TSource source, out TTarget? target) -> bool Riok.Mapperly.Abstractions.MapperAttribute.PreferParameterlessConstructors.get -> bool Riok.Mapperly.Abstractions.MapperAttribute.PreferParameterlessConstructors.set -> void +Riok.Mapperly.Abstractions.MapperAttribute.MaxRecursionDepth.get -> int +Riok.Mapperly.Abstractions.MapperAttribute.MaxRecursionDepth.set -> void +Riok.Mapperly.Abstractions.MapperMaxRecursionDepthAttribute +Riok.Mapperly.Abstractions.MapperMaxRecursionDepthAttribute.MapperMaxRecursionDepthAttribute(int maxRecursionDepth) -> void +Riok.Mapperly.Abstractions.MapperMaxRecursionDepthAttribute.MaxRecursionDepth.get -> int diff --git a/src/Riok.Mapperly/Configuration/MapperConfiguration.cs b/src/Riok.Mapperly/Configuration/MapperConfiguration.cs index 60859f4487e..7d990945a60 100644 --- a/src/Riok.Mapperly/Configuration/MapperConfiguration.cs +++ b/src/Riok.Mapperly/Configuration/MapperConfiguration.cs @@ -110,4 +110,9 @@ public record MapperConfiguration /// When false, accessible constructors are ordered in descending order by their parameter count. /// public bool? PreferParameterlessConstructors { get; init; } + + /// + /// Defines the maximum recursion depth that an IQueryable mapping will use. + /// + public int? MaxRecursionDepth { get; init; } } diff --git a/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs b/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs index 22f86b7f28e..7173bf05674 100644 --- a/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs +++ b/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs @@ -59,6 +59,9 @@ public static MapperAttribute Merge(MapperConfiguration mapperConfiguration, Map ?? defaultMapperConfiguration.PreferParameterlessConstructors ?? mapper.PreferParameterlessConstructors; + mapper.MaxRecursionDepth = + mapperConfiguration.MaxRecursionDepth ?? defaultMapperConfiguration.MaxRecursionDepth ?? mapper.MaxRecursionDepth; + return mapper; } } diff --git a/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs b/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs index cbc0e86859e..069802bdb98 100644 --- a/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs +++ b/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs @@ -33,7 +33,8 @@ MapperConfiguration defaultMapperConfiguration Array.Empty(), Array.Empty(), Mapper.IgnoreObsoleteMembersStrategy, - Mapper.RequiredMappingStrategy + Mapper.RequiredMappingStrategy, + Mapper.MaxRecursionDepth ), Array.Empty() ); @@ -79,13 +80,18 @@ private PropertiesMappingConfiguration BuildPropertiesConfig(IMethodSymbol metho var requiredMapping = _dataAccessor.Access(method).FirstOrDefault() is not { } methodWarnUnmapped ? _defaultConfiguration.Properties.RequiredMappingStrategy : methodWarnUnmapped.RequiredMappingStrategy; + var maxRecursionDepth = _dataAccessor.Access(method).FirstOrDefault() + is not { } methodMaxRecursionDepth + ? _defaultConfiguration.Properties.MaxRecursionDepth + : methodMaxRecursionDepth.MaxRecursionDepth; return new PropertiesMappingConfiguration( ignoredSourceProperties, ignoredTargetProperties, propertyConfigurations, ignoreObsolete, - requiredMapping + requiredMapping, + maxRecursionDepth ); } diff --git a/src/Riok.Mapperly/Configuration/PropertiesMappingConfiguration.cs b/src/Riok.Mapperly/Configuration/PropertiesMappingConfiguration.cs index 9d65612eb8f..658c4332fcc 100644 --- a/src/Riok.Mapperly/Configuration/PropertiesMappingConfiguration.cs +++ b/src/Riok.Mapperly/Configuration/PropertiesMappingConfiguration.cs @@ -7,5 +7,6 @@ public record PropertiesMappingConfiguration( IReadOnlyCollection IgnoredTargets, IReadOnlyCollection ExplicitMappings, IgnoreObsoleteMembersStrategy IgnoreObsoleteMembersStrategy, - RequiredMappingStrategy RequiredMappingStrategy + RequiredMappingStrategy RequiredMappingStrategy, + int MaxRecursionDepth ); diff --git a/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs index 63baeae0c23..b123bd3c3ed 100644 --- a/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs @@ -54,6 +54,15 @@ conversionType is not MappingConversionType.EnumToString and not MappingConversi /// The if a mapping was found or null if none was found. public override INewInstanceMapping? FindMapping(TypeMappingKey mappingKey) { + // check for recursion loop returning null to prevent a loop or default when recursion limit is reached. + var count = _parentTypes.GetDepth(mappingKey); + if (count >= 1) + { + return count >= Configuration.Properties.MaxRecursionDepth + 2 + ? new DefaultMemberMapping(mappingKey.Source, mappingKey.Target) + : null; + } + if (_inlineExpressionMappings.Find(mappingKey) is { } mapping) return mapping; diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs index a32585e6608..594078f8fbb 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs @@ -17,6 +17,7 @@ public class MappingBuilderContext : SimpleMappingBuilderContext { private readonly FormatProviderCollection _formatProviders; private CollectionInfos? _collectionInfos; + internal readonly MappingRecursionDepthTracker _parentTypes; public MappingBuilderContext( SimpleMappingBuilderContext parentCtx, @@ -30,6 +31,10 @@ public MappingBuilderContext( { ObjectFactories = objectFactories; _formatProviders = formatProviders; + _parentTypes = parentCtx is MappingBuilderContext inlineCtx + ? inlineCtx._parentTypes.AddOrIncrement(mappingKey) + : MappingRecursionDepthTracker.Create(mappingKey); + UserSymbol = userSymbol; MappingKey = mappingKey; Configuration = ReadConfiguration(new MappingConfigurationReference(UserSymbol, mappingKey.Source, mappingKey.Target)); diff --git a/src/Riok.Mapperly/Descriptors/MappingRecursionDepthTracker.cs b/src/Riok.Mapperly/Descriptors/MappingRecursionDepthTracker.cs new file mode 100644 index 00000000000..371220ae545 --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/MappingRecursionDepthTracker.cs @@ -0,0 +1,42 @@ +using System.Collections.Immutable; + +namespace Riok.Mapperly.Descriptors; + +/// +/// Immutable wrapper for which tracks the parent types for a mapping. +/// Used to detect self referential loops. +/// +/// Dictionary tracking how many times a type has been seen. +public readonly struct MappingRecursionDepthTracker(ImmutableDictionary parentTypes) +{ + /// + /// Increments how many times a has been mapped. + /// Used to track how many times a parent context has mapped a type. + /// + /// The mapped type. + /// A new with the updated key. + public MappingRecursionDepthTracker AddOrIncrement(TypeMappingKey typeMappingKey) + { + var mappingRecursionCount = parentTypes.GetValueOrDefault(typeMappingKey); + var newParentTypes = parentTypes.SetItem(typeMappingKey, mappingRecursionCount + 1); + return new(newParentTypes); + } + + /// + /// Gets the number of times a has been mapped by the parent contexts. + /// + /// The candidate mapping. + /// The number of times the has been mapped. + public int GetDepth(TypeMappingKey typeMappingKey) => parentTypes.GetValueOrDefault(typeMappingKey); + + /// + /// Creates a new containing the initial type mapping. + /// + /// Initial value. + /// A containing the initial type mapping. + public static MappingRecursionDepthTracker Create(TypeMappingKey mappingKey) + { + var dict = ImmutableDictionary.Empty; + return new(dict.Add(mappingKey, 1)); + } +} diff --git a/src/Riok.Mapperly/Descriptors/Mappings/DefaultMemberMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/DefaultMemberMapping.cs new file mode 100644 index 00000000000..1fe857a8065 --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/Mappings/DefaultMemberMapping.cs @@ -0,0 +1,16 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; + +namespace Riok.Mapperly.Descriptors.Mappings; + +/// +/// Represents a mapping that returns default. +/// +/// target = default; +/// +/// +public class DefaultMemberMapping(ITypeSymbol sourceType, ITypeSymbol targetType) : NewInstanceMapping(sourceType, targetType) +{ + public override ExpressionSyntax Build(TypeMappingBuildContext ctx) => DefaultLiteral(); +} diff --git a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs index 9d3af22a497..dd66298261c 100644 --- a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs +++ b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs @@ -509,4 +509,13 @@ public static class DiagnosticDescriptors DiagnosticSeverity.Error, true ); + + public static readonly DiagnosticDescriptor MaxRecursionDepthMustBeZeroOrMore = new DiagnosticDescriptor( + "RMG056", + $"The value of MaxRecursionDepth cannot be less than zero", + $"The value of MaxRecursionDepth cannot be less than zero", + DiagnosticCategories.Mapper, + DiagnosticSeverity.Error, + true + ); } diff --git a/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionLoopTest.cs b/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionLoopTest.cs new file mode 100644 index 00000000000..17c368f97ec --- /dev/null +++ b/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionLoopTest.cs @@ -0,0 +1,197 @@ +using Riok.Mapperly.Diagnostics; + +namespace Riok.Mapperly.Tests.Mapping; + +[UsesVerify] +public class QueryableProjectionLoopTest +{ + [Fact] + public Task ReferenceLoopInitProperty() + { + var source = TestSourceBuilder.Mapping( + "System.Linq.IQueryable", + "System.Linq.IQueryable", + "class A { public A? Parent { get; set; } }", + "class B { public B? Parent { get; set; } }" + ); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task SetRecursionDepthToZero() + { + var source = TestSourceBuilder.Mapping( + "System.Linq.IQueryable", + "System.Linq.IQueryable", + TestSourceBuilderOptions.Default with + { + MaxRecursionDepth = 0 + }, + "class A { public A? Parent { get; set; } }", + "class B { public B? Parent { get; set; } }" + ); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task SetRecursionDepthToOne() + { + var source = TestSourceBuilder.Mapping( + "System.Linq.IQueryable", + "System.Linq.IQueryable", + TestSourceBuilderOptions.Default with + { + MaxRecursionDepth = 1 + }, + "class A { public A? Parent { get; set; } }", + "class B { public B? Parent { get; set; } }" + ); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task SetRecursionDepthToTwo() + { + var source = TestSourceBuilder.Mapping( + "System.Linq.IQueryable", + "System.Linq.IQueryable", + TestSourceBuilderOptions.Default with + { + MaxRecursionDepth = 2 + }, + "class A { public A? Parent { get; set; } }", + "class B { public B? Parent { get; set; } }" + ); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task MethodAttributeSetRecursionDepthForLoop() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + partial System.Linq.IQueryable Map(System.Linq.IQueryable src); + + [MapperMaxRecursionDepth(2)] + partial B Map(A src); + """, + "class A { public A? Parent { get; set; } }", + "class B { public B? Parent { get; set; } }" + ); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task MethodAttributeOverridesClassSetRecursionDepthForLoop() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + partial System.Linq.IQueryable Map(System.Linq.IQueryable src); + [MapperMaxRecursionDepth(2)] partial B Map(A src); + """, + TestSourceBuilderOptions.Default with + { + MaxRecursionDepth = 4 + }, + "class A { public A? Parent { get; set; } }", + "class B { public B? Parent { get; set; } }" + ); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task AssemblyDefaultShouldWork() + { + var source = TestSourceBuilder.CSharp( + """ + using Riok.Mapperly.Abstractions; + + [assembly: MapperDefaultsAttribute(MaxRecursionDepth = 2)] + [Mapper()] + public partial class MyMapper + { + partial System.Linq.IQueryable Map(System.Linq.IQueryable src); + } + + class A { public A? Parent { get; set; } } + + class B { public B? Parent { get; set; } } + """ + ); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task AttributeShouldOverrideAssemblyDefault() + { + var source = TestSourceBuilder.CSharp( + """ + using Riok.Mapperly.Abstractions; + + [assembly: MapperDefaultsAttribute(MaxRecursionDepth = 2)] + [Mapper(MaxRecursionDepth = 4)] + public partial class MyMapper + { + partial System.Linq.IQueryable Map(System.Linq.IQueryable src); + } + + class A { public A? Parent { get; set; } } + + class B { public B? Parent { get; set; } } + """ + ); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task ReferenceLoopCtor() + { + var source = TestSourceBuilder.Mapping( + "System.Linq.IQueryable", + "System.Linq.IQueryable", + "class A { public A? Parent { get; set; } }", + "class B { public B(B? parent) {} }" + ); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task IndirectReferenceLoop() + { + var source = TestSourceBuilder.Mapping( + "System.Linq.IQueryable", + "System.Linq.IQueryable", + "class A { public string StringValue { get; set; } public C Parent { get; set; } }", + "class B { public string StringValue { get; set; } public D Parent { get; set; } }", + "class C { public A Parent { get; set; } }", + "class D { public B Parent { get; set; } }" + ); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public void WithReferenceHandlingShouldDiagnostic() + { + var source = TestSourceBuilder.Mapping( + "System.Linq.IQueryable", + "System.Linq.IQueryable", + TestSourceBuilderOptions.WithReferenceHandling + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic(DiagnosticDescriptors.QueryableProjectionMappingsDoNotSupportReferenceHandling) + .HaveAssertedAllDiagnostics(); + } +} diff --git a/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionTest.cs b/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionTest.cs index 77b52668c4f..0ae05f642a0 100644 --- a/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionTest.cs @@ -1,5 +1,3 @@ -using Riok.Mapperly.Diagnostics; - namespace Riok.Mapperly.Tests.Mapping; [UsesVerify] @@ -137,32 +135,6 @@ public Task ClassToClassWithUserImplemented() return TestHelper.VerifyGenerator(source); } - [Fact] - public Task ReferenceLoopInitProperty() - { - var source = TestSourceBuilder.Mapping( - "System.Linq.IQueryable", - "System.Linq.IQueryable", - "class A { public A? Parent { get; set; } }", - "class B { public B? Parent { get; set; } }" - ); - - return TestHelper.VerifyGenerator(source); - } - - [Fact] - public Task ReferenceLoopCtor() - { - var source = TestSourceBuilder.Mapping( - "System.Linq.IQueryable", - "System.Linq.IQueryable", - "class A { public A? Parent { get; set; } }", - "class B { public B(B? parent) {} }" - ); - - return TestHelper.VerifyGenerator(source); - } - [Fact] public Task CtorShouldSkipUnmatchedOptionalParameters() { @@ -175,20 +147,4 @@ public Task CtorShouldSkipUnmatchedOptionalParameters() return TestHelper.VerifyGenerator(source); } - - [Fact] - public void WithReferenceHandlingShouldDiagnostic() - { - var source = TestSourceBuilder.Mapping( - "System.Linq.IQueryable", - "System.Linq.IQueryable", - TestSourceBuilderOptions.WithReferenceHandling - ); - - TestHelper - .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) - .Should() - .HaveDiagnostic(DiagnosticDescriptors.QueryableProjectionMappingsDoNotSupportReferenceHandling) - .HaveAssertedAllDiagnostics(); - } } diff --git a/test/Riok.Mapperly.Tests/TestSourceBuilder.cs b/test/Riok.Mapperly.Tests/TestSourceBuilder.cs index e0b98f8d7e3..34d8ad2002b 100644 --- a/test/Riok.Mapperly.Tests/TestSourceBuilder.cs +++ b/test/Riok.Mapperly.Tests/TestSourceBuilder.cs @@ -92,7 +92,8 @@ private static string BuildAttribute(TestSourceBuilderOptions options) Attribute(options.IgnoreObsoleteMembersStrategy), Attribute(options.RequiredMappingStrategy), Attribute(options.IncludedMembers), - Attribute(options.PreferParameterlessConstructors) + Attribute(options.PreferParameterlessConstructors), + Attribute(options.MaxRecursionDepth), }.WhereNotNull(); return $"[Mapper({string.Join(", ", attrs)})]"; @@ -110,6 +111,9 @@ private static string BuildAttribute(TestSourceBuilderOptions options) private static string? Attribute(bool? value, [CallerArgumentExpression("value")] string? expression = null) => value.HasValue ? Attribute(value.Value ? "true" : "false", expression) : null; + private static string? Attribute(int? value, [CallerArgumentExpression("value")] string? expression = null) => + value.HasValue ? Attribute(value.Value.ToString(), expression) : null; + private static string? Attribute(string? value, [CallerArgumentExpression("value")] string? expression = null) { if (value == null) diff --git a/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs b/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs index a3edbe320f3..35c2414b34d 100644 --- a/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs +++ b/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs @@ -18,7 +18,8 @@ public record TestSourceBuilderOptions( RequiredMappingStrategy? RequiredMappingStrategy = null, MemberVisibility? IncludedMembers = null, bool Static = false, - bool PreferParameterlessConstructors = true + bool PreferParameterlessConstructors = true, + int? MaxRecursionDepth = null ) { public const string DefaultMapperClassName = "Mapper"; diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.AssemblyDefaultShouldWork#MyMapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.AssemblyDefaultShouldWork#MyMapper.g.verified.cs new file mode 100644 index 00000000000..bfd87c863df --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.AssemblyDefaultShouldWork#MyMapper.g.verified.cs @@ -0,0 +1,21 @@ +//HintName: MyMapper.g.cs +// +#nullable enable +public partial class MyMapper +{ + partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable src) + { +#nullable disable + return System.Linq.Queryable.Select(src, x => new global::B() + { + Parent = x.Parent != null ? new global::B() + { + Parent = x.Parent.Parent != null ? new global::B() + { + Parent = x.Parent.Parent.Parent != null ? default : default, + } : default, + } : default, + }); +#nullable enable + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.AttributeShouldOverrideAssemblyDefault#MyMapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.AttributeShouldOverrideAssemblyDefault#MyMapper.g.verified.cs new file mode 100644 index 00000000000..6ac222f52f6 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.AttributeShouldOverrideAssemblyDefault#MyMapper.g.verified.cs @@ -0,0 +1,27 @@ +//HintName: MyMapper.g.cs +// +#nullable enable +public partial class MyMapper +{ + partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable src) + { +#nullable disable + return System.Linq.Queryable.Select(src, x => new global::B() + { + Parent = x.Parent != null ? new global::B() + { + Parent = x.Parent.Parent != null ? new global::B() + { + Parent = x.Parent.Parent.Parent != null ? new global::B() + { + Parent = x.Parent.Parent.Parent.Parent != null ? new global::B() + { + Parent = x.Parent.Parent.Parent.Parent.Parent != null ? default : default, + } : default, + } : default, + } : default, + } : default, + }); +#nullable enable + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.IndirectReferenceLoop#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.IndirectReferenceLoop#Mapper.g.verified.cs new file mode 100644 index 00000000000..41f270da051 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.IndirectReferenceLoop#Mapper.g.verified.cs @@ -0,0 +1,75 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + private partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable source) + { +#nullable disable + return System.Linq.Queryable.Select(source, x => new global::B() + { + StringValue = x.StringValue, + Parent = new global::D() + { + Parent = new global::B() + { + StringValue = x.Parent.Parent.StringValue, + Parent = new global::D() + { + Parent = new global::B() + { + StringValue = x.Parent.Parent.Parent.Parent.StringValue, + Parent = new global::D() + { + Parent = new global::B() + { + StringValue = x.Parent.Parent.Parent.Parent.Parent.Parent.StringValue, + Parent = new global::D() + { + Parent = new global::B() + { + StringValue = x.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.StringValue, + Parent = new global::D() + { + Parent = new global::B() + { + StringValue = x.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.StringValue, + Parent = new global::D() + { + Parent = new global::B() + { + StringValue = x.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.StringValue, + Parent = new global::D() + { + Parent = new global::B() + { + StringValue = x.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.StringValue, + Parent = new global::D() + { + Parent = new global::B() + { + StringValue = x.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.StringValue, + Parent = new global::D() + { + Parent = default, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }); +#nullable enable + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.MethodAttributeOverridesClassSetRecursionDepthForLoop#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.MethodAttributeOverridesClassSetRecursionDepthForLoop#Mapper.g.verified.cs new file mode 100644 index 00000000000..00fb31449ab --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.MethodAttributeOverridesClassSetRecursionDepthForLoop#Mapper.g.verified.cs @@ -0,0 +1,35 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable src) + { +#nullable disable + return System.Linq.Queryable.Select(src, x => new global::B() + { + Parent = x.Parent != null ? new global::B() + { + Parent = x.Parent.Parent != null ? new global::B() + { + Parent = x.Parent.Parent.Parent != null ? default : default, + } : default, + } : default, + }); +#nullable enable + } + + partial global::B Map(global::A src) + { + var target = new global::B(); + if (src.Parent != null) + { + target.Parent = Map(src.Parent); + } + else + { + target.Parent = null; + } + return target; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.MethodAttributeSetRecursionDepthForLoop#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.MethodAttributeSetRecursionDepthForLoop#Mapper.g.verified.cs new file mode 100644 index 00000000000..00fb31449ab --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.MethodAttributeSetRecursionDepthForLoop#Mapper.g.verified.cs @@ -0,0 +1,35 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable src) + { +#nullable disable + return System.Linq.Queryable.Select(src, x => new global::B() + { + Parent = x.Parent != null ? new global::B() + { + Parent = x.Parent.Parent != null ? new global::B() + { + Parent = x.Parent.Parent.Parent != null ? default : default, + } : default, + } : default, + }); +#nullable enable + } + + partial global::B Map(global::A src) + { + var target = new global::B(); + if (src.Parent != null) + { + target.Parent = Map(src.Parent); + } + else + { + target.Parent = null; + } + return target; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.ReferenceLoopCtor#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.ReferenceLoopCtor#Mapper.g.verified.cs new file mode 100644 index 00000000000..7c0332c0d5c --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.ReferenceLoopCtor#Mapper.g.verified.cs @@ -0,0 +1,12 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + private partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable source) + { +#nullable disable + return System.Linq.Queryable.Select(source, x => new global::B(x.Parent != null ? new global::B(x.Parent.Parent != null ? new global::B(x.Parent.Parent.Parent != null ? new global::B(x.Parent.Parent.Parent.Parent != null ? new global::B(x.Parent.Parent.Parent.Parent.Parent != null ? new global::B(x.Parent.Parent.Parent.Parent.Parent.Parent != null ? new global::B(x.Parent.Parent.Parent.Parent.Parent.Parent.Parent != null ? new global::B(x.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent != null ? new global::B(x.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent != null ? default : default) : default) : default) : default) : default) : default) : default) : default) : default)); +#nullable enable + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.ReferenceLoopInitProperty#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.ReferenceLoopInitProperty#Mapper.g.verified.cs new file mode 100644 index 00000000000..1e91408940a --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.ReferenceLoopInitProperty#Mapper.g.verified.cs @@ -0,0 +1,39 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + private partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable source) + { +#nullable disable + return System.Linq.Queryable.Select(source, x => new global::B() + { + Parent = x.Parent != null ? new global::B() + { + Parent = x.Parent.Parent != null ? new global::B() + { + Parent = x.Parent.Parent.Parent != null ? new global::B() + { + Parent = x.Parent.Parent.Parent.Parent != null ? new global::B() + { + Parent = x.Parent.Parent.Parent.Parent.Parent != null ? new global::B() + { + Parent = x.Parent.Parent.Parent.Parent.Parent.Parent != null ? new global::B() + { + Parent = x.Parent.Parent.Parent.Parent.Parent.Parent.Parent != null ? new global::B() + { + Parent = x.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent != null ? new global::B() + { + Parent = x.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent != null ? default : default, + } : default, + } : default, + } : default, + } : default, + } : default, + } : default, + } : default, + } : default, + }); +#nullable enable + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.SetRecursionDepthToOne#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.SetRecursionDepthToOne#Mapper.g.verified.cs new file mode 100644 index 00000000000..49888db769d --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.SetRecursionDepthToOne#Mapper.g.verified.cs @@ -0,0 +1,18 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + private partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable source) + { +#nullable disable + return System.Linq.Queryable.Select(source, x => new global::B() + { + Parent = x.Parent != null ? new global::B() + { + Parent = x.Parent.Parent != null ? default : default, + } : default, + }); +#nullable enable + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.SetRecursionDepthToTwo#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.SetRecursionDepthToTwo#Mapper.g.verified.cs new file mode 100644 index 00000000000..f2f51217947 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.SetRecursionDepthToTwo#Mapper.g.verified.cs @@ -0,0 +1,21 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + private partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable source) + { +#nullable disable + return System.Linq.Queryable.Select(source, x => new global::B() + { + Parent = x.Parent != null ? new global::B() + { + Parent = x.Parent.Parent != null ? new global::B() + { + Parent = x.Parent.Parent.Parent != null ? default : default, + } : default, + } : default, + }); +#nullable enable + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.ReferenceLoopInitProperty#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.SetRecursionDepthToZero#Mapper.g.verified.cs similarity index 86% rename from test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.ReferenceLoopInitProperty#Mapper.g.verified.cs rename to test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.SetRecursionDepthToZero#Mapper.g.verified.cs index 8588c1304af..a8785be7d64 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.ReferenceLoopInitProperty#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.SetRecursionDepthToZero#Mapper.g.verified.cs @@ -9,7 +9,7 @@ public partial class Mapper #nullable disable return System.Linq.Queryable.Select(source, x => new global::B() { - Parent = x.Parent != null ? new global::B() : default, + Parent = x.Parent != null ? default : default, }); #nullable enable } diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.SetRecursiveDepthToOne#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.SetRecursiveDepthToOne#Mapper.g.verified.cs new file mode 100644 index 00000000000..49888db769d --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.SetRecursiveDepthToOne#Mapper.g.verified.cs @@ -0,0 +1,18 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + private partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable source) + { +#nullable disable + return System.Linq.Queryable.Select(source, x => new global::B() + { + Parent = x.Parent != null ? new global::B() + { + Parent = x.Parent.Parent != null ? default : default, + } : default, + }); +#nullable enable + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.SetRecursiveDepthToTwo#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.SetRecursiveDepthToTwo#Mapper.g.verified.cs new file mode 100644 index 00000000000..f2f51217947 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.SetRecursiveDepthToTwo#Mapper.g.verified.cs @@ -0,0 +1,21 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + private partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable source) + { +#nullable disable + return System.Linq.Queryable.Select(source, x => new global::B() + { + Parent = x.Parent != null ? new global::B() + { + Parent = x.Parent.Parent != null ? new global::B() + { + Parent = x.Parent.Parent.Parent != null ? default : default, + } : default, + } : default, + }); +#nullable enable + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.ReferenceLoopCtor#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.SetRecursiveDepthToZero#Mapper.g.verified.cs similarity index 81% rename from test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.ReferenceLoopCtor#Mapper.g.verified.cs rename to test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.SetRecursiveDepthToZero#Mapper.g.verified.cs index c5a4bb025c9..a8785be7d64 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.ReferenceLoopCtor#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.SetRecursiveDepthToZero#Mapper.g.verified.cs @@ -7,7 +7,10 @@ public partial class Mapper private partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable source) { #nullable disable - return System.Linq.Queryable.Select(source, x => new global::B(x.Parent != null ? new global::B() : default)); + return System.Linq.Queryable.Select(source, x => new global::B() + { + Parent = x.Parent != null ? default : default, + }); #nullable enable } } \ No newline at end of file