AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.SymbolStartAnalyzer.BlockAnalyzer.cs 40.1 KB
Newer Older
1 2
// Copyright (c) Microsoft.  All Rights Reserved.  Licensed under the Apache License, Version 2.0.  See License.txt in the project root for license information.

3
using System.Collections.Concurrent;
4 5 6 7 8
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.Diagnostics;
9
using Microsoft.CodeAnalysis.FlowAnalysis.SymbolUsageAnalysis;
10 11 12
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
M
Manish Vasani 已提交
13
using Roslyn.Utilities;
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

namespace Microsoft.CodeAnalysis.RemoveUnusedParametersAndValues
{
    internal abstract partial class AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer
    {
        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)"/> to determine whether to bail from analysis or not.
                /// </summary>
                private bool _hasDelegateCreationOrAnonymousFunction;

32
                /// <summary>
33 34 35 36 37 38 39 40
                /// 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.
41 42
                /// We use this value in <see cref="ShouldAnalyze(IOperation, ISymbol)"/> to determine whether to bail from analysis or not.
                /// </summary>
43
                private bool _hasDelegateEscape;
44

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

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

56 57 58 59
                private BlockAnalyzer(SymbolStartAnalyzer symbolStartAnalyzer, Options options)
                {
                    _symbolStartAnalyzer = symbolStartAnalyzer;
                    _options = options;
60
                    _referencedParameters = new ConcurrentDictionary<IParameterSymbol, bool>();
61 62 63 64 65 66 67 68 69
                }

                public static void Analyze(OperationBlockStartAnalysisContext context, SymbolStartAnalyzer symbolStartAnalyzer)
                {
                    if (HasSyntaxErrors() || context.OperationBlocks.IsEmpty)
                    {
                        return;
                    }

70 71 72 73 74 75 76 77 78
                    // 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;
                    }

79 80
                    // All operation blocks for a symbol belong to the same tree.
                    var firstBlock = context.OperationBlocks[0];
M
Manish Vasani 已提交
81 82 83 84 85
                    if (!symbolStartAnalyzer._compilationAnalyzer.TryGetOptions(firstBlock.Syntax.SyntaxTree,
                                                                                firstBlock.Language,
                                                                                context.Options,
                                                                                context.CancellationToken,
                                                                                out var options))
86 87 88 89 90 91 92
                    {
                        return;
                    }

                    var blockAnalyzer = new BlockAnalyzer(symbolStartAnalyzer, options);
                    context.RegisterOperationAction(blockAnalyzer.AnalyzeExpressionStatement, OperationKind.ExpressionStatement);
                    context.RegisterOperationAction(blockAnalyzer.AnalyzeDelegateCreationOrAnonymousFunction, OperationKind.DelegateCreation, OperationKind.AnonymousFunction);
93
                    context.RegisterOperationAction(blockAnalyzer.AnalyzeLocalOrParameterReference, OperationKind.LocalReference, OperationKind.ParameterReference);
94
                    context.RegisterOperationAction(_ => blockAnalyzer._hasInvalidOperation = true, OperationKind.Invalid);
95 96 97 98 99 100 101 102 103
                    context.RegisterOperationBlockEndAction(blockAnalyzer.AnalyzeOperationBlockEnd);

                    return;

                    // Local Functions.
                    bool HasSyntaxErrors()
                    {
                        foreach (var operationBlock in context.OperationBlocks)
                        {
M
Manish Vasani 已提交
104
                            if (operationBlock.Syntax.GetDiagnostics().ToImmutableArrayOrEmpty().HasAnyErrors())
105 106 107 108
                            {
                                return true;
                            }
                        }
109 110 111 112 113 114 115 116 117 118 119 120 121 122

                        return false;
                    }

