Skip to content

Commit de011df

Browse files
authored
Reference live ILLink analyzer (#93259)
In faf883d I inadvertently turned off the trim analyzer for libraries, because the analyzer bits don't flow to the consuming project with the ILLink.Tasks ProjectReference. This adds a separate ProjectReference to use the live analyzer. This addresses #88901 for the ILLink analyzers. Using the live analyzer uncovered some bugs that are fixed in this change: - Invalid assert for field initializers with `throw` statements - Invalid cast to `IMethodSymbol` for the ContainingSymbol of a local. Locals can occur in field initializers, when created from out params. - Wrong warning code produced for assignment to annotated property in an initializer. This was being treated as a call to the setter instead of an assignment to the underlying field: `Type AnnotatedPropertyWithSetter { get; set; } = GetUnknown ();` - Invalid assert for delegate creation where the delegate is created from anything other than a method (for example, a field that has an Action or Func type). - Assert inside GetFlowCaptureValue for the LHS of a compound assignment. I couldn't think of a case yet where we actually need to model the result of the assignment, so I implemented support for at least visiting the LHS by going through the same path as normal assignments (avoiding GetFlowCaptureValue).
1 parent da024f1 commit de011df

8 files changed

+215
-17
lines changed

eng/liveILLink.targets

+7
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@
3939
<SetTargetFramework Condition="'$(MSBuildRuntimeType)' == 'Core'">TargetFramework=$(NetCoreAppToolCurrent)</SetTargetFramework>
4040
<SetTargetFramework Condition="'$(MSBuildRuntimeType)' != 'Core'">TargetFramework=$(NetFrameworkToolCurrent)</SetTargetFramework>
4141
</ProjectReference>
42+
43+
<!-- Need to reference the analyzer project separately, because there's no easy way to get it as a transitive reference of ILLink.Tasks.csproj -->
44+
<ProjectReference Include="$(_ILLinkTasksSourceDir)..\ILLink.RoslynAnalyzer\ILLink.RoslynAnalyzer.csproj"
45+
ReferenceOutputAssembly="false"
46+
Private="false"
47+
OutputItemType="Analyzer"
48+
SetConfiguration="Configuaration=$(ToolsConfiguration)" />
4249
</ItemGroup>
4350

4451
</Project>

src/tools/illink/src/ILLink.RoslynAnalyzer/DataFlow/LocalDataFlowVisitor.cs

+62-15
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,19 @@ public void Transfer (BlockProxy block, LocalDataFlowState<TValue, TValueLattice
7676

7777
// If not, the BranchValue represents a return or throw value associated with the FallThroughSuccessor of this block.
7878
// (ConditionalSuccessor == null iff ConditionKind == None).
79-
// If we get here, we should be analyzing a method body, not an attribute instance since attributes can't have throws or return statements
80-
// Field/property initializers also can't have throws or return statements.
81-
Debug.Assert (OwningSymbol is IMethodSymbol);
79+
// If we get here, we should be analyzing code in a method or field/property initializer,
80+
// not an attribute instance, since attributes can't have throws or return statements
81+
Debug.Assert (OwningSymbol is IMethodSymbol or IFieldSymbol or IPropertySymbol,
82+
$"{OwningSymbol.GetType ()}: {branchValueOperation.Syntax.GetLocation ().GetLineSpan ()}");
8283

8384
// The BranchValue for a thrown value is not involved in dataflow tracking.
8485
if (block.Block.FallThroughSuccessor?.Semantics == ControlFlowBranchSemantics.Throw)
8586
return;
8687

88+
// Field/property initializers can't have return statements.
89+
Debug.Assert (OwningSymbol is IMethodSymbol,
90+
$"{OwningSymbol.GetType ()}: {branchValueOperation.Syntax.GetLocation ().GetLineSpan ()}");
91+
8792
// Return statements with return values are represented in the control flow graph as
8893
// a branch value operation that computes the return value.
8994

@@ -125,9 +130,9 @@ bool IsReferenceToCapturedVariable (ILocalReferenceOperation localReference)
125130

126131
if (local.IsConst)
127132
return false;
128-
129-
var declaringSymbol = (IMethodSymbol) local.ContainingSymbol;
130-
return !ReferenceEquals (declaringSymbol, OwningSymbol);
133+
Debug.Assert (local.ContainingSymbol is IMethodSymbol or IFieldSymbol, // backing field for property initializers
134+
$"{local.ContainingSymbol.GetType ()}: {localReference.Syntax.GetLocation ().GetLineSpan ()}");
135+
return !ReferenceEquals (local.ContainingSymbol, OwningSymbol);
131136
}
132137

133138
TValue GetLocal (ILocalReferenceOperation operation, LocalDataFlowState<TValue, TValueLattice> state)
@@ -159,7 +164,7 @@ void SetLocal (ILocalReferenceOperation operation, TValue value, LocalDataFlowSt
159164
state.Set (local, newValue);
160165
}
161166

162-
TValue ProcessSingleTargetAssignment (IOperation targetOperation, ISimpleAssignmentOperation operation, LocalDataFlowState<TValue, TValueLattice> state, bool merge)
167+
TValue ProcessSingleTargetAssignment (IOperation targetOperation, IAssignmentOperation operation, LocalDataFlowState<TValue, TValueLattice> state, bool merge)
163168
{
164169
switch (targetOperation) {
165170
case IFieldReferenceOperation:
@@ -187,9 +192,14 @@ TValue ProcessSingleTargetAssignment (IOperation targetOperation, ISimpleAssignm
187192
// This can happen in a constructor - there it is possible to assign to a property
188193
// without a setter. This turns into an assignment to the compiler-generated backing field.
189194
// To match the linker, this should warn about the compiler-generated backing field.
190-
// For now, just don't warn. https://github.com/dotnet/linker/issues/2731
195+
// For now, just don't warn. https://github.com/dotnet/runtime/issues/93277
191196
break;
192197
}
198+
// Even if the property has a set method, if the assignment takes place in a property initializer,
199+
// the write becomes a direct write to the underlying field. This should be treated the same as
200+
// the case where there is no set method.
201+
if (OwningSymbol is IPropertySymbol && (ControlFlowGraph.OriginalOperation is not IAttributeOperation))
202+
break;
193203

194204
// Property may be an indexer, in which case there will be one or more index arguments followed by a value argument
195205
ImmutableArray<TValue>.Builder arguments = ImmutableArray.CreateBuilder<TValue> ();
@@ -293,6 +303,20 @@ TValue ProcessSingleTargetAssignment (IOperation targetOperation, ISimpleAssignm
293303
}
294304

295305
public override TValue VisitSimpleAssignment (ISimpleAssignmentOperation operation, LocalDataFlowState<TValue, TValueLattice> state)
306+
{
307+
return ProcessAssignment (operation, state);
308+
}
309+
310+
public override TValue VisitCompoundAssignment (ICompoundAssignmentOperation operation, LocalDataFlowState<TValue, TValueLattice> state)
311+
{
312+
return ProcessAssignment (operation, state);
313+
}
314+
315+
// Note: this is called both for normal assignments and ICompoundAssignmentOperation.
316+
// The resulting value of a compound assignment isn't important for our dataflow analysis
317+
// (we don't model addition of integers, for example), so we just treat these the same
318+
// as normal assignments.
319+
TValue ProcessAssignment (IAssignmentOperation operation, LocalDataFlowState<TValue, TValueLattice> state)
296320
{
297321
var targetOperation = operation.Target;
298322
if (targetOperation is not IFlowCaptureReferenceOperation flowCaptureReference)
@@ -305,7 +329,7 @@ public override TValue VisitSimpleAssignment (ISimpleAssignmentOperation operati
305329
// for simplicity. This could be generalized if we encounter a dataflow behavior where this makes a difference.
306330

307331
Debug.Assert (IsLValueFlowCapture (flowCaptureReference.Id));
308-
Debug.Assert (!flowCaptureReference.GetValueUsageInfo (OwningSymbol).HasFlag (ValueUsageInfo.Read));
332+
Debug.Assert (flowCaptureReference.GetValueUsageInfo (OwningSymbol).HasFlag (ValueUsageInfo.Write));
309333
var capturedReferences = state.Current.CapturedReferences.Get (flowCaptureReference.Id);
310334
if (!capturedReferences.HasMultipleValues) {
311335
// Single captured reference. Treat this as an overwriting assignment.
@@ -360,8 +384,31 @@ public override TValue VisitFlowCaptureReference (IFlowCaptureReferenceOperation
360384
// Debug.Assert (IsLValueFlowCapture (operation.Id));
361385
Debug.Assert (operation.GetValueUsageInfo (OwningSymbol).HasFlag (ValueUsageInfo.Write),
362386
$"{operation.Syntax.GetLocation ().GetLineSpan ()}");
387+
Debug.Assert (operation.GetValueUsageInfo (OwningSymbol).HasFlag (ValueUsageInfo.Reference),
388+
$"{operation.Syntax.GetLocation ().GetLineSpan ()}");
363389
return TopValue;
364390
}
391+
392+
if (operation.GetValueUsageInfo (OwningSymbol).HasFlag (ValueUsageInfo.Write)) {
393+
// If we get here, it means we're visiting a flow capture reference that may be
394+
// assigned to. Similar to the IsInitialization case, this can happen for an out param
395+
// where the variable is declared before being passed as an out param, for example:
396+
397+
// string s;
398+
// Method (out s, b ? 0 : 1);
399+
400+
// The second argument is necessary to create multiple branches so that the compiler
401+
// turns both arguments into flow capture references, instead of just passing a local
402+
// reference for s.
403+
404+
// This can also happen for a deconstruction assignments, where the write is not to a byref.
405+
// Once the analyzer implements support for deconstruction assignments (https://github.com/dotnet/linker/issues/3158),
406+
// we can try enabling this assert to ensure that this case is only hit for byrefs.
407+
// Debug.Assert (operation.GetValueUsageInfo (OwningSymbol).HasFlag (ValueUsageInfo.Reference),
408+
// $"{operation.Syntax.GetLocation ().GetLineSpan ()}");
409+
return TopValue;
410+
}
411+
365412
return GetFlowCaptureValue (operation, state);
366413
}
367414

@@ -428,14 +475,14 @@ public override TValue VisitDelegateCreation (IDelegateCreationOperation operati
428475
return HandleDelegateCreation (lambda.Symbol, instance, operation);
429476
}
430477

431-
Debug.Assert (operation.Target is IMethodReferenceOperation);
432-
if (operation.Target is not IMethodReferenceOperation methodReference)
478+
Debug.Assert (operation.Target is IMemberReferenceOperation,
479+
$"{operation.Target.GetType ()}: {operation.Syntax.GetLocation ().GetLineSpan ()}");
480+
if (operation.Target is not IMemberReferenceOperation memberReference)
433481
return TopValue;
434482

435-
TValue instanceValue = Visit (methodReference.Instance, state);
436-
IMethodSymbol? method = methodReference.Method;
437-
Debug.Assert (method != null);
438-
if (method == null)
483+
TValue instanceValue = Visit (memberReference.Instance, state);
484+
485+
if (memberReference.Member is not IMethodSymbol method)
439486
return TopValue;
440487

441488
// Track references to local functions

src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/AnnotatedMembersAccessedViaReflection.cs

+34
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public static void Main ()
2828
AnnotatedGenerics.Test ();
2929
AnnotationOnGenerics.Test ();
3030
AnnotationOnInteropMethod.Test ();
31+
DelegateCreation.Test ();
3132
}
3233

3334
class AnnotatedField
@@ -874,6 +875,39 @@ public static void Test ()
874875
}
875876
}
876877

878+
class DelegateCreation
879+
{
880+
delegate void UnannotatedDelegate (Type type);
881+
882+
static Action<Type> field;
883+
884+
static Action<Type> Property { get; set; }
885+
886+
[ExpectedWarning ("IL2111", "LocalMethod")]
887+
[ExpectedWarning ("IL2111")]
888+
public static void Test ()
889+
{
890+
// Check that the analyzer is able to analyze delegate creation
891+
// with various targets, without hitting an assert.
892+
UnannotatedDelegate d;
893+
d = new UnannotatedDelegate (field);
894+
d(typeof(int));
895+
d = new UnannotatedDelegate (Property);
896+
d(typeof(int));
897+
898+
d = new UnannotatedDelegate (
899+
([DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] Type t) =>
900+
{ });
901+
d(typeof(int));
902+
d = new UnannotatedDelegate (LocalMethod);
903+
d(typeof(int));
904+
905+
void LocalMethod (
906+
[DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
907+
{ }
908+
}
909+
}
910+
877911
class TestType { }
878912

879913
[RequiresUnreferencedCode ("--RequiresUnreferencedCodeType--")]

src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/ConstructedTypesDataFlow.cs

+17
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,22 @@ static void DeconstructVariableNoAnnotation ((Type type, object instance) input)
2828
type.RequiresPublicMethods ();
2929
}
3030

31+
static (Type type, object instance) GetInput (int unused) => (typeof (string), null);
32+
33+
// https://github.com/dotnet/linker/issues/3158
34+
[ExpectedWarning ("IL2077", ProducedBy = Tool.Trimmer | Tool.NativeAot)]
35+
static void DeconstructVariableFlowCapture (bool b = true)
36+
{
37+
// This creates a control-flow graph where the tuple elements assigned to
38+
// are flow capture references. This is only the case when the variable types
39+
// are declared before the deconstruction assignment, and the assignment creates
40+
// a branch in the control-flow graph.
41+
Type type;
42+
object instance;
43+
(type, instance) = GetInput (b ? 0 : 1);
44+
type.RequiresPublicMethods ();
45+
}
46+
3147
record TypeAndInstance (
3248
[DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)]
3349
[property: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)]
@@ -97,6 +113,7 @@ static void DeconstructRecordManualWithMismatchAnnotation (TypeAndInstanceRecord
97113
public static void Test ()
98114
{
99115
DeconstructVariableNoAnnotation ((typeof (string), null));
116+
DeconstructVariableFlowCapture ();
100117
DeconstructRecordWithAnnotation (new (typeof (string), null));
101118
DeconstructClassWithAnnotation (new (typeof (string), null));
102119
DeconstructRecordManualWithAnnotation (new (typeof (string), null));

src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/ConstructorDataFlow.cs

+32
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,20 @@ public DataFlowInConstructor ()
3535
[ExpectedWarning ("IL2072", nameof (GetUnknown), nameof (RequireAll), CompilerGeneratedCode = true)]
3636
int Property { get; } = RequireAll (GetUnknown ());
3737

38+
[ExpectedWarning ("IL2074", nameof (GetUnknown), nameof (annotatedField), CompilerGeneratedCode = true)]
39+
[DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)]
40+
Type annotatedField = GetUnknown ();
41+
42+
[ExpectedWarning ("IL2074", nameof (GetUnknown), nameof (AnnotatedProperty), CompilerGeneratedCode = true,
43+
ProducedBy = Tool.Trimmer | Tool.NativeAot)] // https://github.com/dotnet/runtime/issues/93277
44+
[DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)]
45+
Type AnnotatedProperty { get; } = GetUnknown ();
46+
47+
[ExpectedWarning ("IL2074", nameof (GetUnknown), nameof (AnnotatedPropertyWithSetter), CompilerGeneratedCode = true,
48+
ProducedBy = Tool.Trimmer | Tool.NativeAot)] // https://github.com/dotnet/runtime/issues/93277
49+
[DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)]
50+
Type AnnotatedPropertyWithSetter { get; set; } = GetUnknown ();
51+
3852
// The analyzer dataflow visitor asserts that we only see a return value
3953
// inside of an IMethodSymbol. This testcase checks that we don't hit asserts
4054
// in case the return statement is in a lambda owned by a field.
@@ -63,6 +77,18 @@ public DataFlowInConstructor ()
6377

6478
static int Execute(Func<int> f) => f();
6579

80+
int fieldWithThrowStatementInInitializer = string.Empty.Length == 0 ? throw new Exception() : 0;
81+
82+
int PropertyWithThrowStatementInInitializer { get; } = string.Empty.Length == 0 ? throw new Exception() : 0;
83+
84+
[ExpectedWarning ("IL2067", nameof (TryGetUnknown), nameof (RequireAll), CompilerGeneratedCode = true,
85+
ProducedBy = Tool.Trimmer | Tool.NativeAot)] // https://github.com/dotnet/linker/issues/2158
86+
int fieldWithLocalReferenceInInitializer = TryGetUnknown (out var type) ? RequireAll (type) : 0;
87+
88+
[ExpectedWarning ("IL2067", nameof (TryGetUnknown), nameof (RequireAll), CompilerGeneratedCode = true,
89+
ProducedBy = Tool.Trimmer | Tool.NativeAot)] // https://github.com/dotnet/linker/issues/2158
90+
int PropertyWithLocalReferenceInInitializer { get; } = TryGetUnknown (out var type) ? RequireAll (type) : 0;
91+
6692
public static void Test ()
6793
{
6894
var instance = new DataFlowInConstructor ();
@@ -91,6 +117,12 @@ public static void Test ()
91117

92118
static Type GetUnknown () => null;
93119

120+
static bool TryGetUnknown (out Type type)
121+
{
122+
type = null;
123+
return true;
124+
}
125+
94126
static int RequireAll ([DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] Type type) => 0;
95127
}
96128
}

