File: Microsoft.NetCore.Analyzers\Tasks\UseValueTasksCorrectly.cs
Web Access
Project: src\sdk\src\Microsoft.CodeAnalysis.NetAnalyzers\src\Microsoft.CodeAnalysis.NetAnalyzers\Microsoft.CodeAnalysis.NetAnalyzers.csproj (Microsoft.CodeAnalysis.NetAnalyzers)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.FlowAnalysis;
using Microsoft.CodeAnalysis.Operations;

namespace Microsoft.NetCore.Analyzers.Tasks
{
    using static MicrosoftNetCoreAnalyzersResources;

    /// <summary>
    /// CA2012: <inheritdoc cref="UseValueTasksCorrectlyTitle"/>
    /// </summary>
    [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
    public sealed class UseValueTasksCorrectlyAnalyzer : DiagnosticAnalyzer
    {
        internal const string RuleId = "CA2012";

        private static readonly LocalizableString s_localizableTitle = CreateLocalizableResourceString(nameof(UseValueTasksCorrectlyTitle));
        private static readonly LocalizableString s_localizableDescription = CreateLocalizableResourceString(nameof(UseValueTasksCorrectlyDescription));

        internal static readonly DiagnosticDescriptor GeneralRule = DiagnosticDescriptorHelper.Create(
            RuleId,
            s_localizableTitle,
            CreateLocalizableResourceString(nameof(UseValueTasksCorrectlyMessage_General)),
            DiagnosticCategory.Reliability,
            RuleLevel.IdeSuggestion,
            s_localizableDescription,
            isPortedFxCopRule: false,
            isDataflowRule: false);

        internal static readonly DiagnosticDescriptor UnconsumedRule = DiagnosticDescriptorHelper.Create(
            RuleId,
            s_localizableTitle,
            CreateLocalizableResourceString(nameof(UseValueTasksCorrectlyMessage_Unconsumed)),
            DiagnosticCategory.Reliability,
            RuleLevel.IdeSuggestion,
            s_localizableDescription,
            isPortedFxCopRule: false,
            isDataflowRule: false);

        internal static readonly DiagnosticDescriptor DoubleConsumptionRule = DiagnosticDescriptorHelper.Create(
            RuleId,
            s_localizableTitle,
            CreateLocalizableResourceString(nameof(UseValueTasksCorrectlyMessage_DoubleConsumption)),
            DiagnosticCategory.Reliability,
            RuleLevel.IdeSuggestion,
            s_localizableDescription,
            isPortedFxCopRule: false,
            isDataflowRule: false);

        internal static readonly DiagnosticDescriptor AccessingIncompleteResultRule = DiagnosticDescriptorHelper.Create(
            RuleId,
            s_localizableTitle,
            CreateLocalizableResourceString(nameof(UseValueTasksCorrectlyMessage_AccessingIncompleteResult)),
            DiagnosticCategory.Reliability,
            RuleLevel.IdeSuggestion,
            s_localizableDescription,
            isPortedFxCopRule: false,
            isDataflowRule: false);

        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(GeneralRule, UnconsumedRule, DoubleConsumptionRule, AccessingIncompleteResultRule);

        public sealed override void Initialize(AnalysisContext context)
        {
            context.EnableConcurrentExecution();
            context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
            context.RegisterCompilationStartAction(compilationContext =>
            {
                var typeProvider = WellKnownTypeProvider.GetOrCreate(compilationContext.Compilation);

                // Get the target ValueTask / ValueTask<T> types. If they don't exist, nothing more to do.
                if (!typeProvider.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingTasksValueTask, out var valueTaskType) ||
                    !typeProvider.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingTasksValueTask1, out var valueTaskOfTType))
                {
                    return;
                }

                // Get the type for System.Diagnostics.Debug. If we can't find it, that's ok, we just won't use it.
                var debugType = typeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemDiagnosticsDebug);

                // Process all invocations. This analyzer works by finding all method invocations that return ValueTasks,
                // and then analyzing those invocations and their surroundings.
                compilationContext.RegisterOperationAction(operationContext =>
                {
                    var operation = operationContext.Operation;
                    var invocation = (IInvocationOperation)operation;

                    // Does the method return ValueTask?  If not, we're done.
                    if (!valueTaskType.Equals(invocation.TargetMethod.ReturnType) &&
                        !valueTaskOfTType.Equals(invocation.TargetMethod.ReturnType.OriginalDefinition))
                    {
                        return;
                    }

                    // The only method that returns a ValueTask on which we allow unlimited later consumption
                    // is ValueTask.Preserve. Use is rare, but special-case it. If this is Preserve, we're done.
                    if (invocation.TargetMethod.Name == nameof(ValueTask.Preserve) &&
                        (valueTaskType.Equals(invocation.TargetMethod.ContainingType) ||
                         valueTaskOfTType.Equals(invocation.TargetMethod.ContainingType.OriginalDefinition)))
                    {
                        return;
                    }

                    // If this is a method call off of the ValueTask, then check to see if it's special.
                    if (invocation.Parent is IInvocationOperation parentIo)
                    {
                        switch (parentIo.TargetMethod.Name)
                        {
                            case nameof(ValueTask.AsTask):
                            case nameof(ValueTask.Preserve):
                                // Using AsTask to convert to a Task is acceptable consumption.
                                // Use Preserve to enable subsequent unlimited consumption is acceptable.
                                return;

                            case nameof(ValueTask.GetAwaiter) when parentIo.Parent is IInvocationOperation { TargetMethod.Name: nameof(ValueTaskAwaiter.GetResult) }:
                                // Warn! Trying to block waiting for a value task isn't supported.
                                operationContext.ReportDiagnostic(invocation.CreateDiagnostic(AccessingIncompleteResultRule));
                                return;

                            case nameof(ValueTask.ConfigureAwait):
                                // ConfigureAwait returns another awaitable. Use that one instead for subsequent analysis.
                                operation = invocation = parentIo;
                                break;
                        }
                    }
                    else if (invocation.Parent is IPropertyReferenceOperation { Property.Name: nameof(ValueTask<int>.Result) })
                    {
                        operationContext.ReportDiagnostic(invocation.CreateDiagnostic(AccessingIncompleteResultRule));
                        return;
                    }
                    // Just let any other direct member access fall through to be a general warning.

                    while (operation.Parent != null) // for walking up the operation tree if necessary
                    {
                        switch (operation.Parent.Kind)
                        {
                            case OperationKind.Await:
                            case OperationKind.Return:
                                // The "99% case" is just awaiting the awaitable. Such usage is good.
                                // The "0.9% case" is delegating to and returning another call's returned awaitable. Also good.
                                return;

                            case OperationKind.Argument:
                                // The "0.09% case" is passing the result of a call directly as an argument to another method.
                                // This could later result in a problem, as now there's a parameter inside the callee that's
                                // holding on to the awaitable, and it could await it twice... but the caller is still correct,
                                // and this analyzer does not perform inter-method analysis.
                                var arg = (IArgumentOperation)operation.Parent;
                                if (arg.Parameter != null)
                                {
                                    var originalType = arg.Parameter.Type.OriginalDefinition;
                                    if (originalType.Equals(valueTaskType) || originalType.Equals(valueTaskOfTType))
                                    {
                                        // However, it's really only expected when the parameter type is explicitly a ValueTask{<T>};
                                        // if it's just, say, a TValue, we're likely on a bad path, such as storing the instance into
                                        // a collection of some kind, e.g. Dictionary<string, ValueTask>.Add(..., vt).
                                        var originalParameter = arg.Parameter.OriginalDefinition;
                                        if (originalParameter.Type.Kind != SymbolKind.TypeParameter)
                                        {
                                            return;
                                        }
                                    }
                                }

                                goto default;

                            case OperationKind.ExpressionStatement:
                            case OperationKind.Discard:
                            case OperationKind.DiscardPattern:
                            case OperationKind.SimpleAssignment when operation.Parent is ISimpleAssignmentOperation sao && sao.Target is IDiscardOperation:
                                // Warn! This is a statement or discard. The result should have been used.
                                operationContext.ReportDiagnostic(invocation.CreateDiagnostic(UnconsumedRule));
                                return;

                            case OperationKind.Conversion:
                                var conversion = (IConversionOperation)operation.Parent;
                                if (conversion.Conversion.IsIdentity)
                                {
                                    // Ignore identity conversions, which can pop in from time to time.
                                    operation = operation.Parent;
                                    continue;
                                }

                                goto default;

                            // At this point, we're "in the weeds", but there are still some rare-but-used valid patterns to check for.

                            case OperationKind.Coalesce:
                            case OperationKind.Conditional:
                            case OperationKind.ConditionalAccess:
                            case OperationKind.SwitchExpression:
                            case OperationKind.SwitchExpressionArm:
                                // This is a ternary, null conditional, or switch expression, so consider the parent expression instead.
                                operation = operation.Parent;
                                continue;

                            default:
                                // Handle atypical / difficult cases that require more analysis.
                                HandleAtypicalValueTaskUsage(operationContext, debugType, operation, invocation);
                                return;
                        }
                    }
                }, OperationKind.Invocation);
            });
        }

        /// <summary>Handles more complicated analysis to warn on more complicated erroneous usage patterns.</summary>
        private static void HandleAtypicalValueTaskUsage(OperationAnalysisContext operationContext, INamedTypeSymbol? debugType, IOperation operation, IInvocationOperation invocation)
        {
            if (TryGetLocalSymbolAssigned(operation.Parent, out var valueTypeSymbol, out var startingBlock))
            {
                // At this point, it's very likely misuse and we could warn. However, there are a few advanced
                // patterns where the ValueTask might be stored into a local and very carefully used. As value
                // tasks are more likely to be used in more performance-sensitive code, we don't want a lot of
                // false positives from using such patterns in such code. So, we try to special-case these
                // advanced patterns by looking at the control flow graph and each block in it. Starting from
                // the entry block, we look to see if there's any "consumption" of the value task in a block. If
                // there are multiple consumptions in that block, it's an immediate error, as you can only consume
                // a value task once. If there's only one consumption, but we've already seen a consumption
                // on the path that got us here, then it's also erroneous. Otherwise, we keep following
                // the flow graph. If we try to jump to a block that's already had the awaiter consumed and
                // the current block did as well, that's also erroneous. Along the way, we further track
                // Debug.Assert(vt.Is*) calls, for cases where the developer wants to inform the reader/analyzer that
                // direct access to the result is known to be ok. This is a heuristic, and there are various things
                // that can thwart it, but it's relatively simple, handles most cases well, and minimizes both false
                // positives and false negatives (e.g. we're not tracking copying the local, but that's rare). This
                // part of the analysis is also expensive, but it should be executed only very rarely.

                // Dictionary to track blocks we've already seen. The TValue for each block is the merged
                // flow state for all paths we've followed into that block.
                var seen = new Dictionary<BasicBlock, FlowState>();

                // Stack of blocks still left to investigate, starting with the entry block. Each entry
                // is the block and its flow state at the time we pushed it onto the stack.
                var stack = new Stack<(BasicBlock, FlowState)>();
                stack.Push((startingBlock, default));

                // Process all blocks until their aren't any remaining.
                while (stack.Count > 0)
                {
                    // Get the next block to process. If we've already seen it, skip it.
                    (var block, var blockState) = stack.Pop();
                    if (seen.ContainsKey(block))
                    {
                        continue;
                    }

                    // Analyze the block. This involves:
                    // - Checking if there are any asserts in the block that declare the ValueTask to have already completed.
                    // - Counting how many "consumptions" of the ValueTask there are in the block.
                    int assertsCompletionIndex = FindFirstAssertsCompletion(block, valueTypeSymbol, debugType);
                    int consumptions = CountConsumptions(block, valueTypeSymbol);
                    blockState.ThisConsumption = consumptions == 1;

                    // Check if the analysis reveals a problem for this block.
                    if (consumptions > 1 || (blockState.ThisConsumption && blockState.PreviousConsumption))
                    {
                        // Warn! Either this block consumed it twice, or it consumed it after a previous
                        // block in the flow to here also consumed it.
                        operationContext.ReportDiagnostic(invocation.CreateDiagnostic(DoubleConsumptionRule));
                        return;
                    }
                    else if (!blockState.KnownCompletion && blockState.ThisConsumption)
                    {
                        // There weren't any assertions prior to this block, and this block does have a consumption,
                        // but we don't know what kind. If it's an await, no problem, but if it's a direct result access
                        // that requires knowing that the ValueTask completed, we need to get more specific and find the first
                        // operation that tries such a direct access.
                        int directAccessIndex = FindFirstDirectResultAccess(block, valueTypeSymbol);
                        if (directAccessIndex >= 0 && (assertsCompletionIndex == -1 || assertsCompletionIndex > directAccessIndex))
                        {
                            // Warn! We found a direct result access without any asserts before it to assert it was completed.
                            operationContext.ReportDiagnostic(invocation.CreateDiagnostic(AccessingIncompleteResultRule));
                            return;
                        }
                    }

                    // We've finished analyzing this block, so add it to our tracking dictionary.
                    seen.Add(block, blockState);

                    // We now to follow the successors. When we flow to them, we update their flow state to highlight
                    // whether there were any asserts in this block, such that they can also consider completion asserted.
                    bool setFallthroughKnownCompletion = assertsCompletionIndex != -1;
                    bool setConditionalKnownCompletion = setFallthroughKnownCompletion;

                    // If there's a conditional successor, we not only need to follow it, we need to evaluate the
                    // condition. That condition might be something like "if (vt.IsCompleted)", in which case
                    // we need to flow that completion knowledge (just as with asserts) into the relevant branch.
                    if (block.ConditionalSuccessor?.Destination is BasicBlock conditional)
                    {
                        if (!(setFallthroughKnownCompletion | setConditionalKnownCompletion) ||
                            block.BranchValue != null)
                        {
                            bool? completionCondition = OperationImpliesCompletion(valueTypeSymbol, block.BranchValue);
                            switch (block.ConditionKind)
                            {
                                case ControlFlowConditionKind.WhenTrue when completionCondition == true:
                                case ControlFlowConditionKind.WhenFalse when completionCondition == false:
                                    setConditionalKnownCompletion = true;
                                    break;
                                case ControlFlowConditionKind.WhenTrue when completionCondition == false:
                                case ControlFlowConditionKind.WhenFalse when completionCondition == true:
                                    setFallthroughKnownCompletion = true;
                                    break;
                            }
                        }

                        HandleSuccessor(conditional, setConditionalKnownCompletion);
                    }

                    // If there's a fallback successor, follow it as well. We computed any necessary completion
                    // value previously, so just pass that along.
                    if (block.FallThroughSuccessor?.Destination is BasicBlock fallthrough)
                    {
                        HandleSuccessor(fallthrough, setFallthroughKnownCompletion);
                    }

                    // Processes a successor, determining whether to push it onto the evaluation stack or update
                    // the seen dictionary with the additional information we've gathered.
                    void HandleSuccessor(BasicBlock successor, bool setKnownCompletion)
                    {
                        if (seen.TryGetValue(successor, out var successorConsumption))
                        {
                            // We've previously seen the successor block. Check its state.
                            if (blockState.ThisConsumption && !successorConsumption.PreviousConsumption)
                            {
                                if (successorConsumption.ThisConsumption)
                                {
                                    // Warn! The ValueTask was consumed on another path into this block, and this block
                                    // also consumes it.
                                    operationContext.ReportDiagnostic(invocation.CreateDiagnostic(DoubleConsumptionRule));
                                    return;
                                }

                                // Update its flow state accordingly; a previous path into it hadn't consumed the ValueTask,
                                // but this one did. In contrast, we don't update the KnownCompletion state, because that
                                // requires all paths into the block to have known completion.
                                successorConsumption.PreviousConsumption = true;
                                seen[successor] = successorConsumption;
                            }
                        }
                        else
                        {
                            // Push the successor onto the evaluation stack.
                            var successorState = blockState;
                            successorState.PreviousConsumption |= blockState.ThisConsumption;
                            successorState.KnownCompletion |= setKnownCompletion;
                            stack.Push((successor, successorState));
                        }
                    }
                }

                // Couldn't prove there was a problem, so err on the side of false negatives and don't warn.
                return;
            }

            // Warn! There was some very atypical consumption of the ValueTask.
            operationContext.ReportDiagnostic(invocation.CreateDiagnostic(GeneralRule));
        }

        /// <summary>Represents the flow state for a basic block.</summary>
