diff --git a/src/Riok.Mapperly/Descriptors/Enumerables/Capacity/EnsureCapacityMethodSetter.cs b/src/Riok.Mapperly/Descriptors/Enumerables/Capacity/EnsureCapacityMethodSetter.cs index 1214487e7e..2cedf750cf 100644 --- a/src/Riok.Mapperly/Descriptors/Enumerables/Capacity/EnsureCapacityMethodSetter.cs +++ b/src/Riok.Mapperly/Descriptors/Enumerables/Capacity/EnsureCapacityMethodSetter.cs @@ -1,3 +1,4 @@ +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Riok.Mapperly.Symbols.Members; using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; @@ -17,7 +18,12 @@ private EnsureCapacityMethodSetter() { } public bool SupportsCoalesceAssignment => false; - public ExpressionSyntax BuildAssignment(ExpressionSyntax? baseAccess, ExpressionSyntax valueToAssign, bool coalesceAssignment = false) + public ExpressionSyntax BuildAssignment( + ExpressionSyntax? baseAccess, + ExpressionSyntax valueToAssign, + INamedTypeSymbol? containingType = null, + bool coalesceAssignment = false + ) { if (baseAccess == null) throw new ArgumentNullException(nameof(baseAccess)); diff --git a/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeFieldAccessor.cs b/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeFieldAccessor.cs index 583a1a6ded..8652333e4a 100644 --- a/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeFieldAccessor.cs +++ b/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeFieldAccessor.cs @@ -43,7 +43,7 @@ public MethodDeclarationSyntax BuildAccessorMethod(SourceEmitterContext ctx) return ctx.SyntaxFactory.PublicStaticExternMethod(returnType, methodName, parameters, [attribute]); } - public ExpressionSyntax BuildAccess(ExpressionSyntax? baseAccess, bool nullConditional = false) + public ExpressionSyntax BuildAccess(ExpressionSyntax? baseAccess, INamedTypeSymbol? containingType = null, bool nullConditional = false) { if (baseAccess == null) throw new ArgumentNullException(nameof(baseAccess)); @@ -54,7 +54,10 @@ public ExpressionSyntax BuildAccess(ExpressionSyntax? baseAccess, bool nullCondi return InvocationWithoutIndention(method); } - var genericClassName = GenericName(className).WithTypeArgumentList(TypeArgumentList(symbol.ContainingType.TypeArguments)); + // Use the passed containingType for type arguments if provided, + // otherwise fall back to the symbol's containing type. + var typeArgs = containingType?.TypeArguments ?? symbol.ContainingType.TypeArguments; + var genericClassName = GenericName(className).WithTypeArgumentList(TypeArgumentList(typeArgs)); var invocation = InvocationExpression(MemberAccess(genericClassName, methodName)) .WithArgumentList(ArgumentListWithoutIndention([baseAccess])); @@ -64,9 +67,14 @@ public ExpressionSyntax BuildAccess(ExpressionSyntax? baseAccess, bool nullCondi return Conditional(IsNotNull(baseAccess), invocation, DefaultLiteral()); } - public ExpressionSyntax BuildAssignment(ExpressionSyntax? baseAccess, ExpressionSyntax valueToAssign, bool coalesceAssignment = false) + public ExpressionSyntax BuildAssignment( + ExpressionSyntax? baseAccess, + ExpressionSyntax valueToAssign, + INamedTypeSymbol? containingType = null, + bool coalesceAssignment = false + ) { - var access = BuildAccess(baseAccess); + var access = BuildAccess(baseAccess, containingType); return Assignment(access, valueToAssign, coalesceAssignment); } } diff --git a/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeGetPropertyAccessor.cs b/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeGetPropertyAccessor.cs index 859e04dba9..8e2c9d3bfa 100644 --- a/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeGetPropertyAccessor.cs +++ b/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeGetPropertyAccessor.cs @@ -46,7 +46,7 @@ public MethodDeclarationSyntax BuildAccessorMethod(SourceEmitterContext ctx) ); } - public ExpressionSyntax BuildAccess(ExpressionSyntax? baseAccess, bool nullConditional = false) + public ExpressionSyntax BuildAccess(ExpressionSyntax? baseAccess, INamedTypeSymbol? containingType = null, bool nullConditional = false) { if (baseAccess == null) throw new ArgumentNullException(nameof(baseAccess)); @@ -57,7 +57,12 @@ public ExpressionSyntax BuildAccess(ExpressionSyntax? baseAccess, bool nullCondi return InvocationWithoutIndention(method); } - var genericClassName = GenericName(className).WithTypeArgumentList(TypeArgumentList(symbol.ContainingType.TypeArguments)); + // Use the passed containingType for type arguments if provided, + // otherwise fall back to the symbol's containing type. + // This is critical for inherited members where the cached symbol's + // type arguments may differ from the actual derived type being mapped. + var typeArgs = containingType?.TypeArguments ?? symbol.ContainingType.TypeArguments; + var genericClassName = GenericName(className).WithTypeArgumentList(TypeArgumentList(typeArgs)); var invocation = InvocationExpression(MemberAccess(genericClassName, methodName)) .WithArgumentList(ArgumentListWithoutIndention([baseAccess])); diff --git a/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeSetPropertyAccessor.cs b/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeSetPropertyAccessor.cs index 23250fe26b..e1c6fd7e86 100644 --- a/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeSetPropertyAccessor.cs +++ b/src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeSetPropertyAccessor.cs @@ -53,7 +53,12 @@ public MethodDeclarationSyntax BuildAccessorMethod(SourceEmitterContext ctx) ); } - public ExpressionSyntax BuildAssignment(ExpressionSyntax? baseAccess, ExpressionSyntax valueToAssign, bool coalesceAssignment = false) + public ExpressionSyntax BuildAssignment( + ExpressionSyntax? baseAccess, + ExpressionSyntax valueToAssign, + INamedTypeSymbol? containingType = null, + bool coalesceAssignment = false + ) { if (baseAccess == null) throw new ArgumentNullException(nameof(baseAccess)); @@ -63,8 +68,13 @@ public ExpressionSyntax BuildAssignment(ExpressionSyntax? baseAccess, Expression return InvocationWithoutIndention(MemberAccess(baseAccess, methodName), valueToAssign); } + // Use the passed containingType for type arguments if provided, + // otherwise fall back to the symbol's containing type. + // This is critical for inherited members where the cached symbol's + // type arguments may differ from the actual derived type being mapped. + var typeArgs = containingType?.TypeArguments ?? symbol.ContainingType.TypeArguments; var args = new[] { baseAccess, valueToAssign }; - var genericClassName = GenericName(className).WithTypeArgumentList(TypeArgumentList(symbol.ContainingType.TypeArguments)); + var genericClassName = GenericName(className).WithTypeArgumentList(TypeArgumentList(typeArgs)); return InvocationExpression(MemberAccess(genericClassName, methodName)).WithArgumentList(ArgumentListWithoutIndention(args)); } } diff --git a/src/Riok.Mapperly/Symbols/Members/ConstructorParameterMember.cs b/src/Riok.Mapperly/Symbols/Members/ConstructorParameterMember.cs index d49fe522c8..8e848e23c7 100644 --- a/src/Riok.Mapperly/Symbols/Members/ConstructorParameterMember.cs +++ b/src/Riok.Mapperly/Symbols/Members/ConstructorParameterMember.cs @@ -36,5 +36,9 @@ public class ConstructorParameterMember(IParameterSymbol symbol, SymbolAccessor 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 ExpressionSyntax BuildAccess( + ExpressionSyntax? baseAccess, + INamedTypeSymbol? containingType = null, + bool nullConditional = false + ) => IdentifierName(Name); } diff --git a/src/Riok.Mapperly/Symbols/Members/FieldMember.cs b/src/Riok.Mapperly/Symbols/Members/FieldMember.cs index eeaa901506..88794b6122 100644 --- a/src/Riok.Mapperly/Symbols/Members/FieldMember.cs +++ b/src/Riok.Mapperly/Symbols/Members/FieldMember.cs @@ -54,13 +54,18 @@ public IMemberSetter BuildSetter(UnsafeAccessorContext ctx) return ctx.GetOrBuildFieldGetter(this); } - public ExpressionSyntax BuildAssignment(ExpressionSyntax? baseAccess, ExpressionSyntax valueToAssign, bool coalesceAssignment = false) + public ExpressionSyntax BuildAssignment( + ExpressionSyntax? baseAccess, + ExpressionSyntax valueToAssign, + INamedTypeSymbol? containingType = null, + bool coalesceAssignment = false + ) { var targetMemberRef = BuildAccess(baseAccess); return Assignment(targetMemberRef, valueToAssign, coalesceAssignment); } - public ExpressionSyntax BuildAccess(ExpressionSyntax? baseAccess, bool nullConditional = false) + public ExpressionSyntax BuildAccess(ExpressionSyntax? baseAccess, INamedTypeSymbol? containingType = null, bool nullConditional = false) { if (baseAccess == null) return SyntaxFactory.IdentifierName(Name); diff --git a/src/Riok.Mapperly/Symbols/Members/IMemberGetter.cs b/src/Riok.Mapperly/Symbols/Members/IMemberGetter.cs index 9a5ce17a86..ddae4298d6 100644 --- a/src/Riok.Mapperly/Symbols/Members/IMemberGetter.cs +++ b/src/Riok.Mapperly/Symbols/Members/IMemberGetter.cs @@ -1,8 +1,9 @@ +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Riok.Mapperly.Symbols.Members; public interface IMemberGetter { - ExpressionSyntax BuildAccess(ExpressionSyntax? baseAccess, bool nullConditional = false); + ExpressionSyntax BuildAccess(ExpressionSyntax? baseAccess, INamedTypeSymbol? containingType = null, bool nullConditional = false); } diff --git a/src/Riok.Mapperly/Symbols/Members/IMemberSetter.cs b/src/Riok.Mapperly/Symbols/Members/IMemberSetter.cs index 5c5e470265..9c8884b010 100644 --- a/src/Riok.Mapperly/Symbols/Members/IMemberSetter.cs +++ b/src/Riok.Mapperly/Symbols/Members/IMemberSetter.cs @@ -1,3 +1,4 @@ +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Riok.Mapperly.Symbols.Members; @@ -6,5 +7,10 @@ public interface IMemberSetter { bool SupportsCoalesceAssignment { get; } - ExpressionSyntax BuildAssignment(ExpressionSyntax? baseAccess, ExpressionSyntax valueToAssign, bool coalesceAssignment = false); + ExpressionSyntax BuildAssignment( + ExpressionSyntax? baseAccess, + ExpressionSyntax valueToAssign, + INamedTypeSymbol? containingType = null, + bool coalesceAssignment = false + ); } diff --git a/src/Riok.Mapperly/Symbols/Members/MemberPathGetter.cs b/src/Riok.Mapperly/Symbols/Members/MemberPathGetter.cs index 8f26a6512e..153cc061f2 100644 --- a/src/Riok.Mapperly/Symbols/Members/MemberPathGetter.cs +++ b/src/Riok.Mapperly/Symbols/Members/MemberPathGetter.cs @@ -56,7 +56,7 @@ public static MemberPathGetter Build(SimpleMappingBuilderContext ctx, MemberPath { return path.AggregateWithPrevious( baseAccess, - (expr, prevProp, prop) => prop.Getter.BuildAccess(expr, prevProp.Member?.IsNullable == true) + (expr, prevProp, prop) => prop.Getter.BuildAccess(expr, prop.Member.ContainingType, prevProp.Member?.IsNullable == true) ); } @@ -66,12 +66,12 @@ public static MemberPathGetter Build(SimpleMappingBuilderContext ctx, MemberPath baseAccess, (a, b) => b.Member.Type.IsNullableValueType() - ? MemberAccess(b.Getter.BuildAccess(a), NullableValueProperty) - : b.Getter.BuildAccess(a) + ? MemberAccess(b.Getter.BuildAccess(a, b.Member.ContainingType), NullableValueProperty) + : b.Getter.BuildAccess(a, b.Member.ContainingType) ); } - return path.Aggregate(baseAccess, (a, b) => b.Getter.BuildAccess(a)); + return path.Aggregate(baseAccess, (a, b) => b.Getter.BuildAccess(a, b.Member.ContainingType)); } /// @@ -99,7 +99,7 @@ private BinaryExpressionSyntax BuildNonNullCondition(ExpressionSyntax baseAccess var conditions = new List(); foreach (var pathPart in nullablePath) { - access = pathPart.Getter.BuildAccess(access); + access = pathPart.Getter.BuildAccess(access, pathPart.Member.ContainingType); if (!pathPart.Member.IsNullable) continue; diff --git a/src/Riok.Mapperly/Symbols/Members/MemberPathSetter.cs b/src/Riok.Mapperly/Symbols/Members/MemberPathSetter.cs index 580fecb182..ba0b0399d0 100644 --- a/src/Riok.Mapperly/Symbols/Members/MemberPathSetter.cs +++ b/src/Riok.Mapperly/Symbols/Members/MemberPathSetter.cs @@ -13,12 +13,19 @@ public class MemberPathSetter private readonly NonEmptyMemberPath _memberPath; private readonly MemberPathGetter _baseAccessGetter; private readonly IMemberSetter _memberSetter; + private readonly IMappableMember _member; - private MemberPathSetter(NonEmptyMemberPath memberPath, MemberPathGetter baseAccessGetter, IMemberSetter memberSetter) + private MemberPathSetter( + NonEmptyMemberPath memberPath, + MemberPathGetter baseAccessGetter, + IMemberSetter memberSetter, + IMappableMember member + ) { _memberPath = memberPath; _baseAccessGetter = baseAccessGetter; _memberSetter = memberSetter; + _member = member; } public bool SupportsCoalesceAssignment => _memberSetter.SupportsCoalesceAssignment; @@ -30,13 +37,13 @@ public static MemberPathSetter Build(SimpleMappingBuilderContext ctx, NonEmptyMe 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); + return new MemberPathSetter(path, objectGetter, memberSetter, path.Member); } public ExpressionSyntax BuildAssignment(ExpressionSyntax? baseAccess, ExpressionSyntax valueToAssign, bool coalesceAssignment = false) { baseAccess = _baseAccessGetter.BuildAccess(baseAccess); - return _memberSetter.BuildAssignment(baseAccess, valueToAssign, coalesceAssignment); + return _memberSetter.BuildAssignment(baseAccess, valueToAssign, _member.ContainingType, coalesceAssignment); } public override bool Equals(object? obj) diff --git a/src/Riok.Mapperly/Symbols/Members/ParameterSourceMember.cs b/src/Riok.Mapperly/Symbols/Members/ParameterSourceMember.cs index 1dc39a84cc..051ed60a28 100644 --- a/src/Riok.Mapperly/Symbols/Members/ParameterSourceMember.cs +++ b/src/Riok.Mapperly/Symbols/Members/ParameterSourceMember.cs @@ -33,7 +33,11 @@ public class ParameterSourceMember(MethodParameter parameter) : IMappableMember, 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 ExpressionSyntax BuildAccess( + ExpressionSyntax? baseAccess, + INamedTypeSymbol? containingType = null, + bool nullConditional = false + ) => IdentifierName(Name); public override bool Equals(object? obj) { diff --git a/src/Riok.Mapperly/Symbols/Members/PropertyMember.cs b/src/Riok.Mapperly/Symbols/Members/PropertyMember.cs index 68865d969c..d0ad48ea92 100644 --- a/src/Riok.Mapperly/Symbols/Members/PropertyMember.cs +++ b/src/Riok.Mapperly/Symbols/Members/PropertyMember.cs @@ -64,7 +64,12 @@ public IMemberSetter BuildSetter(UnsafeAccessorContext ctx) return ctx.GetOrBuildPropertySetter(this); } - public ExpressionSyntax BuildAssignment(ExpressionSyntax? baseAccess, ExpressionSyntax valueToAssign, bool coalesceAssignment = false) + public ExpressionSyntax BuildAssignment( + ExpressionSyntax? baseAccess, + ExpressionSyntax valueToAssign, + INamedTypeSymbol? containingType = null, + bool coalesceAssignment = false + ) { Debug.Assert(CanSetDirectly); ExpressionSyntax targetMember = baseAccess == null ? IdentifierName(Name) : MemberAccess(baseAccess, Name); @@ -72,7 +77,7 @@ public ExpressionSyntax BuildAssignment(ExpressionSyntax? baseAccess, Expression return Assignment(targetMember, valueToAssign, coalesceAssignment); } - public ExpressionSyntax BuildAccess(ExpressionSyntax? baseAccess, bool nullConditional = false) + public ExpressionSyntax BuildAccess(ExpressionSyntax? baseAccess, INamedTypeSymbol? containingType = null, bool nullConditional = false) { Debug.Assert(CanGetDirectly); if (baseAccess == null) diff --git a/test/Riok.Mapperly.Tests/Mapping/UnsafeAccessorTest.cs b/test/Riok.Mapperly.Tests/Mapping/UnsafeAccessorTest.cs index 0fa0b95656..a66673e7b4 100644 --- a/test/Riok.Mapperly.Tests/Mapping/UnsafeAccessorTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/UnsafeAccessorTest.cs @@ -88,6 +88,29 @@ public Task PrivatePropertyInGenericClassMultipleTypeParametersWithConstraints() return TestHelper.VerifyGenerator(source); } + [Fact] + public Task ProtectedPropertyInGenericBaseClassWithDifferentInstantiations() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + partial B1 Map1(A1 source); + partial B2 Map2(A2 source); + """, + TestSourceBuilderOptions.WithMemberVisibility(MemberVisibility.All), + "interface IA { }", + "interface IB : IA { }", + "interface IC : IA { }", + "class A where T : IA { protected T _value { get; set; } }", + "class A1 : A { }", + "class A2 : A { }", + "class B where T : IA { protected T _value { get; set; } }", + "class B1 : B { }", + "class B2 : B { }" + ); + + return TestHelper.VerifyGenerator(source); + } + [Fact] public Task ProtectedProperty() { diff --git a/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivatePropertyInGenericClassMultiple#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivatePropertyInGenericClassMultiple#Mapper.g.verified.cs index 83065d4733..b62cc37989 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivatePropertyInGenericClassMultiple#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.PrivatePropertyInGenericClassMultiple#Mapper.g.verified.cs @@ -15,7 +15,7 @@ public partial class Mapper partial global::B Map(global::A source) { var target = new global::B(); - BAccessor.SetValue(target, AAccessor.GetValue(source)); + BAccessor.SetValue(target, AAccessor.GetValue(source)); return target; } } diff --git a/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.ProtectedPropertyInGenericBaseClassWithDifferentInstantiations#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.ProtectedPropertyInGenericBaseClassWithDifferentInstantiations#Mapper.g.verified.cs new file mode 100644 index 0000000000..cd892d7033 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/UnsafeAccessorTest.ProtectedPropertyInGenericBaseClassWithDifferentInstantiations#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::B1 Map1(global::A1 source) + { + var target = new global::B1(); + BAccessor.SetValue(target, AAccessor.GetValue(source)); + return target; + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + partial global::B2 Map2(global::A2 source) + { + var target = new global::B2(); + BAccessor.SetValue(target, AAccessor.GetValue(source)); + return target; + } +} + +[global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] +static file class AAccessor + where T : global::IA +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get__value")] + public static extern T GetValue(global::A source); +} + +[global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] +static file class BAccessor + where T : global::IA +{ + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set__value")] + public static extern void SetValue(global::B target, T value); +} \ No newline at end of file