File: src\9547feb31df4cc5d\AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.SymbolStartAnalyzer.BlockAnalyzer.cs
Web Access
Project: src\roslyn\src\CodeStyle\Core\Analyzers\Microsoft.CodeAnalysis.CodeStyle.csproj (Microsoft.CodeAnalysis.CodeStyle)
// 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.

using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.FlowAnalysis.SymbolUsageAnalysis;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.RemoveUnusedParametersAndValues;

internal abstract partial class AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer : AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer
{
    private sealed partial class SymbolStartAnalyzer
    {
        private sealed partial class BlockAnalyzer
        {
            private readonly SymbolStartAnalyzer _symbolStartAnalyzer;
            private readonly Options _options;

            /// <summary>
            /// Indicates if the operation block has an <see cref="IDelegateCreationOperation"/> or an <see cref="IAnonymousFunctionOperation"/>.
            /// We use this value in <see cref="ShouldAnalyze(IOperation, ISymbol, ref bool)"/> to determine whether to bail from analysis or not.
            /// </summary>
            private bool _hasDelegateCreationOrAnonymousFunction;

            /// <summary>
            /// Indicates if the operation block has an operation that leads to a delegate escaping the current block,
            /// which would prevent us from performing accurate flow analysis of lambda/local function invocations
            /// within this operation block.
            /// Some examples:
            ///     1. Delegate assigned to a field or property.
            ///     2. Delegate passed as an argument to an invocation or object creation.
            ///     3. Delegate added to an array or wrapped within a tuple.
            ///     4. Delegate converted to a non-delegate type.
            /// We use this value in <see cref="ShouldAnalyze(IOperation, ISymbol, ref bool)"/> to determine whether to bail from analysis or not.
            /// </summary>
            private bool _hasDelegateEscape;

            /// <summary>
            /// Indicates if the operation block has an <see cref="IInvalidOperation"/>.
            /// We use this value in <see cref="ShouldAnalyze(IOperation, ISymbol, ref bool)"/> to determine whether to bail from analysis or not.
            /// </summary>
            private bool _hasInvalidOperation;

            /// <summary>
            /// Parameters which have at least one read/write reference.
            /// </summary>
            private readonly ConcurrentDictionary<IParameterSymbol, bool> _referencedParameters;

            private BlockAnalyzer(SymbolStartAnalyzer symbolStartAnalyzer, Options options)
            {
                _symbolStartAnalyzer = symbolStartAnalyzer;
                _options = options;
                _referencedParameters = [];
            }