                    bool HasConditionalDirectives()
                    {
                        foreach (var operationBlock in context.OperationBlocks)
                        {
                            if (operationBlock.Syntax.DescendantNodes(descendIntoTrivia: true)
                                                     .Any(n => symbolStartAnalyzer._compilationAnalyzer.IsIfConditionalDirective(n)))
                            {
                                return true;
                            }
                        }
123 124 125 126 127 128 129

                        return false;
                    }
                }

                private void AnalyzeExpressionStatement(OperationAnalysisContext context)
                {
M
Manish Vasani 已提交
130
                    if (_options.UnusedValueExpressionStatementSeverity == ReportDiagnostic.Suppress)
131 132 133 134 135 136 137 138 139
                    {
                        return;
                    }

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

                    // Bail out cases for report unused expression value:

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

M
Manish Vasani 已提交
148 149 150 151 152
                    //  2. 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)
153 154 155 156 157 158 159 160 161 162 163
                    {
                        return;
                    }

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

M
Manish Vasani 已提交
164 165
                    //  4. 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
166
                    //     an invocation by prefixing the invocation with keyword "Call".
167 168
                    //     Similarly, we do not want to flag an expression of a C# expression body.
                    if (_symbolStartAnalyzer._compilationAnalyzer.IsCallStatement(expressionStatement) ||
M
Manish Vasani 已提交
169
                        _symbolStartAnalyzer._compilationAnalyzer.IsExpressionOfExpressionBody(expressionStatement))
170 171 172 173 174 175 176 177 178 179 180 181 182 183
                    {
                        return;
                    }

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

                private void AnalyzeDelegateCreationOrAnonymousFunction(OperationAnalysisContext operationAnalysisContext)
184
                {
185 186
                    _hasDelegateCreationOrAnonymousFunction = true;
                    if (!_hasDelegateEscape)
187
                    {
188
                        _hasDelegateEscape = !IsHandledDelegateCreationOrAnonymousFunctionTreeShape(operationAnalysisContext.Operation);
189 190 191
                    }
                }

192
                private void AnalyzeLocalOrParameterReference(OperationAnalysisContext operationAnalysisContext)
193
                {
194
                    if (operationAnalysisContext.Operation is IParameterReferenceOperation parameterReference)
195
                    {
196
                        _referencedParameters.GetOrAdd(parameterReference.Parameter, true);
197 198
                    }

199
                    if (!_hasDelegateEscape)
200
                    {
201
                        _hasDelegateEscape = !IsHandledLocalOrParameterReferenceTreeShape(operationAnalysisContext.Operation);
202 203 204
                    }
                }

205 206 207 208 209 210 211 212 213 214 215 216
                /// <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)"/>.
                /// 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)
217
                {
218
                    Debug.Assert(operation.Kind == OperationKind.DelegateCreation || operation.Kind == OperationKind.AnonymousFunction);
219

220 221 222 223 224 225 226 227
                    // 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;
                    }
228

229 230 231 232 233 234 235 236 237 238
                    // 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;
                    }
239

240 241 242 243 244
                    // 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)
245
                    {
246
                        return IsHandledDelegateCreationOrAnonymousFunctionTreeShape(operation.Parent);
247
                    }
248 249 250

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

253 254 255 256 257 258 259 260 261 262 263 264 265
                /// <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)"/>.
                /// 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)
266
                {
267 268 269 270
                    Debug.Assert(operation.Kind == OperationKind.LocalReference || operation.Kind == OperationKind.ParameterReference);

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

275 276 277 278 279
                    // 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)
280
                    {
281
                        return true;
282 283
                    }

284
                    if (operation.Parent is ISimpleAssignmentOperation assignmentOperation)
285
                    {
286 287 288 289 290
                        // 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)
291 292 293 294
                        {
                            return true;
                        }

295 296 297 298 299 300 301 302
                        // 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))
303 304 305
                        {
                            return true;
                        }
306
                    }
307

308 309 310 311 312 313 314
                    // 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;
315
                    }
316 317 318

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

321 322 323 324 325 326 327 328 329 330 331 332 333 334 335
                /// <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)
                {
                    switch (operationBlock.Kind)
                    {
                        case OperationKind.None:
                        case OperationKind.ParameterInitializer:
                            // Skip blocks from attributes (which have OperationKind.None) and parameter initializers.
                            // We don't have any unused values in such operation blocks.
                            return false;
                    }

M
Manish Vasani 已提交
336 337 338 339 340 341
                    // 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.
342 343 344 345 346 347 348 349 350
                    // 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;
                    }

351 352
                    //  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
M
Manish Vasani 已提交
353 354 355
                    //     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.
