Skip to content

Commit a26121b

Browse files
authored
Propagate DynamicallyAccessedMembers annotations to auto-properties only (#119329) (#119407)
In the trimmer and ILC, we searched for backing fields with heuristics that validated a backing field had a `CompilerGeneratedAttribute`. In C# 14, the `field` keyword / semi-auto property feature means that some properties will have a compiler-generated field, but a user-written accessor. This can cause holes and unexpected behavior with the current heuristics. The semi-auto property accessors do not have `CompilerGeneratedAttribute`, so we can tighten the heuristic to only find backing fields when all accessors _and_ the found backing field have `CompilerGeneratedAttribute`. This should apply backing field propagation heuristics to properties with at least one auto accessor (e.g. `int Prop { get => field; set; }`) only (and also warns when it is unable to find the backing field for these). For all other properties, it is possible to annotate the backing field and accessors to achieve the same behavior. Additional context: #117072 (comment) and #117072 (comment)
1 parent 9ca7546 commit a26121b

File tree

15 files changed

+644
-95
lines changed

15 files changed

+644
-95
lines changed

src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/Dataflow/FlowAnnotations.cs

Lines changed: 71 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5+
using System.Diagnostics;
56
using System.Collections.Generic;
67
using System.Diagnostics.CodeAnalysis;
78
using System.Reflection.Metadata;
@@ -523,7 +524,7 @@ protected override TypeAnnotations CreateValueFromKey(TypeDesc key)
523524
// Look for the compiler generated backing field. If it doesn't work out simply move on. In such case we would still
524525
// propagate the annotation to the setter/getter and later on when analyzing the setter/getter we will warn
525526
// that the field (which ever it is) must be annotated as well.
526-
ScanMethodBodyForFieldAccess(methodBody, write: true, out backingFieldFromSetter);
527+
backingFieldFromSetter = GetAutoPropertyCompilerGeneratedField(methodBody, isWriteAccessor: true);
527528
}
528529

529530
MethodAnnotations? setterAnnotation = null;
@@ -560,16 +561,14 @@ protected override TypeAnnotations CreateValueFromKey(TypeDesc key)
560561
MethodDesc getMethod = property.GetMethod;
561562
if (getMethod != null)
562563
{
563-
564-
// Abstract property backing field propagation doesn't make sense, and any derived property will be validated
565-
// to have the exact same annotations on getter/setter, and thus if it has a detectable backing field that will be validated as well.
564+
// Look only at compiler-generated accessors
566565
MethodIL methodBody = _ilProvider.GetMethodIL(getMethod);
567566
if (methodBody != null)
568567
{
569568
// Look for the compiler generated backing field. If it doesn't work out simply move on. In such case we would still
570569
// propagate the annotation to the setter/getter and later on when analyzing the setter/getter we will warn
571570
// that the field (which ever it is) must be annotated as well.
572-
ScanMethodBodyForFieldAccess(methodBody, write: false, out backingFieldFromGetter);
571+
backingFieldFromGetter = GetAutoPropertyCompilerGeneratedField(methodBody, isWriteAccessor: false);
573572
}
574573

575574
MethodAnnotations? getterAnnotation = null;
@@ -593,27 +592,41 @@ protected override TypeAnnotations CreateValueFromKey(TypeDesc key)
593592
}
594593
}
595594

