Skip to content

Commit 1317b5b

Browse files
YoelDruxmanclaude
andauthored
fix: unsafe accessor generic type args (riok#2116)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent ebb2e9e commit 1317b5b

15 files changed

Lines changed: 149 additions & 26 deletions

src/Riok.Mapperly/Descriptors/Enumerables/Capacity/EnsureCapacityMethodSetter.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using Microsoft.CodeAnalysis;
12
using Microsoft.CodeAnalysis.CSharp.Syntax;
23
using Riok.Mapperly.Symbols.Members;
34
using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper;
@@ -17,7 +18,12 @@ private EnsureCapacityMethodSetter() { }
1718

1819
public bool SupportsCoalesceAssignment => false;
1920

20-
public ExpressionSyntax BuildAssignment(ExpressionSyntax? baseAccess, ExpressionSyntax valueToAssign, bool coalesceAssignment = false)
21+
public ExpressionSyntax BuildAssignment(
22+
ExpressionSyntax? baseAccess,
23+
ExpressionSyntax valueToAssign,
24+
INamedTypeSymbol? containingType = null,
25+
bool coalesceAssignment = false
26+
)
2127
{
2228
if (baseAccess == null)
2329
throw new ArgumentNullException(nameof(baseAccess));

src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeFieldAccessor.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public MethodDeclarationSyntax BuildAccessorMethod(SourceEmitterContext ctx)
4343
return ctx.SyntaxFactory.PublicStaticExternMethod(returnType, methodName, parameters, [attribute]);
4444
}
4545

46-
public ExpressionSyntax BuildAccess(ExpressionSyntax? baseAccess, bool nullConditional = false)
46+
public ExpressionSyntax BuildAccess(ExpressionSyntax? baseAccess, INamedTypeSymbol? containingType = null, bool nullConditional = false)
4747
{
4848
if (baseAccess == null)
4949
throw new ArgumentNullException(nameof(baseAccess));
@@ -54,7 +54,10 @@ public ExpressionSyntax BuildAccess(ExpressionSyntax? baseAccess, bool nullCondi
5454
return InvocationWithoutIndention(method);
5555
}
5656

57-
var genericClassName = GenericName(className).WithTypeArgumentList(TypeArgumentList(symbol.ContainingType.TypeArguments));
57+
// Use the passed containingType for type arguments if provided,
58+
// otherwise fall back to the symbol's containing type.
59+
var typeArgs = containingType?.TypeArguments ?? symbol.ContainingType.TypeArguments;
60+
var genericClassName = GenericName(className).WithTypeArgumentList(TypeArgumentList(typeArgs));
5861
var invocation = InvocationExpression(MemberAccess(genericClassName, methodName))
5962
.WithArgumentList(ArgumentListWithoutIndention([baseAccess]));
6063

@@ -64,9 +67,14 @@ public ExpressionSyntax BuildAccess(ExpressionSyntax? baseAccess, bool nullCondi
6467
return Conditional(IsNotNull(baseAccess), invocation, DefaultLiteral());
6568
}
6669

67-
public ExpressionSyntax BuildAssignment(ExpressionSyntax? baseAccess, ExpressionSyntax valueToAssign, bool coalesceAssignment = false)
70+
public ExpressionSyntax BuildAssignment(
71+
ExpressionSyntax? baseAccess,
72+
ExpressionSyntax valueToAssign,
73+
INamedTypeSymbol? containingType = null,
74+
bool coalesceAssignment = false
75+
)
6876
{
69-
var access = BuildAccess(baseAccess);
77+
var access = BuildAccess(baseAccess, containingType);
7078
return Assignment(access, valueToAssign, coalesceAssignment);
7179
}
7280
}

src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeGetPropertyAccessor.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public MethodDeclarationSyntax BuildAccessorMethod(SourceEmitterContext ctx)
4646
);
4747
}
4848

49-
public ExpressionSyntax BuildAccess(ExpressionSyntax? baseAccess, bool nullConditional = false)
49+
public ExpressionSyntax BuildAccess(ExpressionSyntax? baseAccess, INamedTypeSymbol? containingType = null, bool nullConditional = false)
5050
{
5151
if (baseAccess == null)
5252
throw new ArgumentNullException(nameof(baseAccess));
@@ -57,7 +57,12 @@ public ExpressionSyntax BuildAccess(ExpressionSyntax? baseAccess, bool nullCondi
5757
return InvocationWithoutIndention(method);
5858
}
5959

60-
var genericClassName = GenericName(className).WithTypeArgumentList(TypeArgumentList(symbol.ContainingType.TypeArguments));
60+
// Use the passed containingType for type arguments if provided,
61+
// otherwise fall back to the symbol's containing type.
62+
// This is critical for inherited members where the cached symbol's
63+
// type arguments may differ from the actual derived type being mapped.
64+
var typeArgs = containingType?.TypeArguments ?? symbol.ContainingType.TypeArguments;
65+
var genericClassName = GenericName(className).WithTypeArgumentList(TypeArgumentList(typeArgs));
6166
var invocation = InvocationExpression(MemberAccess(genericClassName, methodName))
6267
.WithArgumentList(ArgumentListWithoutIndention([baseAccess]));
6368

src/Riok.Mapperly/Descriptors/UnsafeAccess/UnsafeSetPropertyAccessor.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,12 @@ public MethodDeclarationSyntax BuildAccessorMethod(SourceEmitterContext ctx)
5353
);
5454
}
5555