356
                    if (_hasDelegateEscape)
357 358 359 360
                    {
                        return false;
                    }

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

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

378
                    //  5. Otherwise, we execute analysis by walking the reaching symbol write chain to attempt to
379 380
                    //     find the target method being invoked.
                    //     This works for most common and simple cases where a local is assigned a lambda and invoked later.
381
                    //     If we are unable to find a target, we will conservatively mark all current symbol writes as read.
382 383 384 385 386 387 388
                    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);
M
Manish Vasani 已提交
389
                    if (_options.UnusedValueAssignmentSeverity == ReportDiagnostic.Suppress &&
390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433
                        !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.
                    var symbolUsageResultsBuilder = PooledHashSet<SymbolUsageResult>.GetInstance();

                    try
                    {
                        // Flag indicating if we found an operation block where all symbol writes were used. 
                        bool hasBlockWithAllUsedWrites;

                        AnalyzeUnusedValueAssignments(context, isComputingUnusedParams, symbolUsageResultsBuilder, out hasBlockWithAllUsedWrites);

                        AnalyzeUnusedParameters(context, isComputingUnusedParams, symbolUsageResultsBuilder, hasBlockWithAllUsedWrites);
                    }
                    finally
                    {
                        symbolUsageResultsBuilder.Free();
                    }
                }

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

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

434
                        // First perform the fast, aggressive, imprecise operation-tree based analysis.
435 436
                        // 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.
437 438 439
                        // 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).
440 441
                        if (!_hasDelegateCreationOrAnonymousFunction)
                        {
442 443
                            var resultFromOperationBlockAnalysis = SymbolUsageAnalysis.Run(operationBlock, context.OwningSymbol, context.CancellationToken);
                            if (!resultFromOperationBlockAnalysis.HasUnreadSymbolWrites())
444 445
                            {
                                // Assert that even slow pass (dataflow analysis) would have yielded no unused symbol writes.
446 447
                                Debug.Assert(!SymbolUsageAnalysis.Run(context.GetControlFlowGraph(operationBlock), context.OwningSymbol, context.CancellationToken)
                                             .HasUnreadSymbolWrites());
448 449 450 451 452 453

                                hasBlockWithAllUsedSymbolWrites = true;
                                continue;
                            }
                        }

454
                        // Now perform the slower, precise, CFG based dataflow analysis to identify the actual unused symbol writes.
M
Manish Vasani 已提交
455 456
                        var controlFlowGraph = context.GetControlFlowGraph(operationBlock);
                        var symbolUsageResult = SymbolUsageAnalysis.Run(controlFlowGraph, context.OwningSymbol, context.CancellationToken);
457 458
                        symbolUsageResultsBuilder.Add(symbolUsageResult);

459
                        foreach (var (symbol, unreadWriteOperation) in symbolUsageResult.GetUnreadSymbolWrites())
460
                        {
461
                            if (unreadWriteOperation == null)
462 463 464 465 466 467 468 469 470 471
                            {
                                // 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.
472
                                var unusedParameter = (IParameterSymbol)symbol;
473 474 475 476
                                if (isComputingUnusedParams &&
                                    unusedParameter.ContainingSymbol.IsLocalFunction())
                                {
                                    var hasReference = symbolUsageResult.SymbolsRead.Contains(unusedParameter);
477 478 479 480 481 482

                                    bool shouldReport;
                                    switch (unusedParameter.RefKind)
                                    {
                                        case RefKind.Out:
                                            // Do not report out parameters of local functions.
483 484
                                            // If they are unused in the caller, we will flag the
                                            // out argument at the local function callsite.
485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502
                                            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, context.CancellationToken);
                                    }
503 504 505 506 507
                                }

                                continue;
                            }

508
                            if (ShouldReportUnusedValueDiagnostic(symbol, unreadWriteOperation, symbolUsageResult, out var properties))