            public static void Analyze(OperationBlockStartAnalysisContext context, SymbolStartAnalyzer symbolStartAnalyzer)
            {
                if (!ShouldAnalyze(context, symbolStartAnalyzer, out var options))
                    return;

                var blockAnalyzer = new BlockAnalyzer(symbolStartAnalyzer, options);
                context.RegisterOperationAction(blockAnalyzer.AnalyzeExpressionStatement, OperationKind.ExpressionStatement);
                context.RegisterOperationAction(blockAnalyzer.AnalyzeDelegateCreationOrAnonymousFunction, OperationKind.DelegateCreation, OperationKind.AnonymousFunction);
                context.RegisterOperationAction(blockAnalyzer.AnalyzeLocalOrParameterReference, OperationKind.LocalReference, OperationKind.ParameterReference);
                context.RegisterOperationAction(_ => blockAnalyzer._hasInvalidOperation = true, OperationKind.Invalid);
                context.RegisterOperationBlockEndAction(blockAnalyzer.AnalyzeOperationBlockEnd);
                return;

                // Local Functions.
                bool ShouldAnalyze(
                    OperationBlockStartAnalysisContext context,
                    SymbolStartAnalyzer symbolStartAnalyzer,
                    [NotNullWhen(true)] out Options? options)
                {
                    options = null;
                    if (HasSyntaxErrors() || context.OperationBlocks.IsEmpty)
                        return false;

                    // Bail out in presence of conditional directives
                    // This is a workaround for https://github.com/dotnet/roslyn/issues/31820
                    // Issue https://github.com/dotnet/roslyn/issues/31821 tracks
                    // reverting this workaround.
                    if (HasConditionalDirectives())
                        return false;

                    // All operation blocks for a symbol belong to the same tree.
                    var firstBlock = context.OperationBlocks[0];
                    if (!symbolStartAnalyzer._compilationAnalyzer.TryGetOptions(firstBlock.Syntax.SyntaxTree,
                                                                                context.Options,
                                                                                context.Compilation.Options,
                                                                                context.CancellationToken,
                                                                                out options))
                    {
                        return false;
                    }

                    // Ignore methods that are just a single-throw method.  These are often
                    // in-progress pieces of work and we don't want to force the user to fixup other
                    // issues before they've even gotten around to writing their code.
                    if (firstBlock.IsSingleThrowNotImplementedOperation())
                        return false;

                    // If we are analyzing a specific filter tree, skip operation blocks in unrelated trees.
                    if (symbolStartAnalyzer._symbolStartAnalysisContext.FilterTree is { } filterTree &&
                        firstBlock.Syntax.SyntaxTree != filterTree)
                    {
                        return false;
                    }

                    // If we are analyzing a specific filter span, skip operation blocks outside the filter span.
                    if (context.FilterSpan.HasValue)
                    {
                        Contract.ThrowIfFalse(context.FilterSpan != symbolStartAnalyzer._symbolStartAnalysisContext.FilterSpan);
                        Contract.ThrowIfNull(symbolStartAnalyzer._symbolStartAnalysisContext.FilterTree);
                        var root = firstBlock.Syntax.SyntaxTree.GetRoot(context.CancellationToken);
                        var spanStart = firstBlock.Syntax.SpanStart;
                        var memberDecl = symbolStartAnalyzer._compilationAnalyzer.SyntaxFacts.GetContainingMemberDeclaration(root, spanStart, useFullSpan: false);
                        if (memberDecl != null && !context.ShouldAnalyzeSpan(memberDecl.Span))
                            return false;
                    }

                    return true;
                }

                bool HasSyntaxErrors()
                {
                    foreach (var operationBlock in context.OperationBlocks)
                    {
                        if (operationBlock.Syntax.GetDiagnostics().ToImmutableArrayOrEmpty().HasAnyErrors())
                            return true;
                    }

                    return false;
                }

                bool HasConditionalDirectives()
                {
                    foreach (var operationBlock in context.OperationBlocks)
                    {
                        if (operationBlock.Syntax.DescendantNodes(descendIntoTrivia: true)
                                                 .Any(symbolStartAnalyzer._compilationAnalyzer.IsIfConditionalDirective))
                        {
                            return true;
                        }
                    }

                    return false;
                }
            }

            private void AnalyzeExpressionStatement(OperationAnalysisContext context)
            {
                if (_options.UnusedValueExpressionStatementNotification.Severity == ReportDiagnostic.Suppress)
                {
                    return;
                }

                var expressionStatement = (IExpressionStatementOperation)context.Operation;
                var value = expressionStatement.Operation;

                // Bail out cases for report unused expression value:

                //  1. Null type, error type and void returning method invocations: no value being dropped here.
                if (value.Type == null ||
                    value.Type.IsErrorType() ||
                    value.Type.SpecialType == SpecialType.System_Void)
                {
                    return;
                }

                //  2. Bail out if the return type is dynamic as it could actually be void returning, and throw at runtime
                if (value.Type.TypeKind == TypeKind.Dynamic)
                {
                    return;
                }

                //  3. Bail out for semantic error (invalid operation) cases.
                //     Also bail out for constant expressions in expression statement syntax, say as "1;",
                //     which do not seem to have an invalid operation in the operation tree.
                if (value is IInvalidOperation ||
                    value.ConstantValue.HasValue)
                {
                    return;
                }

                //  4. Assignments, increment/decrement operations: value is actually being assigned.
                if (value is IAssignmentOperation or
                    IIncrementOrDecrementOperation)
                {
                    return;
                }

                //  5. Bail out for null-conditional assignments (e.g. `a?.b = c`, possibly nested),
                //     The analyzer is expected to behave the same way as it does with assignments(4).
                if (value is IConditionalAccessOperation conditionalAccess &&
                    GetInnermostWhenNotNull(conditionalAccess) is IAssignmentOperation)
                {
                    return;
                }

                //  6. Bail out if there is language specific syntax to indicate an explicit discard.
                //     For example, VB call statement is used to explicitly ignore the value returned by
                //     an invocation by prefixing the invocation with keyword "Call".
                //     Similarly, we do not want to flag an expression of a C# expression body.
                if (_symbolStartAnalyzer._compilationAnalyzer.IsCallStatement(expressionStatement) ||
                    _symbolStartAnalyzer._compilationAnalyzer.IsExpressionOfExpressionBody(expressionStatement))
                {
                    return;
                }

                var properties = s_propertiesMap[(_options.UnusedValueExpressionStatementPreference, isUnusedLocalAssignment: false, isRemovableAssignment: false)];
                var diagnostic = DiagnosticHelper.Create(s_expressionValueIsUnusedRule,
                                                         value.Syntax.GetLocation(),
                                                         _options.UnusedValueExpressionStatementNotification,
                                                         context.Options,
                                                         additionalLocations: null,
                                                         properties);
                context.ReportDiagnostic(diagnostic);
            }

