File: Microsoft.NetCore.Analyzers\Tasks\DoNotUseNonCancelableTaskDelayWithWhenAny.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.Immutable;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Analyzer.Utilities.Lightup;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using static Microsoft.NetCore.Analyzers.MicrosoftNetCoreAnalyzersResources;

namespace Microsoft.NetCore.Analyzers.Tasks
{
    /// <summary>
    /// CA2027: <inheritdoc cref="DoNotUseNonCancelableTaskDelayWithWhenAnyTitle"/>
    /// </summary>
    [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
    public sealed class DoNotUseNonCancelableTaskDelayWithWhenAny : DiagnosticAnalyzer
    {
        internal const string RuleId = "CA2027";

        internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create(
            RuleId,
            CreateLocalizableResourceString(nameof(DoNotUseNonCancelableTaskDelayWithWhenAnyTitle)),
            CreateLocalizableResourceString(nameof(DoNotUseNonCancelableTaskDelayWithWhenAnyMessage)),
            DiagnosticCategory.Reliability,
            RuleLevel.IdeSuggestion,
            CreateLocalizableResourceString(nameof(DoNotUseNonCancelableTaskDelayWithWhenAnyDescription)),
            isPortedFxCopRule: false,
            isDataflowRule: false);

        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Rule);

        public override void Initialize(AnalysisContext context)
        {
            context.EnableConcurrentExecution();
            context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);

            context.RegisterCompilationStartAction(context =>
            {
                var compilation = context.Compilation;

                if (!compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingTasksTask, out var taskType) ||
                    !compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingCancellationToken, out var cancellationTokenType))
                {
                    return;
                }

                context.RegisterOperationAction(context =>
                {
                    var invocation = (IInvocationOperation)context.Operation;

                    // Check if this is a call to Task.WhenAny
                    var method = invocation.TargetMethod;
                    if (!SymbolEqualityComparer.Default.Equals(method.ContainingType, taskType) ||
                        !method.IsStatic ||
                        method.Name != nameof(Task.WhenAny))
                    {
                        return;
                    }

                    // Count the total number of tasks passed to WhenAny
                    int taskCount = 0;
                    List<IOperation>? taskDelayOperations = null;

                    // Task.WhenAny has params parameters, so arguments are often implicitly wrapped in an array
                    // We need to check inside the array initializer or collection expression
                    for (int i = 0; i < invocation.Arguments.Length; i++)
                    {
                        var argument = invocation.Arguments[i].Value.WalkDownConversion();

                        // Check if this is an array creation
                        if (argument is IArrayCreationOperation { Initializer: not null } arrayCreation)
                        {
                            // Check each element in the array
                            foreach (var element in arrayCreation.Initializer.ElementValues)
                            {
                                taskCount++;
                                if (IsNonCancelableTaskDelay(element, taskType, cancellationTokenType))
                                {
                                    (taskDelayOperations ??= []).Add(element);
                                }
                            }
                        }
                        else if (ICollectionExpressionOperationWrapper.IsInstance(argument))
                        {
                            // Check each element in the collection expression
                            var collectionExpression = ICollectionExpressionOperationWrapper.FromOperation(argument);
                            foreach (var element in collectionExpression.Elements)
                            {
                                taskCount++;
                                if (IsNonCancelableTaskDelay(element, taskType, cancellationTokenType))
                                {
                                    (taskDelayOperations ??= []).Add(element);
                                }
                            }
                        }
                        else
                        {
                            // Direct argument (not params or array)
                            taskCount++;
                            if (IsNonCancelableTaskDelay(argument, taskType, cancellationTokenType))
                            {
                                (taskDelayOperations ??= []).Add(argument);
                            }
                        }
                    }

                    // Only report diagnostics if there are at least 2 tasks total
                    // (avoid flagging Task.WhenAny(Task.Delay(...)) which may be used to avoid exceptions)
                    if (taskCount >= 2 && taskDelayOperations is not null)
                    {
                        foreach (var operation in taskDelayOperations)
                        {
                            context.ReportDiagnostic(operation.CreateDiagnostic(Rule));
                        }
                    }
                }, OperationKind.Invocation);
            });
        }

        private static bool IsNonCancelableTaskDelay(IOperation operation, INamedTypeSymbol taskType, INamedTypeSymbol cancellationTokenType)
        {
            operation = operation.WalkDownConversion();

            if (operation is not IInvocationOperation invocation)
            {
                return false;
            }

            // Check if this is Task.Delay
            var method = invocation.TargetMethod;
            if (!SymbolEqualityComparer.Default.Equals(method.ContainingType, taskType) ||
                !method.IsStatic ||
                method.Name != nameof(Task.Delay))
            {
                return false;
            }

            // Check if any parameter is a CancellationToken, in which case we consider it cancelable
            foreach (var parameter in method.Parameters)
            {
                if (SymbolEqualityComparer.Default.Equals(parameter.Type, cancellationTokenType))
                {
                    return false;
                }
            }

            return true; // Task.Delay without CancellationToken
        }
    }
}