596-
FieldDesc? backingField;
597-
if (backingFieldFromGetter != null && backingFieldFromSetter != null &&
598-
backingFieldFromGetter != backingFieldFromSetter)
599-
{
600-
_logger.LogWarning(property, DiagnosticId.DynamicallyAccessedMembersCouldNotFindBackingField, property.GetDisplayName());
601-
backingField = null;
602-
}
603-
else
604-
{
605-
backingField = backingFieldFromGetter ?? backingFieldFromSetter;
606-
}
607-
608-
if (backingField != null)
595+
if (IsAutoProperty(property))
609596
{
610-
if (annotatedFields.Any(a => a.Field == backingField))
597+
FieldDesc? backingField = null;
598+
if ((property.SetMethod is not null
599+
&& property.SetMethod.HasCustomAttribute("System.Runtime.CompilerServices", "CompilerGeneratedAttribute")
600+
&& backingFieldFromSetter is null)
601+
|| (property.GetMethod is not null
602+
&& property.GetMethod.HasCustomAttribute("System.Runtime.CompilerServices", "CompilerGeneratedAttribute")
603+
&& backingFieldFromGetter is null))
611604
{
612-
_logger.LogWarning(backingField, DiagnosticId.DynamicallyAccessedMembersOnPropertyConflictsWithBackingField, property.GetDisplayName(), backingField.GetDisplayName());
605+
// We failed to find the backing field of an auto-property accessor
606+
_logger.LogWarning(property, DiagnosticId.DynamicallyAccessedMembersCouldNotFindBackingField, property.GetDisplayName());
607+
}
608+
else if (backingFieldFromGetter is not null && backingFieldFromSetter is not null
609+
&& backingFieldFromSetter != backingFieldFromGetter)
610+
{
611+
// We found two different backing fields for the getter and the setter
612+
_logger.LogWarning(property, DiagnosticId.DynamicallyAccessedMembersCouldNotFindBackingField, property.GetDisplayName());
613613
}
614614
else
615615
{
616-
annotatedFields.Add(new FieldAnnotation(backingField, annotation));
616+
// We either have a single auto-property accessor or both accessors point to the same backing field
617+
backingField = backingFieldFromSetter ?? backingFieldFromGetter;
618+
}
619+
620+
if (backingField != null)
621+
{
622+
if (annotatedFields.Any(a => a.Field == backingField))
623+
{
624+
_logger.LogWarning(backingField, DiagnosticId.DynamicallyAccessedMembersOnPropertyConflictsWithBackingField, property.GetDisplayName(), backingField.GetDisplayName());
625+
}
626+
else
627+
{
628+
annotatedFields.Add(new FieldAnnotation(backingField, annotation));
629+
}
617630
}
618631
}
619632
}
@@ -651,59 +664,69 @@ protected override TypeAnnotations CreateValueFromKey(TypeDesc key)
651664
return attrs;
652665
}
653666

654-
private static bool ScanMethodBodyForFieldAccess(MethodIL body, bool write, out FieldDesc? found)
667+
/// <summary>
668+
/// Returns true if the property has a single accessor which is compiler generated,
669+
/// indicating that it is an auto-property.
670+
/// </summary>
671+
/// <remarks>
672+
/// Ideally this would be tightened to only return true if both accessors are auto-property accessors,
673+
/// but it allows for either for back compatibility with existing behavior.
674+
/// </remarks>
675+
private static bool IsAutoProperty(PropertyPseudoDesc property)
676+
{
677+
return property.SetMethod?.HasCustomAttribute("System.Runtime.CompilerServices", "CompilerGeneratedAttribute") == true
678+
|| property.GetMethod?.HasCustomAttribute("System.Runtime.CompilerServices", "CompilerGeneratedAttribute") == true;
679+
}
680+
681+
private static FieldDesc? GetAutoPropertyCompilerGeneratedField(MethodIL body, bool isWriteAccessor)
655682
{
656683
// Tries to find the backing field for a property getter/setter.
657684
// Returns true if this is a method body that we can unambiguously analyze.
658685
// The found field could still be null if there's no backing store.
659-
found = null;
660686

687+
// Only analyze compiler-generated accessors
688+
if (!body.OwningMethod.HasCustomAttribute("System.Runtime.CompilerServices", "CompilerGeneratedAttribute"))
689+
{
690+
return null;
691+
}
692+
693+
FieldDesc? found = null;
661694
ILReader ilReader = new ILReader(body.GetILBytes());
662695

663696
while (ilReader.HasNext)
664697
{
665698
ILOpcode opcode = ilReader.ReadILOpcode();
666699
switch (opcode)
667700
{
668-
case ILOpcode.ldsfld when !write:
669-
case ILOpcode.ldfld when !write:
670-
case ILOpcode.stsfld when write:
671-
case ILOpcode.stfld when write:
701+
case ILOpcode.ldsfld when !isWriteAccessor:
702+
case ILOpcode.ldfld when !isWriteAccessor:
703+
case ILOpcode.stsfld when isWriteAccessor:
704+
case ILOpcode.stfld when isWriteAccessor:
705+
{
706+
// Multiple field accesses - ambiguous, fail
707+
if (found != null)
672708
{
673-
// This writes/reads multiple fields - can't guess which one is the backing store.
674-
// Return failure.
675-
if (found != null)
676-
{
677-
found = null;
678-
return false;
679-
}
680-
found = (FieldDesc)body.GetObject(ilReader.ReadILToken());
709+
return null;
681710
}
711+
found = (FieldDesc)body.GetObject(ilReader.ReadILToken());
682712
break;
713+
}
683714
default:
684715
ilReader.Skip(opcode);
685716
break;
686717
}
687718
}
688719

689-
if (found == null)
690-
{
691-
// Doesn't access any fields. Could be e.g. "Type Foo => typeof(Bar);"
692-
// Return success.
693-
return true;
694-
}
695-
696-
if (found.OwningType != body.OwningMethod.OwningType ||
720+
if (found is null ||
721+
found.OwningType != body.OwningMethod.OwningType ||
697722
found.IsStatic != body.OwningMethod.Signature.IsStatic ||
698723
!found.HasCustomAttribute("System.Runtime.CompilerServices", "CompilerGeneratedAttribute"))
699724
{
700-
// A couple heuristics to make sure we got the right field.
701-
// Return failure.
702-
found = null;
703-
return false;
725+
// Heuristics failed - not a backing field
726+
return null;
704727
}
705728

706-
return true;
729+
return found;
707730
}
708731
}
709732

