|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable disable
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.FlowAnalysis.SymbolUsageAnalysis;
internal static partial class SymbolUsageAnalysis
{
/// <summary>
/// Operations walker used for walking high-level operation tree
/// as well as control flow graph based operations.
/// </summary>
private sealed class Walker : OperationWalker
{
private AnalysisData _currentAnalysisData;
private ISymbol _currentContainingSymbol;
private IOperation _currentRootOperation;
private CancellationToken _cancellationToken;
private PooledDictionary<IAssignmentOperation, PooledHashSet<(ISymbol, IOperation)>> _pendingWritesMap;
private static readonly ObjectPool<Walker> s_visitorPool = new(() => new Walker());
private Walker() { }
public static void AnalyzeOperationsAndUpdateData(
ISymbol containingSymbol,
IEnumerable<IOperation> operations,
AnalysisData analysisData,
CancellationToken cancellationToken)
{
var visitor = s_visitorPool.Allocate();
try
{
visitor.Visit(containingSymbol, operations, analysisData, cancellationToken);
}
finally
{
s_visitorPool.Free(visitor);
}
}
private void Visit(ISymbol containingSymbol, IEnumerable<IOperation> operations, AnalysisData analysisData, CancellationToken cancellationToken)
{
Debug.Assert(_currentContainingSymbol == null);
Debug.Assert(_currentAnalysisData == null);
Debug.Assert(_currentRootOperation == null);
Debug.Assert(_pendingWritesMap == null);
_pendingWritesMap = PooledDictionary<IAssignmentOperation, PooledHashSet<(ISymbol, IOperation)>>.GetInstance();
try
{
_currentContainingSymbol = containingSymbol;
_currentAnalysisData = analysisData;
_cancellationToken = cancellationToken;
foreach (var operation in operations)
{
cancellationToken.ThrowIfCancellationRequested();
_currentRootOperation = operation;
Visit(operation);
}
}
finally
{
_currentContainingSymbol = null;
_currentAnalysisData = null;
_currentRootOperation = null;
_cancellationToken = default;
foreach (var pendingWrites in _pendingWritesMap.Values)
{
pendingWrites.Free();
}
_pendingWritesMap.Free();
_pendingWritesMap = null;
}
}
private void OnReadReferenceFound(ISymbol symbol)
=> _currentAnalysisData.OnReadReferenceFound(symbol);
private void OnWriteReferenceFound(ISymbol symbol, IOperation operation, ValueUsageInfo valueUsageInfo)
{
// maybeWritten == 'ref' argument.
var isRef = valueUsageInfo == ValueUsageInfo.ReadableWritableReference;
_currentAnalysisData.OnWriteReferenceFound(symbol, operation, maybeWritten: isRef, isRef);
ProcessPossibleDelegateCreationAssignment(symbol, operation);
}
private void OnLValueCaptureFound(ISymbol symbol, IOperation operation, CaptureId captureId)
=> _currentAnalysisData.OnLValueCaptureFound(symbol, operation, captureId);
private void OnLValueDereferenceFound(CaptureId captureId)
=> _currentAnalysisData.OnLValueDereferenceFound(captureId);
private void OnReferenceFound(ISymbol symbol, IOperation operation)
{
Debug.Assert(symbol != null);
var valueUsageInfo = operation.GetValueUsageInfo(_currentContainingSymbol);
var isReadFrom = valueUsageInfo.IsReadFrom();
var isWrittenTo = valueUsageInfo.IsWrittenTo();
if (isWrittenTo && MakePendingWrite(operation, symbolOpt: symbol))
{
// Certain writes are processed at a later visit
// and are marked as a pending write for post processing.
// For example, consider the write to 'x' in "x = M(x, ...)".
// We visit the Target (left) of assignment before visiting the Value (right)
// of the assignment, as there might be expressions on the left that are evaluated first.
// We don't want to mark the symbol read while processing the left of assignment
// as there can be references on the right, which reads the prior value.
// Instead we mark this as a pending write, which will be processed when we finish visiting the assignment.
isWrittenTo = false;
}
if (isReadFrom)
{
if (operation.Parent is IFlowCaptureOperation flowCapture &&
_currentAnalysisData.IsLValueFlowCapture(flowCapture.Id))
{
OnLValueCaptureFound(symbol, operation, flowCapture.Id);
// For compound assignments, the flow capture can be both an R-Value and an L-Value capture.
if (_currentAnalysisData.IsRValueFlowCapture(flowCapture.Id))
{
OnReadReferenceFound(symbol);
}
}
else
{
OnReadReferenceFound(symbol);
}
}
if (isWrittenTo)
{
OnWriteReferenceFound(symbol, operation, valueUsageInfo);
}
if (operation.Parent is IIncrementOrDecrementOperation &&
operation.Parent.Parent?.Kind != OperationKind.ExpressionStatement)
{
OnReadReferenceFound(symbol);
}
}
private bool MakePendingWrite(IOperation operation, ISymbol symbolOpt)
{
Debug.Assert(symbolOpt != null || operation.Kind == OperationKind.FlowCaptureReference);
if (operation.Parent is IAssignmentOperation assignmentOperation &&
assignmentOperation.Target == operation)
{
var set = PooledHashSet<(ISymbol, IOperation)>.GetInstance();
set.Add((symbolOpt, operation));
_pendingWritesMap.Add(assignmentOperation, set);
return true;
}
else if (operation.IsInLeftOfDeconstructionAssignment(out var deconstructionAssignment))
{
if (!_pendingWritesMap.TryGetValue(deconstructionAssignment, out var set))
{
set = PooledHashSet<(ISymbol, IOperation)>.GetInstance();
_pendingWritesMap.Add(deconstructionAssignment, set);
}
set.Add((symbolOpt, operation));
return true;
}
return false;
}
private void ProcessPendingWritesForAssignmentTarget(IAssignmentOperation operation)
{
if (_pendingWritesMap.TryGetValue(operation, out var pendingWrites))
{
var isUsedCompoundAssignment = operation.IsAnyCompoundAssignment() &&
operation.Parent?.Kind != OperationKind.ExpressionStatement;
foreach (var (symbolOpt, write) in pendingWrites)
{
if (write.Kind != OperationKind.FlowCaptureReference)
{
Debug.Assert(symbolOpt != null);
OnWriteReferenceFound(symbolOpt, write, ValueUsageInfo.Write);
if (isUsedCompoundAssignment)
{
OnReadReferenceFound(symbolOpt);
}
}
else
{
Debug.Assert(symbolOpt == null);
var captureReference = (IFlowCaptureReferenceOperation)write;
Debug.Assert(_currentAnalysisData.IsLValueFlowCapture(captureReference.Id));
OnLValueDereferenceFound(captureReference.Id);
}
}
_pendingWritesMap.Remove(operation);
}
}
public override void VisitSimpleAssignment(ISimpleAssignmentOperation operation)
{
base.VisitSimpleAssignment(operation);
ProcessPendingWritesForAssignmentTarget(operation);
}
public override void VisitCompoundAssignment(ICompoundAssignmentOperation operation)
{
base.VisitCompoundAssignment(operation);
ProcessPendingWritesForAssignmentTarget(operation);
}
public override void VisitCoalesceAssignment(ICoalesceAssignmentOperation operation)
{
base.VisitCoalesceAssignment(operation);
ProcessPendingWritesForAssignmentTarget(operation);
}
public override void VisitDeconstructionAssignment(IDeconstructionAssignmentOperation operation)
{
base.VisitDeconstructionAssignment(operation);
ProcessPendingWritesForAssignmentTarget(operation);
}
public override void VisitLocalReference(ILocalReferenceOperation operation)
{
if (operation.Local.IsRef)
{
// Bail out for ref locals.
// We need points to analysis for analyzing writes to ref locals, which is currently not supported.
return;
}
OnReferenceFound(operation.Local, operation);
}
public override void VisitParameterReference(IParameterReferenceOperation operation)
{
if (operation.Parameter.IsPrimaryConstructor(_cancellationToken))
{
// Bail out for primary constructor parameters.
return;
}
OnReferenceFound(operation.Parameter, operation);
}
public override void VisitVariableDeclarator(IVariableDeclaratorOperation operation)
{
var variableInitializer = operation.GetVariableInitializer();
if (variableInitializer != null ||
operation.Parent is IForEachLoopOperation forEachLoop && forEachLoop.LoopControlVariable == operation ||
operation.Parent is ICatchClauseOperation catchClause && catchClause.ExceptionDeclarationOrExpression == operation)
{
OnWriteReferenceFound(operation.Symbol, operation, ValueUsageInfo.Write);
}
base.VisitVariableDeclarator(operation);
}
public override void VisitFlowCaptureReference(IFlowCaptureReferenceOperation operation)
{
base.VisitFlowCaptureReference(operation);
if (_currentAnalysisData.IsLValueFlowCapture(operation.Id) &&
!MakePendingWrite(operation, symbolOpt: null))
{
OnLValueDereferenceFound(operation.Id);
}
}
public override void VisitDeclarationPattern(IDeclarationPatternOperation operation)
{
if (operation.DeclaredSymbol is not null)
{
OnReferenceFound(operation.DeclaredSymbol, operation);
}
}
public override void VisitRecursivePattern(IRecursivePatternOperation operation)
{
base.VisitRecursivePattern(operation);
if (operation.DeclaredSymbol is not null)
{
OnReferenceFound(operation.DeclaredSymbol, operation);
}
}
public override void VisitListPattern(IListPatternOperation operation)
{
base.VisitListPattern(operation);
if (operation.DeclaredSymbol is not null)
{
OnReferenceFound(operation.DeclaredSymbol, operation);
}
}
public override void VisitInvocation(IInvocationOperation operation)
{
base.VisitInvocation(operation);
switch (operation.TargetMethod.MethodKind)
{
case MethodKind.AnonymousFunction:
case MethodKind.DelegateInvoke:
if (operation.Instance != null)
{
AnalyzePossibleDelegateInvocation(operation.Instance);
}
else
{
_currentAnalysisData.ResetState();
}
break;
case MethodKind.LocalFunction:
AnalyzeLocalFunctionInvocation(operation.TargetMethod);
break;
}
}
private void AnalyzeLocalFunctionInvocation(IMethodSymbol localFunction)
{
Debug.Assert(localFunction.IsLocalFunction());
var newAnalysisData = _currentAnalysisData.AnalyzeLocalFunctionInvocation(localFunction, _cancellationToken);
_currentAnalysisData.SetCurrentBlockAnalysisDataFrom(newAnalysisData);
}
private void AnalyzeLambdaInvocation(IFlowAnonymousFunctionOperation lambda)
{
var newAnalysisData = _currentAnalysisData.AnalyzeLambdaInvocation(lambda, _cancellationToken);
_currentAnalysisData.SetCurrentBlockAnalysisDataFrom(newAnalysisData);
}
public override void VisitArgument(IArgumentOperation operation)
{
base.VisitArgument(operation);
if (_currentAnalysisData.IsTrackingDelegateCreationTargets &&
operation.Value.Type.IsDelegateType())
{
// Delegate argument might be captured and invoked multiple times.
// So, conservatively reset the state.
_currentAnalysisData.ResetState();
}
}
public override void VisitLocalFunction(ILocalFunctionOperation operation)
{
// Skip visiting if we are doing an operation tree walk.
// This will only happen if the operation is not the current root operation.
if (_currentRootOperation != operation)
{
return;
}
base.VisitLocalFunction(operation);
}
public override void VisitAnonymousFunction(IAnonymousFunctionOperation operation)
{
// Skip visiting if we are doing an operation tree walk.
// This will only happen if the operation is not the current root operation.
if (_currentRootOperation != operation)
{
return;
}
base.VisitAnonymousFunction(operation);
}
public override void VisitFlowAnonymousFunction(IFlowAnonymousFunctionOperation operation)
{
// Skip visiting if we are not analyzing an invocation of this lambda.
// This will only happen if the operation is not the current root operation.
if (_currentRootOperation != operation)
{
return;
}
base.VisitFlowAnonymousFunction(operation);
}
private void ProcessPossibleDelegateCreationAssignment(ISymbol symbol, IOperation write)
{
if (!_currentAnalysisData.IsTrackingDelegateCreationTargets ||
symbol.GetSymbolType()?.TypeKind != TypeKind.Delegate)
{
return;
}
IOperation initializerValue = null;
if (write is IVariableDeclaratorOperation variableDeclarator)
{
initializerValue = variableDeclarator.GetVariableInitializer()?.Value;
}
else if (write.Parent is ISimpleAssignmentOperation simpleAssignment)
{
initializerValue = simpleAssignment.Value;
}
if (initializerValue != null)
{
ProcessPossibleDelegateCreation(initializerValue, write);
}
}
private void ProcessPossibleDelegateCreation(IOperation creation, IOperation write)
{
var currentOperation = creation;
while (true)
{
switch (currentOperation.Kind)
{
case OperationKind.Conversion:
currentOperation = ((IConversionOperation)currentOperation).Operand;
continue;
case OperationKind.Parenthesized:
currentOperation = ((IParenthesizedOperation)currentOperation).Operand;
continue;
case OperationKind.DelegateCreation:
currentOperation = ((IDelegateCreationOperation)currentOperation).Target;
continue;
case OperationKind.AnonymousFunction:
// We don't support lambda target analysis for operation tree
// and control flow graph should have replaced 'AnonymousFunction' nodes
// with 'FlowAnonymousFunction' nodes.
throw ExceptionUtilities.Unreachable();
case OperationKind.FlowAnonymousFunction:
_currentAnalysisData.SetLambdaTargetForDelegate(write, (IFlowAnonymousFunctionOperation)currentOperation);
return;
case OperationKind.MethodReference:
var methodReference = (IMethodReferenceOperation)currentOperation;
if (methodReference.Method.IsLocalFunction())
{
_currentAnalysisData.SetLocalFunctionTargetForDelegate(write, methodReference);
}
else
{
_currentAnalysisData.SetEmptyInvocationTargetsForDelegate(write);
}
return;
case OperationKind.LocalReference:
var localReference = (ILocalReferenceOperation)currentOperation;
_currentAnalysisData.SetTargetsFromSymbolForDelegate(write, localReference.Local);
return;
case OperationKind.ParameterReference:
var parameterReference = (IParameterReferenceOperation)currentOperation;
_currentAnalysisData.SetTargetsFromSymbolForDelegate(write, parameterReference.Parameter);
return;
case OperationKind.Literal:
if (currentOperation.ConstantValue.Value is null)
{
_currentAnalysisData.SetEmptyInvocationTargetsForDelegate(write);
}
return;
default:
return;
}
}
}
private void AnalyzePossibleDelegateInvocation(IOperation operation)
{
Debug.Assert(operation.Type.IsDelegateType());
if (!_currentAnalysisData.IsTrackingDelegateCreationTargets)
{
return;
}
ProcessPossibleDelegateCreation(creation: operation, write: operation);
if (!_currentAnalysisData.TryGetDelegateInvocationTargets(operation, out var targets))
{
// Failed to identify targets, so conservatively reset the state.
_currentAnalysisData.ResetState();
return;
}
switch (targets.Count)
{
case 0:
// None of the delegate invocation targets are lambda/local functions.
break;
case 1:
// Single target.
// If we know it is an explicit invocation that will certainly be invoked,
// analyze it explicitly and overwrite current state.
AnalyzeDelegateInvocation(targets.Single());
break;
default:
// Multiple potential lambda/local function targets.
// Analyze each one, merging the outputs from all.
var savedCurrentAnalysisData = _currentAnalysisData.CreateBlockAnalysisData();
savedCurrentAnalysisData.SetAnalysisDataFrom(_currentAnalysisData.CurrentBlockAnalysisData);
var mergedAnalysisData = _currentAnalysisData.CreateBlockAnalysisData();
foreach (var target in targets)
{
_currentAnalysisData.SetCurrentBlockAnalysisDataFrom(savedCurrentAnalysisData);
AnalyzeDelegateInvocation(target);
mergedAnalysisData = BasicBlockAnalysisData.Merge(mergedAnalysisData,
_currentAnalysisData.CurrentBlockAnalysisData, _currentAnalysisData.TrackAllocatedBlockAnalysisData);
}
_currentAnalysisData.SetCurrentBlockAnalysisDataFrom(mergedAnalysisData);
break;
}
return;
// Local functions.
void AnalyzeDelegateInvocation(IOperation target)
{
switch (target.Kind)
{
case OperationKind.FlowAnonymousFunction:
AnalyzeLambdaInvocation((IFlowAnonymousFunctionOperation)target);
break;
case OperationKind.MethodReference:
AnalyzeLocalFunctionInvocation(((IMethodReferenceOperation)target).Method);
break;
default:
throw ExceptionUtilities.Unreachable();
}
}
}
}
}
|