            private static IOperation GetInnermostWhenNotNull(IConditionalAccessOperation operation)
            {
                while (operation.WhenNotNull is IConditionalAccessOperation inner)
                    operation = inner;

                return operation.WhenNotNull;
            }

            private void AnalyzeDelegateCreationOrAnonymousFunction(OperationAnalysisContext operationAnalysisContext)
            {
                _hasDelegateCreationOrAnonymousFunction = true;
                if (!_hasDelegateEscape)
                {
                    _hasDelegateEscape = !IsHandledDelegateCreationOrAnonymousFunctionTreeShape(operationAnalysisContext.Operation);
                }
            }

            private void AnalyzeLocalOrParameterReference(OperationAnalysisContext operationAnalysisContext)
            {
                if (operationAnalysisContext.Operation is IParameterReferenceOperation parameterReference)
                {
                    _referencedParameters.GetOrAdd(parameterReference.Parameter, true);
                }

                if (!_hasDelegateEscape)
                {
                    _hasDelegateEscape = !IsHandledLocalOrParameterReferenceTreeShape(operationAnalysisContext.Operation);
                }
            }

            /// <summary>
            /// We handle only certain operation tree shapes in flow analysis
            /// when delegate creations are involved (lambdas/local functions).
            /// We track assignments of lambdas/local functions to parameters/locals,
            /// assignments of parameters/locals to other parameters/locals of delegate types,
            /// and then delegate invocations through parameter/locals.
            /// For the remaining unknown ones, we conservatively mark the operation as leading to
            /// delegate escape, and corresponding bail out from flow analysis in <see cref="ShouldAnalyze(IOperation, ISymbol, ref bool)"/>.
            /// This function checks the operation tree shape in context of
            /// an <see cref="IDelegateCreationOperation"/> or an <see cref="IAnonymousFunctionOperation"/>.
            /// </summary>
            private static bool IsHandledDelegateCreationOrAnonymousFunctionTreeShape(IOperation operation)
            {
                Debug.Assert(operation.Kind is OperationKind.DelegateCreation or OperationKind.AnonymousFunction);

                // 1. Delegate creation or anonymous function variable initializer is handled.
                //    For example, for 'Action a = () => { ... };', the lambda is the variable initializer
                //    and we track that 'a' points to this lambda during flow analysis
                //    and analyze lambda body at invocation sites 'a();' 
                if (operation.Parent is IVariableInitializerOperation)
                {
                    return true;
                }

                // 2. Delegate creation or anonymous function assigned to a local or parameter are handled.
                //    For example, for 'Action a; a = () => { ... };', the lambda is assigned to local 'a'
                //    and we track that 'a' points to this lambda during flow analysis
                //    and analyze lambda body at invocation sites 'a();' 
                if (operation.Parent is ISimpleAssignmentOperation assignment &&
                    (assignment.Target.Kind == OperationKind.LocalReference ||
                     assignment.Target.Kind == OperationKind.ParameterReference))
                {
                    return true;
                }

                // 3. For anonymous functions parented by delegate creation, we analyze the parent operation.
                //    For example, for 'Action a = () => { ... };', the lambda generates an anonymous function
                //    operation parented by a delegate creation.
                if (operation.Kind == OperationKind.AnonymousFunction &&
                    operation.Parent is IDelegateCreationOperation)
                {
                    return IsHandledDelegateCreationOrAnonymousFunctionTreeShape(operation.Parent);
                }

                // 4. Otherwise, conservatively consider this as an unhandled delegate escape.
                return false;
            }