src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/MethodByRefParameterDataFlow.cs

+20
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public static void Main ()
3636
Type nullType4 = null;
3737
TestAssigningToRefParameter_Mismatch (nullType4, ref nullType4);
3838
TestPassingRefsWithImplicitThis ();
39+
TestPassingCapturedOutParameter ();
3940
LocalMethodsAndLambdas.Test ();
4041
}
4142

@@ -183,6 +184,25 @@ static void TestPassingRefsWithImplicitThis ()
183184
param3.RequiresPublicFields ();
184185
}
185186

187+
static bool TryGetAnnotatedValueWithExtraUnusedParameter (
188+
[DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] out Type typeWithMethods,
189+
int unused)
190+
{
191+
typeWithMethods = null;
192+
return false;
193+
}
194+
195+
static void TestPassingCapturedOutParameter (bool b = true)
196+
{
197+
Type typeWithMethods;
198+
// The ternary operator for the second argument causes _both_ arguments to
199+
// become flow-capture references. The ternary operator introduces two separate
200+
// branches, where a capture is created for typeWithMethods before the branch
201+
// out. This capture is then passed as the first argument.
202+
TryGetAnnotatedValueWithExtraUnusedParameter (out typeWithMethods, b ? 0 : 1);
203+
typeWithMethods.RequiresPublicMethods ();
204+
}
205+
186206
[return: DynamicallyAccessedMembersAttribute (DynamicallyAccessedMemberTypes.PublicFields)]
187207
static Type GetTypeWithFields () => null;
188208

