Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
// 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.Immutable;
using System.Linq;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
Expand Down Expand Up @@ -54,7 +52,10 @@ public override void Initialize(AnalysisContext context)
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 =>
Expand All @@ -75,27 +76,33 @@ public override void Initialize(AnalysisContext context)
}

context.RegisterOperationAction(context => AnalyzeAwaitOperation(context, taskTypes), OperationKind.Await);
if (configuredAsyncDisposable is not null)

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

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

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

Expand All @@ -111,7 +118,7 @@ private static void AnalyzeAwaitOperation(OperationAnalysisContext context, Immu
}
}

private static void AnalyzeUsingOperation(OperationAnalysisContext context, INamedTypeSymbol configuredAsyncDisposable)
private static void AnalyzeUsingOperation(OperationAnalysisContext context, INamedTypeSymbol iAsyncDisposable, INamedTypeSymbol configuredAsyncDisposable)
{
var usingExpression = (IUsingOperation)context.Operation;
if (!usingExpression.IsAsynchronous)
Expand All @@ -121,12 +128,16 @@ private static void AnalyzeUsingOperation(OperationAnalysisContext context, INam

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

foreach (var declaration in variableDeclarationGroup.Declarations)
{
foreach (var declarator in declaration.Declarators)
{
// Get the type of the expression being awaited and check it's a task type.
if (declarator.Symbol.Type != configuredAsyncDisposable)
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));
Expand All @@ -136,20 +147,24 @@ private static void AnalyzeUsingOperation(OperationAnalysisContext context, INam
}
}

private static void AnalyzeUsingDeclarationOperation(OperationAnalysisContext context, INamedTypeSymbol configuredAsyncDisposable)
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)
{
// Get the type of the expression being awaited and check it's a task type.
if (declarator.Symbol.Type != configuredAsyncDisposable)
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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Immutable;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Testing;
using Test.Utilities;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Testing;
using VerifyCS = Test.Utilities.CSharpCodeFixVerifier<
Microsoft.CodeQuality.Analyzers.ApiDesignGuidelines.DoNotDirectlyAwaitATaskAnalyzer,
Microsoft.CodeQuality.Analyzers.ApiDesignGuidelines.DoNotDirectlyAwaitATaskFixer>;
Expand Down Expand Up @@ -823,5 +821,87 @@ public void Test(IEnumerable<int> enumerable)
LanguageVersion = LanguageVersion.CSharp8
}.RunAsync(CancellationToken.None);
}

[TestMethod, WorkItem(53461, "https://github.com/dotnet/sdk/issues/53461")]
public Task CSharpNoDiagnosticForPatternBasedAwaitUsing_UsingStatement()
{
return new VerifyCS.Test
{
TestCode = """
using System.Runtime.CompilerServices;
using System.Threading.Tasks;

public struct CustomAsyncDisposable
{
public ConfiguredValueTaskAwaitable DisposeAsync() => default;
}

public static class Class
{
public static async Task Test()
{
await using (var cad = new CustomAsyncDisposable())
{
}
}
}
""",
LanguageVersion = LanguageVersion.CSharp8
}.RunAsync(CancellationToken.None);
}

[TestMethod, WorkItem(53461, "https://github.com/dotnet/sdk/issues/53461")]
public Task CSharpNoDiagnosticForPatternBasedAwaitUsing_UsingDeclaration()
{
return new VerifyCS.Test
{
TestCode = """
using System.Runtime.CompilerServices;
using System.Threading.Tasks;

public struct CustomAsyncDisposable
{
public ConfiguredValueTaskAwaitable DisposeAsync() => default;
}

public static class Class
{
public static async Task Test()
{
await using var cad = new CustomAsyncDisposable();
}
}
""",
LanguageVersion = LanguageVersion.CSharp8
}.RunAsync(CancellationToken.None);
}

[TestMethod, WorkItem(53461, "https://github.com/dotnet/sdk/issues/53461")]
public Task CSharpNoDiagnosticForPatternBasedAwaitForEach()
{
return new VerifyCS.Test
{
TestCode = """
using System.Runtime.CompilerServices;
using System.Threading.Tasks;

public struct CustomAsyncEnumerable<T>
{
public ConfiguredCancelableAsyncEnumerable<T>.Enumerator GetAsyncEnumerator() => default;
}

public static class Class
{
public static async Task Test()
{
await foreach (var i in new CustomAsyncEnumerable<int>())
{
}
}
}
""",
LanguageVersion = LanguageVersion.CSharp8
}.RunAsync(CancellationToken.None);
}
}
}
}
Comment on lines 906 to +907

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'll leave this up to you, feel free to ignore and close if you don't think its meaningful enough to provide.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@DoctorKrolic if you could consider this, possibly for a follow up, that'd be great. I didn't feel it worth blocking on but would be add a little more robustness

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I am currently on vacation, will look at it when I return

Loading