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(); + } +}