            /// <summary>
            /// We handle only certain operation tree shapes in flow analysis
            /// when delegate creations are involved (lambdas/local functions).
            /// We track assignments of lambdas/local functions to parameters/locals,
            /// assignments of parameters/locals to other parameters/locals of delegate types,
            /// and then delegate invocations through parameter/locals.
            /// For the remaining unknown ones, we conservatively mark the operation as leading to
            /// delegate escape, and corresponding bail out from flow analysis in <see cref="ShouldAnalyze(IOperation, ISymbol, ref bool)"/>.
            /// This function checks the operation tree shape in context of
            /// an <see cref="IParameterReferenceOperation"/> or an <see cref="ILocalReferenceOperation"/>
            /// of delegate type.
            /// </summary>
            private static bool IsHandledLocalOrParameterReferenceTreeShape(IOperation operation)
            {
                Debug.Assert(operation.Kind is OperationKind.LocalReference or OperationKind.ParameterReference);

                // 1. We are only interested in parameters or locals of delegate type.
                if (!operation.Type.IsDelegateType())
                {
                    return true;
                }

                // 2. Delegate invocations are handled.
                //    For example, for 'Action a = () => { ... };  a();'
                //    we track that 'a' points to the lambda during flow analysis
                //    and analyze lambda body at invocation sites 'a();' 
                if (operation.Parent is IInvocationOperation)
                {
                    return true;
                }

                if (operation.Parent is ISimpleAssignmentOperation assignmentOperation)
                {
                    // 3. Parameter/local as target of an assignment is handled.
                    //    For example, for 'a = () => { ... };  a();'
                    //    assignment of a lambda to a local/parameter 'a' is tracked during flow analysis
                    //    and we analyze lambda body at invocation sites 'a();' 
                    if (assignmentOperation.Target == operation)
                    {
                        return true;
                    }

                    // 4. Assignment from a parameter or local is only handled if being
                    //    assigned to some parameter or local of delegate type.
                    //    For example, for 'a = () => { ... }; b = a;  b();'
                    //    assignment of a local/parameter 'b = a' is tracked during flow analysis
                    //    and we analyze lambda body at invocation sites 'b();' 
                    if (assignmentOperation.Target.Type.IsDelegateType() &&
                        (assignmentOperation.Target.Kind == OperationKind.LocalReference ||
                        assignmentOperation.Target.Kind == OperationKind.ParameterReference))
                    {
                        return true;
                    }
                }

                // 5. Binary operations on parameter/local are fine.
                //    For example, 'a = () => { ... }; if (a != null) { a(); }'
                //    the binary operation 'a != null' is fine and does not lead
                //    to a delegate escape.
                if (operation.Parent is IBinaryOperation)
                {
                    return true;
                }

                // 6. Otherwise, conservatively consider this as an unhandled delegate escape.
                return false;
            }

