File: Microsoft.NetCore.Analyzers\Runtime\UseAsyncMethodInAsyncContext.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;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

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

    /// <summary>
    /// CA1849: <inheritdoc cref="UseAsyncMethodInAsyncContextTitle"/>
    /// This analyzer suggests using the async version of a method when inside a Task-returning method
    /// In addition, calling Task.Wait(), Task.Result or Task.GetAwaiter().GetResult() will produce a diagnostic
    /// </summary>
    [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
    public sealed class UseAsyncMethodInAsyncContext : DiagnosticAnalyzer
    {
        internal const string RuleId = "CA1849";
        internal const string MandatoryAsyncSuffix = "Async";

        private static readonly LocalizableString s_localizableTitle = CreateLocalizableResourceString(nameof(UseAsyncMethodInAsyncContextTitle));
        private static readonly LocalizableString s_localizableDescription = CreateLocalizableResourceString(nameof(UseAsyncMethodInAsyncContextDescription));

        internal static readonly DiagnosticDescriptor Descriptor = DiagnosticDescriptorHelper.Create(RuleId,
                                                                                      s_localizableTitle,
                                                                                      CreateLocalizableResourceString(nameof(UseAsyncMethodInAsyncContextMessage)),
                                                                                      DiagnosticCategory.Performance,
                                                                                      RuleLevel.Disabled,
                                                                                      s_localizableDescription,
                                                                                      isPortedFxCopRule: false,
                                                                                      isDataflowRule: false);

        internal static readonly DiagnosticDescriptor DescriptorNoAlternativeMethod = DiagnosticDescriptorHelper.Create(RuleId,
                                                                              s_localizableTitle,
                                                                              CreateLocalizableResourceString(nameof(UseAsyncMethodInAsyncContextMessage_NoAlternative)),
                                                                              DiagnosticCategory.Performance,
                                                                              RuleLevel.Disabled,
                                                                              s_localizableDescription,
                                                                              isPortedFxCopRule: false,
                                                                              isDataflowRule: false);

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

        public override void Initialize(AnalysisContext context)
        {
            context.EnableConcurrentExecution();
            context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
            context.RegisterCompilationStartAction(context =>
            {
                var wellKnownTypeProvider = WellKnownTypeProvider.GetOrCreate(context.Compilation);
                ConcurrentDictionary<string, INamedTypeSymbol> syncBlockingTypes = new();
                GetTypeAndAddToDictionary("Task", WellKnownTypeNames.SystemThreadingTasksTask, syncBlockingTypes, wellKnownTypeProvider);
                GetTypeAndAddToDictionary("TaskGeneric", WellKnownTypeNames.SystemThreadingTasksTask1, syncBlockingTypes, wellKnownTypeProvider);
                GetTypeAndAddToDictionary("ValueTask", WellKnownTypeNames.SystemThreadingTasksValueTask, syncBlockingTypes, wellKnownTypeProvider);
                GetTypeAndAddToDictionary("IAsyncEnumerableGeneric", WellKnownTypeNames.SystemCollectionsGenericIAsyncEnumerable1, syncBlockingTypes, wellKnownTypeProvider);
                GetTypeAndAddToDictionary("AsyncMethodBuilderAttribute", WellKnownTypeNames.SystemRuntimeCompilerServicesAsyncMethodBuilderAttribute, syncBlockingTypes, wellKnownTypeProvider);

                List<SyncBlockingSymbol> syncBlockingSymbols = new();
                GetSymbolAndAddToList("Wait", WellKnownTypeNames.SystemThreadingTasksTask, SymbolKind.Method, syncBlockingSymbols, wellKnownTypeProvider);
                GetSymbolAndAddToList("WaitAll", WellKnownTypeNames.SystemThreadingTasksTask, SymbolKind.Method, syncBlockingSymbols, wellKnownTypeProvider);
                GetSymbolAndAddToList("WaitAny", WellKnownTypeNames.SystemThreadingTasksTask, SymbolKind.Method, syncBlockingSymbols, wellKnownTypeProvider);
                GetSymbolAndAddToList("Result", WellKnownTypeNames.SystemThreadingTasksTask1, SymbolKind.Property, syncBlockingSymbols, wellKnownTypeProvider);
                GetSymbolAndAddToList("Result", WellKnownTypeNames.SystemThreadingTasksValueTask, SymbolKind.Property, syncBlockingSymbols, wellKnownTypeProvider);
                GetSymbolAndAddToList("GetResult", WellKnownTypeNames.SystemRuntimeCompilerServicesTaskAwaiter, SymbolKind.Method, syncBlockingSymbols, wellKnownTypeProvider);
                GetSymbolAndAddToList("GetResult", WellKnownTypeNames.SystemRuntimeCompilerServicesValueTaskAwaiter, SymbolKind.Method, syncBlockingSymbols, wellKnownTypeProvider);
                GetSymbolAndAddToList("Sleep", WellKnownTypeNames.SystemThreadingThread, SymbolKind.Method, syncBlockingSymbols, wellKnownTypeProvider);

                if (syncBlockingTypes.IsEmpty)
                {
                    return;
                }

                if (!wellKnownTypeProvider.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemObsoleteAttribute, out INamedTypeSymbol? systemObsoleteAttribute))
                {
                    return;
                }

                INamedTypeSymbol? semaphoreSlimType = wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingSemaphoreSlim);
                INamedTypeSymbol? timeSpanType = wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemTimeSpan);
                INamedTypeSymbol intType = context.Compilation.GetSpecialType(SpecialType.System_Int32);
                IFieldSymbol? timeSpanZero = timeSpanType?.GetMembers(nameof(TimeSpan.Zero))
                    .OfType<IFieldSymbol>()
                    .FirstOrDefault();
                ImmutableArray<IMethodSymbol> semaphoreSlimWaitWithTimeoutMethods = semaphoreSlimType
                    ?.GetMembers(nameof(SemaphoreSlim.Wait))
                    .OfType<IMethodSymbol>()
                    .Where(m => m.Parameters.Length > 0
                                && (SymbolEqualityComparer.Default.Equals(m.Parameters[0].Type, intType)
                                    || SymbolEqualityComparer.Default.Equals(m.Parameters[0].Type, timeSpanType)))
                    .ToImmutableArray() ?? ImmutableArray<IMethodSymbol>.Empty;

                ImmutableArray<IMethodSymbol> excludedMethods = GetExcludedMethods(wellKnownTypeProvider);
                context.RegisterOperationAction(context =>
                {
                    if (IsInTaskReturningMethodOrDelegate(context, syncBlockingTypes))
                    {
                        if (context.Operation is IInvocationOperation invocationOperation)
                        {
                            var methodSymbol = invocationOperation.TargetMethod;
                            if (excludedMethods.Contains(methodSymbol.OriginalDefinition, SymbolEqualityComparer.Default)
                                || InspectAndReportBlockingMemberAccess(context, methodSymbol, syncBlockingSymbols, SymbolKind.Method)
                                || IsSemaphoreSlimWaitWithZeroArgumentInvocation(invocationOperation, timeSpanZero, semaphoreSlimWaitWithTimeoutMethods))
                            {
                                // Don't return double-diagnostics.
                                return;
                            }

                            // Also consider all method calls to check for Async-suffixed alternatives.
                            var semanticModel = context.Operation.SemanticModel!;

                            if (!methodSymbol.Name.EndsWith(MandatoryAsyncSuffix, StringComparison.Ordinal) &&
                                !HasAsyncCompatibleReturnType(methodSymbol, syncBlockingTypes))
                            {
                                IEnumerable<IMethodSymbol> methodSymbols = semanticModel.LookupSymbols(
                                    context.Operation.Syntax.GetLocation().SourceSpan.Start,
                                    methodSymbol.ContainingType,
                                    methodSymbol.Name + MandatoryAsyncSuffix,
                                    includeReducedExtensionMethods: true)
                                    .OfType<IMethodSymbol>();

                                string containingMethodName = "";
                                if (context.ContainingSymbol is IMethodSymbol parentMethod)
                                {
                                    containingMethodName = parentMethod.Name;
                                }

                                foreach (IMethodSymbol method in methodSymbols)
                                {
                                    if (!method.HasAnyAttribute(systemObsoleteAttribute)
                                        && HasSupersetOfParameterTypes(method, methodSymbol)
                                        && method.Name != containingMethodName
                                        && HasAsyncCompatibleReturnType(method, syncBlockingTypes))
                                    {
                                        Diagnostic diagnostic = invocationOperation.CreateDiagnostic(
                                            Descriptor,
                                            invocationOperation.TargetMethod.ToDisplayString(GetLanguageSpecificFormat(invocationOperation)),
                                            method.ToDisplayString(GetLanguageSpecificFormat(invocationOperation)));

                                        context.ReportDiagnostic(diagnostic);

                                        return;
                                    }
                                }
                            }
                        }
                        else
                        {
                            var propertyReferenceOperation = (IPropertyReferenceOperation)context.Operation;
                            if (propertyReferenceOperation.Parent is not INameOfOperation)
                            {
                                InspectAndReportBlockingMemberAccess(context, propertyReferenceOperation.Property, syncBlockingSymbols, SymbolKind.Property);
                            }
                        }
                    }
                }, OperationKind.Invocation, OperationKind.PropertyReference);
            });
        }

        private static bool IsSemaphoreSlimWaitWithZeroArgumentInvocation(IInvocationOperation invocation, IFieldSymbol? timeSpanZero, ImmutableArray<IMethodSymbol> semaphoreSlimWaitWithTimeoutMethods)
        {
            if (!semaphoreSlimWaitWithTimeoutMethods.Contains(invocation.TargetMethod, SymbolEqualityComparer.Default))
            {
                return false;
            }

            Debug.Assert(!invocation.Arguments.IsEmpty);

            IOperation argumentValue = invocation.Arguments[0].Value;

            return argumentValue.HasConstantValue(0)
                   || timeSpanZero is not null
                   && argumentValue is IFieldReferenceOperation fieldReference
                   && SymbolEqualityComparer.Default.Equals(fieldReference.Field, timeSpanZero);
        }

        private static SymbolDisplayFormat GetLanguageSpecificFormat(IOperation operation) =>
                operation.Language == LanguageNames.CSharp ? SymbolDisplayFormat.CSharpShortErrorMessageFormat : SymbolDisplayFormat.VisualBasicShortErrorMessageFormat;

        internal class SyncBlockingSymbol
        {
            public SyncBlockingSymbol(string Name, string Namespace, SymbolKind Kind, ISymbol Value)
            {
                this.Name = Name;
                this.Namespace = Namespace;
                this.Kind = Kind;
                this.Value = Value;
            }

            public string Name { get; set; }
            public string Namespace { get; set; }
            public SymbolKind Kind { get; set; }
            public ISymbol Value { get; set; }
        }

        private static void GetTypeAndAddToDictionary(string key, string typeName, ConcurrentDictionary<string, INamedTypeSymbol> syncBlockingTypes, WellKnownTypeProvider wellKnownTypeProvider)
        {
            if (wellKnownTypeProvider.TryGetOrCreateTypeByMetadataName(typeName, out INamedTypeSymbol? typeValue))
            {
                syncBlockingTypes.AddOrUpdate(key, typeValue, (k, v) => v);
            }
        }

        private static void GetSymbolAndAddToList(string symbolName, string metadataName, SymbolKind kind, List<SyncBlockingSymbol> syncBlockingSymbols, WellKnownTypeProvider wellKnownTypeProvider)
        {
            if (wellKnownTypeProvider.TryGetOrCreateTypeByMetadataName(metadataName, out INamedTypeSymbol? typeValue))
            {
                ISymbol? symbolValue = typeValue
                    .GetMembers(symbolName)
                    .FirstOrDefault(s => s.Kind == kind);

                if (symbolValue is not null)
                {
                    syncBlockingSymbols.Add(new SyncBlockingSymbol(symbolName, metadataName, kind, symbolValue));
                }
            }
        }

        private static ImmutableArray<IMethodSymbol> GetExcludedMethods(WellKnownTypeProvider wellKnownTypeProvider)
        {
            var entityFrameworkTypeNames = new[]
            {
                WellKnownTypeNames.MicrosoftEntityFrameworkCoreDbContext,
                WellKnownTypeNames.MicrosoftEntityFrameworkCoreDbSet1
            };

            var methodsBuilder = ImmutableArray.CreateBuilder<IMethodSymbol>();

            foreach (var entityFrameworkTypeName in entityFrameworkTypeNames)
            {
                if (wellKnownTypeProvider.TryGetOrCreateTypeByMetadataName(entityFrameworkTypeName, out INamedTypeSymbol? entityFrameworkType))
                {
                    foreach (var method in entityFrameworkType.GetMembers().OfType<IMethodSymbol>())
                    {
                        if (method.Name is "Add" or "AddRange")
                        {
                            methodsBuilder.Add(method);
                        }
                    }
                }
            }

            if (wellKnownTypeProvider.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftEntityFrameworkCoreDbContextFactory, out INamedTypeSymbol? dbContextFactoryType))
            {
                foreach (var method in dbContextFactoryType.GetMembers("CreateDbContext").OfType<IMethodSymbol>())
                {
                    methodsBuilder.Add(method);
                }
            }

            return methodsBuilder.ToImmutable();
        }

        /// <summary>
        /// Determines whether the given method has parameters to cover all the parameter types in another method.
        /// </summary>
        /// <param name="candidateMethod">The candidate method.</param>
        /// <param name="baselineMethod">The baseline method.</param>
        /// <returns>
        ///   <c>true</c> if <paramref name="candidateMethod"/> has a superset of parameter types found in <paramref name="baselineMethod"/>; otherwise <c>false</c>.
        /// </returns>
        private static bool HasSupersetOfParameterTypes(IMethodSymbol candidateMethod, IMethodSymbol baselineMethod)
        {
            return candidateMethod.Parameters.All(candidateParameter => candidateParameter.HasExplicitDefaultValue || baselineMethod.Parameters.Any(baselineParameter => baselineParameter.Type?.Equals(candidateParameter.Type) ?? false));
        }

        private static bool HasAsyncCompatibleReturnType(IMethodSymbol methodSymbol, ConcurrentDictionary<string, INamedTypeSymbol> syncBlockingTypes)
        {
            if (methodSymbol.ReturnType is null)
            {
                return false;
            }

            ISymbol returnType = methodSymbol.ReturnType;

            static bool CheckReturnTypeMatch(string targetType, ISymbol returnType, ConcurrentDictionary<string, INamedTypeSymbol> syncBlockingTypes)
                => syncBlockingTypes.TryGetValue(targetType, out INamedTypeSymbol? targetTypeValue)
                && targetTypeValue.Equals(returnType.OriginalDefinition);

            return CheckReturnTypeMatch("Task", returnType, syncBlockingTypes)
                || CheckReturnTypeMatch("TaskGeneric", returnType, syncBlockingTypes)
                || CheckReturnTypeMatch("ValueTask", returnType, syncBlockingTypes)
                || CheckReturnTypeMatch("IAsyncEnumerableGeneric", returnType, syncBlockingTypes)
                || (syncBlockingTypes.TryGetValue("AsyncMethodBuilderAttribute", out INamedTypeSymbol? asyncMethodBuilderAttributeTypeValue)
                && returnType.HasAnyAttribute(asyncMethodBuilderAttributeTypeValue));
        }

        private static IMethodSymbol? GetParentMethodOrDelegate(OperationAnalysisContext context)
        {
            var containingAnonymousFunction = context.Operation.TryGetContainingAnonymousFunctionOrLocalFunction();
            if (containingAnonymousFunction is not null)
            {
                return containingAnonymousFunction;
            }

            var containingSymbol = context.ContainingSymbol;
            while (containingSymbol is not null)
            {
                if (containingSymbol is IMethodSymbol method)
                {
                    return method;
                }

                containingSymbol = containingSymbol.ContainingSymbol;
            }

            return null;
        }

        private static bool IsInTaskReturningMethodOrDelegate(OperationAnalysisContext context, ConcurrentDictionary<string, INamedTypeSymbol> syncBlockingTypes)
        {
            // We want to scan invocations that occur inside Task and Task<T>-returning delegates or methods.
            // That is: methods that either are or could be made async.
            IMethodSymbol? parentMethod = GetParentMethodOrDelegate(context);

            if (parentMethod == null)
            {
                return false;
            }

            return HasAsyncCompatibleReturnType(parentMethod, syncBlockingTypes);
        }

        private static bool InspectAndReportBlockingMemberAccess(OperationAnalysisContext context, ISymbol memberSymbol, List<SyncBlockingSymbol> syncBlockingSymbols, SymbolKind kind)
        {
            foreach (SyncBlockingSymbol symbol in syncBlockingSymbols)
            {
                if (symbol.Kind != kind)
                    continue;
                if (symbol.Value.Equals(memberSymbol.OriginalDefinition))
                {
                    Diagnostic diagnostic = context.Operation.Syntax.CreateDiagnostic(
                        DescriptorNoAlternativeMethod,
                        symbol.Value.ToDisplayString(GetLanguageSpecificFormat(context.Operation))
                    );

                    context.ReportDiagnostic(diagnostic);
                    return true;
                }
            }

            return false;
        }
    }
}