src/tools/illink/src/ILLink.RoslynAnalyzer/IPropertySymbolExtensions.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

44
using Microsoft.CodeAnalysis;
5+
using Microsoft.CodeAnalysis.CSharp.Syntax;
56

67
namespace ILLink.RoslynAnalyzer
78
{
@@ -30,5 +31,37 @@ public static class IPropertySymbolExtensions
3031
}
3132
return setMethod;
3233
}
34+
35+
public static bool IsAutoProperty(this IPropertySymbol property)
36+
{
37+
if (property.IsAbstract)
38+
return false;
39+
40+
return (property.GetMethod?.IsAutoAccessor() ?? false) || (property.SetMethod?.IsAutoAccessor() ?? false);
41+
}
42+
43+
private static bool IsAutoAccessor(this IMethodSymbol method)
44+
{
45+
if (method == null || method.IsAbstract)
46+
return false;
47+
48+
foreach (var decl in method.DeclaringSyntaxReferences)
49+
{
50+
var syntax = decl.GetSyntax();
51+
// Auto property accessors have no body in their syntax
52+
switch (syntax)
53+
{
54+
case AccessorDeclarationSyntax a:
55+
if (a.Body is not null)
56+
return false;
57+
if (a.ExpressionBody is not null)
58+
return false;
59+
return true;
60+
default:
61+
break;
62+
}
63+
}
64+
return false;
65+
}
3366
}
3467
}

src/tools/illink/src/ILLink.RoslynAnalyzer/TrimAnalysis/FlowAnnotations.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,13 @@ internal static DynamicallyAccessedMemberTypes GetFieldAnnotation(IFieldSymbol f
117117
if (!field.OriginalDefinition.Type.IsTypeInterestingForDataflow(isByRef: field.RefKind is not RefKind.None))
118118
return DynamicallyAccessedMemberTypes.None;
119119

120+
if (field.AssociatedSymbol is IPropertySymbol property)
121+
{
122+
// If this is an auto property, we get the property annotation
123+
if (property.IsAutoProperty())
124+
return property.GetDynamicallyAccessedMemberTypes();
125+
}
126+
120127
return field.GetDynamicallyAccessedMemberTypes();
121128
}
122129

0 commit comments

Comments
 (0)