diff --git a/ChangeLog.md b/ChangeLog.md index b63539d593..ca9d42ed1a 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add analyzer "Dispose resource asynchronously" ([RCS1261](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1261)) ([PR](https://github.com/dotnet/roslynator/pull/1285)). + ### Changed - Improve refactoring 'Remove comment' [RR0098](https://josefpihrt.github.io/docs/roslynator/refactorings/RR0098) ([PR](https://github.com/dotnet/roslynator/pull/1284)) diff --git a/src/Analyzers.CodeFixes/CSharp/CodeFixes/DisposeResourceAsynchronouslyCodeFixProvider.cs b/src/Analyzers.CodeFixes/CSharp/CodeFixes/DisposeResourceAsynchronouslyCodeFixProvider.cs new file mode 100644 index 0000000000..4cb7542786 --- /dev/null +++ b/src/Analyzers.CodeFixes/CSharp/CodeFixes/DisposeResourceAsynchronouslyCodeFixProvider.cs @@ -0,0 +1,186 @@ +// Copyright (c) .NET Foundation and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Immutable; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Roslynator.CodeFixes; +using Roslynator.CSharp.SyntaxRewriters; + +namespace Roslynator.CSharp.CodeFixes; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(DisposeResourceAsynchronouslyCodeFixProvider))] +[Shared] +public sealed class DisposeResourceAsynchronouslyCodeFixProvider : BaseCodeFixProvider +{ + private const string Title = "Dispose resource asynchronously"; + + public override ImmutableArray FixableDiagnosticIds + { + get { return ImmutableArray.Create(DiagnosticIdentifiers.DisposeResourceAsynchronously); } + } + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + SyntaxNode root = await context.GetSyntaxRootAsync().ConfigureAwait(false); + + if (!TryFindFirstAncestorOrSelf( + root, + context.Span, + out SyntaxNode node, + predicate: f => f.IsKind(SyntaxKind.LocalDeclarationStatement, SyntaxKind.UsingStatement))) + { + return; + } + + Diagnostic diagnostic = context.Diagnostics[0]; + Document document = context.Document; + + switch (diagnostic.Id) + { + case DiagnosticIdentifiers.DisposeResourceAsynchronously: + { + if (node is LocalDeclarationStatementSyntax localDeclaration) + { + CodeAction codeAction = CodeAction.Create( + Title, + ct => RefactorAsync(document, localDeclaration, ct), + GetEquivalenceKey(diagnostic)); + + context.RegisterCodeFix(codeAction, diagnostic); + } + else if (node is UsingStatementSyntax usingStatement) + { + CodeAction codeAction = CodeAction.Create( + Title, + ct => RefactorAsync(document, usingStatement, ct), + GetEquivalenceKey(diagnostic)); + + context.RegisterCodeFix(codeAction, diagnostic); + } + else + { + throw new InvalidOperationException(); + } + + break; + } + } + } + + private static async Task RefactorAsync( + Document document, + UsingStatementSyntax usingStatement, + CancellationToken cancellationToken) + { + UsingStatementSyntax newUsingStatement = usingStatement.WithAwaitKeyword(SyntaxFactory.Token(SyntaxKind.AwaitKeyword)); + + SemanticModel semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + return await RefactorAsync(document, usingStatement, newUsingStatement, semanticModel, cancellationToken).ConfigureAwait(false); + } + + private static async Task RefactorAsync( + Document document, + LocalDeclarationStatementSyntax localDeclaration, + CancellationToken cancellationToken) + { + LocalDeclarationStatementSyntax newLocalDeclaration = localDeclaration.WithAwaitKeyword(SyntaxFactory.Token(SyntaxKind.AwaitKeyword)); + + SemanticModel semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + return await RefactorAsync(document, localDeclaration, newLocalDeclaration, semanticModel, cancellationToken).ConfigureAwait(false); + } + + private static async Task RefactorAsync( + Document document, + StatementSyntax statement, + StatementSyntax newStatement, + SemanticModel semanticModel, + CancellationToken cancellationToken) + { + for (SyntaxNode node = statement.Parent; node is not null; node = node.Parent) + { + if (node is MethodDeclarationSyntax methodDeclaration) + { + if (methodDeclaration.Modifiers.Contains(SyntaxKind.AsyncKeyword)) + return await document.ReplaceNodeAsync(statement, newStatement, cancellationToken).ConfigureAwait(false); + + MethodDeclarationSyntax newNode = methodDeclaration.ReplaceNode(statement, newStatement); + + IMethodSymbol methodSymbol = semanticModel.GetDeclaredSymbol(methodDeclaration, cancellationToken); + + UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol); + var newBody = (BlockSyntax)rewriter.VisitBlock(newNode.Body); + + newNode = newNode + .WithBody(newBody) + .InsertModifier(SyntaxKind.AsyncKeyword); + + return await document.ReplaceNodeAsync(methodDeclaration, newNode, cancellationToken).ConfigureAwait(false); + } + else if (node is LocalFunctionStatementSyntax localFunction) + { + if (localFunction.Modifiers.Contains(SyntaxKind.AsyncKeyword)) + return await document.ReplaceNodeAsync(statement, newStatement, cancellationToken).ConfigureAwait(false); + + LocalFunctionStatementSyntax newNode = localFunction.ReplaceNode(statement, newStatement); + + IMethodSymbol methodSymbol = semanticModel.GetDeclaredSymbol(localFunction, cancellationToken); + + UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol); + var newBody = (BlockSyntax)rewriter.VisitBlock(newNode.Body); + + newNode = newNode + .WithBody(newBody) + .InsertModifier(SyntaxKind.AsyncKeyword); + + return await document.ReplaceNodeAsync(localFunction, newNode, cancellationToken).ConfigureAwait(false); + } + else if (node is LambdaExpressionSyntax lambdaExpression) + { + if (lambdaExpression.Modifiers.Contains(SyntaxKind.AsyncKeyword)) + return await document.ReplaceNodeAsync(statement, newStatement, cancellationToken).ConfigureAwait(false); + + LambdaExpressionSyntax newNode = lambdaExpression.ReplaceNode(statement, newStatement); + + var methodSymbol = (IMethodSymbol)semanticModel.GetSymbol(lambdaExpression, cancellationToken); + + UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol); + var newBody = (BlockSyntax)rewriter.VisitBlock((BlockSyntax)newNode.Body); + + newNode = newNode + .WithBody(newBody) + .InsertModifier(SyntaxKind.AsyncKeyword); + + return await document.ReplaceNodeAsync(lambdaExpression, newNode, cancellationToken).ConfigureAwait(false); + } + else if (node is AnonymousMethodExpressionSyntax anonymousMethod) + { + if (anonymousMethod.Modifiers.Contains(SyntaxKind.AsyncKeyword)) + return await document.ReplaceNodeAsync(statement, newStatement, cancellationToken).ConfigureAwait(false); + + AnonymousMethodExpressionSyntax newNode = anonymousMethod.ReplaceNode(statement, newStatement); + + var methodSymbol = (IMethodSymbol)semanticModel.GetSymbol(anonymousMethod, cancellationToken); + + UseAsyncAwaitRewriter rewriter = UseAsyncAwaitRewriter.Create(methodSymbol); + var newBody = (BlockSyntax)rewriter.VisitBlock((BlockSyntax)newNode.Body); + + newNode = newNode + .WithBody(newBody) + .InsertModifier(SyntaxKind.AsyncKeyword); + + return await document.ReplaceNodeAsync(anonymousMethod, newNode, cancellationToken).ConfigureAwait(false); + } + } + + throw new InvalidOperationException(); + } +} diff --git a/src/Analyzers.CodeFixes/CSharp/CodeFixes/LocalDeclarationStatementCodeFixProvider.cs b/src/Analyzers.CodeFixes/CSharp/CodeFixes/LocalDeclarationStatementCodeFixProvider.cs index 27253d3a37..45db233aaf 100644 --- a/src/Analyzers.CodeFixes/CSharp/CodeFixes/LocalDeclarationStatementCodeFixProvider.cs +++ b/src/Analyzers.CodeFixes/CSharp/CodeFixes/LocalDeclarationStatementCodeFixProvider.cs @@ -34,21 +34,21 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) if (!TryFindFirstAncestorOrSelf(root, context.Span, out LocalDeclarationStatementSyntax localDeclaration)) return; - foreach (Diagnostic diagnostic in context.Diagnostics) + Diagnostic diagnostic = context.Diagnostics[0]; + Document document = context.Document; + + switch (diagnostic.Id) { - switch (diagnostic.Id) - { - case DiagnosticIdentifiers.InlineLocalVariable: - { - CodeAction codeAction = CodeAction.Create( - "Inline local variable", - ct => RefactorAsync(context.Document, localDeclaration, ct), - GetEquivalenceKey(diagnostic)); - - context.RegisterCodeFix(codeAction, diagnostic); - break; - } - } + case DiagnosticIdentifiers.InlineLocalVariable: + { + CodeAction codeAction = CodeAction.Create( + "Inline local variable", + ct => RefactorAsync(document, localDeclaration, ct), + GetEquivalenceKey(diagnostic)); + + context.RegisterCodeFix(codeAction, diagnostic); + break; + } } } diff --git a/src/Analyzers.CodeFixes/CSharp/CodeFixes/UseAsyncAwaitCodeFixProvider.cs b/src/Analyzers.CodeFixes/CSharp/CodeFixes/UseAsyncAwaitCodeFixProvider.cs index e63f949aa3..f8dbdf99b5 100644 --- a/src/Analyzers.CodeFixes/CSharp/CodeFixes/UseAsyncAwaitCodeFixProvider.cs +++ b/src/Analyzers.CodeFixes/CSharp/CodeFixes/UseAsyncAwaitCodeFixProvider.cs @@ -14,7 +14,6 @@ using Roslynator.CodeFixes; using Roslynator.CSharp.SyntaxRewriters; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; -using static Roslynator.CSharp.CSharpFactory; namespace Roslynator.CSharp.CodeFixes; @@ -22,10 +21,6 @@ namespace Roslynator.CSharp.CodeFixes; [Shared] public sealed class UseAsyncAwaitCodeFixProvider : BaseCodeFixProvider { - private static readonly SyntaxAnnotation[] _asyncAwaitAnnotation = new[] { new SyntaxAnnotation() }; - - private static readonly SyntaxAnnotation[] _asyncAwaitAnnotationAndFormatterAnnotation = new SyntaxAnnotation[] { _asyncAwaitAnnotation[0], Formatter.Annotation }; - public override ImmutableArray FixableDiagnosticIds { get { return ImmutableArray.Create(DiagnosticIdentifiers.UseAsyncAwait); } @@ -140,93 +135,4 @@ private static async Task RefactorAsync( throw new InvalidOperationException(); } - - private class UseAsyncAwaitRewriter : SkipFunctionRewriter - { - private UseAsyncAwaitRewriter(bool keepReturnStatement) - { - KeepReturnStatement = keepReturnStatement; - } - - public bool KeepReturnStatement { get; } - - public static UseAsyncAwaitRewriter Create(IMethodSymbol methodSymbol) - { - ITypeSymbol returnType = methodSymbol.ReturnType.OriginalDefinition; - - var keepReturnStatement = false; - - if (returnType.EqualsOrInheritsFrom(MetadataNames.System_Threading_Tasks_ValueTask_T) - || returnType.EqualsOrInheritsFrom(MetadataNames.System_Threading_Tasks_Task_T)) - { - keepReturnStatement = true; - } - - return new UseAsyncAwaitRewriter(keepReturnStatement: keepReturnStatement); - } - - public override SyntaxNode VisitReturnStatement(ReturnStatementSyntax node) - { - ExpressionSyntax expression = node.Expression; - - if (expression?.IsKind(SyntaxKind.AwaitExpression) == false) - { - if (KeepReturnStatement) - { - return node.WithExpression(AwaitExpression(expression.WithoutTrivia().Parenthesize()).WithTriviaFrom(expression)); - } - else - { - return ExpressionStatement(AwaitExpression(expression.WithoutTrivia().Parenthesize()).WithTriviaFrom(expression)) - .WithLeadingTrivia(node.GetLeadingTrivia()) - .WithAdditionalAnnotations(_asyncAwaitAnnotationAndFormatterAnnotation); - } - } - - return base.VisitReturnStatement(node); - } - - public override SyntaxNode VisitBlock(BlockSyntax node) - { - node = (BlockSyntax)base.VisitBlock(node); - - SyntaxList statements = node.Statements; - - statements = RewriteStatements(statements); - - return node.WithStatements(statements); - } - - public override SyntaxNode VisitSwitchSection(SwitchSectionSyntax node) - { - node = (SwitchSectionSyntax)base.VisitSwitchSection(node); - - SyntaxList statements = node.Statements; - - statements = RewriteStatements(statements); - - return node.WithStatements(statements); - } - - private static SyntaxList RewriteStatements(SyntaxList statements) - { - for (int i = statements.Count - 1; i >= 0; i--) - { - StatementSyntax statement = statements[i]; - - if (statement.HasAnnotation(_asyncAwaitAnnotation[0])) - { - statements = statements.Replace( - statement, - statement.WithoutAnnotations(_asyncAwaitAnnotation).WithTrailingTrivia(NewLine())); - - statements = statements.Insert( - i + 1, - ReturnStatement().WithTrailingTrivia(statement.GetTrailingTrivia()).WithFormatterAnnotation()); - } - } - - return statements; - } - } } diff --git a/src/Analyzers.CodeFixes/CSharp/SyntaxRewriters/UseAsyncAwaitRewriter.cs b/src/Analyzers.CodeFixes/CSharp/SyntaxRewriters/UseAsyncAwaitRewriter.cs new file mode 100644 index 0000000000..e61262b8f2 --- /dev/null +++ b/src/Analyzers.CodeFixes/CSharp/SyntaxRewriters/UseAsyncAwaitRewriter.cs @@ -0,0 +1,132 @@ +// Copyright (c) .NET Foundation and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Formatting; +using Roslynator; +using Roslynator.CSharp; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static Roslynator.CSharp.CSharpFactory; + +namespace Roslynator.CSharp.SyntaxRewriters; + +internal sealed class UseAsyncAwaitRewriter : SkipFunctionRewriter +{ + private static readonly SyntaxAnnotation[] _asyncAwaitAnnotation = new[] { new SyntaxAnnotation() }; + + private static readonly SyntaxAnnotation[] _asyncAwaitAnnotationAndFormatterAnnotation = new SyntaxAnnotation[] { _asyncAwaitAnnotation[0], Formatter.Annotation }; + + private UseAsyncAwaitRewriter(bool keepReturnStatement) + { + KeepReturnStatement = keepReturnStatement; + } + + public bool KeepReturnStatement { get; } + + public static UseAsyncAwaitRewriter Create(IMethodSymbol methodSymbol) + { + ITypeSymbol returnType = methodSymbol.ReturnType.OriginalDefinition; + + var keepReturnStatement = false; + + if (returnType.EqualsOrInheritsFrom(MetadataNames.System_Threading_Tasks_ValueTask_T) + || returnType.EqualsOrInheritsFrom(MetadataNames.System_Threading_Tasks_Task_T)) + { + keepReturnStatement = true; + } + + return new UseAsyncAwaitRewriter(keepReturnStatement: keepReturnStatement); + } + + public override SyntaxNode VisitReturnStatement(ReturnStatementSyntax node) + { + ExpressionSyntax expression = node.Expression; + + if (expression?.IsKind(SyntaxKind.AwaitExpression) == false) + { + if (KeepReturnStatement) + { + return node.WithExpression(AwaitExpression(expression.WithoutTrivia().Parenthesize()).WithTriviaFrom(expression)); + } + else + { + return ExpressionStatement(AwaitExpression(expression.WithoutTrivia().Parenthesize()).WithTriviaFrom(expression)) + .WithLeadingTrivia(node.GetLeadingTrivia()) + .WithAdditionalAnnotations(_asyncAwaitAnnotationAndFormatterAnnotation); + } + } + + return base.VisitReturnStatement(node); + } + + public override SyntaxNode VisitBlock(BlockSyntax node) + { + var newNode = (BlockSyntax)base.VisitBlock(node); + + SyntaxList statements = newNode.Statements; + + statements = RewriteStatements(node, statements); + + return newNode.WithStatements(statements); + } + + public override SyntaxNode VisitSwitchSection(SwitchSectionSyntax node) + { + var newNode = (SwitchSectionSyntax)base.VisitSwitchSection(node); + + SyntaxList statements = newNode.Statements; + + statements = RewriteStatements(node, statements); + + return newNode.WithStatements(statements); + } + + private static SyntaxList RewriteStatements(SyntaxNode parent, SyntaxList statements) + { + if (!statements.Any()) + return statements; + + int startIndex = statements.Count - 1; + + if (parent.IsKind(SyntaxKind.Block) + && (IsMethodLike(parent.Parent) + || (parent.IsParentKind(SyntaxKind.UsingStatement) + && parent.Parent.IsParentKind(SyntaxKind.Block) + && IsMethodLike(parent.Parent.Parent.Parent)))) + { + if (startIndex == 0) + return statements; + + startIndex--; + } + + for (int i = startIndex; i >= 0; i--) + { + StatementSyntax statement = statements[i]; + + if (statement.HasAnnotation(_asyncAwaitAnnotation[0])) + { + statements = statements.Replace( + statement, + statement.WithoutAnnotations(_asyncAwaitAnnotation).WithTrailingTrivia(NewLine())); + + statements = statements.Insert( + i + 1, + ReturnStatement().WithTrailingTrivia(statement.GetTrailingTrivia()).WithFormatterAnnotation()); + } + } + + return statements; + } + + private static bool IsMethodLike(SyntaxNode node) + { + return node.IsKind( + SyntaxKind.MethodDeclaration, + SyntaxKind.LocalFunctionStatement, + SyntaxKind.SimpleLambdaExpression, + SyntaxKind.ParenthesizedLambdaExpression, + SyntaxKind.AnonymousMethodExpression); + } +} diff --git a/src/Analyzers.xml b/src/Analyzers.xml index a3ba32474a..667be75270 100644 --- a/src/Analyzers.xml +++ b/src/Analyzers.xml @@ -7414,8 +7414,9 @@ void M() - + RCS1259 + RemoveEmptySyntax Codestin Search App Remove empty {0}. Info @@ -7431,11 +7432,11 @@ void M() * empty statement - + RCS1260 + AddOrRemoveTrailingComma Codestin Search App {0} trailing comma. - General Info false @@ -7468,6 +7469,27 @@ void M() + + RCS1261 + DisposeResourceAsynchronously + Codestin Search App + Info + true + + + + + + + RCS9001 UsePatternMatching diff --git a/src/Analyzers/CSharp/Analysis/DisposeResourceAsynchronouslyAnalyzer.cs b/src/Analyzers/CSharp/Analysis/DisposeResourceAsynchronouslyAnalyzer.cs new file mode 100644 index 0000000000..5f8e4c9a50 --- /dev/null +++ b/src/Analyzers/CSharp/Analysis/DisposeResourceAsynchronouslyAnalyzer.cs @@ -0,0 +1,141 @@ +// Copyright (c) .NET Foundation and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Roslynator.CSharp.Syntax; + +namespace Roslynator.CSharp.Analysis; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class DisposeResourceAsynchronouslyAnalyzer : BaseDiagnosticAnalyzer +{ + private static ImmutableArray _supportedDiagnostics; + + public override ImmutableArray SupportedDiagnostics + { + get + { + if (_supportedDiagnostics.IsDefault) + Immutable.InterlockedInitialize(ref _supportedDiagnostics, DiagnosticRules.DisposeResourceAsynchronously); + + return _supportedDiagnostics; + } + } + + public override void Initialize(AnalysisContext context) + { + base.Initialize(context); + + context.RegisterSyntaxNodeAction(f => AnalyzeLocalDeclarationStatement(f), SyntaxKind.LocalDeclarationStatement); + context.RegisterSyntaxNodeAction(f => AnalyzeUsingStatement(f), SyntaxKind.UsingStatement); + } + + private static void AnalyzeLocalDeclarationStatement(SyntaxNodeAnalysisContext context) + { + var statement = (LocalDeclarationStatementSyntax)context.Node; + + SingleLocalDeclarationStatementInfo localInfo = SyntaxInfo.SingleLocalDeclarationStatementInfo(statement); + + if (!localInfo.Success) + return; + + if (!localInfo.UsingKeyword.IsKind(SyntaxKind.UsingKeyword)) + return; + + if (localInfo.AwaitKeyword.IsKind(SyntaxKind.AwaitKeyword)) + return; + + ExpressionSyntax value = localInfo.Value; + + if (value is null) + return; + + Analyze(context, statement, statement.UsingKeyword, value); + } + + private static void AnalyzeUsingStatement(SyntaxNodeAnalysisContext context) + { + var statement = (UsingStatementSyntax)context.Node; + + if (statement.AwaitKeyword.IsKind(SyntaxKind.AwaitKeyword)) + return; + + VariableDeclaratorSyntax declaration = statement.Declaration?.Variables.SingleOrDefault(shouldThrow: false); + + if (declaration is null) + return; + + ExpressionSyntax value = declaration.Initializer?.Value; + + if (value is null) + return; + + Analyze(context, statement, statement.UsingKeyword, value); + } + + private static void Analyze(SyntaxNodeAnalysisContext context, StatementSyntax statement, SyntaxToken usingKeyword, ExpressionSyntax value) + { + ITypeSymbol typeSymbol = context.SemanticModel.GetTypeSymbol(value, context.CancellationToken); + + if (typeSymbol?.Implements(MetadataNames.System_IAsyncDisposable, allInterfaces: true) != true) + return; + + for (SyntaxNode node = statement.Parent; node is not null; node = node.Parent) + { + if (node is MemberDeclarationSyntax) + { + if (node is MethodDeclarationSyntax methodDeclaration) + Analyze(context, methodDeclaration.Modifiers, usingKeyword, methodDeclaration); + + break; + } + else if (node is LocalFunctionStatementSyntax localFunction) + { + Analyze(context, localFunction.Modifiers, usingKeyword, localFunction); + break; + } + else if (node is LambdaExpressionSyntax lambdaExpression) + { + Analyze(context, lambdaExpression.Modifiers, usingKeyword, lambdaExpression); + break; + } + else if (node is AnonymousMethodExpressionSyntax anonymousMethod) + { + Analyze(context, anonymousMethod.Modifiers, usingKeyword, anonymousMethod); + break; + } + } + } + + private static void Analyze( + SyntaxNodeAnalysisContext context, + SyntaxTokenList modifiers, + SyntaxToken usingKeyword, + SyntaxNode containingMethod) + { + if (modifiers.Contains(SyntaxKind.AsyncKeyword)) + { + ReportDiagnostic(context, usingKeyword); + } + else + { + var methodSymbol = ((containingMethod.IsKind(SyntaxKind.MethodDeclaration, SyntaxKind.LocalFunctionStatement)) + ? context.SemanticModel.GetDeclaredSymbol(containingMethod, context.CancellationToken) + : context.SemanticModel.GetSymbol(containingMethod, context.CancellationToken)) as IMethodSymbol; + + if (methodSymbol?.IsErrorType() == false + && SymbolUtility.IsAwaitable(methodSymbol.ReturnType)) + { + ReportDiagnostic(context, usingKeyword); + } + } + } + + private static void ReportDiagnostic(SyntaxNodeAnalysisContext context, SyntaxToken usingKeyword) + { + context.ReportDiagnostic(DiagnosticRules.DisposeResourceAsynchronously, usingKeyword); + } +} diff --git a/src/Analyzers/CSharp/Analysis/InlineLocalVariableAnalyzer.cs b/src/Analyzers/CSharp/Analysis/InlineLocalVariableAnalyzer.cs index ff035a2ce0..cb0da58ca1 100644 --- a/src/Analyzers/CSharp/Analysis/InlineLocalVariableAnalyzer.cs +++ b/src/Analyzers/CSharp/Analysis/InlineLocalVariableAnalyzer.cs @@ -63,7 +63,7 @@ private static void AnalyzeLocalDeclarationStatement(SyntaxNodeAnalysisContext c if (!localDeclarationInfo.Success) return; - if (localDeclarationInfo.Statement.UsingKeyword.IsKind(SyntaxKind.UsingKeyword)) + if (localDeclarationInfo.UsingKeyword.IsKind(SyntaxKind.UsingKeyword)) return; ExpressionSyntax value = localDeclarationInfo.Value; diff --git a/src/Analyzers/CSharp/DiagnosticIdentifiers.Generated.cs b/src/Analyzers/CSharp/DiagnosticIdentifiers.Generated.cs index a53b49bd0e..7665cff120 100644 --- a/src/Analyzers/CSharp/DiagnosticIdentifiers.Generated.cs +++ b/src/Analyzers/CSharp/DiagnosticIdentifiers.Generated.cs @@ -217,5 +217,6 @@ public static partial class DiagnosticIdentifiers public const string UnnecessaryEnumFlag = "RCS1258"; public const string RemoveEmptySyntax = "RCS1259"; public const string AddOrRemoveTrailingComma = "RCS1260"; + public const string DisposeResourceAsynchronously = "RCS1261"; } } \ No newline at end of file diff --git a/src/Analyzers/CSharp/DiagnosticRules.Generated.cs b/src/Analyzers/CSharp/DiagnosticRules.Generated.cs index d5e27b5707..702de01938 100644 --- a/src/Analyzers/CSharp/DiagnosticRules.Generated.cs +++ b/src/Analyzers/CSharp/DiagnosticRules.Generated.cs @@ -2569,5 +2569,17 @@ public static partial class DiagnosticRules helpLinkUri: DiagnosticIdentifiers.AddOrRemoveTrailingComma, customTags: Array.Empty()); + /// RCS1261 + public static readonly DiagnosticDescriptor DisposeResourceAsynchronously = DiagnosticDescriptorFactory.Create( + id: DiagnosticIdentifiers.DisposeResourceAsynchronously, + title: "Resource can be disposed asynchronously.", + messageFormat: "Resource can be disposed asynchronously.", + category: DiagnosticCategories.Roslynator, + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: null, + helpLinkUri: DiagnosticIdentifiers.DisposeResourceAsynchronously, + customTags: Array.Empty()); + } } \ No newline at end of file diff --git a/src/CSharp/CSharp/ModifierList.cs b/src/CSharp/CSharp/ModifierList.cs index 4bebb71496..b12632efbb 100644 --- a/src/CSharp/CSharp/ModifierList.cs +++ b/src/CSharp/CSharp/ModifierList.cs @@ -175,6 +175,11 @@ public static TNode Insert(TNode node, SyntaxKind kind, IComparer.Instance.Insert((LocalFunctionStatementSyntax)(SyntaxNode)node, kind, comparer); case SyntaxKind.Parameter: return (TNode)(SyntaxNode)ModifierList.Instance.Insert((ParameterSyntax)(SyntaxNode)node, kind, comparer); + case SyntaxKind.SimpleLambdaExpression: + case SyntaxKind.ParenthesizedLambdaExpression: + return (TNode)(SyntaxNode)ModifierList.Instance.Insert((LambdaExpressionSyntax)(SyntaxNode)node, kind, comparer); + case SyntaxKind.AnonymousMethodExpression: + return (TNode)(SyntaxNode)ModifierList.Instance.Insert((AnonymousMethodExpressionSyntax)(SyntaxNode)node, kind, comparer); case SyntaxKind.IncompleteMember: return (TNode)(SyntaxNode)ModifierList.Instance.Insert((IncompleteMemberSyntax)(SyntaxNode)node, kind, comparer); } diff --git a/src/CSharp/CSharp/ModifierList`1.cs b/src/CSharp/CSharp/ModifierList`1.cs index 0ab9cac761..a22e384b3b 100644 --- a/src/CSharp/CSharp/ModifierList`1.cs +++ b/src/CSharp/CSharp/ModifierList`1.cs @@ -97,6 +97,12 @@ private static object GetInstance() if (typeof(TNode) == typeof(RecordDeclarationSyntax)) return new RecordDeclarationModifierList(); + if (typeof(TNode) == typeof(LambdaExpressionSyntax)) + return new LambdaExpressionModifierList(); + + if (typeof(TNode) == typeof(AnonymousMethodExpressionSyntax)) + return new AnonymousMethodExpressionModifierList(); + throw new InvalidOperationException(); } @@ -775,4 +781,40 @@ internal override RecordDeclarationSyntax WithModifiers(RecordDeclarationSyntax return node.WithModifiers(modifiers); } } + + private class LambdaExpressionModifierList : ModifierList + { + internal override SyntaxList GetAttributeLists(LambdaExpressionSyntax node) + { + return node.AttributeLists; + } + + internal override SyntaxTokenList GetModifiers(LambdaExpressionSyntax node) + { + return node.Modifiers; + } + + internal override LambdaExpressionSyntax WithModifiers(LambdaExpressionSyntax node, SyntaxTokenList modifiers) + { + return node.WithModifiers(modifiers); + } + } + + private class AnonymousMethodExpressionModifierList : ModifierList + { + internal override SyntaxList GetAttributeLists(AnonymousMethodExpressionSyntax node) + { + return default; + } + + internal override SyntaxTokenList GetModifiers(AnonymousMethodExpressionSyntax node) + { + return node.Modifiers; + } + + internal override AnonymousMethodExpressionSyntax WithModifiers(AnonymousMethodExpressionSyntax node, SyntaxTokenList modifiers) + { + return node.WithModifiers(modifiers); + } + } } diff --git a/src/CSharp/CSharp/Syntax/SingleLocalDeclarationStatementInfo.cs b/src/CSharp/CSharp/Syntax/SingleLocalDeclarationStatementInfo.cs index e8a12f467f..16e2ccea79 100644 --- a/src/CSharp/CSharp/Syntax/SingleLocalDeclarationStatementInfo.cs +++ b/src/CSharp/CSharp/Syntax/SingleLocalDeclarationStatementInfo.cs @@ -28,6 +28,16 @@ private SingleLocalDeclarationStatementInfo( /// public LocalDeclarationStatementSyntax Statement { get; } + /// + /// The 'using' keyword. + /// + public SyntaxToken UsingKeyword => Statement?.UsingKeyword ?? default; + + /// + /// The 'await' keyword. + /// + public SyntaxToken AwaitKeyword => Statement?.AwaitKeyword ?? default; + /// /// The variable declarator. /// diff --git a/src/Core/MetadataNames.cs b/src/Core/MetadataNames.cs index f17b234697..03949d2f85 100644 --- a/src/Core/MetadataNames.cs +++ b/src/Core/MetadataNames.cs @@ -36,6 +36,7 @@ internal static class MetadataNames public static readonly MetadataName System_FormattableString = MetadataName.Parse("System.FormattableString"); public static readonly MetadataName System_Func_T2 = MetadataName.Parse("System.Func`2"); public static readonly MetadataName System_Func_T3 = MetadataName.Parse("System.Func`3"); + public static readonly MetadataName System_IAsyncDisposable = MetadataName.Parse("System.IAsyncDisposable"); public static readonly MetadataName System_IEquatable_T = MetadataName.Parse("System.IEquatable`1"); public static readonly MetadataName System_IComparable = MetadataName.Parse("System.IComparable"); public static readonly MetadataName System_IComparable_T = MetadataName.Parse("System.IComparable`1"); diff --git a/src/Tests/Analyzers.Tests/RCS1229UseAsyncAwaitTests.cs b/src/Tests/Analyzers.Tests/RCS1229UseAsyncAwaitTests.cs index b1c9100134..2403b7ffe9 100644 --- a/src/Tests/Analyzers.Tests/RCS1229UseAsyncAwaitTests.cs +++ b/src/Tests/Analyzers.Tests/RCS1229UseAsyncAwaitTests.cs @@ -80,7 +80,6 @@ async Task M() using (default(IDisposable)) { await DoAsync(); - return; } } diff --git a/src/Tests/Analyzers.Tests/RCS1261DisposeResourceAsynchronouslyTests.cs b/src/Tests/Analyzers.Tests/RCS1261DisposeResourceAsynchronouslyTests.cs new file mode 100644 index 0000000000..e2c916a528 --- /dev/null +++ b/src/Tests/Analyzers.Tests/RCS1261DisposeResourceAsynchronouslyTests.cs @@ -0,0 +1,916 @@ +// Copyright (c) .NET Foundation and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Roslynator.CSharp.CodeFixes; +using Roslynator.Testing.CSharp; +using Xunit; + +namespace Roslynator.CSharp.Analysis.Tests; + +public class RCS1261DisposeResourceAsynchronouslyTests : AbstractCSharpDiagnosticVerifier +{ + public override DiagnosticDescriptor Descriptor { get; } = DiagnosticRules.DisposeResourceAsynchronously; + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.DisposeResourceAsynchronously)] + public async Task Test_Method_LocalStatement_WithAsync() + { + await VerifyDiagnosticAndFixAsync(@" +using System; +using System.Threading.Tasks; + +class C +{ + async Task FooAsync() + { + [|using|] var disposable = await GetDisposableAsync(); + } + + private Task GetDisposableAsync() => throw new NotImplementedException(); +} + +internal class Disposable : IDisposable, IAsyncDisposable +{ + public void Dispose() => throw new NotImplementedException(); + public ValueTask DisposeAsync() => throw new NotImplementedException(); +} +", @" +using System; +using System.Threading.Tasks; + +class C +{ + async Task FooAsync() + { + await using var disposable = await GetDisposableAsync(); + } + + private Task GetDisposableAsync() => throw new NotImplementedException(); +} + +internal class Disposable : IDisposable, IAsyncDisposable +{ + public void Dispose() => throw new NotImplementedException(); + public ValueTask DisposeAsync() => throw new NotImplementedException(); +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.DisposeResourceAsynchronously)] + public async Task Test_Method_LocalStatement_WithoutAsync_Task() + { + await VerifyDiagnosticAndFixAsync(@" +using System; +using System.Threading.Tasks; + +class C +{ + Task FooAsync() + { + [|using|] var disposable = GetDisposable(); + + return Task.CompletedTask; + } + + private Disposable GetDisposable() => throw new NotImplementedException(); +} + +internal class Disposable : IDisposable, IAsyncDisposable +{ + public void Dispose() => throw new NotImplementedException(); + public ValueTask DisposeAsync() => throw new NotImplementedException(); +} +", @" +using System; +using System.Threading.Tasks; + +class C +{ + async Task FooAsync() + { + await using var disposable = GetDisposable(); + + await Task.CompletedTask; + } + + private Disposable GetDisposable() => throw new NotImplementedException(); +} + +internal class Disposable : IDisposable, IAsyncDisposable +{ + public void Dispose() => throw new NotImplementedException(); + public ValueTask DisposeAsync() => throw new NotImplementedException(); +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.DisposeResourceAsynchronously)] + public async Task Test_Method_LocalStatement_WithoutAsync_TaskOfT() + { + await VerifyDiagnosticAndFixAsync(@" +using System; +using System.Threading.Tasks; + +class C +{ + Task FooAsync() + { + [|using|] var disposable = GetDisposable(); + + return Task.FromResult(""""); + } + + private Disposable GetDisposable() => throw new NotImplementedException(); +} + +internal class Disposable : IDisposable, IAsyncDisposable +{ + public void Dispose() => throw new NotImplementedException(); + public ValueTask DisposeAsync() => throw new NotImplementedException(); +} +", @" +using System; +using System.Threading.Tasks; + +class C +{ + async Task FooAsync() + { + await using var disposable = GetDisposable(); + + return await Task.FromResult(""""); + } + + private Disposable GetDisposable() => throw new NotImplementedException(); +} + +internal class Disposable : IDisposable, IAsyncDisposable +{ + public void Dispose() => throw new NotImplementedException(); + public ValueTask DisposeAsync() => throw new NotImplementedException(); +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.DisposeResourceAsynchronously)] + public async Task Test_LocalFunction_LocalStatement_WithAsync() + { + await VerifyDiagnosticAndFixAsync(@" +using System; +using System.Threading.Tasks; + +class C +{ + void M() + { + async Task FooAsync() + { + [|using|] var disposable = await GetDisposableAsync(); + } + } + + private Task GetDisposableAsync() => throw new NotImplementedException(); +} + +internal class Disposable : IDisposable, IAsyncDisposable +{ + public void Dispose() => throw new NotImplementedException(); + public ValueTask DisposeAsync() => throw new NotImplementedException(); +} +", @" +using System; +using System.Threading.Tasks; + +class C +{ + void M() + { + async Task FooAsync() + { + await using var disposable = await GetDisposableAsync(); + } + } + + private Task GetDisposableAsync() => throw new NotImplementedException(); +} + +internal class Disposable : IDisposable, IAsyncDisposable +{ + public void Dispose() => throw new NotImplementedException(); + public ValueTask DisposeAsync() => throw new NotImplementedException(); +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.DisposeResourceAsynchronously)] + public async Task Test_LocalFunction_LocalStatement_WithoutAsync_Task() + { + await VerifyDiagnosticAndFixAsync(@" +using System; +using System.Threading.Tasks; + +class C +{ + void M() + { + Task FooAsync() + { + [|using|] var disposable = GetDisposable(); + + return Task.CompletedTask; + } + } + + private Disposable GetDisposable() => throw new NotImplementedException(); +} + +internal class Disposable : IDisposable, IAsyncDisposable +{ + public void Dispose() => throw new NotImplementedException(); + public ValueTask DisposeAsync() => throw new NotImplementedException(); +} +", @" +using System; +using System.Threading.Tasks; + +class C +{ + void M() + { + async Task FooAsync() + { + await using var disposable = GetDisposable(); + + await Task.CompletedTask; + } + } + + private Disposable GetDisposable() => throw new NotImplementedException(); +} + +internal class Disposable : IDisposable, IAsyncDisposable +{ + public void Dispose() => throw new NotImplementedException(); + public ValueTask DisposeAsync() => throw new NotImplementedException(); +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.DisposeResourceAsynchronously)] + public async Task Test_LocalFunction_LocalStatement_WithoutAsync_TaskOfT() + { + await VerifyDiagnosticAndFixAsync(@" +using System; +using System.Threading.Tasks; + +class C +{ + void M() + { + Task FooAsync() + { + [|using|] var disposable = GetDisposable(); + + return Task.FromResult(""""); + } + } + + private Disposable GetDisposable() => throw new NotImplementedException(); +} + +internal class Disposable : IDisposable, IAsyncDisposable +{ + public void Dispose() => throw new NotImplementedException(); + public ValueTask DisposeAsync() => throw new NotImplementedException(); +} +", @" +using System; +using System.Threading.Tasks; + +class C +{ + void M() + { + async Task FooAsync() + { + await using var disposable = GetDisposable(); + + return await Task.FromResult(""""); + } + } + + private Disposable GetDisposable() => throw new NotImplementedException(); +} + +internal class Disposable : IDisposable, IAsyncDisposable +{ + public void Dispose() => throw new NotImplementedException(); + public ValueTask DisposeAsync() => throw new NotImplementedException(); +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.DisposeResourceAsynchronously)] + public async Task Test_Method_UsingStatement_WithAsync() + { + await VerifyDiagnosticAndFixAsync(@" +using System; +using System.Threading.Tasks; + +class C +{ + async Task FooAsync() + { + [|using|] (var disposable = await GetDisposableAsync()) { } + } + + private Task GetDisposableAsync() => throw new NotImplementedException(); +} + +internal class Disposable : IDisposable, IAsyncDisposable +{ + public void Dispose() => throw new NotImplementedException(); + public ValueTask DisposeAsync() => throw new NotImplementedException(); +} +", @" +using System; +using System.Threading.Tasks; + +class C +{ + async Task FooAsync() + { + await using (var disposable = await GetDisposableAsync()) { } + } + + private Task GetDisposableAsync() => throw new NotImplementedException(); +} + +internal class Disposable : IDisposable, IAsyncDisposable +{ + public void Dispose() => throw new NotImplementedException(); + public ValueTask DisposeAsync() => throw new NotImplementedException(); +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.DisposeResourceAsynchronously)] + public async Task Test_Method_UsingStatement_WithoutAsync_Task() + { + await VerifyDiagnosticAndFixAsync(@" +using System; +using System.Threading.Tasks; + +class C +{ + Task FooAsync() + { + [|using|] (var disposable = GetDisposable()) { } + + return Task.CompletedTask; + } + + private Disposable GetDisposable() => throw new NotImplementedException(); +} + +internal class Disposable : IDisposable, IAsyncDisposable +{ + public void Dispose() => throw new NotImplementedException(); + public ValueTask DisposeAsync() => throw new NotImplementedException(); +} +", @" +using System; +using System.Threading.Tasks; + +class C +{ + async Task FooAsync() + { + await using (var disposable = GetDisposable()) { } + + await Task.CompletedTask; + } + + private Disposable GetDisposable() => throw new NotImplementedException(); +} + +internal class Disposable : IDisposable, IAsyncDisposable +{ + public void Dispose() => throw new NotImplementedException(); + public ValueTask DisposeAsync() => throw new NotImplementedException(); +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.DisposeResourceAsynchronously)] + public async Task Test_Method_UsingStatement_WithoutAsync_TaskOfT() + { + await VerifyDiagnosticAndFixAsync(@" +using System; +using System.Threading.Tasks; + +class C +{ + Task FooAsync() + { + [|using|] (var disposable = GetDisposable()) { } + + return Task.FromResult(""""); + } + + private Disposable GetDisposable() => throw new NotImplementedException(); +} + +internal class Disposable : IDisposable, IAsyncDisposable +{ + public void Dispose() => throw new NotImplementedException(); + public ValueTask DisposeAsync() => throw new NotImplementedException(); +} +", @" +using System; +using System.Threading.Tasks; + +class C +{ + async Task FooAsync() + { + await using (var disposable = GetDisposable()) { } + + return await Task.FromResult(""""); + } + + private Disposable GetDisposable() => throw new NotImplementedException(); +} + +internal class Disposable : IDisposable, IAsyncDisposable +{ + public void Dispose() => throw new NotImplementedException(); + public ValueTask DisposeAsync() => throw new NotImplementedException(); +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.DisposeResourceAsynchronously)] + public async Task Test_LocalFunction_UsingStatement_WithAsync() + { + await VerifyDiagnosticAndFixAsync(@" +using System; +using System.Threading.Tasks; + +class C +{ + void M() + { + async Task FooAsync() + { + [|using|] (var disposable = await GetDisposableAsync()) { } + } + } + + private Task GetDisposableAsync() => throw new NotImplementedException(); +} + +internal class Disposable : IDisposable, IAsyncDisposable +{ + public void Dispose() => throw new NotImplementedException(); + public ValueTask DisposeAsync() => throw new NotImplementedException(); +} +", @" +using System; +using System.Threading.Tasks; + +class C +{ + void M() + { + async Task FooAsync() + { + await using (var disposable = await GetDisposableAsync()) { } + } + } + + private Task GetDisposableAsync() => throw new NotImplementedException(); +} + +internal class Disposable : IDisposable, IAsyncDisposable +{ + public void Dispose() => throw new NotImplementedException(); + public ValueTask DisposeAsync() => throw new NotImplementedException(); +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.DisposeResourceAsynchronously)] + public async Task Test_LocalFunction_UsingStatement_WithoutAsync_Task() + { + await VerifyDiagnosticAndFixAsync(@" +using System; +using System.Threading.Tasks; + +class C +{ + void M() + { + Task FooAsync() + { + [|using|] (var disposable = GetDisposable()) { } + + return Task.CompletedTask; + } + } + + private Disposable GetDisposable() => throw new NotImplementedException(); +} + +internal class Disposable : IDisposable, IAsyncDisposable +{ + public void Dispose() => throw new NotImplementedException(); + public ValueTask DisposeAsync() => throw new NotImplementedException(); +} +", @" +using System; +using System.Threading.Tasks; + +class C +{ + void M() + { + async Task FooAsync() + { + await using (var disposable = GetDisposable()) { } + + await Task.CompletedTask; + } + } + + private Disposable GetDisposable() => throw new NotImplementedException(); +} + +internal class Disposable : IDisposable, IAsyncDisposable +{ + public void Dispose() => throw new NotImplementedException(); + public ValueTask DisposeAsync() => throw new NotImplementedException(); +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.DisposeResourceAsynchronously)] + public async Task Test_LocalFunction_UsingStatement_WithoutAsync_TaskOfT() + { + await VerifyDiagnosticAndFixAsync(@" +using System; +using System.Threading.Tasks; + +class C +{ + void M() + { + Task FooAsync() + { + [|using|] (var disposable = GetDisposable()) { } + + return Task.FromResult(""""); + } + } + + private Disposable GetDisposable() => throw new NotImplementedException(); +} + +internal class Disposable : IDisposable, IAsyncDisposable +{ + public void Dispose() => throw new NotImplementedException(); + public ValueTask DisposeAsync() => throw new NotImplementedException(); +} +", @" +using System; +using System.Threading.Tasks; + +class C +{ + void M() + { + async Task FooAsync() + { + await using (var disposable = GetDisposable()) { } + + return await Task.FromResult(""""); + } + } + + private Disposable GetDisposable() => throw new NotImplementedException(); +} + +internal class Disposable : IDisposable, IAsyncDisposable +{ + public void Dispose() => throw new NotImplementedException(); + public ValueTask DisposeAsync() => throw new NotImplementedException(); +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.DisposeResourceAsynchronously)] + public async Task Test_SimpleLambda_LocalStatement_WithAsync() + { + await VerifyDiagnosticAndFixAsync(@" +using System; +using System.Threading.Tasks; + +class C +{ + void Foo() + { + Func action = async f => + { + [|using|] var disposable = await GetDisposableAsync(); + }; + } + + private Task GetDisposableAsync() => throw new NotImplementedException(); +} + +internal class Disposable : IDisposable, IAsyncDisposable +{ + public void Dispose() => throw new NotImplementedException(); + public ValueTask DisposeAsync() => throw new NotImplementedException(); +} +", @" +using System; +using System.Threading.Tasks; + +class C +{ + void Foo() + { + Func action = async f => + { + await using var disposable = await GetDisposableAsync(); + }; + } + + private Task GetDisposableAsync() => throw new NotImplementedException(); +} + +internal class Disposable : IDisposable, IAsyncDisposable +{ + public void Dispose() => throw new NotImplementedException(); + public ValueTask DisposeAsync() => throw new NotImplementedException(); +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.DisposeResourceAsynchronously)] + public async Task Test_SimpleLambda_LocalStatement_WithoutAsync() + { + await VerifyDiagnosticAndFixAsync(@" +using System; +using System.Threading.Tasks; + +class C +{ + void Foo() + { + Func action = f => + { + [|using|] var disposable = GetDisposable(); + return Task.CompletedTask; + }; + } + + private Disposable GetDisposable() => throw new NotImplementedException(); +} + +internal class Disposable : IDisposable, IAsyncDisposable +{ + public void Dispose() => throw new NotImplementedException(); + public ValueTask DisposeAsync() => throw new NotImplementedException(); +} +", @" +using System; +using System.Threading.Tasks; + +class C +{ + void Foo() + { + Func action = async f => + { + await using var disposable = GetDisposable(); + await Task.CompletedTask; + }; + } + + private Disposable GetDisposable() => throw new NotImplementedException(); +} + +internal class Disposable : IDisposable, IAsyncDisposable +{ + public void Dispose() => throw new NotImplementedException(); + public ValueTask DisposeAsync() => throw new NotImplementedException(); +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.DisposeResourceAsynchronously)] + public async Task Test_ParenthesizedLambda_LocalStatement_WithAsync() + { + await VerifyDiagnosticAndFixAsync(@" +using System; +using System.Threading.Tasks; + +class C +{ + void Foo() + { + var action = async () => + { + [|using|] var disposable = await GetDisposableAsync(); + }; + } + + private Task GetDisposableAsync() => throw new NotImplementedException(); +} + +internal class Disposable : IDisposable, IAsyncDisposable +{ + public void Dispose() => throw new NotImplementedException(); + public ValueTask DisposeAsync() => throw new NotImplementedException(); +} +", @" +using System; +using System.Threading.Tasks; + +class C +{ + void Foo() + { + var action = async () => + { + await using var disposable = await GetDisposableAsync(); + }; + } + + private Task GetDisposableAsync() => throw new NotImplementedException(); +} + +internal class Disposable : IDisposable, IAsyncDisposable +{ + public void Dispose() => throw new NotImplementedException(); + public ValueTask DisposeAsync() => throw new NotImplementedException(); +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.DisposeResourceAsynchronously)] + public async Task Test_ParenthesizedLambda_LocalStatement_WithoutAsync() + { + await VerifyDiagnosticAndFixAsync(@" +using System; +using System.Threading.Tasks; + +class C +{ + void Foo() + { + Func action = (f) => + { + [|using|] var disposable = GetDisposable(); + return Task.CompletedTask; + }; + } + + private Disposable GetDisposable() => throw new NotImplementedException(); +} + +internal class Disposable : IDisposable, IAsyncDisposable +{ + public void Dispose() => throw new NotImplementedException(); + public ValueTask DisposeAsync() => throw new NotImplementedException(); +} +", @" +using System; +using System.Threading.Tasks; + +class C +{ + void Foo() + { + Func action = async (f) => + { + await using var disposable = GetDisposable(); + await Task.CompletedTask; + }; + } + + private Disposable GetDisposable() => throw new NotImplementedException(); +} + +internal class Disposable : IDisposable, IAsyncDisposable +{ + public void Dispose() => throw new NotImplementedException(); + public ValueTask DisposeAsync() => throw new NotImplementedException(); +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.DisposeResourceAsynchronously)] + public async Task Test_AnonymousMethod_LocalStatement_WithAsync() + { + await VerifyDiagnosticAndFixAsync(@" +using System; +using System.Threading.Tasks; + +class C +{ + void Foo() + { + Func action = async delegate (string f) + { + [|using|] var disposable = await GetDisposableAsync(); + }; + } + + private Task GetDisposableAsync() => throw new NotImplementedException(); +} + +internal class Disposable : IDisposable, IAsyncDisposable +{ + public void Dispose() => throw new NotImplementedException(); + public ValueTask DisposeAsync() => throw new NotImplementedException(); +} +", @" +using System; +using System.Threading.Tasks; + +class C +{ + void Foo() + { + Func action = async delegate (string f) + { + await using var disposable = await GetDisposableAsync(); + }; + } + + private Task GetDisposableAsync() => throw new NotImplementedException(); +} + +internal class Disposable : IDisposable, IAsyncDisposable +{ + public void Dispose() => throw new NotImplementedException(); + public ValueTask DisposeAsync() => throw new NotImplementedException(); +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.DisposeResourceAsynchronously)] + public async Task Test_AnonymousMethod_LocalStatement_WithoutAsync() + { + await VerifyDiagnosticAndFixAsync(@" +using System; +using System.Threading.Tasks; + +class C +{ + void Foo() + { + Func action = delegate (string f) + { + [|using|] var disposable = GetDisposable(); + return Task.CompletedTask; + }; + } + + private Disposable GetDisposable() => throw new NotImplementedException(); +} + +internal class Disposable : IDisposable, IAsyncDisposable +{ + public void Dispose() => throw new NotImplementedException(); + public ValueTask DisposeAsync() => throw new NotImplementedException(); +} +", @" +using System; +using System.Threading.Tasks; + +class C +{ + void Foo() + { + Func action = async delegate (string f) + { + await using var disposable = GetDisposable(); + await Task.CompletedTask; + }; + } + + private Disposable GetDisposable() => throw new NotImplementedException(); +} + +internal class Disposable : IDisposable, IAsyncDisposable +{ + public void Dispose() => throw new NotImplementedException(); + public ValueTask DisposeAsync() => throw new NotImplementedException(); +} +"); + } +} diff --git a/src/Tools/Metadata/MetadataFile.cs b/src/Tools/Metadata/MetadataFile.cs index 0214fe8a84..c2b2a13e8e 100644 --- a/src/Tools/Metadata/MetadataFile.cs +++ b/src/Tools/Metadata/MetadataFile.cs @@ -441,6 +441,8 @@ public static void CleanAnalyzers(string filePath) } } + doc.Root.ReplaceAll(doc.Root.Elements().OrderBy(f => f.Element("Id").Value)); + using (var sw = new StreamWriter(filePath)) using (XmlWriter xw = XmlWriter.Create(sw, new XmlWriterSettings() { Indent = true, Encoding = Encoding.UTF8 })) doc.Save(xw); diff --git a/src/VisualStudioCode/package/src/configurationFiles.generated.ts b/src/VisualStudioCode/package/src/configurationFiles.generated.ts index 16b47225a9..f73aaaa890 100644 --- a/src/VisualStudioCode/package/src/configurationFiles.generated.ts +++ b/src/VisualStudioCode/package/src/configurationFiles.generated.ts @@ -903,6 +903,9 @@ roslynator_analyzers.enabled_by_default = true|false #dotnet_diagnostic.rcs1260.severity = none # Options: roslynator_trailing_comma_style +# Resource can be disposed asynchronously +#dotnet_diagnostic.rcs1261.severity = suggestion + # Use pattern matching #dotnet_diagnostic.rcs9001.severity = silent