File: Microsoft.CodeQuality.Analyzers\ApiDesignGuidelines\DoNotDirectlyAwaitATask.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 Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace Microsoft.CodeQuality.Analyzers.ApiDesignGuidelines
{
    using static MicrosoftCodeQualityAnalyzersResources;

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

        public static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create(
            RuleId,
            CreateLocalizableResourceString(nameof(DoNotDirectlyAwaitATaskTitle)),
            CreateLocalizableResourceString(nameof(DoNotDirectlyAwaitATaskMessage)),
            DiagnosticCategory.Reliability,
            RuleLevel.Disabled,
            description: CreateLocalizableResourceString(nameof(DoNotDirectlyAwaitATaskDescription)),
            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 =>
            {
                if (context.Compilation.SyntaxTrees.FirstOrDefault() is not SyntaxTree tree ||
                    !context.Options.GetOutputKindsOption(Rule, tree, context.Compilation).Contains(context.Compilation.Options.OutputKind))
                {
                    // Configured to skip analysis for the compilation's output kind
                    return;
                }

                var wellKnownTypeProvider = WellKnownTypeProvider.GetOrCreate(context.Compilation);
                if (!TryGetTaskTypes(wellKnownTypeProvider, out ImmutableArray<INamedTypeSymbol> taskTypes))
                {
                    return;
                }

                var iAsyncDisposable = wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemIAsyncDisposable);
                var configuredAsyncDisposable = wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemRuntimeCompilerServicesConfiguredAsyncDisposable);

                var iAsyncEnumerable = wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsGenericIAsyncEnumerable1);
                var configuredAsyncEnumerable = wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemRuntimeCompilerServicesConfiguredCancelableAsyncEnumerable);

                context.RegisterOperationBlockStartAction(context =>
                {
                    if (context.OwningSymbol is IMethodSymbol method)
                    {
                        if (method.IsAsync &&
                            method.ReturnsVoid &&
                            context.Options.GetBoolOptionValue(
                                optionName: EditorConfigOptionNames.ExcludeAsyncVoidMethods,
                                rule: Rule,
                                method,
                                context.Compilation,
                                defaultValue: false))
                        {
                            // Configured to skip this analysis in async void methods.
                            return;
                        }

                        context.RegisterOperationAction(context => AnalyzeAwaitOperation(context, taskTypes), OperationKind.Await);

                        if (iAsyncDisposable is not null && configuredAsyncDisposable is not null)
                        {
                            context.RegisterOperationAction(context => AnalyzeUsingOperation(context, iAsyncDisposable, configuredAsyncDisposable), OperationKind.Using);
                            context.RegisterOperationAction(context => AnalyzeUsingDeclarationOperation(context, iAsyncDisposable, configuredAsyncDisposable), OperationKind.UsingDeclaration);
                        }

                        if (iAsyncEnumerable is not null && configuredAsyncEnumerable is not null)
                        {
                            context.RegisterOperationAction(ctx => AnalyzeAwaitForEachLoopOperation(ctx, iAsyncEnumerable, configuredAsyncEnumerable), OperationKind.Loop);
                        }
                    }
                });
            });
        }

        private static void AnalyzeAwaitForEachLoopOperation(OperationAnalysisContext context, INamedTypeSymbol iAsyncEnumerable, INamedTypeSymbol configuredAsyncEnumerable)
        {
            if (context.Operation is IForEachLoopOperation { IsAsynchronous: true, Collection.Type: not null } forEachOperation)
            {
                var collectionTypeOriginalDefinition = forEachOperation.Collection.Type.OriginalDefinition;

                if (!collectionTypeOriginalDefinition.Equals(configuredAsyncEnumerable, SymbolEqualityComparer.Default) &&
                    context.Compilation.ClassifyCommonConversion(collectionTypeOriginalDefinition, iAsyncEnumerable) is { Exists: true, IsImplicit: true })
                {
                    context.ReportDiagnostic(forEachOperation.Collection.CreateDiagnostic(Rule));
                }
            }
        }

        private static void AnalyzeAwaitOperation(OperationAnalysisContext context, ImmutableArray<INamedTypeSymbol> taskTypes)
        {
            var awaitExpression = (IAwaitOperation)context.Operation;

            // Get the type of the expression being awaited and check it's a task type.
            ITypeSymbol? typeOfAwaitedExpression = awaitExpression.Operation.Type;
            if (typeOfAwaitedExpression != null && taskTypes.Contains(typeOfAwaitedExpression.OriginalDefinition))
            {
                context.ReportDiagnostic(awaitExpression.Operation.Syntax.CreateDiagnostic(Rule));
            }
        }

        private static void AnalyzeUsingOperation(OperationAnalysisContext context, INamedTypeSymbol iAsyncDisposable, INamedTypeSymbol configuredAsyncDisposable)
        {
            var usingExpression = (IUsingOperation)context.Operation;
            if (!usingExpression.IsAsynchronous)
            {
                return;
            }

            if (usingExpression.Resources is IVariableDeclarationGroupOperation variableDeclarationGroup)
            {
                var compilation = context.Compilation;

                foreach (var declaration in variableDeclarationGroup.Declarations)
                {
                    foreach (var declarator in declaration.Declarators)
                    {
                        var declaratorSymbolType = declarator.Symbol.Type;

                        if (!declaratorSymbolType.Equals(configuredAsyncDisposable, SymbolEqualityComparer.Default) &&
                            compilation.ClassifyCommonConversion(declaratorSymbolType, iAsyncDisposable) is { Exists: true, IsImplicit: true })
                        {
                            var reportingOperation = declarator.Initializer?.Value ?? declarator;
                            context.ReportDiagnostic(reportingOperation.CreateDiagnostic(Rule));
                        }
                    }
                }
            }
        }

        private static void AnalyzeUsingDeclarationOperation(OperationAnalysisContext context, INamedTypeSymbol iAsyncDisposable, INamedTypeSymbol configuredAsyncDisposable)
        {
            var usingExpression = (IUsingDeclarationOperation)context.Operation;
            if (!usingExpression.IsAsynchronous)
            {
                return;
            }

            var compilation = context.Compilation;

            foreach (var declaration in usingExpression.DeclarationGroup.Declarations)
            {
                foreach (var declarator in declaration.Declarators)
                {
                    var declaratorSymbolType = declarator.Symbol.Type;

                    if (!declaratorSymbolType.Equals(configuredAsyncDisposable, SymbolEqualityComparer.Default) &&
                        compilation.ClassifyCommonConversion(declaratorSymbolType, iAsyncDisposable) is { Exists: true, IsImplicit: true })
                    {
                        var reportingOperation = declarator.Initializer?.Value ?? declarator;
                        context.ReportDiagnostic(reportingOperation.CreateDiagnostic(Rule));
                    }
                }
            }
        }

        private static bool TryGetTaskTypes(WellKnownTypeProvider typeProvider, out ImmutableArray<INamedTypeSymbol> taskTypes)
        {
            INamedTypeSymbol? taskType = typeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingTasksTask);
            INamedTypeSymbol? taskOfTType = typeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingTasksTask1);

            if (taskType == null || taskOfTType == null)
            {
                taskTypes = ImmutableArray<INamedTypeSymbol>.Empty;
                return false;
            }

            INamedTypeSymbol? valueTaskType = typeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingTasksValueTask);
            INamedTypeSymbol? valueTaskOfTType = typeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingTasksValueTask1);

            taskTypes = valueTaskType != null && valueTaskOfTType != null ?
                ImmutableArray.Create(taskType, taskOfTType, valueTaskType, valueTaskOfTType) :
                ImmutableArray.Create(taskType, taskOfTType);

            return true;
        }
    }
}