Skip to content

Commit fb7860a

Browse files
authored
Lambda and local function suppressions (dotnet/linker#2689)
This adds support for RequiresUnreferencedCode and UnconditionalSuppressMessage on lambdas and local functions, relying on heuristics and knowledge of compiler implementation details to detect lambdas and local functions. This approach scans the code for IL references to lambdas and local functions, which has some limitations. - Unused local functions aren't referenced by the containing method, so warnings from these will not be suppressed by suppressions on the containing method. Lambdas don't seem to have this problem, because they contain a reference to the generated method as part of the delegate conversion. - The IL doesn't in general contain enough information to determine the nesting of the scopes of lambdas and local functions, so we make no attempt to do this. We only try to determine to which user method a lambda or local function belongs. So suppressions on a lambda or local function will not silence warnings from a nested lambda or local function in the same scope. This also adds warnings for reflection access to compiler-generated state machine members, and to lambdas or local functions. For these, the analyzer makes no attempt to determine what the actual IL corresponding to the user code will be, so it produces fewer warnings. The linker will warn for what is actually in IL. Commit migrated from dotnet/linker@cb11422
1 parent 6240ad3 commit fb7860a

File tree

9 files changed

+1033
-137
lines changed

9 files changed

+1033
-137
lines changed

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,13 @@ private static bool IsInRequiresScope (this ISymbol member, string requiresAttri
5757
if (member is ITypeSymbol)
5858
return false;
5959

60-
if (member.HasAttribute (requiresAttribute) && !member.IsStaticConstructor ())
61-
return true;
60+
while (true) {
61+
if (member.HasAttribute (requiresAttribute) && !member.IsStaticConstructor ())
62+
return true;
63+
if (member.ContainingSymbol is not IMethodSymbol method)
64+
break;
65+
member = method;
66+
}
6267

6368
if (member.ContainingType is ITypeSymbol containingType && containingType.HasAttribute (requiresAttribute))
6469
return true;

src/tools/illink/src/linker/Linker.Steps/MarkStep.cs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2901,13 +2901,15 @@ internal bool ShouldSuppressAnalysisWarningsForRequiresUnreferencedCode ()
29012901
if (originMember is not IMemberDefinition member)
29022902
return false;
29032903

2904-
MethodDefinition? userDefinedMethod = Context.CompilerGeneratedState.GetUserDefinedMethodForCompilerGeneratedMember (member);
2905-
if (userDefinedMethod == null)
2906-
return false;
2907-
2908-
Debug.Assert (userDefinedMethod != originMember);
2904+
MethodDefinition? owningMethod;
2905+
while (Context.CompilerGeneratedState.TryGetOwningMethodForCompilerGeneratedMember (member, out owningMethod)) {
2906+
Debug.Assert (owningMethod != member);
2907+
if (Annotations.IsMethodInRequiresUnreferencedCodeScope (owningMethod))
2908+
return true;
2909+
member = owningMethod;
2910+
}
29092911

2910-
return Annotations.IsMethodInRequiresUnreferencedCodeScope (userDefinedMethod);
2912+
return false;
29112913
}
29122914

29132915
internal void CheckAndReportRequiresUnreferencedCode (MethodDefinition method)

src/tools/illink/src/linker/Linker/Annotations.cs

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -593,18 +593,21 @@ public bool TryGetLinkerAttribute<T> (IMemberDefinition member, [NotNullWhen (re
593593
/// </summary>
594594
/// <remarks>Unlike <see cref="IsMethodInRequiresUnreferencedCodeScope(MethodDefinition)"/> only static methods
595595
/// and .ctors are reported as requiring unreferenced code when the declaring type has RUC on it.</remarks>
596-
internal bool DoesMethodRequireUnreferencedCode (MethodDefinition method, [NotNullWhen (returnValue: true)] out RequiresUnreferencedCodeAttribute? attribute)
596+
internal bool DoesMethodRequireUnreferencedCode (MethodDefinition originalMethod, [NotNullWhen (returnValue: true)] out RequiresUnreferencedCodeAttribute? attribute)
597597
{
598-
if (method.IsStaticConstructor ()) {
599-
attribute = null;
600-
return false;
601-
}
602-
if (TryGetLinkerAttribute (method, out attribute))
603-
return true;
598+
MethodDefinition? method = originalMethod;
599+
do {
600+
if (method.IsStaticConstructor ()) {
601+
attribute = null;
602+
return false;
603+
}
604+
if (TryGetLinkerAttribute (method, out attribute))
605+
return true;
604606

605-
if ((method.IsStatic || method.IsConstructor) && method.DeclaringType is not null &&
606-
TryGetLinkerAttribute (method.DeclaringType, out attribute))
607-
return true;
607+
if ((method.IsStatic || method.IsConstructor) && method.DeclaringType is not null &&
608+
TryGetLinkerAttribute (method.DeclaringType, out attribute))
609+
return true;
610+
} while (context.CompilerGeneratedState.TryGetOwningMethodForCompilerGeneratedMember (method, out method));
608611

609612
return false;
610613
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Generic;
5+
using Mono.Cecil;
6+
7+
namespace Mono.Linker
8+
{
9+
class CallGraph
10+
{
11+
readonly Dictionary<MethodDefinition, HashSet<MethodDefinition>> callGraph;
12+
13+
public CallGraph () => callGraph = new Dictionary<MethodDefinition, HashSet<MethodDefinition>> ();
14+
15+
public void TrackCall (MethodDefinition fromMethod, MethodDefinition toMethod)
16+
{
17+
if (!callGraph.TryGetValue (fromMethod, out HashSet<MethodDefinition>? toMethods)) {
18+
toMethods = new HashSet<MethodDefinition> ();
19+
callGraph.Add (fromMethod, toMethods);
20+
}
21+
toMethods.Add (toMethod);
22+
}
23+
24+
public IEnumerable<MethodDefinition> GetReachableMethods (MethodDefinition start)
25+
{
26+
Queue<MethodDefinition> queue = new ();
27+
HashSet<MethodDefinition> visited = new ();
28+
visited.Add (start);
29+
queue.Enqueue (start);
30+
while (queue.TryDequeue (out MethodDefinition? method)) {
31+
if (!callGraph.TryGetValue (method, out HashSet<MethodDefinition>? callees))
32+
continue;
33+
34+
foreach (var callee in callees) {
35+
if (visited.Add (callee)) {
36+
queue.Enqueue (callee);
37+
yield return callee;
38+
}
39+
}
40+
}
41+
}
42+
}
43+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Mono.Linker
5+
{
6+
class CompilerGeneratedNames
7+
{
8+
internal static bool IsGeneratedMemberName (string memberName)
9+
{
10+
return memberName.Length > 0 && memberName[0] == '<';
11+
}
12+
13+
internal static bool IsLambdaDisplayClass (string className)
14+
{
15+
if (!IsGeneratedMemberName (className))
16+
return false;
17+
18+
// This is true for static lambdas (which are emitted into a class like <>c)
19+
// and for instance lambdas (which are emitted into a class like <>c__DisplayClass1_0)
20+
return className.StartsWith ("<>c");
21+
}
22+
23+
internal static bool IsLambdaOrLocalFunction (string methodName) => IsLambdaMethod (methodName) || IsLocalFunction (methodName);
24+
25+
// Lambda methods have generated names like "<UserMethod>b__0_1" where "UserMethod" is the name
26+
// of the original user code that contains the lambda method declaration.
27+
internal static bool IsLambdaMethod (string methodName)
28+
{
29+
if (!IsGeneratedMemberName (methodName))
30+
return false;
31+
32+
int i = methodName.IndexOf ('>', 1);
33+
if (i == -1)
34+
return false;
35+
36+
// Ignore the method ordinal/generation and lambda ordinal/generation.
37+
return methodName[i + 1] == 'b';
38+
}
39+
40+
// Local functions have generated names like "<UserMethod>g__LocalFunction|0_1" where "UserMethod" is the name
41+
// of the original user code that contains the lambda method declaration, and "LocalFunction" is the name of
42+
// the local function.
43+
internal static bool IsLocalFunction (string methodName)
44+
{
45+
if (!IsGeneratedMemberName (methodName))
46+
return false;
47+
48+
int i = methodName.IndexOf ('>', 1);
49+
if (i == -1)
50+
return false;
51+
52+
// Ignore the method ordinal/generation and local function ordinal/generation.
53+
return methodName[i + 1] == 'g';
54+
}
55+
}
56+
}
Lines changed: 131 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
// Licensed to the .NET Foundation under one or more agreements.
1+
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Collections.Generic;
5+
using System.Diagnostics;
6+
using System.Diagnostics.CodeAnalysis;
57
using ILLink.Shared;
68
using Mono.Cecil;
9+
using Mono.Cecil.Cil;
710

811
namespace Mono.Linker
912
{
@@ -12,27 +15,68 @@ public class CompilerGeneratedState
1215
{
1316
readonly LinkContext _context;
1417
readonly Dictionary<TypeDefinition, MethodDefinition> _compilerGeneratedTypeToUserCodeMethod;
18+
readonly Dictionary<MethodDefinition, MethodDefinition> _compilerGeneratedMethodToUserCodeMethod;
1519
readonly HashSet<TypeDefinition> _typesWithPopulatedCache;
1620

1721
public CompilerGeneratedState (LinkContext context)
1822
{
1923
_context = context;
2024
_compilerGeneratedTypeToUserCodeMethod = new Dictionary<TypeDefinition, MethodDefinition> ();
25+
_compilerGeneratedMethodToUserCodeMethod = new Dictionary<MethodDefinition, MethodDefinition> ();
2126
_typesWithPopulatedCache = new HashSet<TypeDefinition> ();
2227
}
2328

24-
static bool HasRoslynCompilerGeneratedName (TypeDefinition type) =>
25-
type.Name.Contains ('<') || (type.DeclaringType != null && HasRoslynCompilerGeneratedName (type.DeclaringType));
29+
static IEnumerable<TypeDefinition> GetCompilerGeneratedNestedTypes (TypeDefinition type)
30+
{
31+
foreach (var nestedType in type.NestedTypes) {
32+
if (!CompilerGeneratedNames.IsGeneratedMemberName (nestedType.Name))
33+
continue;
34+
35+
yield return nestedType;
36+
37+
foreach (var recursiveNestedType in GetCompilerGeneratedNestedTypes (nestedType))
38+
yield return recursiveNestedType;
39+
}
40+
}
2641

2742
void PopulateCacheForType (TypeDefinition type)
2843
{
2944
// Avoid repeat scans of the same type
3045
if (!_typesWithPopulatedCache.Add (type))
3146
return;
3247

33-
foreach (MethodDefinition method in type.Methods) {
48+
var callGraph = new CallGraph ();
49+
var callingMethods = new HashSet<MethodDefinition> ();
50+
51+
void ProcessMethod (MethodDefinition method)
52+
{
53+
if (!CompilerGeneratedNames.IsLambdaOrLocalFunction (method.Name)) {
54+
// If it's not a nested function, track as an entry point to the call graph.
55+
var added = callingMethods.Add (method);
56+
Debug.Assert (added);
57+
}
58+
59+
// Discover calls or references to lambdas or local functions. This includes
60+
// calls to local functions, and lambda assignments (which use ldftn).
61+
if (method.Body != null) {
62+
foreach (var instruction in method.Body.Instructions) {
63+
if (instruction.OpCode.OperandType != OperandType.InlineMethod)
64+
continue;
65+
66+
MethodDefinition? lambdaOrLocalFunction = _context.TryResolve ((MethodReference) instruction.Operand);
67+
if (lambdaOrLocalFunction == null)
68+
continue;
69+
70+
if (!CompilerGeneratedNames.IsLambdaOrLocalFunction (lambdaOrLocalFunction.Name))
71+
continue;
72+
73+
callGraph.TrackCall (method, lambdaOrLocalFunction);
74+
}
75+
}
76+
77+
// Discover state machine methods.
3478
if (!method.HasCustomAttributes)
35-
continue;
79+
return;
3680

3781
foreach (var attribute in method.CustomAttributes) {
3882
if (attribute.AttributeType.Namespace != "System.Runtime.CompilerServices")
@@ -43,17 +87,53 @@ void PopulateCacheForType (TypeDefinition type)
4387
case "AsyncStateMachineAttribute":
4488
case "IteratorStateMachineAttribute":
4589
TypeDefinition? stateMachineType = GetFirstConstructorArgumentAsType (attribute);
46-
if (stateMachineType != null) {
47-
if (!_compilerGeneratedTypeToUserCodeMethod.TryAdd (stateMachineType, method)) {
48-
var alreadyAssociatedMethod = _compilerGeneratedTypeToUserCodeMethod[stateMachineType];
49-
_context.LogWarning (new MessageOrigin (method), DiagnosticId.MethodsAreAssociatedWithStateMachine, method.GetDisplayName (), alreadyAssociatedMethod.GetDisplayName (), stateMachineType.GetDisplayName ());
50-
}
90+
if (stateMachineType == null)
91+
break;
92+
Debug.Assert (stateMachineType.DeclaringType == type ||
93+
(CompilerGeneratedNames.IsGeneratedMemberName (stateMachineType.DeclaringType.Name) &&
94+
stateMachineType.DeclaringType.DeclaringType == type));
95+
if (!_compilerGeneratedTypeToUserCodeMethod.TryAdd (stateMachineType, method)) {
96+
var alreadyAssociatedMethod = _compilerGeneratedTypeToUserCodeMethod[stateMachineType];
97+
_context.LogWarning (new MessageOrigin (method), DiagnosticId.MethodsAreAssociatedWithStateMachine, method.GetDisplayName (), alreadyAssociatedMethod.GetDisplayName (), stateMachineType.GetDisplayName ());
5198
}
5299

53100
break;
54101
}
55102
}
56103
}
104+
105+
// Look for state machine methods, and methods which call local functions.
106+
foreach (MethodDefinition method in type.Methods)
107+
ProcessMethod (method);
108+
109+
// Also scan compiler-generated state machine methods (in case they have calls to nested functions),
110+
// and nested functions inside compiler-generated closures (in case they call other nested functions).
111+
112+
// State machines can be emitted into lambda display classes, so we need to go down at least two
113+
// levels to find calls from iterator nested functions to other nested functions. We just recurse into
114+
// all compiler-generated nested types to avoid depending on implementation details.
115+
116+
foreach (var nestedType in GetCompilerGeneratedNestedTypes (type)) {
117+
foreach (var method in nestedType.Methods)
118+
ProcessMethod (method);
119+
}
120+
121+
// Now we've discovered the call graphs for calls to nested functions.
122+
// Use this to map back from nested functions to the declaring user methods.
123+
124+
// Note: This maps all nested functions back to the user code, not to the immediately
125+
// declaring local function. The IL doesn't contain enough information in general for
126+
// us to determine the nesting of local functions and lambdas.
127+
128+
// Note: this only discovers nested functions which are referenced from the user
129+
// code or its referenced nested functions. There is no reliable way to determine from
130+
// IL which user code an unused nested function belongs to.
131+
foreach (var userDefinedMethod in callingMethods) {
132+
foreach (var nestedFunction in callGraph.GetReachableMethods (userDefinedMethod)) {
133+
Debug.Assert (CompilerGeneratedNames.IsLambdaOrLocalFunction (nestedFunction.Name));
134+
_compilerGeneratedMethodToUserCodeMethod.Add (nestedFunction, userDefinedMethod);
135+
}
136+
}
57137
}
58138

59139
static TypeDefinition? GetFirstConstructorArgumentAsType (CustomAttribute attribute)
@@ -64,27 +144,54 @@ void PopulateCacheForType (TypeDefinition type)
64144
return attribute.ConstructorArguments[0].Value as TypeDefinition;
65145
}
66146

67-
public MethodDefinition? GetUserDefinedMethodForCompilerGeneratedMember (IMemberDefinition sourceMember)
147+
// For state machine types/members, maps back to the state machine method.
148+
// For local functions and lambdas, maps back to the owning method in user code (not the declaring
149+
// lambda or local function, because the IL doesn't contain enough information to figure this out).
150+
public bool TryGetOwningMethodForCompilerGeneratedMember (IMemberDefinition sourceMember, [NotNullWhen (true)] out MethodDefinition? owningMethod)
68151
{
152+
owningMethod = null;
69153
if (sourceMember == null)
70-
return null;
154+
return false;
71155

72-
TypeDefinition compilerGeneratedType = (sourceMember as TypeDefinition) ?? sourceMember.DeclaringType;
73-
if (_compilerGeneratedTypeToUserCodeMethod.TryGetValue (compilerGeneratedType, out MethodDefinition? userDefinedMethod))
74-
return userDefinedMethod;
156+
MethodDefinition? compilerGeneratedMethod = sourceMember as MethodDefinition;
157+
if (compilerGeneratedMethod != null) {
158+
if (_compilerGeneratedMethodToUserCodeMethod.TryGetValue (compilerGeneratedMethod, out owningMethod))
159+
return true;
160+
}
75161

76-
// Only handle async or iterator state machine
77-
// So go to the declaring type and check if it's compiler generated (as a perf optimization)
78-
if (!HasRoslynCompilerGeneratedName (compilerGeneratedType) || compilerGeneratedType.DeclaringType == null)
79-
return null;
162+
TypeDefinition sourceType = (sourceMember as TypeDefinition) ?? sourceMember.DeclaringType;
163+
164+
if (_compilerGeneratedTypeToUserCodeMethod.TryGetValue (sourceType, out owningMethod))
165+
return true;
166+
167+
if (!CompilerGeneratedNames.IsGeneratedMemberName (sourceMember.Name) && !CompilerGeneratedNames.IsGeneratedMemberName (sourceType.Name))
168+
return false;
169+
170+
// sourceType is a state machine type, or the type containing a lambda or local function.
171+
var typeToCache = sourceType;
80172

81-
// Now go to its declaring type and search all methods to find the one which points to the type as its
173+
// Look in the declaring type if this is a compiler-generated type (state machine or display class).
174+
// State machines can be emitted into display classes, so we may also need to go one more level up.
175+
// To avoid depending on implementation details, we go up until we see a non-compiler-generated type.
176+
// This is the counterpart to GetCompilerGeneratedNestedTypes.
177+
while (typeToCache != null && CompilerGeneratedNames.IsGeneratedMemberName (typeToCache.Name))
178+
typeToCache = typeToCache.DeclaringType;
179+
180+
if (typeToCache == null)
181+
return false;
182+
183+
// Search all methods to find the one which points to the type as its
82184
// state machine implementation.
83-
PopulateCacheForType (compilerGeneratedType.DeclaringType);
84-
if (_compilerGeneratedTypeToUserCodeMethod.TryGetValue (compilerGeneratedType, out userDefinedMethod))
85-
return userDefinedMethod;
185+
PopulateCacheForType (typeToCache);
186+
if (compilerGeneratedMethod != null) {
187+
if (_compilerGeneratedMethodToUserCodeMethod.TryGetValue (compilerGeneratedMethod, out owningMethod))
188+
return true;
189+
}
190+
191+
if (_compilerGeneratedTypeToUserCodeMethod.TryGetValue (sourceType, out owningMethod))
192+
return true;
86193

87-
return null;
194+
return false;
88195
}
89196
}
90197
}

0 commit comments

Comments
 (0)