diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceGenericTypeMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceGenericTypeMapping.cs index 2fe8547ed2..d82a08b206 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceGenericTypeMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceGenericTypeMapping.cs @@ -1,6 +1,8 @@ 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; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; @@ -35,7 +37,9 @@ ITypeSymbol objectType public override MethodDeclarationSyntax BuildMethod(SourceEmitterContext ctx) { var methodSyntax = (MethodDeclarationSyntax)Method.DeclaringSyntaxReferences.First().GetSyntax(); - return base.BuildMethod(ctx).WithTypeParameterList(methodSyntax.TypeParameterList); + return base.BuildMethod(ctx) + .WithTypeParameterList(methodSyntax.TypeParameterList) + .WithConstraintClauses(List(GetTypeParameterConstraintClauses())); } protected override ExpressionSyntax BuildTargetType() @@ -44,6 +48,52 @@ protected override ExpressionSyntax BuildTargetType() return TypeOfExpression(FullyQualifiedIdentifier(Method.ReturnType.NonNullable())); } + protected virtual IEnumerable GetTypeParameterConstraintClauses() + { + foreach (var tp in Method.TypeParameters) + { + var constraints = new List(); + + if (tp.HasUnmanagedTypeConstraint) + { + constraints.Add(TypeConstraint(IdentifierName("unmanaged")).AddLeadingSpace()); + } + else if (tp.HasValueTypeConstraint) + { + constraints.Add(ClassOrStructConstraint(SyntaxKind.StructConstraint).AddLeadingSpace()); + } + else if (tp.HasNotNullConstraint) + { + constraints.Add(TypeConstraint(IdentifierName("notnull")).AddLeadingSpace()); + } + else if (tp.HasReferenceTypeConstraint) + { + constraints.Add(ClassOrStructConstraint(SyntaxKind.ClassConstraint).AddLeadingSpace()); + } + + foreach (var c in tp.ConstraintTypes) + { + constraints.Add(TypeConstraint(FullyQualifiedIdentifier(c)).AddLeadingSpace()); + } + + if (tp.HasConstructorConstraint) + { + constraints.Add(ConstructorConstraint().AddLeadingSpace()); + } + + if (!constraints.Any()) + { + continue; + } + + yield return TypeParameterConstraintClause( + IdentifierName(tp.Name).AddLeadingSpace().AddTrailingSpace(), + SeparatedList(constraints) + ) + .AddLeadingSpace(); + } + } + protected override ExpressionSyntax? BuildSwitchArmWhenClause(ExpressionSyntax targetType, RuntimeTargetTypeMapping mapping) { return mapping.IsAssignableToMethodTargetType ? null : base.BuildSwitchArmWhenClause(targetType, mapping); diff --git a/src/Riok.Mapperly/Emit/Syntax/SyntaxIndentationExtensions.cs b/src/Riok.Mapperly/Emit/Syntax/SyntaxIndentationExtensions.cs index b3ae2a1b4c..94c58076b1 100644 --- a/src/Riok.Mapperly/Emit/Syntax/SyntaxIndentationExtensions.cs +++ b/src/Riok.Mapperly/Emit/Syntax/SyntaxIndentationExtensions.cs @@ -86,6 +86,9 @@ public static TSyntax AddTrailingLineFeed(this TSyntax syntax, int inde return syntax.WithTrailingTrivia(trivia); } + public static TSyntax AddLeadingSpace(this TSyntax syntax) + where TSyntax : SyntaxNode => syntax.WithLeadingTrivia(syntax.GetLeadingTrivia().Add(ElasticSpace)); + public static TSyntax AddTrailingSpace(this TSyntax syntax) where TSyntax : SyntaxNode => syntax.WithTrailingTrivia(syntax.GetTrailingTrivia().Add(ElasticSpace)); diff --git a/test/Riok.Mapperly.Tests/GeneratedMethod.cs b/test/Riok.Mapperly.Tests/GeneratedMethod.cs index 0445075df9..21900b2b39 100644 --- a/test/Riok.Mapperly.Tests/GeneratedMethod.cs +++ b/test/Riok.Mapperly.Tests/GeneratedMethod.cs @@ -1,3 +1,4 @@ +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Riok.Mapperly.Tests; @@ -8,6 +9,7 @@ public GeneratedMethod(MethodDeclarationSyntax declarationSyntax) { Name = declarationSyntax.Identifier.ToString(); Signature = $"{declarationSyntax.ReturnType.ToString()} {Name}{declarationSyntax.ParameterList.ToString().Trim()}"; + ConstraintClauses = ExtractParameterConstraints(declarationSyntax.ConstraintClauses); Body = ExtractBody(declarationSyntax); } @@ -15,8 +17,18 @@ public GeneratedMethod(MethodDeclarationSyntax declarationSyntax) public string Signature { get; } + public string? ConstraintClauses { get; } + public string Body { get; } + private static string? ExtractParameterConstraints(SyntaxList typeParameterConstraints) + { + if (typeParameterConstraints.Count == 0) + return null; + + return typeParameterConstraints.ToFullString().Trim(' ', '\r', '\n').ReplaceLineEndings(); + } + /// /// Builds the method body without the method body braces and without the method body level indentation. /// diff --git a/test/Riok.Mapperly.Tests/MapperGenerationResultAssertions.cs b/test/Riok.Mapperly.Tests/MapperGenerationResultAssertions.cs index 81569f7328..ef2f797f9d 100644 --- a/test/Riok.Mapperly.Tests/MapperGenerationResultAssertions.cs +++ b/test/Riok.Mapperly.Tests/MapperGenerationResultAssertions.cs @@ -121,6 +121,19 @@ public MapperGenerationResultAssertions HaveMethodBody(string methodName, [Strin public MapperGenerationResultAssertions HaveMapMethodBody([StringSyntax(StringSyntax.CSharp)] string mapperMethodBody) => HaveMethodBody(TestSourceBuilder.DefaultMapMethodName, mapperMethodBody); + public MapperGenerationResultAssertions HaveMapMethodWithGenericConstraints( + string methodName, + [StringSyntax(StringSyntax.CSharp)] string? constraintClauses + ) + { + _mapper.Methods[methodName].ConstraintClauses.Should().Be(constraintClauses); + return this; + } + + public MapperGenerationResultAssertions HaveMapMethodWithGenericConstraints( + [StringSyntax(StringSyntax.CSharp)] string? constraintClauses + ) => HaveMapMethodWithGenericConstraints(TestSourceBuilder.DefaultMapMethodName, constraintClauses); + private IReadOnlyCollection GetDiagnostics(DiagnosticDescriptor descriptor) { if (_mapper.DiagnosticsByDescriptorId.TryGetValue(descriptor.Id, out var diagnostics)) diff --git a/test/Riok.Mapperly.Tests/Mapping/GenericTest.cs b/test/Riok.Mapperly.Tests/Mapping/GenericTest.cs index 85a41e1a3c..f18813e420 100644 --- a/test/Riok.Mapperly.Tests/Mapping/GenericTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/GenericTest.cs @@ -122,7 +122,8 @@ public void WithGenericSource() _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(object)} as there is no known type mapping", nameof(source)), }; """ - ); + ) + .HaveMapMethodWithGenericConstraints(null); } [Fact] @@ -135,11 +136,14 @@ partial object Map(TSource source) partial B MapToB(A source); partial D MapToD(C source); + partial F MapToF(E source); """, - "record struct A(string Value);", + "record A(string Value);", "record struct B(string Value);", "record C(string Value1);", - "record D(string Value1);" + "record D(string Value1);", + "record E(string Value) : A(Value);", + "record struct F(string Value) : B(Value);" ); TestHelper .GenerateMapper(source) @@ -148,13 +152,13 @@ partial object Map(TSource source) """ return source switch { + global::E x => MapToF(x), global::A x => MapToB(x), - global::C x => MapToD(x), - null => throw new System.ArgumentNullException(nameof(source)), _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(object)} as there is no known type mapping", nameof(source)), }; """ - ); + ) + .HaveMapMethodWithGenericConstraints("where TSource : global::A"); } [Fact] @@ -185,7 +189,8 @@ partial object Map(TSource source) _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(object)} as there is no known type mapping", nameof(source)), }; """ - ); + ) + .HaveMapMethodWithGenericConstraints("where TSource : notnull"); } [Fact] @@ -215,7 +220,8 @@ partial object Map(TSource source) _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(object)} as there is no known type mapping", nameof(source)), }; """ - ); + ) + .HaveMapMethodWithGenericConstraints("where TSource : struct"); } [Fact] @@ -245,7 +251,8 @@ partial object Map(TSource source) _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(object)} as there is no known type mapping", nameof(source)), }; """ - ); + ) + .HaveMapMethodWithGenericConstraints("where TSource : unmanaged"); } [Fact] @@ -275,7 +282,8 @@ partial object Map(TSource source) _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(object)} as there is no known type mapping", nameof(source)), }; """ - ); + ) + .HaveMapMethodWithGenericConstraints("where TSource : class"); } [Fact] @@ -306,7 +314,8 @@ partial object Map(TSource source) _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(object)} as there is no known type mapping", nameof(source)), }; """ - ); + ) + .HaveMapMethodWithGenericConstraints("where TSource : class"); } [Fact] @@ -337,7 +346,8 @@ partial object Map(TSource source) _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(object)} as there is no known type mapping", nameof(source)), }; """ - ); + ) + .HaveMapMethodWithGenericConstraints("where TSource : class"); } [Fact] @@ -369,7 +379,8 @@ partial TTarget Map(TSource source) _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(TTarget)} as there is no known type mapping", nameof(source)), }; """ - ); + ) + .HaveMapMethodWithGenericConstraints("where TSource : class where TTarget : class"); } [Fact] @@ -404,7 +415,8 @@ public void WithGenericSourceSpecificTarget() _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(global::BaseDto)} as there is no known type mapping", nameof(source)), }; """ - ); + ) + .HaveMapMethodWithGenericConstraints(null); } [Fact] @@ -434,7 +446,8 @@ public void WithGenericTarget() _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(TTarget)} as there is no known type mapping", nameof(source)), }; """ - ); + ) + .HaveMapMethodWithGenericConstraints(null); } [Fact] @@ -464,7 +477,8 @@ partial TTarget Map(object source) _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(TTarget)} as there is no known type mapping", nameof(source)), }; """ - ); + ) + .HaveMapMethodWithGenericConstraints("where TTarget : global::D"); } [Fact] @@ -498,7 +512,8 @@ public void WithGenericTargetSpecificSource() _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(TTarget)} as there is no known type mapping", nameof(source)), }; """ - ); + ) + .HaveMapMethodWithGenericConstraints(null); } [Fact] @@ -529,7 +544,8 @@ partial TTarget Map(TSource source) _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(TTarget)} as there is no known type mapping", nameof(source)), }; """ - ); + ) + .HaveMapMethodWithGenericConstraints("where TSource : global::C where TTarget : global::D"); } [Fact] @@ -562,7 +578,26 @@ public void WithUserImplementedMethodsShouldBeIncluded() _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(TTarget)} as there is no known type mapping", nameof(source)), }; """ - ); + ) + .HaveMapMethodWithGenericConstraints(null); + } + + [Fact] + public Task WithGenericConstructorConstraint() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + private partial TTarget Map(TSource source) where TSource : new() where TTarget : new(); + + private partial B MapToB(A source); + private partial D MapToD(C source); + """, + "record struct A(string Value) { public A() : this(default!) {} }", + "record struct B(string Value) { public B() : this(default!) {} }", + "record C(string Value1);", + "record D(string Value1);" + ); + return TestHelper.VerifyGenerator(source); } [Fact] diff --git a/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithGenericConstructorConstraint#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithGenericConstructorConstraint#Mapper.g.verified.cs new file mode 100644 index 0000000000..0cc8c12fa8 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithGenericConstructorConstraint#Mapper.g.verified.cs @@ -0,0 +1,31 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial TTarget Map(TSource source) where TSource : new() where TTarget : new() + { + return source switch + { + global::A x when typeof(TTarget).IsAssignableFrom(typeof(global::B)) => (TTarget)(object)MapToB(x), + null => throw new System.ArgumentNullException(nameof(source)), + _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(TTarget)} as there is no known type mapping", nameof(source)), + }; + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial global::B MapToB(global::A source) + { + var target = new global::B(); + target.Value = source.Value; + return target; + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private partial global::D MapToD(global::C source) + { + var target = new global::D(source.Value1); + return target; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithTypeConstrainedQueryableGenericSourceAndTarget#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithTypeConstrainedQueryableGenericSourceAndTarget#Mapper.g.verified.cs index 0e23b99293..2642468043 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithTypeConstrainedQueryableGenericSourceAndTarget#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithTypeConstrainedQueryableGenericSourceAndTarget#Mapper.g.verified.cs @@ -4,7 +4,7 @@ public partial class Mapper { [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] - private partial TTarget Map(TSource source) + private partial TTarget Map(TSource source) where TSource : global::System.Linq.IQueryable where TTarget : global::System.Linq.IQueryable { return source switch {