// 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; } } }