Skip to content

Commit

Permalink
[ILLink analyzer] Move generic parameter analysis to dataflow (#95482)
Browse files Browse the repository at this point in the history
Move generic parameter analysis to dataflow and align behavior
more closely with NativeAot.

Roughly follows the implementation approach used by NativeAot in
GenericArgumentDataFlow.cs to make it easier to share the
logic (without actually sharing the code for now).

Fixes #95121.

Some notes on the behavior:

- Gets rid of some analyzer warnings in cases where NativeAot
  doesn't warn either

- Doesn't warn for generics in `typeof`

- Doesn't warn for generics in signatures of reflectable
  members (ILC does this to work around an incorrect suppression
  in DI, as discussed in
  #81358).

- Generics in base types or interface types are still analyzed
  outside of dataflow
  • Loading branch information
sbomer authored Dec 21, 2023
1 parent e994165 commit 0cf461b
Show file tree
Hide file tree
Showing 12 changed files with 340 additions and 259 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -560,15 +560,15 @@ public override TValue VisitDelegateCreation (IDelegateCreationOperation operati
targetMethodSymbol = lambda.Symbol;
break;
case IMethodReferenceOperation methodReference:
IMethodSymbol method = methodReference.Method.OriginalDefinition;
if (method.ContainingSymbol is IMethodSymbol) {
IMethodSymbol methodDefinition = methodReference.Method.OriginalDefinition;
if (methodDefinition.ContainingSymbol is IMethodSymbol) {
// Track references to local functions
var localFunction = method;
var localFunction = methodDefinition;
Debug.Assert (localFunction.MethodKind == MethodKind.LocalFunction);
var localFunctionCFG = ControlFlowGraph.GetLocalFunctionControlFlowGraphInScope (localFunction);
InterproceduralState.TrackMethod (new MethodBodyValue (localFunction, localFunctionCFG));
}
targetMethodSymbol = method;
targetMethodSymbol = methodReference.Method;
break;
case IMemberReferenceOperation:
case IInvocationOperation:
Expand All @@ -585,7 +585,7 @@ public override TValue VisitDelegateCreation (IDelegateCreationOperation operati
return HandleDelegateCreation (targetMethodSymbol, operation, state.Current.Context);
}

public abstract TValue HandleDelegateCreation (IMethodSymbol methodReference, IOperation operation, TContext context);
public abstract TValue HandleDelegateCreation (IMethodSymbol methodReference, IOperation operation, in TContext context);

public override TValue VisitPropertyReference (IPropertyReferenceOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,9 @@
using System.Diagnostics.CodeAnalysis;
using ILLink.RoslynAnalyzer.TrimAnalysis;
using ILLink.Shared;
using ILLink.Shared.DataFlow;
using ILLink.Shared.TrimAnalysis;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace ILLink.RoslynAnalyzer
{
Expand Down Expand Up @@ -106,85 +102,24 @@ public override void Initialize (AnalysisContext context)
// Examine generic instantiations in base types and interface list
context.RegisterSymbolAction (context => {
var type = (INamedTypeSymbol) context.Symbol;
var location = GetPrimaryLocation (type.Locations);
// RUC on type doesn't silence DAM warnings about generic base/interface types.
// This knowledge lives in IsInRequiresUnreferencedCodeAttributeScope,
// which we still call for consistency here, but it is expected to return false.
if (type.IsInRequiresUnreferencedCodeAttributeScope (out _))
return;
if (type.BaseType is INamedTypeSymbol baseType) {
foreach (var diagnostic in ProcessGenericParameters (baseType, location))
context.ReportDiagnostic (diagnostic);
}
foreach (var interfaceType in type.Interfaces) {
foreach (var diagnostic in ProcessGenericParameters (interfaceType, location))
context.ReportDiagnostic (diagnostic);
}
}, SymbolKind.NamedType);
// Examine generic instantiations in method return type and parameters.
// This includes property getters and setters.
context.RegisterSymbolAction (context => {
var method = (IMethodSymbol) context.Symbol;
if (method.IsInRequiresUnreferencedCodeAttributeScope (out _))
return;
var returnType = method.ReturnType;
foreach (var diagnostic in ProcessGenericParameters (returnType, GetPrimaryLocation (method.Locations)))
context.ReportDiagnostic (diagnostic);
foreach (var parameter in method.Parameters) {
foreach (var diagnostic in ProcessGenericParameters (parameter.Type, GetPrimaryLocation (parameter.Locations)))
context.ReportDiagnostic (diagnostic);
}
}, SymbolKind.Method);
// Examine generic instantiations in field type.
context.RegisterSymbolAction (context => {
var field = (IFieldSymbol) context.Symbol;
if (field.IsInRequiresUnreferencedCodeAttributeScope (out _))
return;
foreach (var diagnostic in ProcessGenericParameters (field.Type, GetPrimaryLocation (field.Locations)))
context.ReportDiagnostic (diagnostic);
}, SymbolKind.Field);
// Examine generic instantiations in invocations of generically instantiated methods,
// or methods on generically instantiated types.
context.RegisterOperationAction (context => {
if (context.ContainingSymbol.IsInRequiresUnreferencedCodeAttributeScope (out _))
return;
var invocation = (IInvocationOperation) context.Operation;
var methodSymbol = invocation.TargetMethod;
foreach (var diagnostic in ProcessMethodGenericParameters (methodSymbol, invocation.Syntax.GetLocation ()))
context.ReportDiagnostic (diagnostic);
}, OperationKind.Invocation);
// Examine generic instantiations in delegate creation of generically instantiated methods.
context.RegisterOperationAction (context => {
if (context.ContainingSymbol.IsInRequiresUnreferencedCodeAttributeScope (out _))
return;
var delegateCreation = (IDelegateCreationOperation) context.Operation;
if (delegateCreation.Target is not IMethodReferenceOperation methodReference)
return;
var location = GetPrimaryLocation (type.Locations);
DiagnosticContext diagnosticContext = new (location);
if (methodReference.Method is not IMethodSymbol methodSymbol)
return;
foreach (var diagnostic in ProcessMethodGenericParameters (methodSymbol, delegateCreation.Syntax.GetLocation()))
context.ReportDiagnostic (diagnostic);
}, OperationKind.DelegateCreation);
// Examine generic instantiations in object creation of generically instantiated types.
context.RegisterOperationAction (context => {
if (context.ContainingSymbol.IsInRequiresUnreferencedCodeAttributeScope (out _))
return;
if (type.BaseType is INamedTypeSymbol baseType)
GenericArgumentDataFlow.ProcessGenericArgumentDataFlow (diagnosticContext, baseType);
var objectCreation = (IObjectCreationOperation) context.Operation;
if (objectCreation.Type is not ITypeSymbol typeSymbol)
return;
foreach (var interfaceType in type.Interfaces)
GenericArgumentDataFlow.ProcessGenericArgumentDataFlow (diagnosticContext, interfaceType);
foreach (var diagnostic in ProcessGenericParameters (typeSymbol, objectCreation.Syntax.GetLocation()))
foreach (var diagnostic in diagnosticContext.Diagnostics)
context.ReportDiagnostic (diagnostic);
}, OperationKind.ObjectCreation);
}, SymbolKind.NamedType);
context.RegisterSymbolAction (context => {
VerifyMemberOnlyApplyToTypesOrStrings (context, context.Symbol);
VerifyDamOnPropertyAndAccessorMatch (context, (IMethodSymbol) context.Symbol);
Expand All @@ -202,66 +137,6 @@ public override void Initialize (AnalysisContext context)
});
}

static IEnumerable<Diagnostic> ProcessMethodGenericParameters (IMethodSymbol methodSymbol, Location location)
{
foreach (var diagnostic in ProcessGenericParameters (methodSymbol, location))
yield return diagnostic;

if (methodSymbol.IsStatic && methodSymbol.ContainingType is not null) {
foreach (var diagnostic in ProcessGenericParameters (methodSymbol.ContainingType, location))
yield return diagnostic;
}
}

static IEnumerable<Diagnostic> ProcessGenericParameters (ISymbol symbol, Location location)
{
// Avoid unnecessary execution if not NamedType or Method
if (symbol is not INamedTypeSymbol && symbol is not IMethodSymbol)
yield break;

ImmutableArray<ITypeParameterSymbol> typeParams = default;
ImmutableArray<ITypeSymbol> typeArgs = default;
switch (symbol) {
case INamedTypeSymbol type:
typeParams = type.TypeParameters;
typeArgs = type.TypeArguments;
break;
case IMethodSymbol targetMethod:
typeParams = targetMethod.TypeParameters;
typeArgs = targetMethod.TypeArguments;
break;
}

if (typeParams != null) {
Debug.Assert (typeParams.Length == typeArgs.Length);

for (int i = 0; i < typeParams.Length; i++) {
// Syntax like typeof (Foo<>) will have an ErrorType as the type argument.
// These uninstantiated generics should not produce warnings.
if (typeArgs[i].Kind == SymbolKind.ErrorType)
continue;
var sourceValue = SingleValueExtensions.FromTypeSymbol (typeArgs[i])!;
var targetValue = new GenericParameterValue (typeParams[i]);
foreach (var diagnostic in GetDynamicallyAccessedMembersDiagnostics (sourceValue, targetValue, location))
yield return diagnostic;
}
}
}

static List<Diagnostic> GetDynamicallyAccessedMembersDiagnostics (SingleValue sourceValue, SingleValue targetValue, Location location)
{
// The target should always be an annotated value, but the visitor design currently prevents
// declaring this in the type system.
if (targetValue is not ValueWithDynamicallyAccessedMembers targetWithDynamicallyAccessedMembers)
throw new NotImplementedException ();

var diagnosticContext = new DiagnosticContext (location);
var requireDynamicallyAccessedMembersAction = new RequireDynamicallyAccessedMembersAction (diagnosticContext, default (ReflectionAccessAnalyzer));
requireDynamicallyAccessedMembersAction.Invoke (sourceValue, targetWithDynamicallyAccessedMembers);

return diagnosticContext.Diagnostics;
}

static void VerifyMemberOnlyApplyToTypesOrStrings (SymbolAnalysisContext context, ISymbol member)
{
var location = GetPrimaryLocation (member.Locations);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis;
using ILLink.Shared.DataFlow;
using ILLink.Shared.TrimAnalysis;
using ILLink.Shared.TypeSystemProxy;

namespace ILLink.RoslynAnalyzer.TrimAnalysis
{
internal static class GenericArgumentDataFlow
{
public static void ProcessGenericArgumentDataFlow (DiagnosticContext diagnosticContext, INamedTypeSymbol type)
{
ProcessGenericArgumentDataFlow (diagnosticContext, type.TypeArguments, type.TypeParameters);
}

public static void ProcessGenericArgumentDataFlow (DiagnosticContext diagnosticContext, IMethodSymbol method)
{
ProcessGenericArgumentDataFlow (diagnosticContext, method.TypeArguments, method.TypeParameters);

ProcessGenericArgumentDataFlow (diagnosticContext, method.ContainingType);
}

public static void ProcessGenericArgumentDataFlow (DiagnosticContext diagnosticContext, IFieldSymbol field)
{
ProcessGenericArgumentDataFlow (diagnosticContext, field.ContainingType);
}

static void ProcessGenericArgumentDataFlow (
DiagnosticContext diagnosticContext,
ImmutableArray<ITypeSymbol> typeArguments,
ImmutableArray<ITypeParameterSymbol> typeParameters)
{
for (int i = 0; i < typeArguments.Length; i++) {
var typeArgument = typeArguments[i];
// Apply annotations to the generic argument
var genericParameterValue = new GenericParameterValue (typeParameters[i]);
if (genericParameterValue.DynamicallyAccessedMemberTypes != DynamicallyAccessedMemberTypes.None) {
SingleValue genericArgumentValue = SingleValueExtensions.FromTypeSymbol (typeArgument)!;
var requireDynamicallyAccessedMembersAction = new RequireDynamicallyAccessedMembersAction (diagnosticContext, default (ReflectionAccessAnalyzer));
requireDynamicallyAccessedMembersAction.Invoke (genericArgumentValue, genericParameterValue);
}

// Recursively process generic argument data flow on the generic argument if it itself is generic
if (typeArgument is INamedTypeSymbol namedTypeArgument && namedTypeArgument.IsGenericType) {
ProcessGenericArgumentDataFlow (diagnosticContext, namedTypeArgument);
}
}
}

public static bool RequiresGenericArgumentDataFlow (INamedTypeSymbol type)
{
if (type.IsGenericType) {
if (RequiresGenericArgumentDataFlow (type.TypeParameters))
return true;

foreach (var typeArgument in type.TypeArguments) {
if (typeArgument is INamedTypeSymbol namedTypeSymbol && namedTypeSymbol.IsGenericType
&& RequiresGenericArgumentDataFlow (namedTypeSymbol))
return true;
}
}

return false;
}

public static bool RequiresGenericArgumentDataFlow (IMethodSymbol method)
{
if (method.IsGenericMethod) {
if (RequiresGenericArgumentDataFlow (method.TypeParameters))
return true;

foreach (var typeArgument in method.TypeArguments) {
if (typeArgument is INamedTypeSymbol namedTypeSymbol && namedTypeSymbol.IsGenericType
&& RequiresGenericArgumentDataFlow (namedTypeSymbol))
return true;
}
}

return RequiresGenericArgumentDataFlow (method.ContainingType);
}

public static bool RequiresGenericArgumentDataFlow (IFieldSymbol field)
{
return RequiresGenericArgumentDataFlow (field.ContainingType);
}

static bool RequiresGenericArgumentDataFlow (ImmutableArray<ITypeParameterSymbol> typeParameters)
{
foreach (var typeParameter in typeParameters) {
var genericParameterValue = new GenericParameterValue (typeParameter);
if (genericParameterValue.DynamicallyAccessedMemberTypes != DynamicallyAccessedMemberTypes.None)
return true;
}

return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Collections.Generic;
using System.Diagnostics;
using ILLink.RoslynAnalyzer.DataFlow;
using ILLink.Shared.TrimAnalysis;
using Microsoft.CodeAnalysis;

namespace ILLink.RoslynAnalyzer.TrimAnalysis
{
public readonly record struct TrimAnalysisGenericInstantiationPattern
{
public ISymbol GenericInstantiation { get; init; }
public IOperation Operation { get; init; }
public ISymbol OwningSymbol { get; init; }
public FeatureContext FeatureContext { get; init; }

public TrimAnalysisGenericInstantiationPattern (
ISymbol genericInstantiation,
IOperation operation,
ISymbol owningSymbol,
FeatureContext featureContext)
{
GenericInstantiation = genericInstantiation;
Operation = operation;
OwningSymbol = owningSymbol;
FeatureContext = featureContext.DeepCopy ();
}

public TrimAnalysisGenericInstantiationPattern Merge (
FeatureContextLattice featureContextLattice,
TrimAnalysisGenericInstantiationPattern other)
{
Debug.Assert (Operation == other.Operation);
Debug.Assert (SymbolEqualityComparer.Default.Equals (GenericInstantiation, other.GenericInstantiation));
Debug.Assert (SymbolEqualityComparer.Default.Equals (OwningSymbol, other.OwningSymbol));

return new TrimAnalysisGenericInstantiationPattern (
GenericInstantiation,
Operation,
OwningSymbol,
featureContextLattice.Meet (FeatureContext, other.FeatureContext));
}

public IEnumerable<Diagnostic> CollectDiagnostics (DataFlowAnalyzerContext context)
{
DiagnosticContext diagnosticContext = new (Operation.Syntax.GetLocation ());
if (context.EnableTrimAnalyzer &&
!OwningSymbol.IsInRequiresUnreferencedCodeAttributeScope (out _) &&
!FeatureContext.IsEnabled (RequiresUnreferencedCodeAnalyzer.UnreferencedCode)) {
switch (GenericInstantiation) {
case INamedTypeSymbol type:
GenericArgumentDataFlow.ProcessGenericArgumentDataFlow (diagnosticContext, type);
break;

case IMethodSymbol method:
GenericArgumentDataFlow.ProcessGenericArgumentDataFlow (diagnosticContext, method);
break;

case IFieldSymbol field:
GenericArgumentDataFlow.ProcessGenericArgumentDataFlow (diagnosticContext, field);
break;
}
}

return diagnosticContext.Diagnostics;
}
}
}
Loading

0 comments on commit 0cf461b

Please sign in to comment.