#pragma warning disable CA1815 // Override equals and operator equals on value types
        private struct FlowState
#pragma warning restore CA1815
        {
            /// <summary>Gets or sets whether the ValueTask was known to have completed, either due to an assert or a condition proving it.</summary>
            public bool KnownCompletion { get; set; }
            /// <summary>Gets or sets whether a previous block in the flow consumed the ValueTask.</summary>
            public bool PreviousConsumption { get; set; }
            /// <summary>Gets or sets whether this block consumed the ValueTask.</summary>
            public bool ThisConsumption { get; set; }
        }

        private static bool TryGetLocalSymbolAssigned(IOperation? operation, [NotNullWhen(true)] out ISymbol? symbol, [NotNullWhen(true)] out BasicBlock? startingBlock)
        {
            ControlFlowGraph? cfg;
            switch (operation?.Kind)
            {
                case OperationKind.VariableInitializer when operation.Parent is IVariableDeclaratorOperation decl:
                    if (decl.TryGetEnclosingControlFlowGraph(out cfg))
                    {
                        symbol = decl.Symbol;
                        startingBlock = cfg.GetEntry();
                        return true;
                    }

                    break;

                case OperationKind.SimpleAssignment:
                    var assn = (ISimpleAssignmentOperation)operation;
                    if (assn.TryGetEnclosingControlFlowGraph(out cfg))
                    {
                        switch (assn.Target)
                        {
                            case ILocalReferenceOperation local:
                                symbol = local.Local;
                                startingBlock = cfg.GetEntry();
                                return true;

                            case IParameterReferenceOperation parameter:
                                symbol = parameter.Parameter;
                                startingBlock = cfg.GetEntry();
                                return true;
                        }
                    }

                    break;
            }

            symbol = null;
            startingBlock = null;
            return false;
        }

        /// <summary>Counts the number of operations in the block that represent a consumption of the ValueTask, such as awaiting it or calling GetAwaiter().GetResult().</summary>
        /// <param name="block">The block to be searched.</param>
        /// <param name="valueTaskSymbol">The ValueTask symbol for which we're searching.</param>
        /// <returns>
        /// The number of found consumption operations. This is generally 0 or 1, but could be greater than 1 in the case
        /// of bad usage; it stops counting at 2, as there's no need to differentiate any values greater than 1.
        /// </returns>
        private static int CountConsumptions(BasicBlock block, ISymbol valueTaskSymbol)
        {
            int count = 0;
            foreach (var op in block.DescendantOperations())
            {
                if (IsLocalOrParameterSymbolReference(op, valueTaskSymbol) &&
                    op.Parent?.Kind switch
                    {
                        OperationKind.Await => true,
                        OperationKind.Return => true,
                        OperationKind.Argument => true,
                        OperationKind.Invocation => true, // e.g. AsTask()
                        OperationKind.PropertyReference when op.Parent is IPropertyReferenceOperation { Property.Name: "Result" } => true,
                        _ => false
                    })
                {
                    if (++count > 1)
                    {
                        // The only relevant values are 0 (no consumptions), 1 (valid consumption), and > 1 (too many consumptions in the same block).
                        // As such, we can stop iterating when we hit > 1.
                        break;
                    }
                }
            }

            return count;
        }

        /// <summary>Finds the first expression statement in the block that does Debug.Assert(vt.Is*).</summary>
        /// <param name="block">The block to be searched.</param>
        /// <param name="valueTaskSymbol">The ValueTask symbol for which we're searching.</param>
        /// <param name="debugType">The type of System.Diagnostics.Debug.</param>
        /// <returns>The index of the first Debug.Assert(vt.Is*) statement, or -1 if none was found.</returns>
        private static int FindFirstAssertsCompletion(BasicBlock block, ISymbol valueTaskSymbol, INamedTypeSymbol? debugType)
        {
            if (debugType != null)
            {
                for (var i = 0; i < block.Operations.Length; i++)
                {
                    if (block.Operations[i] is IExpressionStatementOperation stmt &&
                        stmt.Operation is IInvocationOperation assert &&
                        assert.TargetMethod?.Name == nameof(Debug.Assert) &&
                        assert.TargetMethod.ContainingType.Equals(debugType) &&
                        !assert.Arguments.IsEmpty &&
                        OperationImpliesCompletion(valueTaskSymbol, assert.Arguments[0].Value) == true)
                    {
                        return i;
                    }
                }
            }

            return -1;
        }

        /// <summary>Finds the first operation in the block containing a direct access to a ValueTask's result, e.g. GetAwaiter().GetResult().</summary>
        /// <param name="block">The block to be searched.</param>
        /// <param name="valueTaskSymbol">The ValueTask symbol for which we're searching.</param>
        /// <returns>The index found, or -1 if none could be found.</returns>
        private static int FindFirstDirectResultAccess(BasicBlock block, ISymbol valueTaskSymbol)
        {
            // First search the body of the block.
            var operations = block.Operations;
            for (int i = 0; i < operations.Length; i++)
            {
                foreach (var op in operations[i].DescendantsAndSelf())
                {
                    if (HasDirectResultAccess(op))
                    {
                        return i;
                    }
                }
            }

            // Also search the branch value.
            if (block.BranchValue != null)
            {
                foreach (var op in block.BranchValue.DescendantsAndSelf())
                {
                    if (HasDirectResultAccess(op))
                    {
                        return operations.Length;
                    }
                }
            }

            return -1;

            // Determines if the operation itself is a direct access to the ValueTask's Result or GetAwaiter().GetResult().
            bool HasDirectResultAccess(IOperation op) =>
                IsLocalOrParameterSymbolReference(op, valueTaskSymbol) &&
                op.Parent?.Kind switch
                {
                    OperationKind.PropertyReference when op.Parent is IPropertyReferenceOperation { Property.Name: nameof(ValueTask<int>.Result) } => true,
                    OperationKind.Invocation when op.Parent is IInvocationOperation { TargetMethod.Name: nameof(ValueTask.GetAwaiter) } => true,
                    _ => false
                };
        }

        /// <summary>Gets whether <paramref name="op"/> implies that the ValueTask has completed.</summary>
        /// <param name="valueTaskSymbol">The ValueTask symbol for which we're searching.</param>
        /// <param name="op">The operation to examine.</param>
        /// <returns>true if the operation implies the ValueTask has completed, false if the operation implies the ValueTask has not completed, and null if it's undetermined.</returns>
        private static bool? OperationImpliesCompletion(ISymbol valueTaskSymbol, IOperation? op)
        {
            if (op != null)
            {
                switch (op.Kind)
                {
                    case OperationKind.PropertyReference when IsCompletedReference(op as IPropertyReferenceOperation):
                        return true;

                    case OperationKind.Unary when op is IUnaryOperation { OperatorKind: UnaryOperatorKind.Not } unary && IsCompletedReference(unary.Operand as IPropertyReferenceOperation):
                        return false;
                }
            }

            return null;

            bool IsCompletedReference(IPropertyReferenceOperation? prop) =>
                prop?.Instance != null &&
                IsLocalOrParameterSymbolReference(prop.Instance, valueTaskSymbol) &&
                prop.Property.Name switch
                {
                    nameof(ValueTask.IsCompleted) => true,
                    nameof(ValueTask.IsCompletedSuccessfully) => true,
                    nameof(ValueTask.IsFaulted) => true,
                    nameof(ValueTask.IsCanceled) => true,
                    _ => false
                };
        }

        private static bool IsLocalOrParameterSymbolReference(IOperation op, ISymbol valueTaskSymbol) =>
            op?.Kind switch
            {
                OperationKind.LocalReference => ((ILocalReferenceOperation)op).Local.Equals(valueTaskSymbol),
                OperationKind.ParameterReference => ((IParameterReferenceOperation)op).Parameter.Equals(valueTaskSymbol),
                _ => false,
            };
    }
}