Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.

using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeQuality.Analyzers.ApiDesignGuidelines;

namespace Microsoft.CodeQuality.CSharp.Analyzers.ApiDesignGuidelines
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class CSharpDoNotDirectlyAwaitATask : DoNotDirectlyAwaitATaskAnalyzer
{
protected override void RegisterLanguageSpecificChecks(OperationBlockStartAnalysisContext context, INamedTypeSymbol configuredAsyncEnumerable)
{
context.RegisterOperationAction(ctx => AnalyzeAwaitForEachLoopOperation(ctx, configuredAsyncEnumerable), OperationKind.Loop);
}

private static void AnalyzeAwaitForEachLoopOperation(OperationAnalysisContext context, INamedTypeSymbol configuredAsyncEnumerable)
{
if (context.Operation is IForEachLoopOperation { IsAsynchronous: true, Collection.Type: not null } forEachOperation
&& !forEachOperation.Collection.Type.OriginalDefinition.Equals(configuredAsyncEnumerable, SymbolEqualityComparer.Default))
{
context.ReportDiagnostic(forEachOperation.Collection.CreateDiagnostic(Rule));
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CollinAlpert all the code here seems to be compilable at the shared layer. I understand only C# supports async foreach loops, but even having this code in the shared layer will do the right thing by bailing out early for VB. I would move this code down to the shared layer and we can definitely conclude that this PR is preferable over #5377

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.

using System;
using System.Collections.Immutable;
using System.Linq;
using Analyzer.Utilities;
Expand All @@ -15,8 +16,7 @@ namespace Microsoft.CodeQuality.Analyzers.ApiDesignGuidelines
/// <summary>
/// CA2007: <inheritdoc cref="DoNotDirectlyAwaitATaskTitle"/>
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
public sealed class DoNotDirectlyAwaitATaskAnalyzer : DiagnosticAnalyzer
public abstract class DoNotDirectlyAwaitATaskAnalyzer : DiagnosticAnalyzer
{
internal const string RuleId = "CA2007";

Expand Down Expand Up @@ -46,12 +46,14 @@ public override void Initialize(AnalysisContext context)
return;
}

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

var configuredAsyncDisposable = context.Compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemRuntimeCompilerServicesConfiguredAsyncDisposable);
var configuredAsyncDisposable = wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemRuntimeCompilerServicesConfiguredAsyncDisposable);
var configuredAsyncEnumerable = wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemRuntimeCompilerServicesConfiguredCancelableAsyncEnumerable);

context.RegisterOperationBlockStartAction(context =>
{
Expand All @@ -76,11 +78,22 @@ public override void Initialize(AnalysisContext context)
context.RegisterOperationAction(context => AnalyzeUsingOperation(context, configuredAsyncDisposable), OperationKind.Using);
context.RegisterOperationAction(context => AnalyzeUsingDeclarationOperation(context, configuredAsyncDisposable), OperationKind.UsingDeclaration);
}

if (configuredAsyncEnumerable is not null)
{
RegisterLanguageSpecificChecks(context, configuredAsyncEnumerable);
}
}
});
});
}

#pragma warning disable RS1012
protected virtual void RegisterLanguageSpecificChecks(OperationBlockStartAnalysisContext context, INamedTypeSymbol configuredAsyncEnumerable)
#pragma warning restore RS1012
{
}

private static void AnalyzeAwaitOperation(OperationAnalysisContext context, ImmutableArray<INamedTypeSymbol> taskTypes)
{
var awaitExpression = (IAwaitOperation)context.Operation;
Expand Down Expand Up @@ -140,19 +153,19 @@ private static void AnalyzeUsingDeclarationOperation(OperationAnalysisContext co
}
}

private static bool TryGetTaskTypes(Compilation compilation, out ImmutableArray<INamedTypeSymbol> taskTypes)
private static bool TryGetTaskTypes(WellKnownTypeProvider typeProvider, out ImmutableArray<INamedTypeSymbol> taskTypes)
{
INamedTypeSymbol? taskType = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingTasksTask);
INamedTypeSymbol? taskOfTType = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingTasksTask1);
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 = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingTasksValueTask);
INamedTypeSymbol? valueTaskOfTType = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingTasksValueTask1);
INamedTypeSymbol? valueTaskType = typeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingTasksValueTask);
INamedTypeSymbol? valueTaskOfTType = typeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingTasksValueTask1);

