diff --git a/analyzers/its/expected/BlazorSample/S6966-BlazorSample-net7.0.json b/analyzers/its/expected/BlazorSample/S6966-BlazorSample-net7.0.json
new file mode 100644
index 00000000000..adc4239634e
--- /dev/null
+++ b/analyzers/its/expected/BlazorSample/S6966-BlazorSample-net7.0.json
@@ -0,0 +1,10 @@
+{
+ "Issues": [
+ {
+ "Id": "S6966",
+ "Message": "Await RunAsync instead.",
+ "Uri": "https://github.com/SonarSource/sonar-dotnet/blob/master/analyzers/its/Projects/BlazorSample/BlazorSample/Program.cs#L29",
+ "Location": "Line 29 Position 1-10"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/analyzers/its/expected/CSharpLatest/S6966-NetCore31.MVC.ConfigurableRules-netcoreapp3.1.Views.json b/analyzers/its/expected/CSharpLatest/S6966-NetCore31.MVC.ConfigurableRules-netcoreapp3.1.Views.json
new file mode 100644
index 00000000000..a78adad83c1
--- /dev/null
+++ b/analyzers/its/expected/CSharpLatest/S6966-NetCore31.MVC.ConfigurableRules-netcoreapp3.1.Views.json
@@ -0,0 +1,10 @@
+{
+ "Issues": [
+ {
+ "Id": "S6966",
+ "Message": "Await RenderSectionAsync instead.",
+ "Uri": "https://github.com/SonarSource/sonar-dotnet/blob/master/analyzers/its/Projects/CSharpLatest/NetCore31WithConfigurableRules/Views/Shared/_Layout.cshtml#L46",
+ "Location": "Line 46 Position 7-48"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/analyzers/its/expected/ManuallyAddedNoncompliantIssues.CS/S6966-AspNetCore6-net6.0.json b/analyzers/its/expected/ManuallyAddedNoncompliantIssues.CS/S6966-AspNetCore6-net6.0.json
new file mode 100644
index 00000000000..b10b3a5fa62
--- /dev/null
+++ b/analyzers/its/expected/ManuallyAddedNoncompliantIssues.CS/S6966-AspNetCore6-net6.0.json
@@ -0,0 +1,10 @@
+{
+ "Issues": [
+ {
+ "Id": "S6966",
+ "Message": "Await RunAsync instead.",
+ "Uri": "https://github.com/SonarSource/sonar-dotnet/blob/master/analyzers/its/Projects/ManuallyAddedNoncompliantIssues.CS/AspNetCore6/Program.cs#L27",
+ "Location": "Line 27 Position 1-10"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/analyzers/its/expected/ManuallyAddedNoncompliantIssues.CS/S6966-AspNetCore7-net7.0.json b/analyzers/its/expected/ManuallyAddedNoncompliantIssues.CS/S6966-AspNetCore7-net7.0.json
new file mode 100644
index 00000000000..6b27d03caf0
--- /dev/null
+++ b/analyzers/its/expected/ManuallyAddedNoncompliantIssues.CS/S6966-AspNetCore7-net7.0.json
@@ -0,0 +1,10 @@
+{
+ "Issues": [
+ {
+ "Id": "S6966",
+ "Message": "Await RunAsync instead.",
+ "Uri": "https://github.com/SonarSource/sonar-dotnet/blob/master/analyzers/its/Projects/ManuallyAddedNoncompliantIssues.CS/AspNetCore7/Program.cs#L27",
+ "Location": "Line 27 Position 1-10"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/analyzers/its/expected/ManuallyAddedNoncompliantIssues.CS/S6966-AspNetCore8-net8.0.json b/analyzers/its/expected/ManuallyAddedNoncompliantIssues.CS/S6966-AspNetCore8-net8.0.json
new file mode 100644
index 00000000000..3f5538e5205
--- /dev/null
+++ b/analyzers/its/expected/ManuallyAddedNoncompliantIssues.CS/S6966-AspNetCore8-net8.0.json
@@ -0,0 +1,10 @@
+{
+ "Issues": [
+ {
+ "Id": "S6966",
+ "Message": "Await RunAsync instead.",
+ "Uri": "https://github.com/SonarSource/sonar-dotnet/blob/master/analyzers/its/Projects/ManuallyAddedNoncompliantIssues.CS/AspNetCore8/Program.cs#L27",
+ "Location": "Line 27 Position 1-10"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/analyzers/its/expected/RazorSample/S6966-RazorSample-net7.0.json b/analyzers/its/expected/RazorSample/S6966-RazorSample-net7.0.json
new file mode 100644
index 00000000000..d19cf47db78
--- /dev/null
+++ b/analyzers/its/expected/RazorSample/S6966-RazorSample-net7.0.json
@@ -0,0 +1,10 @@
+{
+ "Issues": [
+ {
+ "Id": "S6966",
+ "Message": "Await RunAsync instead.",
+ "Uri": "https://github.com/SonarSource/sonar-dotnet/blob/master/analyzers/its/Projects/RazorSample/RazorSample/Program.cs#L25",
+ "Location": "Line 25 Position 1-10"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/analyzers/its/expected/akka.net/S6966-Akka.Cluster.Sharding-netstandard2.0.json b/analyzers/its/expected/akka.net/S6966-Akka.Cluster.Sharding-netstandard2.0.json
new file mode 100644
index 00000000000..4364c712392
--- /dev/null
+++ b/analyzers/its/expected/akka.net/S6966-Akka.Cluster.Sharding-netstandard2.0.json
@@ -0,0 +1,10 @@
+{
+ "Issues": [
+ {
+ "Id": "S6966",
+ "Message": "Await StartProxyAsync instead.",
+ "Uri": "https://github.com/SonarSource/sonar-dotnet/blob/master/analyzers/its/Projects/akka.net/src/contrib/cluster/Akka.Cluster.Sharding/ClusterSharding.cs#L742",
+ "Location": "Line 742 Position 24-92"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/analyzers/rspec/cs/S6966.html b/analyzers/rspec/cs/S6966.html
new file mode 100644
index 00000000000..99eaf88ba54
--- /dev/null
+++ b/analyzers/rspec/cs/S6966.html
@@ -0,0 +1,70 @@
+
In an async method, any blocking operations should be avoided.
+Why is this an issue?
+Using a synchronous method instead of its asynchronous
+counterpart in an async method blocks the execution and is considered bad practice for several reasons:
+
+
+ Resource Utilization
+
+
+ Each thread consumes system resources, such as memory. When a thread is blocked, it’s not doing any useful work, but it’s still consuming these
+ resources. This can lead to inefficient use of system resources.
+
+
+ Scalability
+
+
+ Blocking threads can limit the scalability of your application. In a high-load scenario where many operations are happening concurrently, each
+ blocked thread represents a missed opportunity to do useful work. This can prevent your application from effectively handling increased load.
+
+
+ Performance
+
+
+ Blocking threads can degrade the performance of your application. If all threads in the thread pool become blocked, new tasks can’t start
+ executing until an existing task completes and frees up a thread. This can lead to delays and poor responsiveness.
+
+
+Instead of blocking, it’s recommended to use the async operator with async methods. This
+allows the system to release the current thread back to the thread pool until the awaited task is complete, improving scalability and
+responsiveness.
+How to fix it
+Code examples
+Noncompliant code example
+
+public async Task Examples(Stream stream, DbSet<Person> dbSet)
+{
+ stream.Read(array, 0, 1024); // Noncompliant
+ File.ReadAllLines("path"); // Noncompliant
+ dbSet.ToList(); // Noncompliant in Entity Framework Core queries
+ dbSet.FirstOrDefault(x => x.Age >= 18); // Noncompliant in Entity Framework Core queries
+}
+
+Compliant solution
+
+public async Task Examples(Stream stream, DbSet<Person> dbSet)
+{
+ await stream.ReadAsync(array, 0, 1024);
+ await File.ReadAllLinesAsync("path");
+ await dbSet.ToListAsync();
+ await dbSet.FirstOrDefaultAsync(x => x.Age >= 18);
+}
+
+Resources
+Documentation
+
+Articles & blog posts
+
+
diff --git a/analyzers/rspec/cs/S6966.json b/analyzers/rspec/cs/S6966.json
new file mode 100644
index 00000000000..02f3331eb45
--- /dev/null
+++ b/analyzers/rspec/cs/S6966.json
@@ -0,0 +1,23 @@
+{
+ "title": "Awaitable method should be used",
+ "type": "CODE_SMELL",
+ "status": "ready",
+ "remediation": {
+ "func": "Constant\/Issue",
+ "constantCost": "5min"
+ },
+ "tags": [
+ "async-await"
+ ],
+ "defaultSeverity": "Major",
+ "ruleSpecification": "RSPEC-6966",
+ "sqKey": "S6966",
+ "scope": "All",
+ "quickfix": "targeted",
+ "code": {
+ "impacts": {
+ "RELIABILITY": "MEDIUM"
+ },
+ "attribute": "COMPLETE"
+ }
+}
diff --git a/analyzers/rspec/cs/Sonar_way_profile.json b/analyzers/rspec/cs/Sonar_way_profile.json
index 5006acc1f55..c2b1e24c7a2 100644
--- a/analyzers/rspec/cs/Sonar_way_profile.json
+++ b/analyzers/rspec/cs/Sonar_way_profile.json
@@ -317,6 +317,6 @@
"S6930",
"S6931",
"S6934",
- "S6961"
+ "S6966"
]
}
diff --git a/analyzers/src/SonarAnalyzer.CSharp/Extensions/ExpressionSyntaxExtensions.cs b/analyzers/src/SonarAnalyzer.CSharp/Extensions/ExpressionSyntaxExtensions.cs
index 2f75c0e0210..52fed73eedb 100644
--- a/analyzers/src/SonarAnalyzer.CSharp/Extensions/ExpressionSyntaxExtensions.cs
+++ b/analyzers/src/SonarAnalyzer.CSharp/Extensions/ExpressionSyntaxExtensions.cs
@@ -97,5 +97,20 @@ public static bool TryGetExpressionComparedToNull(this ExpressionSyntax expressi
return false;
}
+
+ ///
+ /// Returns the expression, representing the left side of the dot. This is useful for finding the expression of an invoked expression.
+ /// For the expression of the invocation M() in the expression this.A.B.M() the member access this.A.B is returned and
+ /// for this.A?.B?.M() the member binding .B is returned.
+ ///
+ /// The expression to start the search of. Should be an MemberAccess or a MemberBinding.
+ /// The expression left of the dot or question mark dot.
+ public static ExpressionSyntax GetLeftOfDot(this ExpressionSyntax expression) =>
+ expression switch
+ {
+ MemberAccessExpressionSyntax memberAccessExpression => memberAccessExpression.Expression,
+ MemberBindingExpressionSyntax memberBindingExpression => memberBindingExpression.GetParentConditionalAccessExpression()?.Expression,
+ _ => null,
+ };
}
}
diff --git a/analyzers/src/SonarAnalyzer.CSharp/Extensions/SyntaxNodeExtensions.cs b/analyzers/src/SonarAnalyzer.CSharp/Extensions/SyntaxNodeExtensions.cs
index 1bbff8b46c3..f262ca1d14e 100644
--- a/analyzers/src/SonarAnalyzer.CSharp/Extensions/SyntaxNodeExtensions.cs
+++ b/analyzers/src/SonarAnalyzer.CSharp/Extensions/SyntaxNodeExtensions.cs
@@ -31,18 +31,21 @@ public static partial class SyntaxNodeExtensions
SyntaxKind.AddAccessorDeclaration,
SyntaxKind.AnonymousMethodExpression,
SyntaxKind.BaseConstructorInitializer,
+ SyntaxKind.CompilationUnit,
SyntaxKind.ConstructorDeclaration,
SyntaxKind.ConversionOperatorDeclaration,
SyntaxKind.DestructorDeclaration,
- SyntaxKind.EqualsValueClause,
+ SyntaxKind.EnumMemberDeclaration,
+ SyntaxKind.FieldDeclaration,
SyntaxKind.GetAccessorDeclaration,
- SyntaxKind.GlobalStatement,
SyntaxKindEx.InitAccessorDeclaration,
SyntaxKindEx.LocalFunctionStatement,
SyntaxKind.MethodDeclaration,
SyntaxKind.OperatorDeclaration,
+ SyntaxKind.Parameter,
SyntaxKind.ParenthesizedLambdaExpression,
SyntaxKindEx.PrimaryConstructorBaseType,
+ SyntaxKind.PropertyDeclaration,
SyntaxKind.RemoveAccessorDeclaration,
SyntaxKind.SetAccessorDeclaration,
SyntaxKind.SimpleLambdaExpression,
diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseAwaitableMethod.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseAwaitableMethod.cs
new file mode 100644
index 00000000000..4a8c48dd9cb
--- /dev/null
+++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseAwaitableMethod.cs
@@ -0,0 +1,180 @@
+/*
+ * SonarAnalyzer for .NET
+ * Copyright (C) 2015-2024 SonarSource SA
+ * mailto: contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Shared.Extensions;
+using WellKnownExtensionMethodContainer = SonarAnalyzer.Common.MultiValueDictionary;
+namespace SonarAnalyzer.Rules.CSharp;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class UseAwaitableMethod : SonarDiagnosticAnalyzer
+{
+ private const string DiagnosticId = "S6966";
+ private const string MessageFormat = "Await {0} instead.";
+
+ private static readonly DiagnosticDescriptor Rule = DescriptorFactory.Create(DiagnosticId, MessageFormat);
+
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule);
+
+ protected override void Initialize(SonarAnalysisContext context) =>
+ context.RegisterCompilationStartAction(compilationStart =>
+ {
+ // Not every async method is defined in the same class/interface as its non-async counterpart.
+ // For example the EntityFrameworkQueryableExtensions.AnyAsync() method provides an async version of the Enumerable.Any() method for IQueryable types.
+ // WellKnownExtensionMethodContainer stores where to look for the async versions of certain methods from a type, e.g. async versions of methods from
+ // System.Linq.Enumerable can be found in Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.
+ var wellKnownExtensionMethodContainer = BuildWellKnownExtensionMethodContainers(compilationStart.Compilation);
+ context.RegisterCodeBlockStartAction(CSharpGeneratedCodeRecognizer.Instance, codeBlockStart =>
+ {
+ if (IsAsyncCodeBlock(codeBlockStart.CodeBlock))
+ {
+ codeBlockStart.RegisterNodeAction(nodeContext =>
+ {
+ var invocationExpression = (InvocationExpressionSyntax)nodeContext.Node;
+
+ var awaitableAlternatives = FindAwaitableAlternatives(wellKnownExtensionMethodContainer, codeBlockStart.CodeBlock, invocationExpression,
+ nodeContext.SemanticModel, nodeContext.ContainingSymbol, nodeContext.Cancel);
+ if (awaitableAlternatives.FirstOrDefault() is { Name: { } alternative })
+ {
+ nodeContext.ReportIssue(Rule, invocationExpression, alternative);
+ }
+ }, SyntaxKind.InvocationExpression);
+ }
+ });
+ });
+
+ private static WellKnownExtensionMethodContainer BuildWellKnownExtensionMethodContainers(Compilation compilation)
+ {
+ var wellKnownExtensionMethodContainer = new WellKnownExtensionMethodContainer();
+ var queryable = compilation.GetTypeByMetadataName(KnownType.System_Linq_Queryable);
+ var enumerable = compilation.GetTypeByMetadataName(KnownType.System_Linq_Enumerable);
+ if (queryable is not null && enumerable is not null)
+ {
+ if (compilation.GetTypeByMetadataName(KnownType.Microsoft_EntityFrameworkCore_EntityFrameworkQueryableExtensions) is { } entityFrameworkQueryableExtensions)
+ {
+ wellKnownExtensionMethodContainer.Add(queryable, entityFrameworkQueryableExtensions);
+ wellKnownExtensionMethodContainer.Add(enumerable, entityFrameworkQueryableExtensions);
+ }
+ if (compilation.GetTypeByMetadataName(KnownType.Microsoft_EntityFrameworkCore_RelationalQueryableExtensions) is { } relationalQueryableExtensions)
+ {
+ wellKnownExtensionMethodContainer.Add(queryable, relationalQueryableExtensions);
+ wellKnownExtensionMethodContainer.Add(enumerable, relationalQueryableExtensions);
+ }
+ }
+ if (compilation.GetTypeByMetadataName(KnownType.System_Net_Sockets_Socket) is { } socket
+ && compilation.GetTypeByMetadataName(KnownType.System_Net_Sockets_SocketTaskExtensions) is { } socketTaskExtensions)
+ {
+ wellKnownExtensionMethodContainer.Add(socket, socketTaskExtensions);
+ }
+ return wellKnownExtensionMethodContainer;
+ }
+
+ private static ImmutableArray FindAwaitableAlternatives(WellKnownExtensionMethodContainer wellKnownExtensionMethodContainer, SyntaxNode codeBlock,
+ InvocationExpressionSyntax invocationExpression, SemanticModel semanticModel, ISymbol containingSymbol, CancellationToken cancel)
+ {
+ var awaitableRoot = GetAwaitableRootOfInvocation(invocationExpression);
+ if (awaitableRoot is { Parent: AwaitExpressionSyntax })
+ {
+ return ImmutableArray.Empty; // Invocation result is already awaited.
+ }
+ if (invocationExpression.EnclosingScope() is { } scope && !IsAsyncCodeBlock(scope))
+ {
+ return ImmutableArray.Empty; // Not in an async scope
+ }
+ if (semanticModel.GetSymbolInfo(invocationExpression, cancel).Symbol is IMethodSymbol methodSymbol
+ && !methodSymbol.IsAwaitableNonDynamic(semanticModel, invocationExpression.SpanStart)) // The invoked method returns something awaitable (but it isn't awaited).
+ {
+ // Perf: Before doing (expensive) speculative re-binding in SpeculativeBindCandidates, we check if there is an "..Async()" alternative in scope.
+ var invokedType = invocationExpression.Expression.GetLeftOfDot() is { } expression && semanticModel.GetTypeInfo(expression) is { Type: { } type }
+ ? type // A dotted expression: Lookup the type, left of the dot (this may be different from methodSymbol.ContainingType)
+ : containingSymbol.ContainingType; // If not dotted, than the scope is the current type. Local function support is missing here.
+ var members = GetMethodSymbolsInScope($"{methodSymbol.Name}Async", wellKnownExtensionMethodContainer, invokedType, methodSymbol.ContainingType);
+ var awaitableCandidates = members.Where(x => x.IsAwaitableNonDynamic(semanticModel, invocationExpression.SpanStart));
+ var awaitableAlternatives = SpeculativeBindCandidates(semanticModel, codeBlock, awaitableRoot, invocationExpression, awaitableCandidates).ToImmutableArray();
+ return awaitableAlternatives;
+ }
+ return ImmutableArray.Empty;
+ }
+
+ private static IEnumerable GetMethodSymbolsInScope(string methodName, WellKnownExtensionMethodContainer wellKnownExtensionMethodContainer,
+ ITypeSymbol invokedType, ITypeSymbol methodContainer) =>
+ ((ITypeSymbol[])[.. invokedType.GetSelfAndBaseTypes(), .. WellKnownExtensionMethodContainer(wellKnownExtensionMethodContainer, methodContainer), methodContainer])
+ .Distinct()
+ .SelectMany(x => x.GetMembers(methodName))
+ .OfType();
+
+ private static IEnumerable WellKnownExtensionMethodContainer(WellKnownExtensionMethodContainer lookup, ITypeSymbol invokedType) =>
+ lookup.TryGetValue(invokedType, out var extensionMethodContainer)
+ ? extensionMethodContainer
+ : [];
+
+ private static IEnumerable SpeculativeBindCandidates(SemanticModel semanticModel, SyntaxNode codeBlock, SyntaxNode awaitableRoot,
+ InvocationExpressionSyntax invocationExpression, IEnumerable awaitableCandidates) =>
+ awaitableCandidates.Where(x => SpeculativeBindCandidate(semanticModel, x, codeBlock, awaitableRoot, invocationExpression));
+
+ private static bool SpeculativeBindCandidate(SemanticModel semanticModel, IMethodSymbol candidate, SyntaxNode codeBlock, SyntaxNode awaitableRoot, InvocationExpressionSyntax invocationExpression)
+ {
+ var root = codeBlock.SyntaxTree.GetRoot();
+ var invocationIdentifierName = invocationExpression.GetMethodCallIdentifier()?.Parent as IdentifierNameSyntax;
+
+ var invocationAnnotation = new SyntaxAnnotation();
+ var replace = root.ReplaceNodes([awaitableRoot, invocationIdentifierName, invocationExpression], (original, newNode) =>
+ {
+ var result = newNode;
+ if (original == invocationIdentifierName)
+ {
+ result = SyntaxFactory.IdentifierName(candidate.Name).WithTriviaFrom(invocationIdentifierName);
+ }
+ if (original == invocationExpression)
+ {
+ result = result.WithAdditionalAnnotations(invocationAnnotation);
+ }
+ if (original == awaitableRoot && result is ExpressionSyntax resultExpression)
+ {
+ result = SyntaxFactory.AwaitExpression(resultExpression);
+ }
+ return result;
+ });
+ var invocationReplaced = replace.GetAnnotatedNodes(invocationAnnotation).First();
+ var speculativeSymbolInfo = semanticModel.GetSpeculativeSymbolInfo(invocationReplaced.SpanStart, invocationReplaced, SpeculativeBindingOption.BindAsExpression);
+ var speculativeSymbol = speculativeSymbolInfo.Symbol as IMethodSymbol;
+ return candidate.Equals(speculativeSymbol) || candidate.Equals(speculativeSymbol?.ReducedFrom);
+ }
+
+ private static ExpressionSyntax GetAwaitableRootOfInvocation(ExpressionSyntax expression) =>
+ expression switch
+ {
+ { Parent: ConditionalAccessExpressionSyntax conditional } => conditional.GetRootConditionalAccessExpression(),
+ { Parent: MemberAccessExpressionSyntax memberAccess } => memberAccess.GetRootConditionalAccessExpression() ?? GetAwaitableRootOfInvocation(memberAccess),
+ { Parent: PostfixUnaryExpressionSyntax { RawKind: (int)SyntaxKindEx.SuppressNullableWarningExpression } parent } => GetAwaitableRootOfInvocation(parent),
+ { Parent: ParenthesizedExpressionSyntax parent } => GetAwaitableRootOfInvocation(parent),
+ { } self => self,
+ };
+
+ private static bool IsAsyncCodeBlock(SyntaxNode codeBlock) =>
+ codeBlock switch
+ {
+ CompilationUnitSyntax => true,
+ BaseMethodDeclarationSyntax { Modifiers: { } modifiers } => modifiers.AnyOfKind(SyntaxKind.AsyncKeyword),
+ AnonymousFunctionExpressionSyntax { AsyncKeyword: { } asyncKeyword } => asyncKeyword.IsKind(SyntaxKind.AsyncKeyword),
+ var localFunction when LocalFunctionStatementSyntaxWrapper.IsInstance(localFunction) => ((LocalFunctionStatementSyntaxWrapper)localFunction).Modifiers.AnyOfKind(SyntaxKind.AsyncKeyword),
+ _ => false,
+ };
+}
diff --git a/analyzers/src/SonarAnalyzer.Common/Extensions/ISymbolExtensions.Roslyn.cs b/analyzers/src/SonarAnalyzer.Common/Extensions/ISymbolExtensions.Roslyn.cs
new file mode 100644
index 00000000000..b49f9001cd8
--- /dev/null
+++ b/analyzers/src/SonarAnalyzer.Common/Extensions/ISymbolExtensions.Roslyn.cs
@@ -0,0 +1,83 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#nullable enable
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.CodeAnalysis.Shared.Extensions;
+
+// Copied from https://github.com/dotnet/roslyn/blob/ca66296efa86bd8078508fe7b38b91b415364f78/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/ISymbolExtensions.cs
+[ExcludeFromCodeCoverage]
+public static class ISymbolExtensions
+{
+ ///
+ /// If the is a method symbol, returns if the method's return type is "awaitable", but not if it's .
+ /// If the is a type symbol, returns if that type is "awaitable".
+ /// An "awaitable" is any type that exposes a GetAwaiter method which returns a valid "awaiter". This GetAwaiter method may be an instance method or an extension method.
+ ///
+ ///
+ /// Copied from .
+ ///
+ public static bool IsAwaitableNonDynamic(this ISymbol? symbol, SemanticModel semanticModel, int position)
+ {
+ var methodSymbol = symbol as IMethodSymbol;
+ ITypeSymbol? typeSymbol = null;
+
+ if (methodSymbol == null)
+ {
+ typeSymbol = symbol as ITypeSymbol;
+ if (typeSymbol == null)
+ {
+ return false;
+ }
+ }
+ else
+ {
+ if (methodSymbol.ReturnType == null)
+ {
+ return false;
+ }
+ }
+
+ // otherwise: needs valid GetAwaiter
+ var potentialGetAwaiters = semanticModel.LookupSymbols(position,
+ container: typeSymbol ?? methodSymbol!.ReturnType.OriginalDefinition,
+ name: WellKnownMemberNames.GetAwaiter,
+ includeReducedExtensionMethods: true);
+ var getAwaiters = potentialGetAwaiters.OfType().Where(x => !x.Parameters.Any());
+ return getAwaiters.Any(VerifyGetAwaiter);
+ }
+
+ // Copied from https://github.com/dotnet/roslyn/blob/ca66296efa86bd8078508fe7b38b91b415364f78/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/ISymbolExtensions.cs#L611
+ private static bool VerifyGetAwaiter(IMethodSymbol getAwaiter)
+ {
+ var returnType = getAwaiter.ReturnType;
+ if (returnType == null)
+ {
+ return false;
+ }
+
+ // bool IsCompleted { get }
+ if (!returnType.GetMembers().OfType().Any(p => p.Name == WellKnownMemberNames.IsCompleted && p.Type.SpecialType == SpecialType.System_Boolean && p.GetMethod != null))
+ {
+ return false;
+ }
+
+ var methods = returnType.GetMembers().OfType();
+
+ // NOTE: (vladres) The current version of C# Spec, §7.7.7.3 'Runtime evaluation of await expressions', requires that
+ // NOTE: the interface method INotifyCompletion.OnCompleted or ICriticalNotifyCompletion.UnsafeOnCompleted is invoked
+ // NOTE: (rather than any OnCompleted method conforming to a certain pattern).
+ // NOTE: Should this code be updated to match the spec?
+
+ // void OnCompleted(Action)
+ // Actions are delegates, so we'll just check for delegates.
+ if (!methods.Any(x => x.Name == WellKnownMemberNames.OnCompleted && x.ReturnsVoid && x.Parameters is { Length: 1 } parameter && parameter[0] is { Type.TypeKind: TypeKind.Delegate }))
+ return false;
+
+ // void GetResult() || T GetResult()
+ return methods.Any(m => m.Name == WellKnownMemberNames.GetResult && !m.Parameters.Any());
+ }
+}
diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs
index 80ee19193ec..6ec98cf264e 100644
--- a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs
+++ b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs
@@ -106,6 +106,7 @@ public sealed partial class KnownType
public static readonly KnownType Microsoft_EntityFrameworkCore_DbContext = new("Microsoft.EntityFrameworkCore.DbContext");
public static readonly KnownType Microsoft_EntityFrameworkCore_DbContextOptionsBuilder = new("Microsoft.EntityFrameworkCore.DbContextOptionsBuilder");
public static readonly KnownType Microsoft_EntityFrameworkCore_DbSet_TEntity = new("Microsoft.EntityFrameworkCore.DbSet", "TEntity");
+ public static readonly KnownType Microsoft_EntityFrameworkCore_EntityFrameworkQueryableExtensions = new("Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions");
public static readonly KnownType Microsoft_EntityFrameworkCore_Migrations_Migration = new("Microsoft.EntityFrameworkCore.Migrations.Migration");
public static readonly KnownType Microsoft_EntityFrameworkCore_MySQLDbContextOptionsExtensions = new("Microsoft.EntityFrameworkCore.MySQLDbContextOptionsExtensions");
public static readonly KnownType Microsoft_EntityFrameworkCore_NpgsqlDbContextOptionsExtensions = new("Microsoft.EntityFrameworkCore.NpgsqlDbContextOptionsExtensions");
@@ -391,6 +392,7 @@ public sealed partial class KnownType
public static readonly KnownType System_Net_Security_SslPolicyErrors = new("System.Net.Security.SslPolicyErrors");
public static readonly KnownType System_Net_SecurityProtocolType = new("System.Net.SecurityProtocolType");
public static readonly KnownType System_Net_Sockets_Socket = new("System.Net.Sockets.Socket");
+ public static readonly KnownType System_Net_Sockets_SocketTaskExtensions = new("System.Net.Sockets.SocketTaskExtensions");
public static readonly KnownType System_Net_Sockets_TcpClient = new("System.Net.Sockets.TcpClient");
public static readonly KnownType System_Net_Sockets_UdpClient = new("System.Net.Sockets.UdpClient");
public static readonly KnownType System_Net_WebClient = new("System.Net.WebClient");
diff --git a/analyzers/tests/SonarAnalyzer.Test/Extensions/SyntaxNodeExtensionsTest.cs b/analyzers/tests/SonarAnalyzer.Test/Extensions/SyntaxNodeExtensionsTest.cs
index 27f024af618..6e11c9860d8 100644
--- a/analyzers/tests/SonarAnalyzer.Test/Extensions/SyntaxNodeExtensionsTest.cs
+++ b/analyzers/tests/SonarAnalyzer.Test/Extensions/SyntaxNodeExtensionsTest.cs
@@ -690,7 +690,7 @@ public X M()
}
}
""";
- var node = NodeBetweenMarkers(code, LanguageNames.CSharp);
+ var node = NodeBetweenMarkers(code, AnalyzerLanguage.CSharp);
var parentConditional = ExtensionsCS.GetParentConditionalAccessExpression(node);
parentConditional.ToString().Should().Be(parent);
parentConditional.Expression.ToString().Should().Be(parentExpression);
@@ -726,7 +726,7 @@ Return Nothing
End Function
End Class
""";
- var node = NodeBetweenMarkers(code, LanguageNames.VisualBasic);
+ var node = NodeBetweenMarkers(code, AnalyzerLanguage.VisualBasic);
var parentConditional = ExtensionsVB.GetParentConditionalAccessExpression(node);
parentConditional.ToString().Should().Be(parent);
parentConditional.Expression.ToString().Should().Be(parentExpression);
@@ -758,7 +758,7 @@ public X M()
}}
}}
";
- var node = NodeBetweenMarkers(code, LanguageNames.CSharp);
+ var node = NodeBetweenMarkers(code, AnalyzerLanguage.CSharp);
var parentConditional = ExtensionsCS.GetRootConditionalAccessExpression(node);
parentConditional.ToString().Should().Be(expression.Replace("$$", string.Empty));
}
@@ -789,7 +789,7 @@ public void M(int p) {
}
}
""";
- var node = NodeBetweenMarkers(code, LanguageNames.CSharp);
+ var node = NodeBetweenMarkers(code, AnalyzerLanguage.CSharp);
var argumentList = ExtensionsCS.ArgumentList(node).Arguments;
var argument = argumentList.Should().ContainSingle().Which;
(argument is { Expression: SyntaxCS.LiteralExpressionSyntax { Token.ValueText: "1" } }).Should().BeTrue();
@@ -810,7 +810,7 @@ public C(int p): base(p) { }
}
""";
- var node = NodeBetweenMarkers(code, LanguageNames.CSharp);
+ var node = NodeBetweenMarkers(code, AnalyzerLanguage.CSharp);
var argumentList = ExtensionsCS.ArgumentList(node).Arguments;
var argument = argumentList.Should().ContainSingle().Which;
(argument is { Expression: SyntaxCS.LiteralExpressionSyntax { Token.ValueText: "1" } }).Should().BeTrue();
@@ -823,7 +823,7 @@ public void ArgumentList_CS_PrimaryConstructorBaseType()
public class Base(int p);
public class Derived(int p): $$Base(1)$$;
""";
- var node = NodeBetweenMarkers(code, LanguageNames.CSharp);
+ var node = NodeBetweenMarkers(code, AnalyzerLanguage.CSharp);
var argumentList = ExtensionsCS.ArgumentList(node).Arguments;
var argument = argumentList.Should().ContainSingle().Which;
(argument is { Expression: SyntaxCS.LiteralExpressionSyntax { Token.ValueText: "1" } }).Should().BeTrue();
@@ -840,7 +840,7 @@ public void M() {
}
}
""";
- var node = NodeBetweenMarkers(code, LanguageNames.CSharp);
+ var node = NodeBetweenMarkers(code, AnalyzerLanguage.CSharp);
ExtensionsCS.ArgumentList(node).Should().BeNull();
}
@@ -860,7 +860,7 @@ public void M() {
}
}
""";
- var node = NodeBetweenMarkers(code, LanguageNames.CSharp);
+ var node = NodeBetweenMarkers(code, AnalyzerLanguage.CSharp);
var sut = () => ExtensionsCS.ArgumentList(node);
sut.Should().Throw();
}
@@ -887,7 +887,7 @@ Public Sub M(p As Integer)
End Sub
End Class
""";
- var node = NodeBetweenMarkers(code, LanguageNames.VisualBasic, getInnermostNodeForTie: true);
+ var node = NodeBetweenMarkers(code, AnalyzerLanguage.VisualBasic, getInnermostNodeForTie: true);
var argumentList = ExtensionsVB.ArgumentList(node);
var argument = argumentList.Arguments.Should().ContainSingle().Which;
(argument.GetExpression() is SyntaxVB.LiteralExpressionSyntax { Token.ValueText: "1" }).Should().BeTrue();
@@ -904,7 +904,7 @@ Public Sub M()
End Sub
End Class
""";
- var node = NodeBetweenMarkers(code, LanguageNames.VisualBasic, getInnermostNodeForTie: true);
+ var node = NodeBetweenMarkers(code, AnalyzerLanguage.VisualBasic, getInnermostNodeForTie: true);
var argumentList = ExtensionsVB.ArgumentList(node);
argumentList.Arguments.Should().SatisfyRespectively(
a => (a.GetExpression() is SyntaxVB.IdentifierNameSyntax { Identifier.ValueText: "s" }).Should().BeTrue(),
@@ -919,7 +919,7 @@ public void ArgumentList_VB_Attribute()
Public Class C
End Class
""";
- var node = NodeBetweenMarkers(code, LanguageNames.VisualBasic, getInnermostNodeForTie: true);
+ var node = NodeBetweenMarkers(code, AnalyzerLanguage.VisualBasic, getInnermostNodeForTie: true);
var argumentList = ExtensionsVB.ArgumentList(node);
var argument = argumentList.Arguments.Should().ContainSingle().Which;
(argument.GetExpression() is SyntaxVB.LiteralExpressionSyntax { Token.ValueText: "1" }).Should().BeTrue();
@@ -939,7 +939,7 @@ Dim arr(0) As Integer
End Sub
End Class
""";
- var node = NodeBetweenMarkers(code, LanguageNames.VisualBasic, getInnermostNodeForTie: true);
+ var node = NodeBetweenMarkers(code, AnalyzerLanguage.VisualBasic, getInnermostNodeForTie: true);
var argumentList = ExtensionsVB.ArgumentList(node);
var argument = argumentList.Arguments.Should().ContainSingle().Which;
(argument.GetExpression() is SyntaxVB.LiteralExpressionSyntax { Token.ValueText: "1" }).Should().BeTrue();
@@ -955,7 +955,7 @@ Public Sub M()
End Sub
End Class
""";
- var node = NodeBetweenMarkers(code, LanguageNames.VisualBasic, getInnermostNodeForTie: false);
+ var node = NodeBetweenMarkers(code, AnalyzerLanguage.VisualBasic, getInnermostNodeForTie: false);
ExtensionsVB.ArgumentList(node).Should().BeNull();
}
@@ -974,7 +974,7 @@ Public Sub M()
End Sub
End Class
""";
- var node = NodeBetweenMarkers(code, LanguageNames.VisualBasic, getInnermostNodeForTie: true);
+ var node = NodeBetweenMarkers(code, AnalyzerLanguage.VisualBasic, getInnermostNodeForTie: true);
var sut = () => ExtensionsVB.ArgumentList(node);
sut.Should().Throw();
}
@@ -992,7 +992,7 @@ public class C
{
$${{declarations}}$$
}
- """, LanguageNames.CSharp);
+ """, AnalyzerLanguage.CSharp);
var actual = ExtensionsCS.ParameterList(node);
actual.Should().NotBeNull();
var entry = actual.Parameters.Should().ContainSingle().Which;
@@ -1016,7 +1016,7 @@ public void M()
{{declarations}}
}
}
- """, LanguageNames.CSharp);
+ """, AnalyzerLanguage.CSharp);
var actual = ExtensionsCS.ParameterList(node);
actual.Should().NotBeNull();
var entry = actual.Parameters.Should().ContainSingle().Which;
@@ -1031,7 +1031,7 @@ public class C
{
$$~C() { }$$
}
- """, LanguageNames.CSharp);
+ """, AnalyzerLanguage.CSharp);
var actual = ExtensionsCS.ParameterList(node);
actual.Should().NotBeNull();
actual.Parameters.Should().BeEmpty();
@@ -1054,7 +1054,7 @@ public void ParameterList_PrimaryConstructors(string type)
{
}$$
- """, LanguageNames.CSharp);
+ """, AnalyzerLanguage.CSharp);
var actual = ExtensionsCS.ParameterList(node);
actual.Should().NotBeNull();
var entry = actual.Parameters.Should().ContainSingle().Which;
@@ -1072,7 +1072,7 @@ public class C
{
{{declaration}}
}
- """, LanguageNames.CSharp);
+ """, AnalyzerLanguage.CSharp);
var actual = ExtensionsCS.ParameterList(node);
actual.Should().BeNull();
}
@@ -1141,7 +1141,7 @@ unsafe class Test
public Test(int i) { }
{{member}}
}
- """, LanguageNames.CSharp);
+ """, AnalyzerLanguage.CSharp);
var actual = ExtensionsCS.GetIdentifier(node);
if (expected is null)
{
@@ -1163,7 +1163,7 @@ public void GetIdentifier_CompilationUnit(string member, string expected)
{
var node = NodeBetweenMarkers($$"""
{{member}}
- """, LanguageNames.CSharp);
+ """, AnalyzerLanguage.CSharp);
var actual = ExtensionsCS.GetIdentifier(node);
if (expected is null)
{
@@ -1187,7 +1187,7 @@ public class Base(int i)
{
}
public class Derived(int i) {{baseType}} { }
- """, LanguageNames.CSharp);
+ """, AnalyzerLanguage.CSharp);
var actual = ExtensionsCS.GetIdentifier(node);
if (expected is null)
{
@@ -1207,8 +1207,8 @@ public class Derived(int i) {{baseType}} { }
[DataRow("""Derived() { $$var x = 42;$$ }""", SyntaxKind.ConstructorDeclaration)]
[DataRow("""public static implicit operator int(Derived d) => $$42$$;""", SyntaxKind.ConversionOperatorDeclaration)]
[DataRow("""~Derived() { $$var x = 42;$$ }""", SyntaxKind.DestructorDeclaration)]
- [DataRow("""int field = $$int.Parse("42")$$;""", SyntaxKind.EqualsValueClause)]
- [DataRow("""int Property { get; set; } = $$int.Parse("42")$$;""", SyntaxKind.EqualsValueClause)]
+ [DataRow("""int field = $$int.Parse("42")$$;""", SyntaxKind.FieldDeclaration)]
+ [DataRow("""int Property { get; set; } = $$int.Parse("42")$$;""", SyntaxKind.PropertyDeclaration)]
[DataRow("""int Property { set { $$_ = value;$$ } }""", SyntaxKind.SetAccessorDeclaration)]
[DataRow("""int Property { set { $$_ = value;$$ } }""", SyntaxKind.SetAccessorDeclaration)]
[DataRow("""int Method() { return LocalFunction(); int LocalFunction() { $$return 42;$$ } }""", SyntaxKindEx.LocalFunctionStatement)]
@@ -1220,6 +1220,8 @@ public class Derived(int i) {{baseType}} { }
[DataRow("""int Method() { Func lambda = x => $$x + 1$$; return lambda(42); }""", SyntaxKind.SimpleLambdaExpression)]
[DataRow("""event EventHandler SomeEvent { add { int x = 42; } remove { $$int x = 42;$$ } }""", SyntaxKind.RemoveAccessorDeclaration)]
[DataRow("""Derived(int arg) : this($$arg.ToString()$$) { }""", SyntaxKind.ThisConstructorInitializer)]
+ [DataRow("""enum E { A = $$1$$ }""", SyntaxKind.EnumMemberDeclaration)]
+ [DataRow("""void M(int i = $$1$$) { }""", SyntaxKind.Parameter)]
#if NET
[DataRow("""int Property { init { $$_ = value;$$ } }""", SyntaxKindEx.InitAccessorDeclaration)]
[DataRow("""record BaseRec(int I); record DerivedRec(int I): BaseRec($$I++$$);""", SyntaxKindEx.PrimaryConstructorBaseType)]
@@ -1240,22 +1242,33 @@ public class Derived: Base
Derived(string arg) { }
{{member}}
}
- """, LanguageNames.CSharp);
+ """, AnalyzerLanguage.CSharp);
var actual = ExtensionsCS.EnclosingScope(node)?.Kind() ?? SyntaxKind.None;
actual.Should().Be(expectedSyntaxKind);
}
- private static SyntaxNode NodeBetweenMarkers(string code, string language, bool getInnermostNodeForTie = false)
+ [TestMethod]
+ public void EnclosingScope_TopLevelStatements()
+ {
+ var node = NodeBetweenMarkers($$"""
+ using System;
+
+ $$Console.WriteLine("")$$;
+
+ """, AnalyzerLanguage.CSharp, outputKind: OutputKind.ConsoleApplication);
+ var actual = ExtensionsCS.EnclosingScope(node)?.Kind() ?? SyntaxKind.None;
+ actual.Should().Be(SyntaxKind.CompilationUnit);
+ }
+
+ private static SyntaxNode NodeBetweenMarkers(string code, AnalyzerLanguage language, bool getInnermostNodeForTie = false, OutputKind outputKind = OutputKind.DynamicallyLinkedLibrary)
{
var position = code.IndexOf("$$");
var lastPosition = code.LastIndexOf("$$");
var length = lastPosition == position ? 0 : lastPosition - position - "$$".Length;
code = code.Replace("$$", string.Empty);
- var (tree, _) = IsCSharp() ? TestHelper.CompileCS(code) : TestHelper.CompileVB(code);
+ var (tree, _) = TestHelper.Compile(code, ignoreErrors: false, language, outputKind: outputKind);
var node = tree.GetRoot().FindNode(new TextSpan(position, length), getInnermostNodeForTie: getInnermostNodeForTie);
return node;
-
- bool IsCSharp() => language == LanguageNames.CSharp;
}
private static SyntaxToken GetFirstTokenOfKind(SyntaxTree syntaxTree, SyntaxKind kind) =>
diff --git a/analyzers/tests/SonarAnalyzer.Test/PackagingTests/RuleTypeMappingCS.cs b/analyzers/tests/SonarAnalyzer.Test/PackagingTests/RuleTypeMappingCS.cs
index d70ecf1e58e..f2615a021ff 100644
--- a/analyzers/tests/SonarAnalyzer.Test/PackagingTests/RuleTypeMappingCS.cs
+++ b/analyzers/tests/SonarAnalyzer.Test/PackagingTests/RuleTypeMappingCS.cs
@@ -6890,7 +6890,7 @@ internal static class RuleTypeMappingCS
// ["S6963"],
// ["S6964"],
// ["S6965"],
- // ["S6966"],
+ ["S6966"] = "CODE_SMELL",
// ["S6967"],
// ["S6968"],
// ["S6969"],
diff --git a/analyzers/tests/SonarAnalyzer.Test/Rules/UseAwaitableMethodTest.cs b/analyzers/tests/SonarAnalyzer.Test/Rules/UseAwaitableMethodTest.cs
new file mode 100644
index 00000000000..e041152b9ba
--- /dev/null
+++ b/analyzers/tests/SonarAnalyzer.Test/Rules/UseAwaitableMethodTest.cs
@@ -0,0 +1,98 @@
+/*
+ * SonarAnalyzer for .NET
+ * Copyright (C) 2015-2024 SonarSource SA
+ * mailto: contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+using SonarAnalyzer.Rules.CSharp;
+
+namespace SonarAnalyzer.Test.Rules;
+
+[TestClass]
+public class UseAwaitableMethodTest
+{
+ private const string EntityFrameworkVersion = "7.0.18";
+
+ private readonly VerifierBuilder builder = new VerifierBuilder();
+
+ [TestMethod]
+ public void UseAwaitableMethod_CS() =>
+ builder.AddPaths("UseAwaitableMethod.cs").Verify();
+
+ [TestMethod]
+ public void UseAwaitableMethod_CS_Test() =>
+ builder
+ .WithOptions(ParseOptionsHelper.FromCSharp11)
+ .AddReferences(MetadataReferenceFacade.SystemNetPrimitives)
+ .AddReferences(MetadataReferenceFacade.SystemNetSockets)
+ .AddSnippet("""
+ using System;
+ using System.Text;
+ using System.IO;
+ using System.Net;
+ using System.Net.Sockets;
+ using System.Threading.Tasks;
+ using System.Collections.Generic;
+
+ public class Sockets
+ {
+ async Task CreateActionAsync(StreamReader reader)
+ {
+ Action action = () =>
+ {
+ reader.ReadLine(); // Compliant
+ };
+ return action;
+ }
+ }
+ """).Verify();
+
+ [TestMethod]
+ public void UseAwaitableMethod_Sockets() =>
+ builder
+ .AddReferences(MetadataReferenceFacade.SystemNetPrimitives)
+ .AddReferences(MetadataReferenceFacade.SystemNetSockets)
+ .AddPaths("UseAwaitableMethod_Sockets.cs")
+ .Verify();
+
+#if NET
+ [TestMethod]
+ public void UseAwaitableMethod_CSharp9() =>
+ builder
+ .WithTopLevelStatements()
+ .AddPaths("UseAwaitableMethod_CSharp9.cs")
+ .Verify();
+
+ [TestMethod]
+ public void UseAwaitableMethod_CSharp8() =>
+ builder
+ .WithOptions(ParseOptionsHelper.FromCSharp8)
+ .AddPaths("UseAwaitableMethod_CSharp8.cs")
+ .Verify();
+
+ [TestMethod]
+ public void UseAwaitableMethod_EF() =>
+ builder
+ .WithOptions(ParseOptionsHelper.FromCSharp11)
+ .AddReferences([CoreMetadataReference.SystemComponentModelTypeConverter])
+ .AddReferences(NuGetMetadataReference.MicrosoftEntityFrameworkCore(EntityFrameworkVersion))
+ .AddReferences(NuGetMetadataReference.MicrosoftEntityFrameworkCoreRelational(EntityFrameworkVersion))
+ .AddReferences(NuGetMetadataReference.MicrosoftEntityFrameworkCoreSqlServer(EntityFrameworkVersion))
+ .AddPaths("UseAwaitableMethod_EF.cs")
+ .Verify();
+#endif
+}
diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/UseAwaitableMethod.cs b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseAwaitableMethod.cs
new file mode 100644
index 00000000000..33209cc0b19
--- /dev/null
+++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseAwaitableMethod.cs
@@ -0,0 +1,193 @@
+using System.IO;
+using System.Threading.Tasks;
+using System;
+using System.Threading;
+
+public class C
+{
+ public C Child { get; }
+ void VoidMethod() { }
+ Task VoidMethodAsync() => Task.CompletedTask;
+
+ C ReturnMethod() => null;
+ Task ReturnMethodAsync() => Task.FromResult(null);
+
+ bool BoolMethod() => true;
+ Task BoolMethodAsync() => Task.FromResult(true);
+
+ C this[int i] => null;
+ public static C operator +(C c) => default(C);
+ public static C operator +(C c1, C c2) => default(C);
+ public static C operator -(C c) => default(C);
+ public static C operator -(C c1, C c2) => default(C);
+ public static C operator !(C c) => default(C);
+ public static C operator ~(C c) => default(C);
+ public static implicit operator int(C c) => default(C);
+
+ async Task MethodInvocations()
+ {
+ VoidMethod(); // Noncompliant {{Await VoidMethodAsync instead.}}
+ await VoidMethodAsync(); // Compliant
+ VoidMethodAsync(); // Compliant: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-messages/cs4014
+ this.VoidMethod(); // Noncompliant
+ this.Child?.VoidMethod(); // Noncompliant
+ this.Child.Child?.VoidMethod(); // Noncompliant
+
+ ReturnMethod(); // Noncompliant
+ _ = ReturnMethod(); // Noncompliant
+ this.ReturnMethod().ReturnMethod().ReturnMethod();
+// ^^^^^^^^^^^^^^^^^^^
+// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @-1
+// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @-2
+ _ = true ? ReturnMethod() : ReturnMethod();
+ // ^^^^^^^^^^^^^^
+ // ^^^^^^^^^^^^^^ @-1
+ await ReturnMethod().ReturnMethodAsync(); // Noncompliant
+ // ^^^^^^^^^^^^^^
+ }
+
+ public void NonAsyncMethod_VoidReturning()
+ {
+ VoidMethod(); // Compliant
+ }
+
+ public Task NonAsyncMethod_TaskReturning()
+ {
+ VoidMethod(); // Compliant: Enclosing Method must be async
+ return Task.CompletedTask;
+ }
+
+ async Task OperatorPrecedence() // https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/#operator-precedence
+ {
+ _ = new C().ReturnMethod(); // Noncompliant
+ this[0].VoidMethod(); // Noncompliant
+ Child[0].VoidMethod(); // Noncompliant
+ Child.Child[0]?.VoidMethod(); // Noncompliant
+ Child?.Child[0].VoidMethod(); // Noncompliant
+ Child?.Child?[0].VoidMethod(); // Noncompliant
+ Child.Child?[0].VoidMethod(); // Noncompliant
+ Child?.Child?[0]?.VoidMethod(); // Noncompliant
+ _ = Child?.Child?[0]?.ReturnMethod()?.Child[0]; // Noncompliant
+ _ = (ReturnMethod()); // Noncompliant
+ _ = nameof(VoidMethod); // Compliant
+ _ = !BoolMethod(); // Noncompliant
+ _ = BoolMethod() ? ReturnMethod() : default(C);
+ // ^^^^^^^^^^^^
+ // ^^^^^^^^^^^^^^ @-1
+ _ = +ReturnMethod(); // Noncompliant
+ _ = -ReturnMethod(); // Noncompliant
+ _ = !ReturnMethod(); // Noncompliant
+ _ = ~ReturnMethod(); // Noncompliant
+ _ = ReturnMethod() + default(C); // Noncompliant
+ _ = ReturnMethod() - default(C); // Noncompliant
+ _ = ReturnMethod() - !ReturnMethod();
+ // ^^^^^^^^^^^^^^
+ // ^^^^^^^^^^^^^^ @-1
+ }
+
+ async Task ExtensionMethods()
+ {
+ this.ExtVoidMethod(); // Noncompliant
+ }
+}
+
+public static class Extensions
+{
+ public static void ExtVoidMethod(this C c) { }
+ public static Task ExtVoidMethodAsync(this C c) => Task.CompletedTask;
+}
+
+public class Overloads
+{
+ public long ImplicitConversionsMethod(int i, IComparable j) => 0;
+ public Task ImplicitConversionsMethodAsync(long otherName1, int otherName2) => Task.FromResult(0);
+ public Task ImplicitConversionsMethodAsync(byte otherName1, byte otherName2) => Task.FromResult((byte)0);
+
+ public void TypeParameter(C c) { }
+ public Task TypeParameter(T t) where T : C => Task.CompletedTask;
+
+ public async Task Test(int i, byte j)
+ {
+ long l1 = ImplicitConversionsMethod(i, j); // Noncompliant Can be resolved to first overload
+ long l2 = ImplicitConversionsMethod(i, (IComparable)j); // Compliant No fitting overload
+ var l3 = ImplicitConversionsMethod((byte)i, j); // Noncompliant Can be resolved to second overload
+ int l4 = (int)ImplicitConversionsMethod((byte)i, j); // Noncompliant Can be resolved to second overload
+
+ TypeParameter(new C()); // Compliant: Adding "await" does never resolve to another overload
+ }
+}
+
+public class Inheritance
+{
+ class Child : Inheritance
+ {
+ public void OuterVoidMethod() { }
+ public void InnerVoidMethod() { }
+ }
+
+ class GrandChild : Child
+ {
+ public GrandChild Chain { get; }
+
+ public async Task Test()
+ {
+ OuterVoidMethod(); // Noncompliant
+ this.OuterVoidMethod(); // Noncompliant
+ (this as Child).OuterVoidMethod(); // Noncompliant
+ this.Chain?.OuterVoidMethod(); // Noncompliant
+ this.Chain?.Chain?.OuterVoidMethod(); // Noncompliant
+ InnerVoidMethod(); // Noncompliant
+ this.InnerVoidMethod(); // Noncompliant
+ (this as Child).InnerVoidMethod(); // Compliant: InnerVoidMethodAsync is defined on GrandChild
+ this.Chain?.InnerVoidMethod(); // Noncompliant
+ this.Chain?.Chain?.InnerVoidMethod(); // Noncompliant
+ }
+
+ public Task InnerVoidMethodAsync() => Task.CompletedTask;
+ }
+
+ public Task OuterVoidMethodAsync() => Task.CompletedTask;
+
+ async Task Test()
+ {
+ var grandChild = new GrandChild();
+ grandChild.OuterVoidMethod(); // Noncompliant
+ grandChild?.OuterVoidMethod(); // Noncompliant
+ grandChild?.Chain?.OuterVoidMethod(); // Noncompliant
+ grandChild.InnerVoidMethod(); // Noncompliant
+ grandChild?.InnerVoidMethod(); // Noncompliant
+ grandChild?.Chain?.InnerVoidMethod(); // Noncompliant
+ }
+}
+
+class AsynchronousLambdas
+{
+ async Task CallAsyncLambda(StreamReader reader)
+ {
+ await Task.Run(async () => {
+ await Foo();
+ reader.ReadLine(); // Noncompliant
+ });
+ Func a = async () =>
+ {
+ await Foo();
+ reader.ReadLine(); // Noncompliant
+ };
+ Func b = async delegate ()
+ {
+ await Foo();
+ reader.ReadLine(); // Noncompliant
+ };
+ }
+
+ async Task CreateActionAsync(StreamReader reader)
+ {
+ Action action = () =>
+ {
+ reader.ReadLine(); // Compliant
+ };
+ return action;
+ }
+
+ Task Foo() => null;
+}
diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/UseAwaitableMethod_CSharp8.cs b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseAwaitableMethod_CSharp8.cs
new file mode 100644
index 00000000000..7ffb9194861
--- /dev/null
+++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseAwaitableMethod_CSharp8.cs
@@ -0,0 +1,43 @@
+using System.IO;
+using System.Threading.Tasks;
+using System;
+
+public class C
+{
+ public C Child { get; }
+ C this[int i] => null;
+ public static implicit operator int(C c) => default(C);
+
+ C ReturnMethod() => null;
+ Task ReturnMethodAsync() => null;
+
+ async Task OperatorPrecedence() // https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/#operator-precedence
+ {
+ _ = ^ReturnMethod(); // Noncompliant
+ _ = Child?.ReturnMethod()!.Child; // Noncompliant
+ _ = (Child!?.Child?[0]!)?.ReturnMethod()!?.Child[0]!; // Noncompliant
+ _ = (ReturnMethod()!); // Noncompliant
+ _ = (ReturnMethod())!; // Noncompliant
+ _ = ((ReturnMethod())!); // Noncompliant
+ }
+
+ async Task LocalFunctions()
+ {
+ VoidMethod(); // FN
+
+ void VoidMethod() { }
+ Task VoidMethodAsync() => null;
+ }
+
+ async Task InLocalFunction()
+ {
+ async Task AsyncLocalFunction(C c)
+ {
+ c.ReturnMethod(); // Noncompliant
+ }
+ void LocalFunction(C c)
+ {
+ c.ReturnMethod(); // Compliant
+ }
+ }
+}
diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/UseAwaitableMethod_CSharp9.cs b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseAwaitableMethod_CSharp9.cs
new file mode 100644
index 00000000000..c3134a9189c
--- /dev/null
+++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseAwaitableMethod_CSharp9.cs
@@ -0,0 +1,6 @@
+using System.IO;
+using System.Threading.Tasks;
+using System;
+
+var ms = new MemoryStream();
+ms.Dispose(); // Noncompliant {{Await DisposeAsync instead.}}
diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/UseAwaitableMethod_EF.cs b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseAwaitableMethod_EF.cs
new file mode 100644
index 00000000000..934fcf50039
--- /dev/null
+++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseAwaitableMethod_EF.cs
@@ -0,0 +1,40 @@
+using Microsoft.EntityFrameworkCore;
+using System.IO;
+using System.Threading.Tasks;
+using System;
+using System.Linq;
+
+public class EnitityFramework
+{
+ public async Task Query()
+ {
+ // Note to implementers: Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions and RelationalQueryableExtensions might be needed to be added to some sort of whitelist for IQueryables
+ DbSet dbSet = default;
+ dbSet.Add(null); // Noncompliant
+ dbSet.AddRange(null); // Noncompliant
+ dbSet.All(x => true); // Noncompliant
+ dbSet.Any(x => true); // Noncompliant
+ dbSet.Average(x => 1); // Noncompliant
+ dbSet.Contains(null); // Noncompliant
+ dbSet.Count(); // Noncompliant
+ dbSet.ElementAt(1); // Compliant: EF 8.0 only
+ dbSet.ElementAtOrDefault(1); // Compliant: EF 8.0 only
+ dbSet.ExecuteDelete(); // Noncompliant
+ dbSet.ExecuteUpdate(x => x.SetProperty(x => x.ToString(), x => string.Empty)); // Noncompliant
+ dbSet.Find(null); // Noncompliant
+ dbSet.First(); // Noncompliant
+ dbSet.FirstOrDefault(); // Noncompliant
+ dbSet.Last(); // Noncompliant
+ dbSet.LastOrDefault(); // Noncompliant
+ dbSet.Load(); // Noncompliant
+ dbSet.LongCount(); // Noncompliant
+ dbSet.Max(); // Noncompliant
+ dbSet.Min(); // Noncompliant
+ dbSet.Single(); // Noncompliant
+ dbSet.SingleOrDefault(); // Noncompliant
+ dbSet.Sum(x => 0); // Noncompliant
+ dbSet.ToArray(); // Noncompliant
+ dbSet.ToDictionary(x => 0); // Noncompliant
+ dbSet.ToList(); // Noncompliant
+ }
+}
diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/UseAwaitableMethod_Sockets.cs b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseAwaitableMethod_Sockets.cs
new file mode 100644
index 00000000000..242d727c490
--- /dev/null
+++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseAwaitableMethod_Sockets.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Text;
+using System.IO;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+
+public class Sockets
+{
+ public async Task Connect()
+ {
+ var hostEndPoint = new IPEndPoint(new IPAddress(new byte[] { 127, 0, 0, 1 }), 80);
+ var buffer = new List>();
+ var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
+ socket.Connect(hostEndPoint); // Noncompliant
+ socket.Accept(); // Noncompliant
+ socket.Receive(buffer, SocketFlags.Broadcast); // Noncompliant
+ socket.Send(buffer, SocketFlags.Broadcast); // Noncompliant
+ }
+}
diff --git a/analyzers/tests/SonarAnalyzer.TestFramework/MetadataReferences/CoreMetadataReference.cs b/analyzers/tests/SonarAnalyzer.TestFramework/MetadataReferences/CoreMetadataReference.cs
index 0bf4a722355..d9e6a8bac33 100644
--- a/analyzers/tests/SonarAnalyzer.TestFramework/MetadataReferences/CoreMetadataReference.cs
+++ b/analyzers/tests/SonarAnalyzer.TestFramework/MetadataReferences/CoreMetadataReference.cs
@@ -40,6 +40,7 @@ public static class CoreMetadataReference
public static MetadataReference SystemCollectionsNonGeneric { get; } = CreateReference("System.Collections.NonGeneric.dll");
public static MetadataReference SystemConsole { get; } = CreateReference("System.Console.dll");
public static MetadataReference SystemComponentModelPrimitives { get; } = CreateReference("System.ComponentModel.Primitives.dll");
+ public static MetadataReference SystemComponentModelTypeConverter { get; } = CreateReference("System.ComponentModel.TypeConverter.dll");
public static MetadataReference SystemDataCommon { get; } = CreateReference("System.Data.Common.dll");
public static MetadataReference SystemDiagnosticsTraceSource { get; } = CreateReference("System.Diagnostics.TraceSource.dll");
public static MetadataReference SystemDiagnosticsProcess { get; } = CreateReference("System.Diagnostics.Process.dll");