src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/MethodByRefReturnDataFlow.cs

+29
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ public static void Main ()
1818
AssignDirectlyToAnnotatedTypeReference ();
1919
AssignToCapturedAnnotatedTypeReference ();
2020
AssignToAnnotatedTypeReferenceWithRequirements ();
21+
TestCompoundAssignment (typeof (int));
22+
TestCompoundAssignmentCapture (typeof (int));
23+
TestCompoundAssignmentMultipleCaptures (typeof (int), typeof (int));
2124
}
2225

2326
[DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)]
@@ -68,6 +71,32 @@ static void AssignToAnnotatedTypeReferenceWithRequirements ()
6871
ReturnAnnotatedTypeWithRequirements (GetWithPublicMethods ()) = GetWithPublicFields ();
6972
}
7073

74+
static int intField;
75+
76+
static ref int GetRefReturnInt ([DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] Type t) => ref intField;
77+
78+
// Ensure analyzer visits the a ref return in the LHS of a compound assignment.
79+
[ExpectedWarning ("IL2067", nameof (GetRefReturnInt), nameof (DynamicallyAccessedMemberTypes) + "." + nameof (DynamicallyAccessedMemberTypes.All))]
80+
public static void TestCompoundAssignment (Type t)
81+
{
82+
GetRefReturnInt (t) += 0;
83+
}
84+
85+
// Ensure analyzer visits LHS of a compound assignment when the assignment target is a flow-capture reference.
86+
[ExpectedWarning ("IL2067", nameof (GetRefReturnInt), nameof (DynamicallyAccessedMemberTypes) + "." + nameof (DynamicallyAccessedMemberTypes.All))]
87+
public static void TestCompoundAssignmentCapture (Type t, bool b = true)
88+
{
89+
GetRefReturnInt (t) += b ? 0 : 1;
90+
}
91+
92+
// Same as above, with assignment to a flow-capture reference that references multiple captured values.
93+
[ExpectedWarning ("IL2067", nameof (GetRefReturnInt), nameof (DynamicallyAccessedMemberTypes) + "." + nameof (DynamicallyAccessedMemberTypes.All))]
94+
[ExpectedWarning ("IL2067", nameof (GetRefReturnInt), nameof (DynamicallyAccessedMemberTypes) + "." + nameof (DynamicallyAccessedMemberTypes.All))]
95+
public static void TestCompoundAssignmentMultipleCaptures (Type t, Type u, bool b = true)
96+
{
97+
(b ? ref GetRefReturnInt (t) : ref GetRefReturnInt (u)) += b ? 0 : 1;
98+
}
99+
71100
[return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)]
72101
static Type GetWithPublicMethods () => null;
73102

0 commit comments

Comments
 (0)