taskTypes = valueTaskType != null && valueTaskOfTType != null ?
ImmutableArray.Create(taskType, taskOfTType, valueTaskType, valueTaskOfTType) :
Expand Down
58 changes: 38 additions & 20 deletions src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,25 @@
]
}
},
"CA2007": {
"id": "CA2007",
"shortDescription": "Consider calling ConfigureAwait on the awaited task",
"fullDescription": "When an asynchronous method awaits a Task directly, continuation occurs in the same thread that created the task. Consider calling Task.ConfigureAwait(Boolean) to signal your intention for continuation. Call ConfigureAwait(false) on the task to schedule continuations to the thread pool, thereby avoiding a deadlock on the UI thread. Passing false is a good option for app-independent libraries. Calling ConfigureAwait(true) on the task has the same behavior as not explicitly calling ConfigureAwait. By explicitly calling this method, you're letting readers know you intentionally want to perform the continuation on the original synchronization context.",
"defaultLevel": "warning",
"helpUri": "https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2007",
"properties": {
"category": "Reliability",
"isEnabledByDefault": false,
"typeName": "CSharpDoNotDirectlyAwaitATask",
"languages": [
"C#"
],
"tags": [
"Telemetry",
"EnabledRuleInAggressiveMode"
]
}
},
"CA2014": {
"id": "CA2014",
"shortDescription": "Do not use stackalloc in loops",
Expand Down Expand Up @@ -3394,26 +3413,6 @@
]
}
},
"CA2007": {
"id": "CA2007",
"shortDescription": "Consider calling ConfigureAwait on the awaited task",
"fullDescription": "When an asynchronous method awaits a Task directly, continuation occurs in the same thread that created the task. Consider calling Task.ConfigureAwait(Boolean) to signal your intention for continuation. Call ConfigureAwait(false) on the task to schedule continuations to the thread pool, thereby avoiding a deadlock on the UI thread. Passing false is a good option for app-independent libraries. Calling ConfigureAwait(true) on the task has the same behavior as not explicitly calling ConfigureAwait. By explicitly calling this method, you're letting readers know you intentionally want to perform the continuation on the original synchronization context.",
"defaultLevel": "warning",
"helpUri": "https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2007",
"properties": {
"category": "Reliability",
"isEnabledByDefault": false,
"typeName": "DoNotDirectlyAwaitATaskAnalyzer",
"languages": [
"C#",
"Visual Basic"
],
"tags": [
"Telemetry",
"EnabledRuleInAggressiveMode"
]
}
},
"CA2008": {
"id": "CA2008",
"shortDescription": "Do not create tasks without passing a TaskScheduler",
Expand Down Expand Up @@ -6544,6 +6543,25 @@
]
}
},
"CA2007": {
"id": "CA2007",
"shortDescription": "Consider calling ConfigureAwait on the awaited task",
"fullDescription": "When an asynchronous method awaits a Task directly, continuation occurs in the same thread that created the task. Consider calling Task.ConfigureAwait(Boolean) to signal your intention for continuation. Call ConfigureAwait(false) on the task to schedule continuations to the thread pool, thereby avoiding a deadlock on the UI thread. Passing false is a good option for app-independent libraries. Calling ConfigureAwait(true) on the task has the same behavior as not explicitly calling ConfigureAwait. By explicitly calling this method, you're letting readers know you intentionally want to perform the continuation on the original synchronization context.",
"defaultLevel": "warning",
"helpUri": "https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2007",
"properties": {
"category": "Reliability",
"isEnabledByDefault": false,
"typeName": "BasicDoNotDirectlyAwaitATask",
"languages": [
"Visual Basic"
],
"tags": [
"Telemetry",
"EnabledRuleInAggressiveMode"
]
}
},
"CA2016": {
"id": "CA2016",
"shortDescription": "Forward the 'CancellationToken' parameter to methods",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
using Microsoft.CodeAnalysis.Testing;
using Test.Utilities;
using Xunit;
using CSharpLanguageVersion = Microsoft.CodeAnalysis.CSharp.LanguageVersion;
using Microsoft.CodeAnalysis.CSharp;
using VerifyCS = Test.Utilities.CSharpCodeFixVerifier<
Microsoft.CodeQuality.Analyzers.ApiDesignGuidelines.DoNotDirectlyAwaitATaskAnalyzer,
Microsoft.CodeQuality.CSharp.Analyzers.ApiDesignGuidelines.CSharpDoNotDirectlyAwaitATask,
Microsoft.CodeQuality.Analyzers.ApiDesignGuidelines.DoNotDirectlyAwaitATaskFixer>;
using VerifyVB = Test.Utilities.VisualBasicCodeFixVerifier<
Microsoft.CodeQuality.Analyzers.ApiDesignGuidelines.DoNotDirectlyAwaitATaskAnalyzer,
Microsoft.CodeQuality.VisualBasic.Analyzers.ApiDesignGuidelines.BasicDoNotDirectlyAwaitATask,
Microsoft.CodeQuality.Analyzers.ApiDesignGuidelines.DoNotDirectlyAwaitATaskFixer>;

namespace Microsoft.CodeQuality.Analyzers.ApiDesignGuidelines.UnitTests
Expand Down Expand Up @@ -174,7 +174,7 @@ public async Task M4()
{
ReferenceAssemblies = ReferenceAssemblies.Default.AddPackages(
ImmutableArray.Create(new PackageIdentity("Microsoft.Bcl.AsyncInterfaces", "5.0.0"))),
LanguageVersion = CSharpLanguageVersion.CSharp8,
LanguageVersion = LanguageVersion.CSharp8,
TestCode = code,
FixedCode = fixedCode,
}.RunAsync();
Expand Down Expand Up @@ -741,5 +741,86 @@ async Task CoreAsync()

await VerifyCS.VerifyCodeFixAsync(code, fixedCode);
}