56-
public ExpressionSyntax BuildAssignment(ExpressionSyntax? baseAccess, ExpressionSyntax valueToAssign, bool coalesceAssignment = false)
56+
public ExpressionSyntax BuildAssignment(
57+
ExpressionSyntax? baseAccess,
58+
ExpressionSyntax valueToAssign,
59+
INamedTypeSymbol? containingType = null,
60+
bool coalesceAssignment = false
61+
)
5762
{
5863
if (baseAccess == null)
5964
throw new ArgumentNullException(nameof(baseAccess));
@@ -63,8 +68,13 @@ public ExpressionSyntax BuildAssignment(ExpressionSyntax? baseAccess, Expression
6368
return InvocationWithoutIndention(MemberAccess(baseAccess, methodName), valueToAssign);
6469
}
6570

71+
// Use the passed containingType for type arguments if provided,
72+
// otherwise fall back to the symbol's containing type.
73+
// This is critical for inherited members where the cached symbol's
74+
// type arguments may differ from the actual derived type being mapped.
75+
var typeArgs = containingType?.TypeArguments ?? symbol.ContainingType.TypeArguments;
6676
var args = new[] { baseAccess, valueToAssign };
67-
var genericClassName = GenericName(className).WithTypeArgumentList(TypeArgumentList(symbol.ContainingType.TypeArguments));
77+
var genericClassName = GenericName(className).WithTypeArgumentList(TypeArgumentList(typeArgs));
6878
return InvocationExpression(MemberAccess(genericClassName, methodName)).WithArgumentList(ArgumentListWithoutIndention(args));
6979
}
7080
}

src/Riok.Mapperly/Symbols/Members/ConstructorParameterMember.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,9 @@ public class ConstructorParameterMember(IParameterSymbol symbol, SymbolAccessor
3636
public IMemberSetter BuildSetter(UnsafeAccessorContext ctx) =>
3737
throw new InvalidOperationException($"Cannot create a setter for {nameof(ParameterSourceMember)}");
3838

39-
public ExpressionSyntax BuildAccess(ExpressionSyntax? baseAccess, bool nullConditional = false) => IdentifierName(Name);
39+
public ExpressionSyntax BuildAccess(
40+
ExpressionSyntax? baseAccess,
41+
INamedTypeSymbol? containingType = null,
42+
bool nullConditional = false
43+
) => IdentifierName(Name);
4044
}

src/Riok.Mapperly/Symbols/Members/FieldMember.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,18 @@ public IMemberSetter BuildSetter(UnsafeAccessorContext ctx)
5454
return ctx.GetOrBuildFieldGetter(this);
5555
}
5656

57-
public ExpressionSyntax BuildAssignment(ExpressionSyntax? baseAccess, ExpressionSyntax valueToAssign, bool coalesceAssignment = false)
57+
public ExpressionSyntax BuildAssignment(
58+
ExpressionSyntax? baseAccess,
59+
ExpressionSyntax valueToAssign,
60+
INamedTypeSymbol? containingType = null,
61+
bool coalesceAssignment = false
62+
)
5863
{
5964
var targetMemberRef = BuildAccess(baseAccess);
6065
return Assignment(targetMemberRef, valueToAssign, coalesceAssignment);
6166
}
6267

