// 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;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using ILLink.RoslynAnalyzer.TrimAnalysis;
using ILLink.Shared.DataFlow;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.FlowAnalysis;
using Microsoft.CodeAnalysis.Operations;
namespace ILLink.RoslynAnalyzer.DataFlow
{
// Visitor which tracks the values of locals in a block. It provides extension points that get called
// whenever a value that comes from a tracked local reference flows into one of the following:
// - field
// - parameter
// - method return
public abstract class LocalDataFlowVisitor<TValue, TContext, TValueLattice, TContextLattice, TConditionValue> :
OperationWalker<LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice>, TValue>,
ITransfer<
BlockProxy,
LocalStateAndContext<TValue, TContext>,
LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice>,
LocalStateAndContextLattice<TValue, TContext, TValueLattice, TContextLattice>,
TConditionValue>
// This struct constraint prevents warnings due to possible null returns from the visitor methods.
// Note that this assumes that default(TValue) is equal to the TopValue.
where TValue : struct, IEquatable<TValue>
where TContext : struct, IEquatable<TContext>
where TValueLattice : ILattice<TValue>
where TContextLattice : ILattice<TContext>
where TConditionValue : struct, INegate<TConditionValue>
{
protected readonly LocalStateAndContextLattice<TValue, TContext, TValueLattice, TContextLattice> LocalStateAndContextLattice;
protected readonly InterproceduralStateLattice<TValue, TValueLattice> InterproceduralStateLattice;
protected readonly Compilation Compilation;
protected readonly ISymbol OwningSymbol;
private readonly ControlFlowGraph ControlFlowGraph;
private readonly SemanticModel _semanticModel;
protected TValue TopValue => LocalStateAndContextLattice.LocalStateLattice.Lattice.ValueLattice.Top;
private readonly ImmutableDictionary<CaptureId, FlowCaptureKind> lValueFlowCaptures;
private readonly ImmutableHashSet<CaptureId> _deconstructionLValueFlowCaptures;
public InterproceduralState<TValue, TValueLattice> InterproceduralState;
private bool IsLValueFlowCapture(CaptureId captureId)
=> lValueFlowCaptures.ContainsKey(captureId);
private bool IsRValueFlowCapture(CaptureId captureId)
=> !lValueFlowCaptures.TryGetValue(captureId, out var captureKind) || captureKind != FlowCaptureKind.LValueCapture;
public LocalDataFlowVisitor(
Compilation compilation,
LocalStateAndContextLattice<TValue, TContext, TValueLattice, TContextLattice> lattice,
ISymbol owningSymbol,
ControlFlowGraph cfg,
ImmutableDictionary<CaptureId, FlowCaptureKind> lValueFlowCaptures,
InterproceduralState<TValue, TValueLattice> interproceduralState)
{
Compilation = compilation;
LocalStateAndContextLattice = lattice;
InterproceduralStateLattice = default;
OwningSymbol = owningSymbol;
ControlFlowGraph = cfg;
_semanticModel = cfg.OriginalOperation.SemanticModel ??
compilation.GetSemanticModel(cfg.OriginalOperation.Syntax.SyntaxTree);
this.lValueFlowCaptures = lValueFlowCaptures;
_deconstructionLValueFlowCaptures = cfg
.DescendantOperations<IFlowCaptureReferenceOperation>(OperationKind.FlowCaptureReference)
.Where(reference => reference.IsInLeftOfDeconstructionAssignment(out _))
.Select(reference => reference.Id)
.ToImmutableHashSet();
InterproceduralState = interproceduralState;
}
public abstract void ApplyCondition(TConditionValue condition, ref LocalStateAndContext<TValue, TContext> localContextState);
public TConditionValue? Transfer(
BlockProxy block,
LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
{
foreach (IOperation operation in block.Block.Operations)
Visit(operation, state);
// Blocks may end with a BranchValue computation. Visit the BranchValue operation after all others.
IOperation? branchValueOperation = block.Block.BranchValue;
if (branchValueOperation == null)
return null;
var branchValue = Visit(branchValueOperation, state);
TConditionValue conditionValue = GetConditionValue(branchValueOperation, state);
if (block.Block.ConditionKind != ControlFlowConditionKind.None)
{
// BranchValue may represent a value used in a conditional branch to the ConditionalSuccessor.
// If so, give the analysis an opportunity to model the checked condition, and return the model
// of the condition back to the generic analysis. It will be applied to the state of each outgoing branch.
return conditionValue;
}
// If not, the BranchValue represents a return or throw value associated with the FallThroughSuccessor of this block.
// (ConditionalSuccessor == null iff ConditionKind == None).
// If we get here, we should be analyzing code in a method or field/property initializer,
// not an attribute instance, since attributes can't have throws or return statements
Debug.Assert(OwningSymbol is IMethodSymbol or IFieldSymbol or IPropertySymbol,
$"{OwningSymbol.GetType()}: {branchValueOperation.Syntax.GetLocation().GetLineSpan()}");
// The BranchValue for a thrown value is not involved in dataflow tracking.
if (block.Block.FallThroughSuccessor?.Semantics == ControlFlowBranchSemantics.Throw)
return null;
// Field/property initializers can't have return statements.
Debug.Assert(OwningSymbol is IMethodSymbol,
$"{OwningSymbol.GetType()}: {branchValueOperation.Syntax.GetLocation().GetLineSpan()}");
// Return statements with return values are represented in the control flow graph as
// a branch value operation that computes the return value.
// Use the branch value operation as the key for the warning store and the location of the warning.
// We don't want the return operation because this might have multiple possible return values in general.
var current = state.Current;
HandleReturnValue(branchValue, branchValueOperation, in current.Context);
// Must be called for every return value even if it did not return an understood condition,
// because the non-understood conditions will produce warnings for FeatureGuard properties.
HandleReturnConditionValue(conditionValue, branchValueOperation);
return null;
}
public abstract TConditionValue GetConditionValue(
IOperation branchValueOperation,
LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state);
public abstract TValue GetFieldTargetValue(IFieldReferenceOperation fieldReference, in TContext context);
public abstract TValue GetBackingFieldTargetValue(IPropertyReferenceOperation propertyReference, in TContext context);
public abstract TValue GetTupleElementValue(IFieldSymbol tupleElement);
// Returns the value produced by applying a user-defined conversion operator to a value.
// The deconstruction path needs this because there the conversion is described by the
// DeconstructionInfo rather than by a conversion operation in the tree.
public abstract TValue GetConversionValue(IMethodSymbol conversionOperator, TValue operandValue);
public abstract TValue GetParameterTargetValue(IParameterSymbol parameter);
public abstract void HandleAssignment(
TValue source,
TValue target,
IOperation operation,
in TContext context);
public abstract TValue HandleArrayElementRead(TValue arrayValue, TValue indexValue, IOperation operation);
public abstract void HandleArrayElementWrite(TValue arrayValue, TValue indexValue, TValue valueToWrite, IOperation operation, bool merge);
// This takes an IOperation rather than an IReturnOperation because the return value
// may (must?) come from BranchValue of an operation whose FallThroughSuccessor is the exit block.
public abstract void HandleReturnValue(
TValue returnValue,
IOperation operation,
in TContext context);
public abstract void HandleReturnConditionValue(
TConditionValue returnConditionValue,
IOperation branchValueOperation);
// This is called for any method call, which includes:
// - Normal invocation operation
// - Accessing property value - which is treated as a call to the getter
// - Setting a property value - which is treated as a call to the setter
// All inputs are already visited and turned into values.
// The return value should be a value representing the return value from the called method.
public abstract TValue HandleMethodCall(
IMethodSymbol calledMethod,
TValue instance,
ImmutableArray<TValue> arguments,
IOperation operation,
in TContext context);
public override TValue VisitLocalReference(ILocalReferenceOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
{
return GetLocal(operation.Local, state);
}
private TValue ProcessBinderCall(IOperation operation, string methodName, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
{
var assemblyType = Compilation.GetTypeByMetadataName("Microsoft.CSharp.RuntimeBinder.Binder");
Debug.Assert(assemblyType != null);
if (assemblyType == null)
return TopValue;
var method = assemblyType.GetMembers(methodName).OfType<IMethodSymbol>().SingleOrDefault();
Debug.Assert(method != null);
if (method == null)
return TopValue;
return ProcessMethodCall(operation, method, null, ImmutableArray<IArgumentOperation>.Empty, state);
}
public override TValue VisitDynamicInvocation(IDynamicInvocationOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
=> ProcessBinderCall(operation, "InvokeMember", state);
public override TValue VisitDynamicObjectCreation(IDynamicObjectCreationOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
=> ProcessBinderCall(operation, "InvokeConstructor", state);
public override TValue VisitDynamicMemberReference(IDynamicMemberReferenceOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
=> ProcessBinderCall(operation, operation.GetValueUsageInfo(OwningSymbol).HasFlag(ValueUsageInfo.Write) ? "SetMember" : "GetMember", state);
public override TValue VisitDynamicIndexerAccess(IDynamicIndexerAccessOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
=> ProcessBinderCall(operation, operation.GetValueUsageInfo(OwningSymbol).HasFlag(ValueUsageInfo.Write) ? "SetIndex" : "GetIndex", state);
private bool IsCapturedVariable(ILocalSymbol local)
{
if (local.IsConst)
return false;
Debug.Assert(local.ContainingSymbol is IMethodSymbol or IFieldSymbol, // backing field for property initializers
$"{local.ContainingSymbol.GetType()}: {local.Locations[0].GetLineSpan()}");
return !ReferenceEquals(local.ContainingSymbol, OwningSymbol);
}
private TValue GetLocal(ILocalSymbol symbol, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
{
var local = new LocalKey(symbol);
if (IsCapturedVariable(symbol))
InterproceduralState.TrackHoistedLocal(local);
// Get the value from the hoisted locals, if it's tracked there.
if (InterproceduralState.TryGetHoistedLocal(local, out TValue? value))
return value.Value;
return state.Get(local);
}
private void SetLocal(ILocalSymbol localSymbol, TValue value, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state, bool merge = false)
{
var local = new LocalKey(localSymbol);
if (IsCapturedVariable(localSymbol))
InterproceduralState.TrackHoistedLocal(local);
// Update the value stored in the hoisted locals, if it's tracked there.
if (InterproceduralState.TrySetHoistedLocal(local, value))
return;
var newValue = merge
? state.Lattice.LocalStateLattice.Lattice.ValueLattice.Meet(state.Get(local), value)
: value;
state.Set(local, newValue);
}
private TValue ProcessSingleTargetAssignment(
IOperation targetOperation,
IAssignmentOperation operation,
LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state,
bool merge
)
{
return ProcessSingleTargetAssignment(targetOperation, operation.Value, operation, state, merge);
}
private TValue ProcessSingleTargetAssignment(
IOperation targetOperation,
IOperation valueOperation,
IOperation assignmentOperation,
LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state,
bool merge
)
{
return ProcessSingleTargetAssignment(targetOperation, valueOperation, precomputedValue: null, assignmentOperation, state, merge);
}
private TValue ProcessSingleTargetAssignment(
IOperation targetOperation,
TValue value,
IOperation assignmentOperation,
LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state,
bool merge,
IReadOnlyDictionary<IOperation, TValue>? savedTargetValues = null
)
{
return ProcessSingleTargetAssignment(
targetOperation,
valueOperation: null,
precomputedValue: value,
assignmentOperation,
state,
merge,
savedTargetValues);
}
private TValue ProcessSingleTargetAssignment(
IOperation targetOperation,
IOperation? valueOperation,
TValue? precomputedValue,
IOperation assignmentOperation,
LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state,
bool merge,
IReadOnlyDictionary<IOperation, TValue>? savedTargetValues = null
)
{
switch (targetOperation)
{
case IFieldReferenceOperation fieldRef:
{
// Visit the instance to ensure that method calls or other operations
// used as the instance are properly analyzed for diagnostics.
VisitTargetSubExpression(fieldRef.Instance, state, savedTargetValues);
var current = state.Current;
TValue targetValue = GetFieldTargetValue(fieldRef, in current.Context);
TValue value = GetAssignmentValue();
HandleAssignment(value, targetValue, assignmentOperation, in current.Context);
return value;
}
case IParameterReferenceOperation parameterRef:
{
var current = state.Current;
TValue targetValue = GetParameterTargetValue(parameterRef.Parameter);
TValue value = GetAssignmentValue();
HandleAssignment(value, targetValue, assignmentOperation, in current.Context);
return value;
}
// The remaining cases don't have a dataflow value that represents LValues, so we need
// to handle the LHS specially.
case IPropertyReferenceOperation propertyRef:
{
// Avoid visiting the property reference because for captured properties, we can't
// correctly detect whether it is used for reading or writing inside of VisitPropertyReference.
// https://github.com/dotnet/roslyn/issues/25057
TValue instanceValue = VisitTargetSubExpression(propertyRef.Instance, state, savedTargetValues);
TValue value = GetAssignmentValue();
IMethodSymbol? setMethod = propertyRef.Property.GetSetMethod();
if (setMethod == null ||
(OwningSymbol is IPropertySymbol && (ControlFlowGraph.OriginalOperation is not IAttributeOperation)))
{
// This can be a write to a byref return of a property getter.
// Skip for now: https://github.com/dotnet/linker/issues/2158
if (propertyRef.Property.RefKind is not RefKind.None)
break;
// This can happen in a constructor - there it is possible to assign to a property
// without a setter. This turns into an assignment to the compiler-generated backing field.
// Handle this similarly to field assignments.
// Even if the property has a set method, if the assignment takes place in a property initializer,
// the write becomes a direct write to the underlying field. This should be treated the same as
// the case where there is no set method.
var current = state.Current;
TValue targetValue = GetBackingFieldTargetValue(propertyRef, in current.Context);
HandleAssignment(value, targetValue, assignmentOperation, in current.Context);
return value;
}
ImmutableArray<TValue>.Builder arguments = ImmutableArray.CreateBuilder<TValue>();
// Handle C# 14 extension property access (see comment in ProcessMethodCall)
if (setMethod.HasExtensionParameterOnType())
{
arguments.Add(instanceValue);
instanceValue = TopValue;
}
// Property may be an indexer, in which case there will be one or more index arguments followed by a value argument
foreach (var val in propertyRef.Arguments)
arguments.Add(Visit(val, state));
arguments.Add(value);
HandleMethodCallHelper(setMethod, instanceValue, arguments.ToImmutableArray(), assignmentOperation, state);
// The return value of a property set expression is the value,
// even though a property setter has no return value.
return value;
}
case IEventReferenceOperation eventRef:
{
// Handles assignment to an event like 'Event = Handler;', which is a write to the underlying field,
// not a call to an event accessor method. There is no Roslyn API to access the field,
// so just visit the instance and the value. https://github.com/dotnet/roslyn/issues/40103
VisitTargetSubExpression(eventRef.Instance, state, savedTargetValues);
return GetAssignmentValue();
}
case IImplicitIndexerReferenceOperation indexerRef:
{
// An implicit reference to an indexer where the argument is a System.Index
TValue instanceValue = VisitTargetSubExpression(indexerRef.Instance, state, savedTargetValues);
TValue indexArgumentValue = VisitTargetSubExpression(indexerRef.Argument, state, savedTargetValues);
TValue value = GetAssignmentValue();
var property = (IPropertySymbol)indexerRef.IndexerSymbol;
var argumentsBuilder = ImmutableArray.CreateBuilder<TValue>();
argumentsBuilder.Add(indexArgumentValue);
argumentsBuilder.Add(value);
IMethodSymbol? setMethod = property.GetSetMethod();
if (setMethod == null)
{
// It might actually be a call to a ref-returning get method,
// like Span<T>.this[int].get. We don't handle ref returns yet.
break;
}
HandleMethodCallHelper(setMethod, instanceValue, argumentsBuilder.ToImmutableArray(), assignmentOperation, state);
return value;
}
// TODO: when setting a property in an attribute, target is an IPropertyReference.
case ILocalReferenceOperation localRef:
{
TValue value = GetAssignmentValue();
SetLocal(localRef.Local, value, state, merge);
return value;
}
case IDeclarationPatternOperation declPattern:
{
if (declPattern.DeclaredSymbol is not ILocalSymbol declaredSymbol)
break;
TValue value = GetAssignmentValue();
SetLocal(declaredSymbol, value, state, merge);
return value;
}
case IArrayElementReferenceOperation arrayElementRef:
{
if (arrayElementRef.Indices.Length != 1)
break;
TValue arrayRef = VisitTargetSubExpression(arrayElementRef.ArrayReference, state, savedTargetValues);
TValue index = VisitTargetSubExpression(arrayElementRef.Indices[0], state, savedTargetValues);
TValue value = GetAssignmentValue();
HandleArrayElementWrite(arrayRef, index, value, assignmentOperation, merge: merge);
return value;
}
case IInlineArrayAccessOperation inlineArrayAccess:
{
TValue arrayRef = VisitTargetSubExpression(inlineArrayAccess.Instance, state, savedTargetValues);
TValue index = VisitTargetSubExpression(inlineArrayAccess.Argument, state, savedTargetValues);
TValue value = GetAssignmentValue();
HandleArrayElementWrite(arrayRef, index, value, assignmentOperation, merge: merge);
return value;
}
case IDiscardOperation:
// Assignments like "_ = SomeMethod();" don't need dataflow tracking.
// Seems like this can't happen with a flow capture operation.
Debug.Assert(targetOperation is not IFlowCaptureReferenceOperation);
break;
case IInstanceReferenceOperation:
// Assignment to 'this' is not tracked currently.
// Not relevant for trimming dataflow.
case IInvocationOperation:
// This can happen for an assignment to a ref return. Skip for now.
// The analyzer doesn't handle refs yet. This should be fixed once the analyzer
// also produces warnings for ref params/locals/returns.
// https://github.com/dotnet/linker/issues/2632
// https://github.com/dotnet/linker/issues/2158
case IDynamicMemberReferenceOperation:
case IDynamicIndexerAccessOperation:
// Assignment to dynamic member/indexer will translate into a call to runtime binder methods
// which should produce warnings, but isn't relevant for dataflow.
Visit(targetOperation, state);
break;
// Keep these cases in sync with those in CapturedReferenceValue, for any that
// can show up in a flow capture reference (for example, where the right-hand side
// is a null-coalescing operator).
default:
UnexpectedOperationHandler.Handle(targetOperation);
break;
}
return GetAssignmentValue();
TValue GetAssignmentValue()
{
if (precomputedValue.HasValue)
return precomputedValue.Value;
Debug.Assert(valueOperation is not null);
return valueOperation is null ? TopValue : Visit(valueOperation, state);
}
}
public override TValue VisitSimpleAssignment(ISimpleAssignmentOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
{
return ProcessAssignment(operation, state);
}
public override TValue VisitIsPattern(IIsPatternOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
{
if (operation.Pattern is IDeclarationPatternOperation declarationPattern)
{
// A declaration pattern is like an assignment to a local
return ProcessSingleTargetAssignment(
declarationPattern,
operation.Value,
operation,
state,
merge: false
);
}
return base.VisitIsPattern(operation, state);
}
public override TValue VisitPropertySubpattern(IPropertySubpatternOperation propPattern, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
{
if (propPattern.Pattern is IDeclarationPatternOperation declPattern)
{
return ProcessSingleTargetAssignment(
declPattern,
propPattern.Member,
propPattern,
state,
merge: false
);
}
return base.VisitPropertySubpattern(propPattern, state);
}
public override TValue VisitCompoundAssignment(ICompoundAssignmentOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
{
return ProcessAssignment(operation, state);
}
// Note: this is called both for normal assignments and ICompoundAssignmentOperation.
// The resulting value of a compound assignment isn't important for our dataflow analysis
// (we don't model addition of integers, for example), so we just treat these the same
// as normal assignments.
private TValue ProcessAssignment(IAssignmentOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
{
var targetOperation = operation.Target;
if (targetOperation is not IFlowCaptureReferenceOperation flowCaptureReference)
return ProcessSingleTargetAssignment(targetOperation, operation, state, merge: false);
// Note: technically we should avoid visiting the target operation in ProcessNonCapturedAssignment when assigning
// to a flow capture reference, because this should be done when the capture is created.
// For example, a flow capture used as both an LValue and a RValue should only evaluate the expression that
// computes the object instance of a property reference once. However, we just visit the instance again below
// for simplicity. This could be generalized if we encounter a dataflow behavior where this makes a difference.
Debug.Assert(IsLValueFlowCapture(flowCaptureReference.Id));
Debug.Assert(flowCaptureReference.GetValueUsageInfo(OwningSymbol).HasFlag(ValueUsageInfo.Write));
var capturedReferences = state.Current.LocalState.CapturedReferences.Get(flowCaptureReference.Id);
Debug.Assert(!capturedReferences.IsUnknown());
if (!capturedReferences.HasMultipleValues)
{
// Single captured reference. Treat this as an overwriting assignment.
var enumerator = capturedReferences.GetKnownValues().GetEnumerator();
enumerator.MoveNext();
targetOperation = enumerator.Current.Reference;
return ProcessSingleTargetAssignment(targetOperation, operation, state, merge: false);
}
// The capture id may have captured multiple references, as in:
// (b ? ref v1 : ref v2) = value;
// We treat this as a possible write to each of the captured references,
// which requires merging with the previous values of each.
// Note: technically this should only visit the RHS of the assignment once.
// For now we visit the RHS in ProcessSingleTargetAssignment for simplicity, and
// rely on the warning deduplication to prevent this from producing multiple warnings
// if the RHS has dataflow warnings.
TValue value = TopValue;
foreach (var capturedReference in capturedReferences.GetKnownValues())
{
targetOperation = capturedReference.Reference;
var singleValue = ProcessSingleTargetAssignment(targetOperation, operation, state, merge: true);
value = LocalStateAndContextLattice.LocalStateLattice.Lattice.ValueLattice.Meet(value, singleValue);
}
return value;
}
private TValue ProcessAssignment(
IOperation targetOperation,
TValue value,
IOperation assignmentOperation,
LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state,
IReadOnlyDictionary<IOperation, TValue> savedTargetValues)
{
if (targetOperation is not IFlowCaptureReferenceOperation flowCaptureReference)
return ProcessSingleTargetAssignment(targetOperation, value, assignmentOperation, state, merge: false, savedTargetValues);
Debug.Assert(IsLValueFlowCapture(flowCaptureReference.Id));
var capturedReferences = state.Current.LocalState.CapturedReferences.Get(flowCaptureReference.Id);
Debug.Assert(!capturedReferences.IsUnknown());
if (!capturedReferences.HasMultipleValues)
{
var enumerator = capturedReferences.GetKnownValues().GetEnumerator();
enumerator.MoveNext();
return ProcessSingleTargetAssignment(enumerator.Current.Reference, value, assignmentOperation, state, merge: false, savedTargetValues);
}
TValue result = TopValue;
foreach (var capturedReference in capturedReferences.GetKnownValues())
{
TValue singleValue = ProcessSingleTargetAssignment(capturedReference.Reference, value, assignmentOperation, state, merge: true, savedTargetValues);
result = LocalStateAndContextLattice.LocalStateLattice.Lattice.ValueLattice.Meet(result, singleValue);
}
return result;
}
public override TValue VisitDeconstructionAssignment(
IDeconstructionAssignmentOperation operation,
LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
{
DeconstructionInfo deconstructionInfo;
if (operation.Syntax is AssignmentExpressionSyntax assignmentSyntax)
{
deconstructionInfo = _semanticModel.GetDeconstructionInfo(assignmentSyntax);
}
else if (operation.Syntax.FirstAncestorOrSelf<ForEachVariableStatementSyntax>() is ForEachVariableStatementSyntax foreachSyntax)
{
deconstructionInfo = _semanticModel.GetDeconstructionInfo(foreachSyntax);
}
else
{
UnexpectedOperationHandler.Handle(operation);
return Visit(operation.Value, state);
}
var savedTargetValues = new Dictionary<IOperation, TValue>();
// Identify target locations before evaluating the source, then reuse those values
// when writing instead of evaluating their sub-expressions again.
VisitDeconstructionTargetSideEffects(operation.Target, state, savedTargetValues);
ITypeSymbol? sourceType = operation.Value.Type;
IOperation source = UnwrapDeconstructionSource(operation.Value);
bool sourceValueIsKnown = source is not ITupleOperation;
TValue sourceValue = sourceValueIsKnown ? Visit(source, state) : TopValue;
// Deconstruction evaluates all source values before assigning any target. Keeping these
// phases separate is required for assignments such as (first, second) = (second, first).
DeconstructionValue deconstructionValue = EvaluateDeconstruction(
operation.Target,
source,
sourceType,
sourceValue,
sourceValueIsKnown,
deconstructionInfo,
operation,
state);
if (deconstructionValue.DoesNotReturn)
{
state.Current = LocalStateAndContextLattice.Top;
return sourceValue;
}
AssignDeconstruction(
operation.Target,
deconstructionValue,
operation,
state,
savedTargetValues);
return sourceValue;
}
private readonly struct DeconstructionValue
{
public TValue Value { get; }
public ImmutableArray<DeconstructionValue> Nested { get; }
public bool IsInvalid { get; }
public bool DoesNotReturn { get; }
public DeconstructionValue(TValue value)
{
Value = value;
Nested = default;
IsInvalid = false;
DoesNotReturn = false;
}
public DeconstructionValue(ImmutableArray<DeconstructionValue> nested)
{
Value = default;
Nested = nested;
IsInvalid = false;
DoesNotReturn = false;
}
private DeconstructionValue(bool isInvalid, bool doesNotReturn)
{
Value = default;
Nested = default;
IsInvalid = isInvalid;
DoesNotReturn = doesNotReturn;
}
public static DeconstructionValue Invalid => new(isInvalid: true, doesNotReturn: false);
public static DeconstructionValue NonReturning => new(isInvalid: false, doesNotReturn: true);
}
private DeconstructionValue EvaluateDeconstruction(
IOperation target,
IOperation? source,
ITypeSymbol? sourceType,
TValue sourceValue,
bool sourceValueIsKnown,
DeconstructionInfo deconstructionInfo,
IDeconstructionAssignmentOperation operation,
LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
{
target = UnwrapDeconstructionTarget(target);
if (deconstructionInfo.Nested.IsDefaultOrEmpty)
{
if (target is ITupleOperation)
{
UnexpectedOperationHandler.Handle(target);
return DeconstructionValue.Invalid;
}
sourceValue = GetDeconstructionSourceValue(source, sourceValue, sourceValueIsKnown, state);
return new DeconstructionValue(
deconstructionInfo.Conversion is { MethodSymbol: IMethodSymbol conversionOperator }
? GetConversionValue(conversionOperator, sourceValue)
: sourceValue);
}
if (target is not ITupleOperation targetTuple ||
targetTuple.Elements.Length != deconstructionInfo.Nested.Length)
{
UnexpectedOperationHandler.Handle(target);
return DeconstructionValue.Invalid;
}
if (deconstructionInfo.Method is IMethodSymbol deconstructMethod)
{
sourceValue = GetDeconstructionSourceValue(source, sourceValue, sourceValueIsKnown, state);
bool isExtensionMethod = deconstructMethod.IsExtensionMethod;
bool hasReceiverArgument = isExtensionMethod || deconstructMethod.HasExtensionParameterOnType();
int outputParameterOffset = isExtensionMethod ? 1 : 0;
int metadataParameterCount = deconstructMethod.GetMetadataParametersCount();
if (metadataParameterCount != targetTuple.Elements.Length + (hasReceiverArgument ? 1 : 0))
{
UnexpectedOperationHandler.Handle(operation);
return DeconstructionValue.Invalid;
}
var arguments = ImmutableArray.CreateBuilder<TValue>(metadataParameterCount);
TValue instanceValue = sourceValue;
if (hasReceiverArgument)
{
instanceValue = TopValue;
arguments.Add(sourceValue);
}
while (arguments.Count < metadataParameterCount)
arguments.Add(TopValue);
// Key this synthesized Deconstruct() call on 'target', not 'source': 'source' may
// already be registered elsewhere under its own unrelated call pattern (e.g. if it's
// a method call), and reusing it here would incorrectly merge the two.
HandleMethodCall(
deconstructMethod,
instanceValue,
arguments.MoveToImmutable(),
target,
state.Current.Context);
if (deconstructMethod.TryGetAttribute(nameof(DoesNotReturnAttribute), out _))
return DeconstructionValue.NonReturning;
IParameterSymbol? receiverParameter = isExtensionMethod
? deconstructMethod.Parameters[0]
: deconstructMethod.ContainingType.ExtensionParameter;
if (receiverParameter is not null && source is not null)
ApplyDoesNotReturnIfCondition(receiverParameter, source, state);
var nestedValues = ImmutableArray.CreateBuilder<DeconstructionValue>(targetTuple.Elements.Length);
for (int i = 0; i < targetTuple.Elements.Length; i++)
{
IParameterSymbol outputParameter = deconstructMethod.Parameters[i + outputParameterOffset];
DeconstructionValue nestedValue = EvaluateDeconstruction(
targetTuple.Elements[i],
source: null,
outputParameter.Type,
GetParameterTargetValue(outputParameter),
sourceValueIsKnown: true,
deconstructionInfo.Nested[i],
operation,
state);
if (nestedValue.DoesNotReturn)
return DeconstructionValue.NonReturning;
nestedValues.Add(nestedValue);
}
return new DeconstructionValue(nestedValues.MoveToImmutable());
}
if (source is ITupleOperation sourceTuple &&
sourceTuple.Elements.Length == targetTuple.Elements.Length)
{
var nestedValues = ImmutableArray.CreateBuilder<DeconstructionValue>(targetTuple.Elements.Length);
for (int i = 0; i < targetTuple.Elements.Length; i++)
{
IOperation sourceElement = UnwrapDeconstructionSource(sourceTuple.Elements[i]);
DeconstructionValue nestedValue = EvaluateDeconstruction(
targetTuple.Elements[i],
sourceElement,
sourceElement.Type,
default,
sourceValueIsKnown: false,
deconstructionInfo.Nested[i],
operation,
state);
if (nestedValue.DoesNotReturn)
return DeconstructionValue.NonReturning;
nestedValues.Add(nestedValue);
}
return new DeconstructionValue(nestedValues.MoveToImmutable());
}
if (sourceType is not INamedTypeSymbol { IsTupleType: true } tupleType ||
tupleType.TupleElements.Length != targetTuple.Elements.Length)
{
UnexpectedOperationHandler.Handle(operation.Value);
return DeconstructionValue.Invalid;
}
var tupleValues = ImmutableArray.CreateBuilder<DeconstructionValue>(targetTuple.Elements.Length);
for (int i = 0; i < targetTuple.Elements.Length; i++)
{
IFieldSymbol tupleElement = tupleType.TupleElements[i];
DeconstructionValue tupleValue = EvaluateDeconstruction(
targetTuple.Elements[i],
source: null,
tupleElement.Type,
GetTupleElementValue(tupleElement),
sourceValueIsKnown: true,
deconstructionInfo.Nested[i],
operation,
state);
if (tupleValue.DoesNotReturn)
return DeconstructionValue.NonReturning;
tupleValues.Add(tupleValue);
}
return new DeconstructionValue(tupleValues.MoveToImmutable());
}
private void AssignDeconstruction(
IOperation target,
DeconstructionValue value,
IDeconstructionAssignmentOperation operation,
LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state,
IReadOnlyDictionary<IOperation, TValue> savedTargetValues)
{
if (value.IsInvalid)
return;
target = UnwrapDeconstructionTarget(target);
if (value.Nested.IsDefaultOrEmpty)
{
ProcessAssignment(target, value.Value, target, state, savedTargetValues);
return;
}
if (target is not ITupleOperation targetTuple ||
targetTuple.Elements.Length != value.Nested.Length)
{
UnexpectedOperationHandler.Handle(operation);
return;
}
for (int i = 0; i < targetTuple.Elements.Length; i++)
AssignDeconstruction(targetTuple.Elements[i], value.Nested[i], operation, state, savedTargetValues);
}
private TValue GetDeconstructionSourceValue(
IOperation? source,
TValue sourceValue,
bool sourceValueIsKnown,
LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
{
if (sourceValueIsKnown)
return sourceValue;
Debug.Assert(source is not null);
return source is null ? TopValue : Visit(source, state);
}
// Evaluates a deconstruction target sub-expression and remembers its value, so that the
// write can reuse it later instead of evaluating the expression a second time.
private void EvaluateTargetSubExpression(
IOperation? operation,
LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state,
Dictionary<IOperation, TValue> savedTargetValues,
bool useStateCapturedTargetValues)
{
if (operation is null)
return;
TValue value;
CapturedTargetValue<TValue> capturedTargetValue = state.Current.LocalState.CapturedTargetValues.Get(new CapturedTargetKey(operation));
if (useStateCapturedTargetValues && capturedTargetValue.HasValue)
value = capturedTargetValue.Value;
else
value = Visit(operation, state);
savedTargetValues[operation] = value;
}
// Returns the value of an assignment target's sub-expression, reusing the value from
// EvaluateTargetSubExpression if it was already evaluated for a deconstruction. For an
// ordinary assignment nothing was evaluated ahead of time, so this just visits it.
private TValue VisitTargetSubExpression(
IOperation? operation,
LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state,
IReadOnlyDictionary<IOperation, TValue>? savedTargetValues)
{
if (operation is not null &&
savedTargetValues is not null &&
savedTargetValues.TryGetValue(operation, out TValue value))
{
return value;
}
if (operation is not null &&
state.Current.LocalState.CapturedTargetValues.Get(new CapturedTargetKey(operation)) is { HasValue: true } capturedValue)
{
return capturedValue.Value;
}
return Visit(operation, state);
}
// Visits the side-effecting sub-expressions that identify a deconstruction target location
// (a property/indexer receiver, or an array reference and its index), without performing
// any write. This runs before the source is visited, so that e.g. 'arr' in
// '(arr[i], b) = (x, y)' is evaluated before 'x'/'y' are read, matching left-to-right
// evaluation order and Roslyn's own lowering (GetAssignmentTargetsAndSideEffects).
//
// Note: IPropertyReferenceOperation's explicit indexer Arguments are intentionally NOT
// visited here, to match ProcessSingleTargetAssignment's IPropertyReferenceOperation case,
// which today only visits those Arguments after the value (a known, pre-existing ordering
// quirk - unlike the implicit System.Index-based indexer and array-element cases below,
// which already visit their index arguments before the value). If that quirk is ever fixed
// in ProcessSingleTargetAssignment, this case should be updated to match.
private void VisitDeconstructionTargetSideEffects(
IOperation target,
LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state,
Dictionary<IOperation, TValue> savedTargetValues,
bool useStateCapturedTargetValues = true)
{
target = UnwrapDeconstructionTarget(target);
if (target is ITupleOperation targetTuple)
{
foreach (var element in targetTuple.Elements)
VisitDeconstructionTargetSideEffects(element, state, savedTargetValues, useStateCapturedTargetValues);
return;
}
switch (target)
{
case IFlowCaptureReferenceOperation flowCaptureReference:
Debug.Assert(IsLValueFlowCapture(flowCaptureReference.Id));
var capturedReferences = state.Current.LocalState.CapturedReferences.Get(flowCaptureReference.Id);
Debug.Assert(!capturedReferences.IsUnknown());
foreach (var capturedReference in capturedReferences.GetKnownValues())
VisitDeconstructionTargetSideEffects(capturedReference.Reference, state, savedTargetValues, useStateCapturedTargetValues);
break;
case IFieldReferenceOperation fieldRef:
EvaluateTargetSubExpression(fieldRef.Instance, state, savedTargetValues, useStateCapturedTargetValues);
break;
case IPropertyReferenceOperation propertyRef:
// Avoid visiting the property reference itself; see the similar comment in
// ProcessSingleTargetAssignment about https://github.com/dotnet/roslyn/issues/25057.
EvaluateTargetSubExpression(propertyRef.Instance, state, savedTargetValues, useStateCapturedTargetValues);
break;
case IEventReferenceOperation eventRef:
EvaluateTargetSubExpression(eventRef.Instance, state, savedTargetValues, useStateCapturedTargetValues);
break;
case IArrayElementReferenceOperation arrayElementRef:
EvaluateTargetSubExpression(arrayElementRef.ArrayReference, state, savedTargetValues, useStateCapturedTargetValues);
foreach (var index in arrayElementRef.Indices)
EvaluateTargetSubExpression(index, state, savedTargetValues, useStateCapturedTargetValues);
break;
case IInlineArrayAccessOperation inlineArrayAccess:
EvaluateTargetSubExpression(inlineArrayAccess.Instance, state, savedTargetValues, useStateCapturedTargetValues);
EvaluateTargetSubExpression(inlineArrayAccess.Argument, state, savedTargetValues, useStateCapturedTargetValues);
break;
case IImplicitIndexerReferenceOperation indexerRef:
EvaluateTargetSubExpression(indexerRef.Instance, state, savedTargetValues, useStateCapturedTargetValues);
EvaluateTargetSubExpression(indexerRef.Argument, state, savedTargetValues, useStateCapturedTargetValues);
break;
default:
// Locals, parameters, discards, and declaration expressions have no
// side-effecting sub-expression to evaluate ahead of the source.
break;
}
}
private static IOperation UnwrapDeconstructionTarget(IOperation target)
{
while (true)
{
switch (target)
{
case IDeclarationExpressionOperation declaration:
target = declaration.Expression;
continue;
case IConversionOperation conversion:
target = conversion.Operand;
continue;
case IParenthesizedOperation parenthesized:
target = parenthesized.Operand;
continue;
default:
return target;
}
}
}
private static IOperation UnwrapDeconstructionSource(IOperation source)
{
while (source is IConversionOperation { OperatorMethod: null } conversion)
source = conversion.Operand;
return source;
}
public override TValue VisitEventAssignment(IEventAssignmentOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
{
var eventReference = (IEventReferenceOperation)operation.EventReference;
TValue instanceValue = Visit(eventReference.Instance, state);
TValue value = Visit(operation.HandlerValue, state);
if (operation.Adds)
{
IMethodSymbol? addMethod = eventReference.Event.AddMethod;
Debug.Assert(addMethod != null);
if (addMethod != null)
HandleMethodCallHelper(addMethod, instanceValue, ImmutableArray.Create(value), operation, state);
return value;
}
else
{
IMethodSymbol? removeMethod = eventReference.Event.RemoveMethod;
Debug.Assert(removeMethod != null);
if (removeMethod != null)
HandleMethodCallHelper(removeMethod, instanceValue, ImmutableArray.Create(value), operation, state);
return value;
}
}
private TValue GetFlowCaptureValue(IFlowCaptureReferenceOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
{
Debug.Assert(!IsLValueFlowCapture(operation.Id),
$"{operation.Syntax.GetLocation().GetLineSpan()}");
Debug.Assert(operation.GetValueUsageInfo(OwningSymbol).HasFlag(ValueUsageInfo.Read),
$"{operation.Syntax.GetLocation().GetLineSpan()}");
return state.Get(new LocalKey(operation.Id));
}
// Similar to VisitLocalReference
public override TValue VisitFlowCaptureReference(IFlowCaptureReferenceOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
{
if (operation.IsInitialization)
{
// This capture reference is a temporary byref. This can happen for string
// interpolation handlers: https://github.com/dotnet/roslyn/issues/57484.
// Should really be treated as creating a new l-value flow capture,
// but this is likely irrelevant for dataflow analysis.
// LValueFlowCaptureProvider doesn't take into account IsInitialization = true,
// so it doesn't properly detect this as an l-value capture.
// Context: https://github.com/dotnet/roslyn/issues/60757
// Debug.Assert(IsLValueFlowCapture(operation.Id));
Debug.Assert(operation.GetValueUsageInfo(OwningSymbol).HasFlag(ValueUsageInfo.Write),
$"{operation.Syntax.GetLocation().GetLineSpan()}");
Debug.Assert(operation.GetValueUsageInfo(OwningSymbol).HasFlag(ValueUsageInfo.Reference),
$"{operation.Syntax.GetLocation().GetLineSpan()}");
return TopValue;
}
if (operation.GetValueUsageInfo(OwningSymbol).HasFlag(ValueUsageInfo.Write))
{
// If we get here, it means we're visiting a flow capture reference that may be
// assigned to. Similar to the IsInitialization case, this can happen for an out param
// where the variable is declared before being passed as an out param, for example:
// string s;
// Method(out s, b ? 0 : 1);
// The second argument is necessary to create multiple branches so that the compiler
// turns both arguments into flow capture references, instead of just passing a local
// reference for s.
// This is also reachable for writes which are not byrefs, because increment/decrement
// (IIncrementOrDecrementOperation) and coalescing assignment (ICoalesceAssignmentOperation)
// are not handled here and fall back to the base visitor, which visits the write target
// directly. Enabling the following assert requires handling those first.
// Debug.Assert(operation.GetValueUsageInfo(OwningSymbol).HasFlag(ValueUsageInfo.Reference),
// $"{operation.Syntax.GetLocation().GetLineSpan()}");
return TopValue;
}
return GetFlowCaptureValue(operation, state);
}
// Similar to VisitSimpleAssignment when assigning to a local, but for values which are captured without a
// corresponding local variable. The "flow capture" is like a local assignment, and the "flow capture reference"
// is like a local reference.
public override TValue VisitFlowCapture(IFlowCaptureOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
{
if (IsLValueFlowCapture(operation.Id))
{
// Should never see an l-value flow capture of another flow capture.
Debug.Assert(operation.Value is not IFlowCaptureReferenceOperation);
if (operation.Value is IFlowCaptureReferenceOperation)
return TopValue;
Dictionary<IOperation, TValue>? evaluatedTargetValues = null;
if (_deconstructionLValueFlowCaptures.Contains(operation.Id))
{
evaluatedTargetValues = new Dictionary<IOperation, TValue>();
VisitDeconstructionTargetSideEffects(
operation.Value,
state,
evaluatedTargetValues,
useStateCapturedTargetValues: false);
}
var capturedRef = new CapturedReferenceValue(operation.Value);
var currentState = state.Current;
currentState.LocalState.CapturedReferences.Set(operation.Id, capturedRef);
if (evaluatedTargetValues is not null)
{
foreach (var evaluatedTargetValue in evaluatedTargetValues)
{
currentState.LocalState.CapturedTargetValues.Set(
new CapturedTargetKey(evaluatedTargetValue.Key),
new CapturedTargetValue<TValue>(evaluatedTargetValue.Value));
}
}
state.Current = currentState;
return TopValue;
}
else
{
TValue capturedValue;
if (operation.Value is IFlowCaptureReferenceOperation captureRef)
{
if (IsLValueFlowCapture(captureRef.Id))
{
// If an r-value captures an l-value, we must dereference the l-value
// and copy out the value to capture.
capturedValue = TopValue;
var capturedReferences = state.Current.LocalState.CapturedReferences.Get(captureRef.Id);
Debug.Assert(!capturedReferences.IsUnknown());
foreach (var capturedReference in capturedReferences.GetKnownValues())
{
var value = Visit(capturedReference.Reference, state);
capturedValue = LocalStateAndContextLattice.LocalStateLattice.Lattice.ValueLattice.Meet(capturedValue, value);
}
}
else
{
capturedValue = state.Get(new LocalKey(captureRef.Id));
}
}
else
{
capturedValue = Visit(operation.Value, state);
}
state.Set(new LocalKey(operation.Id), capturedValue);
return capturedValue;
}
}
public override TValue VisitExpressionStatement(IExpressionStatementOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
{
Visit(operation.Operation, state);
return TopValue;
}
public override TValue VisitInvocation(IInvocationOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
=> ProcessMethodCall(operation, operation.TargetMethod, operation.Instance, operation.Arguments, state);
public override TValue VisitDelegateCreation(IDelegateCreationOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
{
Visit(operation.Target, state);
IMethodSymbol? targetMethodSymbol = null;
switch (operation.Target)
{
case IFlowAnonymousFunctionOperation lambda:
// Tracking lambdas is handled by normal visiting logic for IFlowAnonymousFunctionOperation.
// Instance of a lambda or local function should be the instance of the containing method.
// Don't need to track a dataflow value, since the delegate creation will warn if the
// lambda or local function has an annotated this parameter.
targetMethodSymbol = lambda.Symbol;
break;
case IMethodReferenceOperation methodReference:
IMethodSymbol methodDefinition = methodReference.Method.OriginalDefinition;
if (methodDefinition.ContainingSymbol is IMethodSymbol)
{
// Track references to local functions
var localFunction = methodDefinition;
Debug.Assert(localFunction.MethodKind == MethodKind.LocalFunction);
var localFunctionCFG = ControlFlowGraph.GetLocalFunctionControlFlowGraphInScope(localFunction);
InterproceduralState.TrackMethod(new MethodBodyValue(localFunction, localFunctionCFG));
}
targetMethodSymbol = methodReference.Method;
break;
case IMemberReferenceOperation:
case IInvocationOperation:
// No method symbol.
break;
default:
UnexpectedOperationHandler.Handle(operation.Target);
break;
}
if (targetMethodSymbol == null)
return TopValue;
return HandleDelegateCreation(targetMethodSymbol, operation, state.Current.Context);
}
public abstract TValue HandleDelegateCreation(IMethodSymbol methodReference, IOperation operation, in TContext context);
public override TValue VisitPropertyReference(IPropertyReferenceOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
{
if (operation.GetValueUsageInfo(OwningSymbol).HasFlag(ValueUsageInfo.Write))
{
// Property references may be passed as ref/out parameters.
// This is also reachable for writes which are not byrefs, because increment/decrement
// (IIncrementOrDecrementOperation) and coalescing assignment (ICoalesceAssignmentOperation)
// are not handled here and fall back to the base visitor, which visits the write target
// directly. Enabling the following assert requires handling those first.
// Debug.Assert(operation.GetValueUsageInfo(OwningSymbol).HasFlag(ValueUsageInfo.Reference),
// $"{operation.Syntax.GetLocation().GetLineSpan()}");
return TopValue;
}
// Accessing property for reading is really a call to the getter
// The setter case is handled in assignment operation since here we don't have access to the value to pass to the setter
TValue instanceValue = Visit(operation.Instance, state);
IMethodSymbol getMethod = operation.Property.GetGetMethod()!;
ImmutableArray<TValue>.Builder arguments = ImmutableArray.CreateBuilder<TValue>();
// Handle C# 14 extension property access (see comment in ProcessMethodCall)
if (getMethod.HasExtensionParameterOnType())
{
arguments.Add(instanceValue);
instanceValue = TopValue;
}
// Property may be an indexer, in which case there will be one or more index arguments
foreach (var val in operation.Arguments)
arguments.Add(Visit(val, state));
return HandleMethodCallHelper(getMethod, instanceValue, arguments.ToImmutableArray(), operation, state);
}
public override TValue VisitEventReference(IEventReferenceOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
{
// Writing to an event should not go through this path.
Debug.Assert(operation.GetValueUsageInfo(OwningSymbol).HasFlag(ValueUsageInfo.Read));
if (!operation.GetValueUsageInfo(OwningSymbol).HasFlag(ValueUsageInfo.Read))
return TopValue;
Visit(operation.Instance, state);
// Accessing event for reading retrieves the event delegate from the event's backing field,
// so there is no method call to handle.
return TopValue;
}
public override TValue VisitImplicitIndexerReference(IImplicitIndexerReferenceOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
{
if (operation.GetValueUsageInfo(OwningSymbol).HasFlag(ValueUsageInfo.Write))
{
// Implicit indexer references may be passed as ref/out parameters.
Debug.Assert(operation.GetValueUsageInfo(OwningSymbol).HasFlag(ValueUsageInfo.Reference));
return TopValue;
}
TValue instanceValue = Visit(operation.Instance, state);
TValue indexArgumentValue = Visit(operation.Argument, state);
if (operation.IndexerSymbol is not IPropertySymbol indexerProperty)
{
// For example, System.Span<T>.Slice(int, int).
// Don't try to handle it for now.
return TopValue;
}
IMethodSymbol getMethod = indexerProperty.GetGetMethod()!;
return HandleMethodCallHelper(getMethod, instanceValue, ImmutableArray.Create(indexArgumentValue), operation, state);
}
public override TValue VisitArrayElementReference(IArrayElementReferenceOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
{
if (!operation.GetValueUsageInfo(OwningSymbol).HasFlag(ValueUsageInfo.Read))
return TopValue;
// Accessing an array element for reading is a call to the indexer
// or a plain array access. Just handle plain array access for now.
// Only handle simple index access
if (operation.Indices.Length != 1)
return TopValue;
return HandleArrayElementRead(Visit(operation.ArrayReference, state), Visit(operation.Indices[0], state), operation);
}
public override TValue VisitInlineArrayAccess(IInlineArrayAccessOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
{
Debug.Assert(operation.GetValueUsageInfo(OwningSymbol).HasFlag(ValueUsageInfo.Read));
if (!operation.GetValueUsageInfo(OwningSymbol).HasFlag(ValueUsageInfo.Read))
return TopValue;
return HandleArrayElementRead(Visit(operation.Instance, state), Visit(operation.Argument, state), operation);
}
public override TValue VisitArgument(IArgumentOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
{
return Visit(operation.Value, state);
}
public override TValue VisitReturn(IReturnOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
{
if (operation.ReturnedValue != null)
{
var value = Visit(operation.ReturnedValue, state);
var current = state.Current;
HandleReturnValue(value, operation, in current.Context);
return value;
}
return TopValue;
}
public override TValue VisitConversion(IConversionOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
{
var operandValue = Visit(operation.Operand, state);
return operation.OperatorMethod == null ? operandValue : TopValue;
}
public override TValue VisitObjectCreation(IObjectCreationOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
{
if (operation.Constructor == null)
return TopValue;
return ProcessMethodCall(operation, operation.Constructor, null, operation.Arguments, state);
}
public override TValue VisitFlowAnonymousFunction(IFlowAnonymousFunctionOperation operation, LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
{
// The containing symbol of a lambda is either another method, or a field (for field initializers).
// For property initializers, the containing symbol is the compiler-generated backing field.
// For property accessors, the containing symbol is the accessor method.
Debug.Assert(operation.Symbol.ContainingSymbol is IMethodSymbol or IFieldSymbol);
var lambda = operation.Symbol;
Debug.Assert(lambda.MethodKind == MethodKind.LambdaMethod);
var lambdaCFG = ControlFlowGraph.GetAnonymousFunctionControlFlowGraphInScope(operation);
InterproceduralState.TrackMethod(new MethodBodyValue(lambda, lambdaCFG));
return TopValue;
}
private TValue HandleMethodCallHelper(
IMethodSymbol calledMethod,
TValue instance,
ImmutableArray<TValue> arguments,
IOperation operation,
LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
{
var value = HandleMethodCall(calledMethod, instance, arguments, operation, state.Current.Context);
if (calledMethod.TryGetAttribute(nameof(DoesNotReturnAttribute), out var doesNotReturnAttributeData))
{
// If it doesn't return, then after the called method we are free to assume that we
// are in the Top state. This will have the correct behavior if meeting with another
// state in the CFG. For example:
// var x = SomeTrackedValue;
// if (SomeCondition)
// DoesNotReturn();
// WitnessState(x);
// The state at 'WitnessState(x)' and 'WitnessContext()' is computed as the meet of:
// - the output at the end of the block inside of the if (Top), and
// - the fall-through output of the block with the check (x: SomeTrackedValue).
// Because Top is the identity of meet, the state from before the if block is preserved,
// so 'WitnessState(x)' sees that x has 'SomeTrackedValue' in this case.
state.Current = LocalStateAndContextLattice.Top;
return value;
}
foreach (var parameterProxy in calledMethod.GetParameters())
{
if (parameterProxy.ParameterSymbol is not IParameterSymbol parameter)
continue;
if (!parameter.TryGetAttribute(nameof(DoesNotReturnIfAttribute), out _))
continue;
var argumentIndex = parameterProxy.MetadataIndex;
IArgumentOperation argumentOperation;
switch (operation)
{
case IInvocationOperation callOperation:
argumentOperation = callOperation.Arguments[argumentIndex];
break;
case IObjectCreationOperation callOperation:
argumentOperation = callOperation.Arguments[argumentIndex];
break;
default:
UnexpectedOperationHandler.Handle(operation);
continue;
}
;
ApplyDoesNotReturnIfCondition(parameter, argumentOperation, state);
}
return value;
}
private void ApplyDoesNotReturnIfCondition(
IParameterSymbol parameter,
IOperation argumentOperation,
LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
{
if (!parameter.TryGetAttribute(nameof(DoesNotReturnIfAttribute), out var attributeData) ||
attributeData.ConstructorArguments.Length != 1 ||
attributeData.ConstructorArguments[0] is not
{
Kind: TypedConstantKind.Primitive,
Value: bool doesNotReturnIfConditionValue
})
{
return;
}
// Get the condition value that is being asserted. If the attribute is DoesNotReturnIf(true),
// the condition value needs to be negated so that we can assert the false condition.
TConditionValue conditionValue = GetConditionValue(argumentOperation, state);
var current = state.Current;
ApplyCondition(
!doesNotReturnIfConditionValue
? conditionValue
: conditionValue.Negate(),
ref current);
state.Current = current;
}
private TValue ProcessMethodCall(
IOperation operation,
IMethodSymbol method,
IOperation? instance,
ImmutableArray<IArgumentOperation> arguments,
LocalDataFlowState<TValue, TContext, TValueLattice, TContextLattice> state)
{
TValue instanceValue = Visit(instance, state);
var argumentsBuilder = ImmutableArray.CreateBuilder<TValue>();
// For calls to C# 14 extensions, treat the instance argument as a regular argument.
// The extension method doesn't have an implicit this, yet (unlike for existing extension methods)
// the IOperation represents it as having an Instance.
if (method.HasExtensionParameterOnType())
{
argumentsBuilder.Add(instanceValue);
instanceValue = TopValue;
}
// Build a map from parameter to argument so we can handle named/out-of-order arguments correctly
var parameters = method.Parameters;
var argumentsByParameter = new TValue?[parameters.Length];
foreach (var argument in arguments)
{
// For __arglist argument there might not be any parameter
// __arglist is only legal as the last argument to a method and there's also no supported
// way for it to carry annotations or participate in data flow in any way, so it's OK to ignore it.
// Since it's always last, it's also OK to simply pass a shorter arguments array to the call.
if (argument?.Parameter == null)
break;
var parameter = argument.Parameter;
// Use Ordinal to get the parameter's position
int parameterIndex = parameter.Ordinal;
if (parameterIndex >= 0 && parameterIndex < parameters.Length)
{
argumentsByParameter[parameterIndex] = VisitArgument(argument, state);
}
}
// Add arguments in parameter order, using TopValue for missing arguments (e.g., optional parameters)
for (int i = 0; i < parameters.Length; i++)
{
argumentsBuilder.Add(argumentsByParameter[i] ?? TopValue);
}
// For local functions with generic arguments, the substituted method symbol's containing
// symbol is not the containing method, so we need to check the OriginalDefinition.
if (method.OriginalDefinition.ContainingSymbol is IMethodSymbol)
{
var localFunction = method.OriginalDefinition;
Debug.Assert(localFunction.MethodKind == MethodKind.LocalFunction);
var localFunctionCFG = ControlFlowGraph.GetLocalFunctionControlFlowGraphInScope(localFunction);
InterproceduralState.TrackMethod(new MethodBodyValue(localFunction, localFunctionCFG));
}
return HandleMethodCallHelper(
method,
instanceValue,
argumentsBuilder.ToImmutableArray(),
operation,
state);
}
}
}