            /// <summary>
            /// Method invoked in <see cref="AnalyzeOperationBlockEnd(OperationBlockAnalysisContext)"/>
            /// for each operation block to determine if we should analyze the operation block or bail out.
            /// </summary>
            private bool ShouldAnalyze(IOperation operationBlock, ISymbol owningSymbol, ref bool hasUnknownOperationNoneDescendant)
            {
                switch (operationBlock.Kind)
                {
                    case OperationKind.Attribute:
                    case OperationKind.ParameterInitializer:
                        // Skip blocks from attributes and parameter initializers.
                        // We don't have any unused values in such operation blocks.
                        return false;

                    default:
                        foreach (var operation in operationBlock.Descendants())
                        {
                            switch (operation)
                            {
                                // Workaround for https://github.com/dotnet/roslyn/issues/31007
                                // We cannot perform flow analysis correctly for a ref assignment operation or ref conditional operation until this compiler feature is implemented.
                                case IConditionalOperation conditional when conditional.IsRef:
                                case ISimpleAssignmentOperation assignment when assignment.IsRef:
                                    return false;

                                default:
                                    // Workaround for https://github.com/dotnet/roslyn/issues/27564
                                    // Bail out in presence of OperationKind.None - not implemented IOperation.
                                    if (operation.Kind == OperationKind.None)
                                    {
                                        // `nameof(SomeTypeName)` is a well-known case where operation related to `SomeTypeName` syntax is of kind `None`
                                        hasUnknownOperationNoneDescendant = operation.Parent is not INameOfOperation;
                                        return false;
                                    }

                                    break;
                            }
                        }

                        break;
                }

                // We currently do not support points-to analysis, which is needed to accurately track locations of
                // allocated objects and their aliasing, which enables us to determine if two symbols reference the
                // same object instance at a given program point and also enables us to track the set of runtime objects
                // that a variable can point to.
                // Hence, we cannot accurately track the exact set of delegates that a symbol with delegate type
                // can point to for all control flow cases.
                // We attempt to do our best effort delegate invocation analysis as follows:

                //  1. If we have no delegate creations or lambdas, our current analysis works fine,
                //     return true.
                if (!_hasDelegateCreationOrAnonymousFunction)
                {
                    return true;
                }

                //  2. Bail out if we have a delegate escape via operation tree shapes that we do not understand.
                //     This indicates the delegate targets (such as lambda/local functions) have escaped current method
                //     and can be invoked from a separate method, and these invocations can read values written
                //     to any local/parameter in the current method. We cannot reliably flag any write to a 
                //     local/parameter as unused for such cases.
                if (_hasDelegateEscape)
                {
                    return false;
                }

                //  3. Bail out for method returning delegates or ref/out parameters of delegate type.
                //     We can analyze this correctly when we do points-to-analysis.
                if (owningSymbol is IMethodSymbol method &&
                    (method.ReturnType.IsDelegateType() ||
                     method.Parameters.Any(static p => p.IsRefOrOut() && p.Type.IsDelegateType())))
                {
                    return false;
                }

                //  4. Bail out on invalid operations, i.e. code with semantic errors.
                //     We are likely to have false positives from flow analysis results
                //     as we will not account for potential lambda/local function invocations.
                if (_hasInvalidOperation)
                {
                    return false;
                }

                //  5. Otherwise, we execute analysis by walking the reaching symbol write chain to attempt to
                //     find the target method being invoked.
                //     This works for most common and simple cases where a local is assigned a lambda and invoked later.
                //     If we are unable to find a target, we will conservatively mark all current symbol writes as read.
                return true;
            }

            private void AnalyzeOperationBlockEnd(OperationBlockAnalysisContext context)
            {
                // Bail out if we are neither computing unused parameters nor unused value assignments.
                var isComputingUnusedParams = _options.IsComputingUnusedParams(context.OwningSymbol);
                if (_options.UnusedValueAssignmentSeverity.Severity == ReportDiagnostic.Suppress &&
                    !isComputingUnusedParams)
                {
                    return;
                }

                // We perform analysis to compute unused parameters and value assignments in two passes.
                // Unused value assignments can be identified by analyzing each operation block independently in the first pass.
                // However, to identify unused parameters we need to first analyze all operation blocks and then iterate
                // through the parameters to identify unused ones

                // Builder to store the symbol read/write usage result for each operation block computed during the first pass.
                // These are later used to compute unused parameters in second pass.
                using var _ = PooledHashSet<SymbolUsageResult>.GetInstance(out var symbolUsageResultsBuilder);

                // Flag indicating if we found an operation block where all symbol writes were used. 
                AnalyzeUnusedValueAssignments(context, isComputingUnusedParams, symbolUsageResultsBuilder, out var hasBlockWithAllUsedWrites, out var hasUnknownOperationNoneDescendant);

                AnalyzeUnusedParameters(context, isComputingUnusedParams, symbolUsageResultsBuilder, hasBlockWithAllUsedWrites, hasUnknownOperationNoneDescendant);
            }