63-
public ExpressionSyntax BuildAccess(ExpressionSyntax? baseAccess, bool nullConditional = false)
68+
public ExpressionSyntax BuildAccess(ExpressionSyntax? baseAccess, INamedTypeSymbol? containingType = null, bool nullConditional = false)
6469
{
6570
if (baseAccess == null)
6671
return SyntaxFactory.IdentifierName(Name);
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
using Microsoft.CodeAnalysis;
12
using Microsoft.CodeAnalysis.CSharp.Syntax;
23

34
namespace Riok.Mapperly.Symbols.Members;
45

56
public interface IMemberGetter
67
{
7-
ExpressionSyntax BuildAccess(ExpressionSyntax? baseAccess, bool nullConditional = false);
8+
ExpressionSyntax BuildAccess(ExpressionSyntax? baseAccess, INamedTypeSymbol? containingType = null, bool nullConditional = false);
89
}
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using Microsoft.CodeAnalysis;
12
using Microsoft.CodeAnalysis.CSharp.Syntax;
23

34
namespace Riok.Mapperly.Symbols.Members;
@@ -6,5 +7,10 @@ public interface IMemberSetter
67
{
78
bool SupportsCoalesceAssignment { get; }
89

9-
ExpressionSyntax BuildAssignment(ExpressionSyntax? baseAccess, ExpressionSyntax valueToAssign, bool coalesceAssignment = false);
10+
ExpressionSyntax BuildAssignment(
11+
ExpressionSyntax? baseAccess,
12+
ExpressionSyntax valueToAssign,
13+
INamedTypeSymbol? containingType = null,
14+
bool coalesceAssignment = false
15+
);
1016
}

src/Riok.Mapperly/Symbols/Members/MemberPathGetter.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public static MemberPathGetter Build(SimpleMappingBuilderContext ctx, MemberPath
5656
{
5757
return path.AggregateWithPrevious(
5858
baseAccess,
59-
(expr, prevProp, prop) => prop.Getter.BuildAccess(expr, prevProp.Member?.IsNullable == true)
59+
(expr, prevProp, prop) => prop.Getter.BuildAccess(expr, prop.Member.ContainingType, prevProp.Member?.IsNullable == true)
6060
);
6161
}
6262

@@ -66,12 +66,12 @@ public static MemberPathGetter Build(SimpleMappingBuilderContext ctx, MemberPath
6666
baseAccess,
6767
(a, b) =>
6868
b.Member.Type.IsNullableValueType()
69-
? MemberAccess(b.Getter.BuildAccess(a), NullableValueProperty)
70-
: b.Getter.BuildAccess(a)
69+
? MemberAccess(b.Getter.BuildAccess(a, b.Member.ContainingType), NullableValueProperty)
70+
: b.Getter.BuildAccess(a, b.Member.ContainingType)
7171
);
7272
}
7373

74-
return path.Aggregate(baseAccess, (a, b) => b.Getter.BuildAccess(a));
74+
return path.Aggregate(baseAccess, (a, b) => b.Getter.BuildAccess(a, b.Member.ContainingType));
7575
}
7676

7777
/// <summary>
@@ -99,7 +99,7 @@ private BinaryExpressionSyntax BuildNonNullCondition(ExpressionSyntax baseAccess
9999
var conditions = new List<BinaryExpressionSyntax>();
100100
foreach (var pathPart in nullablePath)
101101
{
102-
access = pathPart.Getter.BuildAccess(access);
102+
access = pathPart.Getter.BuildAccess(access, pathPart.Member.ContainingType);
103103

104104
if (!pathPart.Member.IsNullable)
105105
continue;

src/Riok.Mapperly/Symbols/Members/MemberPathSetter.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,19 @@ public class MemberPathSetter
1313
private readonly NonEmptyMemberPath _memberPath;
1414
private readonly MemberPathGetter _baseAccessGetter;
1515
private readonly IMemberSetter _memberSetter;
16+
private readonly IMappableMember _member;
1617

17-
private MemberPathSetter(NonEmptyMemberPath memberPath, MemberPathGetter baseAccessGetter, IMemberSetter memberSetter)
18+
private MemberPathSetter(
19+
NonEmptyMemberPath memberPath,
20+
MemberPathGetter baseAccessGetter,
21+
IMemberSetter memberSetter,
22+
IMappableMember member
23+
)
1824
{
1925
_memberPath = memberPath;
2026
_baseAccessGetter = baseAccessGetter;
2127
_memberSetter = memberSetter;
28+
_member = member;
2229
}
2330

2431
public bool SupportsCoalesceAssignment => _memberSetter.SupportsCoalesceAssignment;
@@ -30,13 +37,13 @@ public static MemberPathSetter Build(SimpleMappingBuilderContext ctx, NonEmptyMe
3037
var objectPath = MemberPath.Create(path.RootType, path.ObjectPath.ToList());
3138
var objectGetter = objectPath.BuildGetter(ctx);
3239
var memberSetter = path.Member.BuildSetter(ctx.UnsafeAccessorContext);
33-
return new MemberPathSetter(path, objectGetter, memberSetter);
40+
return new MemberPathSetter(path, objectGetter, memberSetter, path.Member);
3441
}
3542

3643
public ExpressionSyntax BuildAssignment(ExpressionSyntax? baseAccess, ExpressionSyntax valueToAssign, bool coalesceAssignment = false)
3744
{
3845
baseAccess = _baseAccessGetter.BuildAccess(baseAccess);
39-
return _memberSetter.BuildAssignment(baseAccess, valueToAssign, coalesceAssignment);
46+
return _memberSetter.BuildAssignment(baseAccess, valueToAssign, _member.ContainingType, coalesceAssignment);
4047
}
4148

4249
public override bool Equals(object? obj)

0 commit comments

Comments
 (0)