Skip to content

Commit c345959

Browse files
authored
Report Requires diagnostics from dataflow analyzer (#92724)
This moves the logic for generating diagnostics about RequiresUnreferencedCode, RequiresAssemblyFiles, and RequiresDynamicCode attributes from the separate Requires* analyzers into the DynamicallyAccessedMembersAnalyzer. This includes logic for warning on access to attributed members, not the warnings about mismatching attributes on base/override methods. The override validation logic is still handled by the respective analyzers. The DynamicallyAccessedMembersAnalyzer is now turned on by any of the settings EnableTrimAnalyzer, EnableAotAnalyzer, or EnableSingleFileAnalyzer. A new type DataFlowAnalyzerContext is used to cache per-compilation state of the analyzers, to avoid recomputing it for each trim analysis pattern that might potentially produce a warning. The context only stores state for enabled analyzers, based on the MSBuild property values. There are a few minor differences in the warning behavior with this change: - Implicit calls to annotated base ctors warn because they are visible in the CFG - Unused local functions aren't discovered by the analysis, so don't warn (matching linker/AOT behavior) Also fixes a few asserts that were being hit with the live analyzer (changes from de011df): - Ref property passed as out parameter - Delegate creation of an invocation operation * Add test for annotated value assigned to event * Use DiagnosticContext for consistency * Move field access handling into dataflow analyzer * Remove unnecessary merge logic for reflection access pattern * PR feedback - Clean up a few unused usings - Remove unused Instance field * Fix ExpectedWarnings for ILLink * Fix assert for ref property as out param And related cases involving implicit indexer references. * Add test for deconstruction assignment to ref property * Fix delegate creation over invocation * Fix ExpectedWarnings for illink * Silence dataflow warnings if EnableTrimAnalyer isn't set Extra warnings were being produced on some projects where the single-file analyzer was enabled, causing the DAM analyzer to light up. The dataflow warnings were missing a check of EnableTrimAnalzer to prevent these warnings. This also lifts the checking of MSBuild property values out into the compilation start action, so it only needs to be done per compilation. The context type has been renamed to reflect that it holds state relevant to all of the analyses handled by the dataflow logic now. * Add test coverage for EnableTrimAnalyzer checks And add missing checks in DAM analyzer
1 parent d7c1f8b commit c345959

33 files changed

+782
-293
lines changed

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

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,8 @@ internal static class AnalyzerOptionsExtensions
1111
{
1212
public static string? GetMSBuildPropertyValue (
1313
this AnalyzerOptions options,
14-
string optionName,
15-
Compilation compilation)
14+
string optionName)
1615
{
17-
// MSBuild property values should be set at compilation level, and cannot have different values per-tree.
18-
// So, we default to first syntax tree.
19-
var tree = compilation.SyntaxTrees.FirstOrDefault ();
20-
if (tree is null) {
21-
return null;
22-
}
23-
2416
return options.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue (
2517
$"build_property.{optionName}", out var value)
2618
? value
@@ -29,10 +21,9 @@ internal static class AnalyzerOptionsExtensions
2921

3022
public static bool IsMSBuildPropertyValueTrue (
3123
this AnalyzerOptions options,
32-
string propertyName,
33-
Compilation compilation)
24+
string propertyName)
3425
{
35-
var propertyValue = GetMSBuildPropertyValue (options, propertyName, compilation);
26+
var propertyValue = GetMSBuildPropertyValue (options, propertyName);
3627
if (!string.Equals (propertyValue?.Trim (), "true", System.StringComparison.OrdinalIgnoreCase))
3728
return false;
3829

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public override void Initialize (AnalysisContext context)
3131
context.ConfigureGeneratedCodeAnalysis (GeneratedCodeAnalysisFlags.ReportDiagnostics);
3232
context.RegisterCompilationStartAction (context => {
3333
var compilation = context.Compilation;
34-
if (!context.Options.IsMSBuildPropertyValueTrue (MSBuildPropertyOptionNames.EnableTrimAnalyzer, compilation))
34+
if (!context.Options.IsMSBuildPropertyValueTrue (MSBuildPropertyOptionNames.EnableTrimAnalyzer))
3535
return;
3636

3737
context.RegisterOperationAction (operationContext => {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public CapturedReferenceValue (IOperation operation)
1616
{
1717
switch (operation.Kind) {
1818
case OperationKind.PropertyReference:
19+
case OperationKind.EventReference:
1920
case OperationKind.LocalReference:
2021
case OperationKind.FieldReference:
2122
case OperationKind.ParameterReference:
@@ -26,7 +27,6 @@ public CapturedReferenceValue (IOperation operation)
2627
case OperationKind.None:
2728
case OperationKind.InstanceReference:
2829
case OperationKind.Invocation:
29-
case OperationKind.EventReference:
3030
case OperationKind.Invalid:
3131
// These will just be ignored when referenced later.
3232
break;

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

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ public void InterproceduralAnalyze ()
5858
return;
5959
}
6060

61-
6261
Debug.Assert (Context.OwningSymbol is not IMethodSymbol methodSymbol ||
6362
methodSymbol.MethodKind is not (MethodKind.LambdaMethod or MethodKind.LocalFunction));
6463
var startMethod = new MethodBodyValue (Context.OwningSymbol, Context.GetControlFlowGraph (OperationBlock));
@@ -68,9 +67,6 @@ public void InterproceduralAnalyze ()
6867
oldInterproceduralState = interproceduralState.Clone ();
6968

7069
foreach (var method in oldInterproceduralState.Methods) {
71-
if (method.OwningSymbol.IsInRequiresUnreferencedCodeAttributeScope (out _))
72-
continue;
73-
7470
AnalyzeMethod (method, ref interproceduralState);
7571
}
7672
}

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

Lines changed: 87 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ public void Transfer (BlockProxy block, LocalDataFlowState<TValue, TValueLattice
9797
HandleReturnValue (branchValue, branchValueOperation);
9898
}
9999

100-
public abstract TValue GetFieldTargetValue (IFieldSymbol field);
100+
public abstract TValue GetFieldTargetValue (IFieldSymbol field, IFieldReferenceOperation fieldReferenceOperation);
101101

102102
public abstract TValue GetParameterTargetValue (IParameterSymbol parameter);
103103

@@ -170,7 +170,7 @@ TValue ProcessSingleTargetAssignment (IOperation targetOperation, IAssignmentOpe
170170
case IFieldReferenceOperation:
171171
case IParameterReferenceOperation: {
172172
TValue targetValue = targetOperation switch {
173-
IFieldReferenceOperation fieldRef => GetFieldTargetValue (fieldRef.Field),
173+
IFieldReferenceOperation fieldRef => GetFieldTargetValue (fieldRef.Field, fieldRef),
174174
IParameterReferenceOperation parameterRef => GetParameterTargetValue (parameterRef.Parameter),
175175
_ => throw new InvalidOperationException ()
176176
};
@@ -212,6 +212,13 @@ TValue ProcessSingleTargetAssignment (IOperation targetOperation, IAssignmentOpe
212212
// even though a property setter has no return value.
213213
return value;
214214
}
215+
case IEventReferenceOperation eventRef: {
216+
// Handles assignment to an event like 'Event = Handler;', which is a write to the underlying field,
217+
// not a call to an event accessor method. There is no Roslyn API to access the field,
218+
// so just visit the instance and the value. https://github.com/dotnet/roslyn/issues/40103
219+
Visit (eventRef.Instance, state);
220+
return Visit (operation.Value, state);
221+
}
215222
case IImplicitIndexerReferenceOperation indexerRef: {
216223
// An implicit reference to an indexer where the argument is a System.Index
217224
TValue instanceValue = Visit (indexerRef.Instance, state);
@@ -276,10 +283,6 @@ TValue ProcessSingleTargetAssignment (IOperation targetOperation, IAssignmentOpe
276283
// also produces warnings for ref params/locals/returns.
277284
// https://github.com/dotnet/linker/issues/2632
278285
// https://github.com/dotnet/linker/issues/2158
279-
case IEventReferenceOperation:
280-
// An event assignment is an assignment to the generated backing field for
281-
// auto-implemented events. There is no Roslyn API to access the field, so
282-
// skip this. https://github.com/dotnet/roslyn/issues/40103
283286
Visit (targetOperation, state);
284287
break;
285288

@@ -359,6 +362,26 @@ TValue ProcessAssignment (IAssignmentOperation operation, LocalDataFlowState<TVa
359362
return value;
360363
}
361364

365+
public override TValue VisitEventAssignment (IEventAssignmentOperation operation, LocalDataFlowState<TValue, TValueLattice> state)
366+
{
367+
var eventReference = (IEventReferenceOperation) operation.EventReference;
368+
TValue instanceValue = Visit (eventReference.Instance, state);
369+
TValue value = Visit (operation.HandlerValue, state);
370+
if (operation.Adds) {
371+
IMethodSymbol? addMethod = eventReference.Event.AddMethod;
372+
Debug.Assert (addMethod != null);
373+
if (addMethod != null)
374+
HandleMethodCall (addMethod, instanceValue, ImmutableArray.Create (value), operation);
375+
return value;
376+
} else {
377+
IMethodSymbol? removeMethod = eventReference.Event.RemoveMethod;
378+
Debug.Assert (removeMethod != null);
379+
if (removeMethod != null)
380+
HandleMethodCall (removeMethod, instanceValue, ImmutableArray.Create (value), operation);
381+
return value;
382+
}
383+
}
384+
362385
TValue GetFlowCaptureValue (IFlowCaptureReferenceOperation operation, LocalDataFlowState<TValue, TValueLattice> state)
363386
{
364387
Debug.Assert (!IsLValueFlowCapture (operation.Id),
@@ -465,43 +488,58 @@ public override TValue VisitInvocation (IInvocationOperation operation, LocalDat
465488

466489
public override TValue VisitDelegateCreation (IDelegateCreationOperation operation, LocalDataFlowState<TValue, TValueLattice> state)
467490
{
468-
if (operation.Target is IFlowAnonymousFunctionOperation lambda) {
469-
VisitFlowAnonymousFunction (lambda, state);
470-
471-
// Instance of a lambda or local function should be the instance of the containing method.
472-
// Don't need to track a dataflow value, since the delegate creation will warn if the
473-
// lambda or local function has an annotated this parameter.
474-
var instance = TopValue;
475-
return HandleDelegateCreation (lambda.Symbol, instance, operation);
476-
}
491+
Visit (operation.Target, state);
477492

478-
Debug.Assert (operation.Target is IMemberReferenceOperation,
479-
$"{operation.Target.GetType ()}: {operation.Syntax.GetLocation ().GetLineSpan ()}");
480-
if (operation.Target is not IMemberReferenceOperation memberReference)
481-
return TopValue;
493+
IMethodSymbol? targetMethodSymbol = null;
494+
switch (operation.Target) {
495+
case IFlowAnonymousFunctionOperation lambda:
496+
// Tracking lambdas is handled by normal visiting logic for IFlowAnonymousFunctionOperation.
482497

483-
TValue instanceValue = Visit (memberReference.Instance, state);
498+
// Instance of a lambda or local function should be the instance of the containing method.
499+
// Don't need to track a dataflow value, since the delegate creation will warn if the
500+
// lambda or local function has an annotated this parameter.
501+
targetMethodSymbol = lambda.Symbol;
502+
break;
503+
case IMethodReferenceOperation methodReference:
504+
IMethodSymbol method = methodReference.Method.OriginalDefinition;
505+
if (method.ContainingSymbol is IMethodSymbol) {
506+
// Track references to local functions
507+
var localFunction = method;
508+
Debug.Assert (localFunction.MethodKind == MethodKind.LocalFunction);;
509+
var localFunctionCFG = ControlFlowGraph.GetLocalFunctionControlFlowGraphInScope (localFunction);
510+
InterproceduralState.TrackMethod (new MethodBodyValue (localFunction, localFunctionCFG));
511+
}
512+
targetMethodSymbol = method;
513+
break;
514+
case IMemberReferenceOperation:
515+
case IInvocationOperation:
516+
// No method symbol.
517+
break;
518+
default:
519+
// Unimplemented case that might need special handling.
520+
// Fail in debug mode only.
521+
Debug.Fail ($"{operation.Target.GetType ()}: {operation.Target.Syntax.GetLocation ().GetLineSpan ()}");
522+
break;
523+
}
484524

485-
if (memberReference.Member is not IMethodSymbol method)
525+
if (targetMethodSymbol == null)
486526
return TopValue;
487527

488-
// Track references to local functions
489-
if (method.OriginalDefinition.ContainingSymbol is IMethodSymbol) {
490-
var localFunction = method.OriginalDefinition;
491-
Debug.Assert (localFunction.MethodKind == MethodKind.LocalFunction);;
492-
var localFunctionCFG = ControlFlowGraph.GetLocalFunctionControlFlowGraphInScope (localFunction);
493-
InterproceduralState.TrackMethod (new MethodBodyValue (localFunction, localFunctionCFG));
494-
}
495-
496-
return HandleDelegateCreation (method, instanceValue, operation);
528+
return HandleDelegateCreation (targetMethodSymbol, operation);
497529
}
498530

499-
public abstract TValue HandleDelegateCreation (IMethodSymbol methodReference, TValue instance, IOperation operation);
531+
public abstract TValue HandleDelegateCreation (IMethodSymbol methodReference, IOperation operation);
500532

501533
public override TValue VisitPropertyReference (IPropertyReferenceOperation operation, LocalDataFlowState<TValue, TValueLattice> state)
502534
{
503-
if (!operation.GetValueUsageInfo (OwningSymbol).HasFlag (ValueUsageInfo.Read))
535+
if (operation.GetValueUsageInfo (OwningSymbol).HasFlag (ValueUsageInfo.Write)) {
536+
// Property references may be passed as ref/out parameters.
537+
// Enable this assert once we have support for deconstruction assignments.
538+
// https://github.com/dotnet/linker/issues/3158
539+
// Debug.Assert (operation.GetValueUsageInfo (OwningSymbol).HasFlag (ValueUsageInfo.Reference),
540+
// $"{operation.Syntax.GetLocation ().GetLineSpan ()}");
504541
return TopValue;
542+
}
505543

506544
// Accessing property for reading is really a call to the getter
507545
// The setter case is handled in assignment operation since here we don't have access to the value to pass to the setter
@@ -516,11 +554,27 @@ public override TValue VisitPropertyReference (IPropertyReferenceOperation opera
516554
return HandleMethodCall (getMethod!, instanceValue, arguments.ToImmutableArray (), operation);
517555
}
518556

519-
public override TValue VisitImplicitIndexerReference (IImplicitIndexerReferenceOperation operation, LocalDataFlowState<TValue, TValueLattice> state)
557+
public override TValue VisitEventReference (IEventReferenceOperation operation, LocalDataFlowState<TValue, TValueLattice> state)
520558
{
559+
// Writing to an event should not go through this path.
560+
Debug.Assert (operation.GetValueUsageInfo (OwningSymbol).HasFlag (ValueUsageInfo.Read));
521561
if (!operation.GetValueUsageInfo (OwningSymbol).HasFlag (ValueUsageInfo.Read))
522562
return TopValue;
523563

564+
Visit (operation.Instance, state);
565+
// Accessing event for reading retrieves the event delegate from the event's backing field,
566+
// so there is no method call to handle.
567+
return TopValue;
568+
}
569+
570+
public override TValue VisitImplicitIndexerReference (IImplicitIndexerReferenceOperation operation, LocalDataFlowState<TValue, TValueLattice> state)
571+
{
572+
if (operation.GetValueUsageInfo (OwningSymbol).HasFlag (ValueUsageInfo.Write)) {
573+
// Implicit indexer references may be passed as ref/out parameters.
574+
Debug.Assert (operation.GetValueUsageInfo (OwningSymbol).HasFlag (ValueUsageInfo.Reference));
575+
return TopValue;
576+
}
577+
524578
TValue instanceValue = Visit (operation.Instance, state);
525579
TValue indexArgumentValue = Visit (operation.Argument, state);
526580

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System.Collections.Generic;
5+
using System.Collections.Immutable;
6+
using Microsoft.CodeAnalysis;
7+
using Microsoft.CodeAnalysis.Diagnostics;
8+
9+
namespace ILLink.RoslynAnalyzer
10+
{
11+
public readonly struct DataFlowAnalyzerContext
12+
{
13+
private readonly Dictionary<RequiresAnalyzerBase, ImmutableArray<ISymbol>> _enabledAnalyzers;
14+
15+
public IEnumerable<RequiresAnalyzerBase> EnabledRequiresAnalyzers => _enabledAnalyzers.Keys;
16+
17+
public ImmutableArray<ISymbol> GetSpecialIncompatibleMembers (RequiresAnalyzerBase analyzer)
18+
{
19+
if (!_enabledAnalyzers.TryGetValue (analyzer, out var members))
20+
throw new System.ArgumentException ($"Analyzer {analyzer.GetType ().Name} is not in the cache");
21+
return members;
22+
}
23+
24+
public readonly bool EnableTrimAnalyzer { get; }
25+
26+
public readonly bool AnyAnalyzersEnabled => EnableTrimAnalyzer || _enabledAnalyzers.Count > 0;
27+
28+
DataFlowAnalyzerContext (Dictionary<RequiresAnalyzerBase, ImmutableArray<ISymbol>> enabledAnalyzers, bool enableTrimAnalyzer)
29+
{
30+
_enabledAnalyzers = enabledAnalyzers;
31+
EnableTrimAnalyzer = enableTrimAnalyzer;
32+
}
33+
34+
public static DataFlowAnalyzerContext Create (AnalyzerOptions options, Compilation compilation, ImmutableArray<RequiresAnalyzerBase> requiresAnalyzers)
35+
{
36+
var enabledAnalyzers = new Dictionary<RequiresAnalyzerBase, ImmutableArray<ISymbol>> ();
37+
foreach (var analyzer in requiresAnalyzers) {
38+
if (analyzer.IsAnalyzerEnabled (options)) {
39+
var incompatibleMembers = analyzer.GetSpecialIncompatibleMembers (compilation);
40+
enabledAnalyzers.Add (analyzer, incompatibleMembers);
41+
}
42+
}
43+
return new DataFlowAnalyzerContext (
44+
enabledAnalyzers,
45+
options.IsMSBuildPropertyValueTrue (MSBuildPropertyOptionNames.EnableTrimAnalyzer));
46+
}
47+
}
48+
}

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

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ public class DynamicallyAccessedMembersAnalyzer : DiagnosticAnalyzer
2525
internal const string DynamicallyAccessedMembersAttribute = nameof (DynamicallyAccessedMembersAttribute);
2626
public const string attributeArgument = "attributeArgument";
2727
public const string FullyQualifiedDynamicallyAccessedMembersAttribute = "System.Diagnostics.CodeAnalysis." + DynamicallyAccessedMembersAttribute;
28+
public static Lazy<ImmutableArray<RequiresAnalyzerBase>> RequiresAnalyzers { get; } = new Lazy<ImmutableArray<RequiresAnalyzerBase>> (GetRequiresAnalyzers);
29+
static ImmutableArray<RequiresAnalyzerBase> GetRequiresAnalyzers () =>
30+
ImmutableArray.Create<RequiresAnalyzerBase> (
31+
new RequiresAssemblyFilesAnalyzer (),
32+
new RequiresUnreferencedCodeAnalyzer (),
33+
new RequiresDynamicCodeAnalyzer ());
2834

2935
public static ImmutableArray<DiagnosticDescriptor> GetSupportedDiagnostics ()
3036
{
@@ -49,6 +55,12 @@ public static ImmutableArray<DiagnosticDescriptor> GetSupportedDiagnostics ()
4955
diagDescriptorsArrayBuilder.Add (DiagnosticDescriptors.GetDiagnosticDescriptor (DiagnosticId.UnrecognizedTypeNameInTypeGetType));
5056
diagDescriptorsArrayBuilder.Add (DiagnosticDescriptors.GetDiagnosticDescriptor (DiagnosticId.UnrecognizedParameterInMethodCreateInstance));
5157
diagDescriptorsArrayBuilder.Add (DiagnosticDescriptors.GetDiagnosticDescriptor (DiagnosticId.ParametersOfAssemblyCreateInstanceCannotBeAnalyzed));
58+
59+
foreach (var requiresAnalyzer in RequiresAnalyzers.Value) {
60+
foreach (var diagnosticDescriptor in requiresAnalyzer.SupportedDiagnostics)
61+
diagDescriptorsArrayBuilder.Add (diagnosticDescriptor);
62+
}
63+
5264
return diagDescriptorsArrayBuilder.ToImmutable ();
5365

5466
void AddRange (DiagnosticId first, DiagnosticId last)
@@ -72,20 +84,23 @@ public override void Initialize (AnalysisContext context)
7284
context.EnableConcurrentExecution ();
7385
context.ConfigureGeneratedCodeAnalysis (GeneratedCodeAnalysisFlags.ReportDiagnostics);
7486
context.RegisterCompilationStartAction (context => {
75-
if (!context.Options.IsMSBuildPropertyValueTrue (MSBuildPropertyOptionNames.EnableTrimAnalyzer, context.Compilation))
87+
var dataFlowAnalyzerContext = DataFlowAnalyzerContext.Create (context.Options, context.Compilation, RequiresAnalyzers.Value);
88+
if (!dataFlowAnalyzerContext.AnyAnalyzersEnabled)
7689
return;
7790

7891
context.RegisterOperationBlockAction (context => {
79-
if (context.OwningSymbol.IsInRequiresUnreferencedCodeAttributeScope (out _))
80-
return;
81-
8292
foreach (var operationBlock in context.OperationBlocks) {
8393
TrimDataFlowAnalysis trimDataFlowAnalysis = new (context, operationBlock);
8494
trimDataFlowAnalysis.InterproceduralAnalyze ();
85-
foreach (var diagnostic in trimDataFlowAnalysis.TrimAnalysisPatterns.CollectDiagnostics ())
95+
foreach (var diagnostic in trimDataFlowAnalysis.CollectDiagnostics (dataFlowAnalyzerContext))
8696
context.ReportDiagnostic (diagnostic);
8797
}
8898
});
99+
100+
// Remaining actions are only for DynamicallyAccessedMembers analysis.
101+
if (!dataFlowAnalyzerContext.EnableTrimAnalyzer)
102+
return;
103+
89104
// Examine generic instantiations in base types and interface list
90105
context.RegisterSymbolAction (context => {
91106
var type = (INamedTypeSymbol) context.Symbol;

0 commit comments

Comments
 (0)