[Fact, WorkItem(6652, "https://github.com/dotnet/roslyn-analyzers/issues/6652")]
public Task CsharpAwaitIAsyncEnumerable_DiagnosticAsync()
{
return new VerifyCS.Test
{
TestCode = @"
using System.Collections.Generic;
using System.Threading.Tasks;

public class C
{
public async Task Test(IAsyncEnumerable<int> enumerable)
{
await foreach(var i in [|enumerable|])
{
}
}
}",
FixedCode = @"
using System.Collections.Generic;
using System.Threading.Tasks;

public class C
{
public async Task Test(IAsyncEnumerable<int> enumerable)
{
await foreach(var i in enumerable.ConfigureAwait(false))
{
}
}
}",
LanguageVersion = LanguageVersion.CSharp8
}.RunAsync();
}

[Theory, WorkItem(6652, "https://github.com/dotnet/roslyn-analyzers/issues/6652")]
[InlineData("true")]
[InlineData("false")]
public Task CsharpAwaitIAsyncEnumerable_NoDiagnosticAsync(string continueOnCapturedContext)
{
return new VerifyCS.Test
{
TestCode = @$"
using System.Collections.Generic;
using System.Threading.Tasks;

public class C
{{
public async Task Test(IAsyncEnumerable<int> enumerable)
{{
await foreach(var i in enumerable.ConfigureAwait({continueOnCapturedContext}))
{{
}}
}}
}}",
LanguageVersion = LanguageVersion.CSharp8
}.RunAsync();
}

[Fact, WorkItem(6652, "https://github.com/dotnet/roslyn-analyzers/issues/6652")]
public Task CsharpForEachEnumerable_NoDiagnosticAsync()
{
return new VerifyCS.Test
{
TestCode = @"
using System.Collections.Generic;
using System.Threading.Tasks;

public class C
{
public void Test(IEnumerable<int> enumerable)
{
foreach(var i in enumerable)
{
}
}
}",
LanguageVersion = LanguageVersion.CSharp8
}.RunAsync();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
' Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.

Imports Microsoft.CodeAnalysis
Imports Microsoft.CodeAnalysis.Diagnostics
Imports Microsoft.CodeQuality.Analyzers.ApiDesignGuidelines

Namespace Microsoft.CodeQuality.VisualBasic.Analyzers.ApiDesignGuidelines

<DiagnosticAnalyzer(LanguageNames.VisualBasic)>
Public NotInheritable Class BasicDoNotDirectlyAwaitATask
Inherits DoNotDirectlyAwaitATaskAnalyzer
End Class
End Namespace
1 change: 1 addition & 0 deletions src/Utilities/Compiler/WellKnownTypeNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ internal static class WellKnownTypeNames
public const string SystemRuntimeCompilerServicesCallerArgumentExpressionAttribute = "System.Runtime.CompilerServices.CallerArgumentExpressionAttribute";
public const string SystemRuntimeCompilerServicesCompilerGeneratedAttribute = "System.Runtime.CompilerServices.CompilerGeneratedAttribute";
public const string SystemRuntimeCompilerServicesConfiguredAsyncDisposable = "System.Runtime.CompilerServices.ConfiguredAsyncDisposable";
public const string SystemRuntimeCompilerServicesConfiguredCancelableAsyncEnumerable = "System.Runtime.CompilerServices.ConfiguredCancelableAsyncEnumerable`1";
public const string SystemRuntimeCompilerServicesConfiguredValueTaskAwaitable = "System.Runtime.CompilerServices.ConfiguredValueTaskAwaitable";
public const string SystemRuntimeCompilerServicesConfiguredValueTaskAwaitable1 = "System.Runtime.CompilerServices.ConfiguredValueTaskAwaitable`1";
public const string SystemRuntimeCompilerServicesDisableRuntimeMarshallingAttribute = "System.Runtime.CompilerServices.DisableRuntimeMarshallingAttribute";
Expand Down