            private void AnalyzeUnusedValueAssignments(
                OperationBlockAnalysisContext context,
                bool isComputingUnusedParams,
                PooledHashSet<SymbolUsageResult> symbolUsageResultsBuilder,
                out bool hasBlockWithAllUsedSymbolWrites,
                out bool hasUnknownOperationNoneDescendant)
            {
                hasBlockWithAllUsedSymbolWrites = false;
                hasUnknownOperationNoneDescendant = false;

                var interpolatedStringHandlerAttribute = context.Compilation.InterpolatedStringHandlerAttributeType();

                foreach (var operationBlock in context.OperationBlocks)
                {
                    if (!ShouldAnalyze(operationBlock, context.OwningSymbol, ref hasUnknownOperationNoneDescendant))
                    {
                        continue;
                    }

                    // First perform the fast, aggressive, imprecise operation-tree based analysis.
                    // This analysis might flag some "used" symbol writes as "unused", but will not miss reporting any truly unused symbol writes.
                    // This initial pass helps us reduce the number of methods for which we perform the slower second pass.
                    // We perform the first fast pass only if there are no delegate creations/lambda methods.
                    // This is due to the fact that tracking which local/parameter points to which delegate creation target
                    // at any given program point needs needs flow analysis (second pass).
                    if (!_hasDelegateCreationOrAnonymousFunction)
                    {
                        var resultFromOperationBlockAnalysis = SymbolUsageAnalysis.Run(operationBlock, context.OwningSymbol, context.CancellationToken);
                        if (!resultFromOperationBlockAnalysis.HasUnreadSymbolWrites())
                        {
                            // Assert that even slow pass (dataflow analysis) would have yielded no unused symbol writes.
                            Debug.Assert(!SymbolUsageAnalysis.Run(context.GetControlFlowGraph(operationBlock), context.OwningSymbol, context.CancellationToken)
                                         .HasUnreadSymbolWrites());

                            hasBlockWithAllUsedSymbolWrites = true;
                            continue;
                        }
                    }

                    // Now perform the slower, precise, CFG based dataflow analysis to identify the actual unused symbol writes.
                    var controlFlowGraph = context.GetControlFlowGraph(operationBlock);
                    var symbolUsageResult = SymbolUsageAnalysis.Run(controlFlowGraph, context.OwningSymbol, context.CancellationToken);
                    symbolUsageResultsBuilder.Add(symbolUsageResult);

                    foreach (var (symbol, unreadWriteOperation) in symbolUsageResult.GetUnreadSymbolWrites())
                    {
                        if (unreadWriteOperation == null)
                        {
                            // Null operation is used for initial write for the parameter from method declaration.
                            // So, the initial value of the parameter is never read in this operation block.
                            // However, we do not report this as an unused parameter here as a different operation block
                            // might be reading the initial parameter value.
                            // For example, a constructor with both a constructor initializer and body will have two different operation blocks
                            // and a parameter must be unused across both these blocks to be marked unused.

                            // However, we do report unused parameters for local function here.
                            // Local function parameters are completely scoped to this operation block, and should be reported per-operation block.
                            var unusedParameter = (IParameterSymbol)symbol;
                            if (isComputingUnusedParams &&
                                unusedParameter.ContainingSymbol.IsLocalFunction())
                            {
                                var hasReference = symbolUsageResult.SymbolsRead.Contains(unusedParameter);

                                bool shouldReport;
                                switch (unusedParameter.RefKind)
                                {
                                    case RefKind.Out:
                                        // Do not report out parameters of local functions.
                                        // If they are unused in the caller, we will flag the
                                        // out argument at the local function callsite.
                                        shouldReport = false;
                                        break;

                                    case RefKind.Ref:
                                        // Report ref parameters only if they have no read/write references.
                                        // Note that we always have one write for the parameter input value from the caller.
                                        shouldReport = !hasReference && symbolUsageResult.GetSymbolWriteCount(unusedParameter) == 1;
                                        break;

                                    default:
                                        shouldReport = true;
                                        break;
                                }

                                if (shouldReport)
                                {
                                    _symbolStartAnalyzer.ReportUnusedParameterDiagnostic(unusedParameter, hasReference, context.ReportDiagnostic, context.Options, interpolatedStringHandlerAttribute, context.CancellationToken);
                                }
                            }

                            continue;
                        }

                        if (ShouldReportUnusedValueDiagnostic(symbol, unreadWriteOperation, symbolUsageResult, out var properties))
                        {
                            var diagnostic = DiagnosticHelper.Create(s_valueAssignedIsUnusedRule,
                                                                     _symbolStartAnalyzer._compilationAnalyzer.GetDefinitionLocationToFade(unreadWriteOperation),
                                                                     _options.UnusedValueAssignmentSeverity,
                                                                     context.Options,
                                                                     additionalLocations: null,
                                                                     properties,
                                                                     symbol.Name);
                            context.ReportDiagnostic(diagnostic);
                        }
                    }
                }

                return;

                // Local functions.
                bool ShouldReportUnusedValueDiagnostic(
                    ISymbol symbol,
                    IOperation unreadWriteOperation,
                    SymbolUsageResult resultFromFlowAnalysis,
                    out ImmutableDictionary<string, string?>? properties)
                {
                    properties = null;

                    // Bail out in following cases:
                    //   1. End user has configured the diagnostic to be suppressed.
                    //   2. Symbol has error type, hence the diagnostic could be noised
                    //   3. Static local symbols. Assignment to static locals
                    //      is not unnecessary as the assigned value can be used on the next invocation.
                    //   4. Ignore special discard symbol names (see https://github.com/dotnet/roslyn/issues/32923).
                    //   5. By-ref parameter or local symbols. Writes to by-ref symbols are
                    //      visible to the caller and may be observed across threads, so they
                    //      should not be flagged as redundant
                    //      (see https://github.com/dotnet/roslyn/issues/44100).
                    if (symbol is IParameterSymbol { RefKind: not RefKind.None } ||
                        symbol is ILocalSymbol { RefKind: not RefKind.None } ||
                        _options.UnusedValueAssignmentSeverity.Severity == ReportDiagnostic.Suppress ||
                        symbol.GetSymbolType().IsErrorType() ||
                        (symbol.IsStatic && symbol.Kind == SymbolKind.Local) ||
                        symbol.IsSymbolWithSpecialDiscardName())
                    {
                        return false;
                    }

                    // Flag to indicate if the symbol has no reads.
                    var isUnusedLocalAssignment = symbol is ILocalSymbol localSymbol &&
                                                  !resultFromFlowAnalysis.SymbolsRead.Contains(localSymbol);

                    var isRemovableAssignment = IsRemovableAssignmentWithoutSideEffects(unreadWriteOperation);

                    if (isUnusedLocalAssignment &&
                        !isRemovableAssignment &&
                        _options.UnusedValueAssignmentPreference == UnusedValuePreference.UnusedLocalVariable)
                    {
                        // Meets current user preference of using unused local symbols for storing computation result.
                        // Skip reporting diagnostic.
                        return false;
                    }

                    properties = s_propertiesMap[(_options.UnusedValueAssignmentPreference, isUnusedLocalAssignment, isRemovableAssignment)];
                    return true;
                }

                // Indicates if the given unused symbol write is a removable assignment.
                // This is true if the expression for the assigned value has no side effects.
                bool IsRemovableAssignmentWithoutSideEffects(IOperation unusedSymbolWriteOperation)
                {
                    if (_symbolStartAnalyzer._compilationAnalyzer.ShouldBailOutFromRemovableAssignmentAnalysis(unusedSymbolWriteOperation))
                    {
                        return false;
                    }

                    if (unusedSymbolWriteOperation.Parent is IAssignmentOperation assignment &&
                        assignment.Target == unusedSymbolWriteOperation)
                    {
                        return IsRemovableAssignmentValueWithoutSideEffects(assignment.Value);
                    }
                    else if (unusedSymbolWriteOperation.Parent is IIncrementOrDecrementOperation)
                    {
                        // As the new value assigned to the incremented/decremented variable is unused,
                        // it is safe to remove the entire increment/decrement operation,
                        // as it cannot have side effects on anything but the variable.
                        return true;
                    }

                    // Assume all other operations can have side effects, and cannot be removed.
                    return false;
                }

                static bool IsRemovableAssignmentValueWithoutSideEffects(IOperation assignmentValue)
                {
                    if (assignmentValue.ConstantValue.HasValue)
                    {
                        // Constant expressions have no side effects.
                        return true;
                    }

                    switch (assignmentValue.Kind)
                    {
                        case OperationKind.ParameterReference:
                        case OperationKind.LocalReference:
                            // Parameter/local references have no side effects and can be removed.
                            return true;

                        case OperationKind.FieldReference:
                            // Field references with null instance (static fields) or 'this' or 'Me' instance can
                            // have no side effects and can be removed.
                            var fieldReference = (IFieldReferenceOperation)assignmentValue;
                            return fieldReference.Instance == null || fieldReference.Instance.Kind == OperationKind.InstanceReference;

                        case OperationKind.DefaultValue:
                            // Default value expressions have no side-effects.
                            return true;

                        case OperationKind.Conversion:
                            // Conversions can theoretically have side-effects as the conversion can throw exception(s).
                            // However, for all practical purposes, we can assume that a non-user defined conversion whose operand
                            // has no side effects can be safely removed.
                            var conversion = (IConversionOperation)assignmentValue;
                            return conversion.OperatorMethod == null &&
                                IsRemovableAssignmentValueWithoutSideEffects(conversion.Operand);
                    }

                    // Assume all other operations can have side effects, and cannot be removed.
                    return false;
                }
            }

