diff --git a/README.md b/README.md
index d6066a19f..7b50c1294 100644
--- a/README.md
+++ b/README.md
@@ -144,5 +144,6 @@ If you are already using other analyzers, you can check [which rules are duplica
|[MA0126](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0126.md)|Design|The list of log parameter types contains a duplicate|⚠️|✔️|❌|
|[MA0127](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0127.md)|Usage|Use String.Equals instead of is pattern|⚠️|❌|❌|
|[MA0128](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0128.md)|Usage|Use 'is' operator instead of SequenceEqual|ℹ️|✔️|✔️|
+|[MA0129](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0129.md)|Usage|Await task in using statement|⚠️|✔️|❌|
\ No newline at end of file
diff --git a/docs/README.md b/docs/README.md
index 1be9ea388..014759033 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -128,6 +128,7 @@
|[MA0126](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0126.md)|Design|The list of log parameter types contains a duplicate|⚠️|✔️|❌|
|[MA0127](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0127.md)|Usage|Use String.Equals instead of is pattern|⚠️|❌|❌|
|[MA0128](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0128.md)|Usage|Use 'is' operator instead of SequenceEqual|ℹ️|✔️|✔️|
+|[MA0129](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0129.md)|Usage|Await task in using statement|⚠️|✔️|❌|
# .editorconfig - default values
@@ -512,6 +513,9 @@ dotnet_diagnostic.MA0127.severity = none
# MA0128: Use 'is' operator instead of SequenceEqual
dotnet_diagnostic.MA0128.severity = suggestion
+
+# MA0129: Await task in using statement
+dotnet_diagnostic.MA0129.severity = warning
```
# .editorconfig - all rules disabled
@@ -897,4 +901,7 @@ dotnet_diagnostic.MA0127.severity = none
# MA0128: Use 'is' operator instead of SequenceEqual
dotnet_diagnostic.MA0128.severity = none
+
+# MA0129: Await task in using statement
+dotnet_diagnostic.MA0129.severity = none
```
diff --git a/docs/Rules/MA0129.md b/docs/Rules/MA0129.md
new file mode 100644
index 000000000..182dd78a2
--- /dev/null
+++ b/docs/Rules/MA0129.md
@@ -0,0 +1,10 @@
+# MA0129 - Await task in using statement
+
+A `Task` doesn't need to be disposed. When used in a `using` statement, most of the time, developers forgot to await it.
+
+````
+Task t = ...;
+using(t) { } // non-compliant
+
+using(await t) { } // ok
+````
diff --git a/src/Meziantou.Analyzer/RuleIdentifiers.cs b/src/Meziantou.Analyzer/RuleIdentifiers.cs
index 6702814c4..fff474375 100644
--- a/src/Meziantou.Analyzer/RuleIdentifiers.cs
+++ b/src/Meziantou.Analyzer/RuleIdentifiers.cs
@@ -131,6 +131,7 @@ internal static class RuleIdentifiers
public const string LoggerParameterType_DuplicateRule = "MA0126";
public const string UseStringEqualsInsteadOfIsPattern = "MA0127";
public const string UseIsPatternInsteadOfSequenceEqual = "MA0128";
+ public const string TaskInUsing = "MA0129";
public static string GetHelpUri(string identifier)
{
diff --git a/src/Meziantou.Analyzer/Rules/TaskInUsingAnalyzer.cs b/src/Meziantou.Analyzer/Rules/TaskInUsingAnalyzer.cs
new file mode 100644
index 000000000..7e7d4812a
--- /dev/null
+++ b/src/Meziantou.Analyzer/Rules/TaskInUsingAnalyzer.cs
@@ -0,0 +1,93 @@
+using System;
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Operations;
+
+namespace Meziantou.Analyzer.Rules;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class TaskInUsingAnalyzer : DiagnosticAnalyzer
+{
+ private static readonly DiagnosticDescriptor s_rule = new(
+ RuleIdentifiers.TaskInUsing,
+ title: "Await task in using statement",
+ messageFormat: "Await task in using statement",
+ RuleCategories.Usage,
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ description: "",
+ helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.TaskInUsing));
+
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(s_rule);
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.EnableConcurrentExecution();
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze);
+
+ context.RegisterCompilationStartAction(ctx =>
+ {
+ var taskSymbol = ctx.Compilation.GetBestTypeByMetadataName("System.Threading.Tasks.Task");
+ if (taskSymbol == null)
+ return;
+
+ var analyzerContext = new AnalyzerContext(taskSymbol);
+ ctx.RegisterOperationAction(analyzerContext.AnalyzeUsing, OperationKind.Using);
+ ctx.RegisterOperationAction(analyzerContext.AnalyzeUsingDeclaration, OperationKind.UsingDeclaration);
+ });
+ }
+
+ private sealed class AnalyzerContext
+ {
+ private INamedTypeSymbol _taskSymbol;
+
+ public AnalyzerContext(INamedTypeSymbol taskSymbol)
+ {
+ _taskSymbol = taskSymbol;
+ }
+
+ public void AnalyzeUsing(OperationAnalysisContext context)
+ {
+ var operation = (IUsingOperation)context.Operation;
+ AnalyzeResource(context, operation.Resources);
+ }
+
+ internal void AnalyzeUsingDeclaration(OperationAnalysisContext context)
+ {
+ var operation = (IUsingDeclarationOperation)context.Operation;
+ AnalyzeResource(context, operation.DeclarationGroup);
+ }
+
+ private void AnalyzeResource(OperationAnalysisContext context, IOperation? operation)
+ {
+ if (operation == null)
+ return;
+
+ if (operation is IVariableDeclarationGroupOperation variableDeclarationGroupOperation)
+ {
+ foreach (var declaration in variableDeclarationGroupOperation.Declarations)
+ {
+ AnalyzeResource(context, declaration);
+ }
+
+ return;
+ }
+
+ if (operation is IVariableDeclarationOperation variableDeclarationOperation)
+ {
+ foreach (var declarator in variableDeclarationOperation.Declarators)
+ {
+ AnalyzeResource(context, declarator.Initializer?.Value);
+ }
+ return;
+ }
+
+ operation = operation.UnwrapImplicitConversionOperations();
+ if (operation.Type != null && operation.Type.IsOrInheritFrom(_taskSymbol))
+ {
+ context.ReportDiagnostic(s_rule, operation);
+ }
+ }
+ }
+}
diff --git a/tests/Meziantou.Analyzer.Test/Rules/TaskInUsingAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/TaskInUsingAnalyzerTests.cs
new file mode 100644
index 000000000..49b8fdbee
--- /dev/null
+++ b/tests/Meziantou.Analyzer.Test/Rules/TaskInUsingAnalyzerTests.cs
@@ -0,0 +1,92 @@
+using System.Threading.Tasks;
+using Meziantou.Analyzer.Rules;
+using Microsoft.CodeAnalysis;
+using TestHelper;
+using Xunit;
+
+namespace Meziantou.Analyzer.Test.Rules;
+public sealed class TaskInUsingAnalyzerTests
+{
+ private static ProjectBuilder CreateProjectBuilder()
+ {
+ return new ProjectBuilder()
+ .WithOutputKind(OutputKind.ConsoleApplication)
+ .WithAnalyzer();
+ }
+
+ [Fact]
+ public async Task SingleTaskInUsing()
+ {
+ const string SourceCode = """
+ using System.Threading.Tasks;
+
+ Task t = null;
+ using ([|t|]) { }
+ """;
+
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task SingleTaskAssignedInUsing()
+ {
+ const string SourceCode = """
+ using System.Threading.Tasks;
+
+ Task t = null;
+ using (var a = [|t|]) { }
+ """;
+
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task MultipleTasksInUsing()
+ {
+ const string SourceCode = """
+ using System.Threading.Tasks;
+
+ Task t1 = null;
+ Task t2 = null;
+ using (Task a = [|t1|], b = [|t2|]) { }
+ """;
+
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task TaskOfTInUsing()
+ {
+ const string SourceCode = """
+ using System.Threading.Tasks;
+
+ Task t1 = null;
+ using ([|t1|]) { }
+ """;
+
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task TaskOfTInUsingStatement()
+ {
+ const string SourceCode = """
+ using System.Threading.Tasks;
+
+ Task t1 = null;
+ using var a = [|t1|];
+ """;
+
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+}