509 510
                            {
                                var diagnostic = DiagnosticHelper.Create(s_valueAssignedIsUnusedRule,
511
                                                                         _symbolStartAnalyzer._compilationAnalyzer.GetDefinitionLocationToFade(unreadWriteOperation),
512 513 514
                                                                         _options.UnusedValueAssignmentSeverity,
                                                                         additionalLocations: null,
                                                                         properties,
515
                                                                         symbol.Name);
516 517 518 519 520 521 522 523 524
                                context.ReportDiagnostic(diagnostic);
                            }
                        }
                    }

                    return;

                    // Local functions.
                    bool ShouldReportUnusedValueDiagnostic(
525 526
                        ISymbol symbol,
                        IOperation unreadWriteOperation,
527 528 529
                        SymbolUsageResult resultFromFlowAnalysis,
                        out ImmutableDictionary<string, string> properties)
                    {
530 531
                        Debug.Assert(!(symbol is ILocalSymbol local) || !local.IsRef);

532
                        properties = null;
533 534 535 536

                        // 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
M
Manish Vasani 已提交
537
                        //   3. Static local symbols. Assignment to static locals
538
                        //      is not unnecessary as the assigned value can be used on the next invocation.
539
                        //   4. Ignore special discard symbol names (see https://github.com/dotnet/roslyn/issues/32923).
540
                        if (_options.UnusedValueAssignmentSeverity == ReportDiagnostic.Suppress ||
541
                            symbol.GetSymbolType().IsErrorType() ||
542
                            (symbol.IsStatic && symbol.Kind == SymbolKind.Local) ||
543
                            IsSymbolWithSpecialDiscardName(symbol))
544 545 546 547 548
                        {
                            return false;
                        }

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

552
                        var isRemovableAssignment = IsRemovableAssignmentWithoutSideEffects(unreadWriteOperation);
553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570

                        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)
                    {
571 572 573 574 575
                        if (_symbolStartAnalyzer._compilationAnalyzer.ShouldBailOutFromRemovableAssignmentAnalysis(unusedSymbolWriteOperation))
                        {
                            return false;
                        }

576 577 578
                        if (unusedSymbolWriteOperation.Parent is IAssignmentOperation assignment &&
                            assignment.Target == unusedSymbolWriteOperation)
                        {
579
                            return IsRemovableAssignmentValueWithoutSideEffects(assignment.Value);
580 581 582
                        }
                        else if (unusedSymbolWriteOperation.Parent is IIncrementOrDecrementOperation)
                        {
M
Manish Vasani 已提交
583 584 585
                            // 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.
586 587 588 589 590 591
                            return true;
                        }

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

C
Cyrus Najmabadi 已提交
593
                    static bool IsRemovableAssignmentValueWithoutSideEffects(IOperation assignmentValue)
594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619
                    {
                        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).
620
                                // However, for all practical purposes, we can assume that a non-user defined conversion whose operand
621
                                // has no side effects can be safely removed.
622 623 624
                                var conversion = (IConversionOperation)assignmentValue;
                                return conversion.OperatorMethod == null &&
                                    IsRemovableAssignmentValueWithoutSideEffects(conversion.Operand);
625 626 627 628 629
                        }

                        // Assume all other operations can have side effects, and cannot be removed.
                        return false;
                    }
630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646
                }

                private void AnalyzeUnusedParameters(
                    OperationBlockAnalysisContext context,
                    bool isComputingUnusedParams,
                    PooledHashSet<SymbolUsageResult> symbolUsageResultsBuilder,
                    bool hasBlockWithAllUsedSymbolWrites)
                {
                    // 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.
                    if (!isComputingUnusedParams)
                    {
                        return;
                    }

647 648
                    // 2. Report unused parameters only for method symbols.
                    if (!(context.OwningSymbol is IMethodSymbol method))
649 650 651 652
                    {
                        return;
                    }

653 654 655 656 657 658 659 660 661 662 663 664 665 666
                    // 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)
667 668 669 670
                    {
                        return;
                    }

671 672
                    // 4. Bail out if symbolUsageResultsBuilder is empty, indicating we skipped analysis for all operation blocks.
                    if (symbolUsageResultsBuilder.Count == 0)
673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707
                    {
                        return;
                    }

                    foreach (var parameter in method.Parameters)
                    {
                        bool isUsed = false;
                        bool 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)
                        {
708
                            _symbolStartAnalyzer._unusedParameters[parameter] = isSymbolRead;
709 710 711 712 713 714 715
                        }
                    }
                }
            }
        }
    }
}