            private void AnalyzeUnusedParameters(
                OperationBlockAnalysisContext context,
                bool isComputingUnusedParams,
                PooledHashSet<SymbolUsageResult> symbolUsageResultsBuilder,
                bool hasBlockWithAllUsedSymbolWrites,
                bool hasUnknownOperationNoneDescendant)
            {
                // Process parameters for the context's OwningSymbol that are unused across all operation blocks.

                // Bail out cases:
                //  1. Skip analysis if we are not computing unused parameters based on user's option preference or have
                //     a descendant operation with OperationKind.None (not yet implemented operation) in not a well-known location.
                if (!isComputingUnusedParams || hasUnknownOperationNoneDescendant)
                {
                    return;
                }

                // 2. Report unused parameters only for method symbols.
                if (context.OwningSymbol is not IMethodSymbol method)
                {
                    return;
                }

                // Mark all unreferenced parameters as unused parameters with no read reference.
                // We do so prior to bail out cases 3. and 4. below
                // so that we flag unreferenced parameters even when we bail out from flow analysis.
                foreach (var parameter in method.Parameters)
                {
                    if (!_referencedParameters.ContainsKey(parameter))
                    {
                        // Unused parameter without a reference.
                        _symbolStartAnalyzer._unusedParameters[parameter] = false;
                    }
                }

                // 3. Bail out if we found a single operation block where all symbol writes were used.
                if (hasBlockWithAllUsedSymbolWrites)
                {
                    return;
                }

                // 4. Bail out if symbolUsageResultsBuilder is empty, indicating we skipped analysis for all operation blocks.
                if (symbolUsageResultsBuilder.Count == 0)
                {
                    return;
                }

                foreach (var parameter in method.Parameters)
                {
                    var isUsed = false;
                    var isSymbolRead = false;
                    var isRefOrOutParam = parameter.IsRefOrOut();

                    // Iterate through symbol usage results for each operation block.
                    foreach (var symbolUsageResult in symbolUsageResultsBuilder)
                    {
                        if (symbolUsageResult.IsInitialParameterValueUsed(parameter))
                        {
                            // Parameter is used in this block.
                            isUsed = true;
                            break;
                        }

                        isSymbolRead |= symbolUsageResult.SymbolsRead.Contains(parameter);

                        // Ref/Out parameters are considered used if they have any reads or writes
                        // Note that we always have one write for the parameter input value from the caller.
                        if (isRefOrOutParam &&
                            (isSymbolRead ||
                            symbolUsageResult.GetSymbolWriteCount(parameter) > 1))
                        {
                            isUsed = true;
                            break;
                        }
                    }

                    if (!isUsed)
                    {
                        // Mark if the symbol's value is read or the symbol is referenced to ensure appropriate diagnostic message is given.
                        _symbolStartAnalyzer._unusedParameters[parameter] = isSymbolRead ||
                            _referencedParameters.ContainsKey(parameter);
                    }
                }
            }
        }
    }
}