diff --git a/docfx/analyzers/VSTHRD002.md b/docfx/analyzers/VSTHRD002.md
index 6a05a94ca..69b021b93 100644
--- a/docfx/analyzers/VSTHRD002.md
+++ b/docfx/analyzers/VSTHRD002.md
@@ -40,6 +40,14 @@ void DoSomething()
}
```
+Accessing a task's result is not reported when the analyzer can prove the task has completed.
+Recognized proofs include awaiting the task (directly or through `Task.WhenAll`), guarding the
+access with a completion property such as `IsCompletedSuccessfully`, and awaiting the task in
+the negative branch of such a guard.
+
+VSTHRD002 can also report project-specific synchronous blocking methods configured in
+`vs-threading.SyncBlockingMethods.txt`. See [Analyzer Configuration](configuration.md#additional-synchronous-blocking-methods-for-vsthrd002).
+
Refer to [Asynchronous and multithreaded programming within VS using the JoinableTaskFactory][1] for more information.
[1]: https://devblogs.microsoft.com/premier-developer/asynchronous-and-multithreaded-programming-within-vs-using-the-joinabletaskfactory/
diff --git a/docfx/analyzers/configuration.md b/docfx/analyzers/configuration.md
index c21d4c7ab..13b3eede3 100644
--- a/docfx/analyzers/configuration.md
+++ b/docfx/analyzers/configuration.md
@@ -105,6 +105,19 @@ excluded from VSTHRD103 analysis by specifying them in a configuration file.
**Generic sample:** ``[Microsoft.EntityFrameworkCore.DbSet`1]::Add``
+## Additional synchronous blocking methods for VSTHRD002
+
+Projects that wrap synchronous waits in their own APIs can configure those methods to be
+reported by VSTHRD002. Instance, static, and extension methods are supported. Because the
+analyzer cannot infer an asynchronous equivalent for a configured method, it does not offer
+the "use await instead" code fix for these diagnostics.
+
+**Filename:** `vs-threading.SyncBlockingMethods.txt`
+
+**Line format:** `[Namespace.TypeName]::MethodName`
+
+**Sample:** `[Contoso.Threading.TaskExtensions]::WaitSynchronously`
+
## Types that require the Async suffix
VSTHRD200 requires methods returning `Task`, `ValueTask`, and other async-focused types
diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpCommonInterest.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpCommonInterest.cs
index bf159a90e..99c2f6b1f 100644
--- a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpCommonInterest.cs
+++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/CSharpCommonInterest.cs
@@ -4,12 +4,15 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
+using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Operations;
namespace Microsoft.VisualStudio.Threading.Analyzers;
@@ -28,6 +31,164 @@ internal static class CSharpCommonInterest
SyntaxKind.AddAccessorDeclaration,
SyntaxKind.RemoveAccessorDeclaration);
+ ///
+ /// Gets a symbol and ref locals that definitely or potentially alias it at the specified syntax node.
+ ///
+ internal static (ImmutableHashSet Definite, ImmutableHashSet Potential) GetSymbolAndRefAliases(
+ SyntaxNodeAnalysisContext context,
+ SyntaxNode node,
+ ISymbol symbol,
+ SyntaxNode? aliasSearchRoot = null,
+ bool includeAllCandidates = false)
+ {
+ SyntaxNode searchRoot = aliasSearchRoot ?? node.AncestorsAndSelf().FirstOrDefault(
+ ancestor => ancestor is AnonymousFunctionExpressionSyntax
+ or LocalFunctionStatementSyntax
+ or BaseMethodDeclarationSyntax
+ or AccessorDeclarationSyntax)
+ ?? node.FirstAncestorOrSelf()?.Parent
+ ?? node;
+ ITypeSymbol? trackedType = symbol switch
+ {
+ ILocalSymbol local => local.Type,
+ IParameterSymbol parameter => parameter.Type,
+ IFieldSymbol field => field.Type,
+ _ => null,
+ };
+
+ var refTargets = new Dictionary>(SymbolEqualityComparer.Default);
+ var potentialOnlyRefLocals = new HashSet(SymbolEqualityComparer.Default);
+ bool DescendIntoChildren(SyntaxNode child) =>
+ child == searchRoot || child is not AnonymousFunctionExpressionSyntax and not LocalFunctionStatementSyntax;
+
+ HashSet GetRefTargets(ISymbol candidate)
+ {
+ if (refTargets.TryGetValue(candidate, out HashSet? targets))
+ {
+ return new HashSet(targets, SymbolEqualityComparer.Default);
+ }
+
+ return new HashSet(SymbolEqualityComparer.Default) { candidate };
+ }
+
+ bool DefinitelyPrecedesNode(SyntaxNode candidate)
+ {
+ StatementSyntax? candidateStatement = candidate.FirstAncestorOrSelf();
+ if (candidateStatement?.Parent is BlockSyntax block)
+ {
+ StatementSyntax? nodeStatement = node.AncestorsAndSelf().OfType().FirstOrDefault(statement => statement.Parent == block);
+ return nodeStatement is object && block.Statements.IndexOf(candidateStatement) < block.Statements.IndexOf(nodeStatement);
+ }
+
+ GlobalStatementSyntax? candidateGlobalStatement = candidate.FirstAncestorOrSelf();
+ GlobalStatementSyntax? nodeGlobalStatement = node.FirstAncestorOrSelf();
+ return candidateGlobalStatement?.Parent is CompilationUnitSyntax compilationUnit
+ && nodeGlobalStatement?.Parent == compilationUnit
+ && compilationUnit.Members.IndexOf(candidateGlobalStatement) < compilationUnit.Members.IndexOf(nodeGlobalStatement);
+ }
+
+ foreach (SyntaxNode candidate in searchRoot.DescendantNodes(DescendIntoChildren)
+ .Where(candidate => (includeAllCandidates || candidate.SpanStart < node.SpanStart)
+ && candidate is VariableDeclaratorSyntax or AssignmentExpressionSyntax)
+ .OrderBy(candidate => candidate.SpanStart))
+ {
+ if (candidate is VariableDeclaratorSyntax variable
+ && variable.Initializer is not null
+ && context.SemanticModel.GetDeclaredSymbol(variable, context.CancellationToken) is ILocalSymbol { RefKind: not RefKind.None } local)
+ {
+ ExpressionSyntax initializer = variable.Initializer.Value is RefExpressionSyntax refInitializer
+ ? refInitializer.Expression
+ : variable.Initializer.Value;
+ if (context.SemanticModel.GetSymbolInfo(UnwrapParentheses(initializer), context.CancellationToken).Symbol is ISymbol initializedFrom)
+ {
+ if (initializedFrom is IMethodSymbol refReturningMethod
+ && (refReturningMethod.ReturnsByRef || refReturningMethod.ReturnsByRefReadonly)
+ && SymbolEqualityComparer.Default.Equals(refReturningMethod.ReturnType, trackedType))
+ {
+ refTargets[local] = new HashSet(SymbolEqualityComparer.Default) { symbol };
+ potentialOnlyRefLocals.Add(local);
+ }
+ else
+ {
+ refTargets[local] = GetRefTargets(initializedFrom);
+ if (potentialOnlyRefLocals.Contains(initializedFrom))
+ {
+ potentialOnlyRefLocals.Add(local);
+ }
+ }
+ }
+ else if (MayAliasTaskStorage(context, initializer, ImmutableHashSet.Create(SymbolEqualityComparer.Default, symbol)))
+ {
+ refTargets[local] = new HashSet(SymbolEqualityComparer.Default) { symbol };
+ potentialOnlyRefLocals.Add(local);
+ }
+ }
+ else if (candidate is AssignmentExpressionSyntax { Right: RefExpressionSyntax refAssignment } assignment
+ && context.SemanticModel.GetSymbolInfo(assignment.Left, context.CancellationToken).Symbol is ILocalSymbol { RefKind: not RefKind.None } reboundLocal)
+ {
+ ExpressionSyntax assignedExpression = UnwrapParentheses(refAssignment.Expression);
+ ISymbol? assignedFrom = context.SemanticModel.GetSymbolInfo(assignedExpression, context.CancellationToken).Symbol;
+ HashSet? assignedTargets = assignedFrom is object
+ ? GetRefTargets(assignedFrom)
+ : MayAliasTaskStorage(context, assignedExpression, ImmutableHashSet.Create(SymbolEqualityComparer.Default, symbol))
+ ? new HashSet(SymbolEqualityComparer.Default) { symbol }
+ : null;
+ if (assignedTargets is object)
+ {
+ if ((!includeAllCandidates && DefinitelyPrecedesNode(candidate))
+ || !refTargets.TryGetValue(reboundLocal, out HashSet? existingTargets))
+ {
+ refTargets[reboundLocal] = assignedTargets;
+ if (assignedFrom is null || potentialOnlyRefLocals.Contains(assignedFrom))
+ {
+ potentialOnlyRefLocals.Add(reboundLocal);
+ }
+ else
+ {
+ potentialOnlyRefLocals.Remove(reboundLocal);
+ }
+ }
+ else
+ {
+ existingTargets.UnionWith(assignedTargets);
+ if (assignedFrom is null || potentialOnlyRefLocals.Contains(assignedFrom))
+ {
+ potentialOnlyRefLocals.Add(reboundLocal);
+ }
+ }
+ }
+ }
+ }
+
+ HashSet symbolTargets = GetRefTargets(symbol);
+ ImmutableHashSet.Builder definiteSymbols = ImmutableHashSet.CreateBuilder(SymbolEqualityComparer.Default);
+ ImmutableHashSet.Builder potentialSymbols = ImmutableHashSet.CreateBuilder(SymbolEqualityComparer.Default);
+ definiteSymbols.Add(symbol);
+ potentialSymbols.Add(symbol);
+ potentialSymbols.UnionWith(symbolTargets);
+ if (symbolTargets.Count == 1)
+ {
+ definiteSymbols.UnionWith(symbolTargets);
+ }
+
+ foreach (KeyValuePair> refTarget in refTargets)
+ {
+ if (symbolTargets.Count == 1
+ && refTarget.Value.SetEquals(symbolTargets)
+ && !potentialOnlyRefLocals.Contains(refTarget.Key))
+ {
+ definiteSymbols.Add(refTarget.Key);
+ }
+
+ if (refTarget.Value.Overlaps(symbolTargets))
+ {
+ potentialSymbols.Add(refTarget.Key);
+ }
+ }
+
+ return (definiteSymbols.ToImmutable(), potentialSymbols.ToImmutable());
+ }
+
///
/// This is an explicit rule to ignore the code that was generated by Xaml2CS.
///
@@ -94,13 +255,17 @@ internal static void InspectMemberAccess(
}
ITypeSymbol? typeReceiver = context.SemanticModel.GetTypeInfo(memberAccessSyntax.Expression).Type;
- if (typeReceiver is object)
+ ISymbol? accessedSymbol = memberAccessSyntax.Parent is InvocationExpressionSyntax invocation
+ ? context.SemanticModel.GetSymbolInfo(invocation, context.CancellationToken).Symbol
+ : context.SemanticModel.GetSymbolInfo(memberAccessSyntax, context.CancellationToken).Symbol;
+ if (typeReceiver is object && accessedSymbol is object)
{
foreach (CommonInterest.SyncBlockingMethod item in problematicMethods)
{
if (memberAccessSyntax.Name.Identifier.Text == item.Method.Name &&
typeReceiver.Name == item.Method.ContainingType.Name &&
- typeReceiver.BelongsToNamespace(item.Method.ContainingType.Namespace))
+ typeReceiver.BelongsToNamespace(item.Method.ContainingType.Namespace) &&
+ IsBuiltInBlockingMember(context, accessedSymbol, item.Method))
{
if (HasTaskCompleted(context, memberAccessSyntax))
{
@@ -114,44 +279,193 @@ internal static void InspectMemberAccess(
}
}
- private static SyntaxNode? GetEnclosingBlock(SyntaxNode? node)
+ ///
+ /// Inspects a conditionally accessed member for configured or built-in synchronous blocking behavior.
+ ///
+ /// The syntax analysis context.
+ /// The member binding to inspect.
+ /// The expression receiving the conditional access.
+ /// The complete conditional access expression.
+ /// The diagnostic descriptor to report.
+ /// The synchronous blocking members recognized by the analyzer.
+ internal static void InspectMemberBinding(
+ SyntaxNodeAnalysisContext context,
+ MemberBindingExpressionSyntax memberBinding,
+ ExpressionSyntax receiver,
+ SyntaxNode accessSyntax,
+ DiagnosticDescriptor descriptor,
+ IEnumerable problematicMethods)
{
- while (node is not null)
+ if (descriptor is null)
+ {
+ throw new ArgumentNullException(nameof(descriptor));
+ }
+
+ if (ShouldIgnoreContext(context) || CSharpUtils.IsWithinNameOf(context.Node as ExpressionSyntax))
+ {
+ return;
+ }
+
+ ITypeSymbol? receiverType = context.SemanticModel.GetTypeInfo(receiver, context.CancellationToken).Type;
+ ISymbol? accessedSymbol = memberBinding.Parent is InvocationExpressionSyntax invocation
+ ? context.SemanticModel.GetSymbolInfo(invocation, context.CancellationToken).Symbol
+ : context.SemanticModel.GetSymbolInfo(memberBinding, context.CancellationToken).Symbol;
+ if (receiverType is null || accessedSymbol is null)
+ {
+ return;
+ }
+
+ foreach (CommonInterest.SyncBlockingMethod item in problematicMethods)
{
- if (node.IsKind(SyntaxKind.Block))
+ if (memberBinding.Name.Identifier.ValueText == item.Method.Name
+ && receiverType.Name == item.Method.ContainingType.Name
+ && receiverType.BelongsToNamespace(item.Method.ContainingType.Namespace)
+ && IsBuiltInBlockingMember(context, accessedSymbol, item.Method))
{
- return node;
- }
+ if (HasTaskCompleted(context, receiver, accessSyntax))
+ {
+ return;
+ }
- node = node.Parent;
+ context.ReportDiagnostic(Diagnostic.Create(descriptor, memberBinding.Name.GetLocation()));
+ }
}
+ }
- return null;
+ ///
+ /// Gets the symbol represented by the normalized task-like receiver of a blocking member access.
+ ///
+ internal static ISymbol? GetTaskReceiverSymbol(SyntaxNodeAnalysisContext context, MemberAccessExpressionSyntax memberAccessSyntax)
+ {
+ ExpressionSyntax receiver = GetTaskReceiver(context, memberAccessSyntax);
+ return context.SemanticModel.GetSymbolInfo(receiver, context.CancellationToken).Symbol;
}
- private static bool IsVariablePassedToInvocation(InvocationExpressionSyntax invocationExpr, string variableName, bool byRef)
+ ///
+ /// Determines whether an async alternative is applicable to the arguments of a synchronous invocation.
+ ///
+ internal static bool IsApplicableAsyncAlternative(
+ SyntaxNodeAnalysisContext context,
+ InvocationExpressionSyntax invocation,
+ IMethodSymbol candidateMethod)
{
- ArgumentListSyntax? argList = invocationExpr.ChildNodes().OfType().FirstOrDefault();
- if (argList is null)
+ SimpleNameSyntax? invokedName = invocation.Expression switch
+ {
+ MemberAccessExpressionSyntax memberAccess => memberAccess.Name,
+ MemberBindingExpressionSyntax memberBinding => memberBinding.Name,
+ SimpleNameSyntax simpleName => simpleName,
+ _ => null,
+ };
+ if (invokedName is null)
+ {
+ return false;
+ }
+
+ SyntaxToken newIdentifier = SyntaxFactory.Identifier(
+ invokedName.Identifier.LeadingTrivia,
+ candidateMethod.Name,
+ invokedName.Identifier.TrailingTrivia);
+ SimpleNameSyntax asyncName = (SimpleNameSyntax)invokedName.ReplaceToken(invokedName.Identifier, newIdentifier);
+ InvocationExpressionSyntax asyncInvocation = invocation.ReplaceNode(invokedName, asyncName);
+
+ ExpressionSyntax speculativeExpression = asyncInvocation;
+ if (invocation.Expression is MemberBindingExpressionSyntax
+ && invocation.FirstAncestorOrSelf() is { } conditionalAccess)
+ {
+ speculativeExpression = conditionalAccess.ReplaceNode(invocation, asyncInvocation);
+ }
+
+ ExpressionSyntax detachedSpeculativeExpression = SyntaxFactory.ParseExpression(speculativeExpression.ToString());
+ SymbolInfo speculativeSymbolInfo = context.SemanticModel.GetSpeculativeSymbolInfo(
+ invocation.SpanStart,
+ detachedSpeculativeExpression,
+ SpeculativeBindingOption.BindAsExpression);
+ if (speculativeSymbolInfo.Symbol is not IMethodSymbol applicableMethod)
{
return false;
}
- foreach (ArgumentSyntax arg in argList.ChildNodes().OfType())
+ IMethodSymbol applicableDefinition = (applicableMethod.ReducedFrom ?? applicableMethod).OriginalDefinition;
+ IMethodSymbol candidateDefinition = (candidateMethod.ReducedFrom ?? candidateMethod).OriginalDefinition;
+ return SymbolEqualityComparer.Default.Equals(applicableDefinition, candidateDefinition);
+ }
+
+ ///
+ /// Determines whether a blocking member access has a receiver that is provably complete.
+ ///
+ internal static bool HasTaskCompleted(SyntaxNodeAnalysisContext context, MemberAccessExpressionSyntax memberAccessSyntax)
+ {
+ ExpressionSyntax taskReceiver = GetTaskReceiver(context, memberAccessSyntax);
+ return HasTaskCompleted(context, taskReceiver, memberAccessSyntax);
+ }
+
+ ///
+ /// Determines whether a task-like expression is provably complete at a syntax node.
+ ///
+ internal static bool HasTaskCompleted(SyntaxNodeAnalysisContext context, ExpressionSyntax taskReceiver, SyntaxNode accessSyntax)
+ => HasTaskCompletedInContinuation(context, taskReceiver, accessSyntax)
+ || HasTaskCompletedCore(context, taskReceiver, accessSyntax);
+
+ private static bool HasTaskCompletedInContinuation(
+ SyntaxNodeAnalysisContext context,
+ ExpressionSyntax taskReceiver,
+ SyntaxNode accessSyntax)
+ {
+ foreach (AnonymousFunctionExpressionSyntax anonymousFunction in accessSyntax.Ancestors().OfType())
{
- // `byRef` includes `out` parameters because they are the same as `ref` except don't require initialization first.
- if (byRef && !arg.RefKindKeyword.IsKind(SyntaxKind.RefKeyword) && !arg.RefKindKeyword.IsKind(SyntaxKind.OutKeyword))
+ ExpressionSyntax callbackExpression = anonymousFunction;
+ while (callbackExpression.Parent is ParenthesizedExpressionSyntax or CastExpressionSyntax)
+ {
+ callbackExpression = (ExpressionSyntax)callbackExpression.Parent;
+ }
+
+ if (callbackExpression.Parent is not ArgumentSyntax anonymousFunctionArgument
+ || anonymousFunctionArgument.Parent?.Parent is not InvocationExpressionSyntax continuationInvocation
+ || context.SemanticModel.GetOperation(continuationInvocation, context.CancellationToken) is not IInvocationOperation continuationOperation
+ || !continuationOperation.Arguments.Any(argument => argument.Parameter?.Ordinal == 0
+ && argument.Syntax.Span.Contains(anonymousFunction.Span)))
{
continue;
}
- IdentifierNameSyntax identiferName = arg.ChildNodes().OfType().FirstOrDefault();
- if (identiferName is null)
+ if (continuationOperation.TargetMethod.Name != nameof(Task.ContinueWith)
+ || !Utils.IsTask(continuationOperation.TargetMethod.ContainingType))
{
- return false;
+ continue;
+ }
+
+ ParameterSyntax? firstParameter = anonymousFunction switch
+ {
+ SimpleLambdaExpressionSyntax lambda => lambda.Parameter,
+ ParenthesizedLambdaExpressionSyntax lambda => lambda.ParameterList.Parameters.FirstOrDefault(),
+ AnonymousMethodExpressionSyntax anonymousMethod => anonymousMethod.ParameterList?.Parameters.FirstOrDefault(),
+ _ => null,
+ };
+ if (firstParameter is null
+ || context.SemanticModel.GetDeclaredSymbol(firstParameter, context.CancellationToken) is not IParameterSymbol completedTask)
+ {
+ continue;
}
- if (identiferName.Identifier.ValueText == variableName)
+ (ImmutableHashSet taskSymbols, ImmutableHashSet potentialTaskSymbols) =
+ GetSymbolAndRefAliases(context, accessSyntax, completedTask);
+ if (accessSyntax.Ancestors().TakeWhile(node => node != anonymousFunction)
+ .Any(node => node is AnonymousFunctionExpressionSyntax or LocalFunctionStatementSyntax))
+ {
+ (ImmutableHashSet outerDefiniteAliases, ImmutableHashSet outerPotentialAliases) = GetSymbolAndRefAliases(
+ context,
+ accessSyntax,
+ completedTask,
+ anonymousFunction,
+ includeAllCandidates: true);
+ taskSymbols = taskSymbols.Union(outerDefiniteAliases);
+ potentialTaskSymbols = potentialTaskSymbols.Union(outerPotentialAliases);
+ }
+
+ ISymbol? receiverSymbol = context.SemanticModel.GetSymbolInfo(UnwrapParentheses(taskReceiver), context.CancellationToken).Symbol;
+ if (receiverSymbol is object
+ && (SymbolEqualityComparer.Default.Equals(receiverSymbol, completedTask) || taskSymbols.Contains(receiverSymbol))
+ && !IsTaskReassignedInContinuation(context, anonymousFunction, accessSyntax, potentialTaskSymbols))
{
return true;
}
@@ -160,132 +474,1032 @@ private static bool IsVariablePassedToInvocation(InvocationExpressionSyntax invo
return false;
}
- private static bool IsTaskCompletedWithWhenAll(SyntaxNodeAnalysisContext context, InvocationExpressionSyntax invocationExpr, string taskVariableName)
+ private static bool IsTaskReassignedInContinuation(
+ SyntaxNodeAnalysisContext context,
+ AnonymousFunctionExpressionSyntax continuation,
+ SyntaxNode accessSyntax,
+ IImmutableSet taskSymbols)
{
- // We only care about awaited invocations, because an un-awaited Task.WhenAll will be an error.
- if (invocationExpr.Parent is not AwaitExpressionSyntax)
+ bool accessIsNested = accessSyntax.Ancestors().TakeWhile(node => node != continuation)
+ .Any(node => node is AnonymousFunctionExpressionSyntax or LocalFunctionStatementSyntax);
+ int beforePosition = accessIsNested ? continuation.Span.End + 1 : accessSyntax.SpanStart;
+
+ foreach (AssignmentExpressionSyntax assignment in continuation.DescendantNodes().OfType())
{
- return false;
+ SyntaxNode? nestedFunction = assignment.Ancestors().TakeWhile(node => node != continuation)
+ .FirstOrDefault(node => node is AnonymousFunctionExpressionSyntax or LocalFunctionStatementSyntax);
+ bool isDeferredWrite = accessIsNested
+ || nestedFunction is LocalFunctionStatementSyntax
+ || nestedFunction?.SpanStart < accessSyntax.SpanStart;
+ if ((assignment.SpanStart < beforePosition || isDeferredWrite)
+ && IsAssignmentToTask(context, assignment.Left, taskSymbols))
+ {
+ return true;
+ }
}
- IEnumerable? memberAccessList = invocationExpr.ChildNodes().OfType();
- if (memberAccessList.Count() != 1)
+ foreach (ArgumentSyntax argument in continuation.DescendantNodes().OfType())
{
- return false;
+ SyntaxNode? nestedFunction = argument.Ancestors().TakeWhile(node => node != continuation)
+ .FirstOrDefault(node => node is AnonymousFunctionExpressionSyntax or LocalFunctionStatementSyntax);
+ bool isDeferredWrite = accessIsNested
+ || nestedFunction is LocalFunctionStatementSyntax
+ || nestedFunction?.SpanStart < accessSyntax.SpanStart;
+ if ((argument.SpanStart < beforePosition || isDeferredWrite)
+ && (argument.RefKindKeyword.IsKind(SyntaxKind.RefKeyword) || argument.RefKindKeyword.IsKind(SyntaxKind.OutKeyword))
+ && MayAliasTaskStorage(context, argument.Expression, taskSymbols))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static ExpressionSyntax UnwrapParentheses(ExpressionSyntax expression)
+ {
+ while (expression is ParenthesizedExpressionSyntax parenthesized)
+ {
+ expression = parenthesized.Expression;
}
- MemberAccessExpressionSyntax? memberAccess = memberAccessList.First();
+ return expression;
+ }
+
+ private static bool IsBuiltInBlockingMember(
+ SyntaxNodeAnalysisContext context,
+ ISymbol accessedSymbol,
+ CommonInterest.QualifiedMember expectedMember)
+ {
+ if (accessedSymbol is not IMethodSymbol { ReducedFrom: not null } reducedMethod)
+ {
+ return true;
+ }
- // Does the invocation have the expected `Task.WhenAll` syntax? This is cheaper to verify before looking up its semantic type.
- bool correctSyntax = memberAccess.Expression is IdentifierNameSyntax { Identifier.ValueText: Types.Task.TypeName }
- && memberAccess.Name is IdentifierNameSyntax { Identifier.ValueText: Types.Task.WhenAll };
+ if (expectedMember.IsMatch(reducedMethod.ReducedFrom))
+ {
+ return true;
+ }
- if (!correctSyntax)
+ if (expectedMember.Name != nameof(Task.Wait)
+ || context.Compilation.GetTypeByMetadataName(Types.Task.FullName) is not INamedTypeSymbol taskType)
{
return false;
}
- // Is this `Task.WhenAll` invocation from the System.Threading.Tasks.Task type?
- ITypeSymbol? classType = context.SemanticModel.GetTypeInfo(memberAccess.Expression).Type;
- var correctType = classType?.Name == Types.Task.TypeName && classType.BelongsToNamespace(Types.Task.Namespace);
- if (!correctType)
+ return reducedMethod.Parameters.IsEmpty
+ && reducedMethod.ReturnsVoid
+ && Utils.IsEqualToOrDerivedFrom(reducedMethod.ReceiverType, taskType);
+ }
+
+ private static ExpressionSyntax GetTaskReceiver(SyntaxNodeAnalysisContext context, MemberAccessExpressionSyntax memberAccessSyntax)
+ {
+ ExpressionSyntax receiver = UnwrapParentheses(memberAccessSyntax.Expression);
+ if (receiver is InvocationExpressionSyntax getAwaiterInvocation
+ && getAwaiterInvocation.Expression is MemberAccessExpressionSyntax { Name.Identifier.ValueText: "GetAwaiter" } getAwaiterAccess
+ && IsSupportedGetAwaiterInvocation(context, getAwaiterInvocation, getAwaiterAccess.Expression))
+ {
+ receiver = UnwrapParentheses(getAwaiterAccess.Expression);
+ }
+
+ if (receiver is InvocationExpressionSyntax configureAwaitInvocation
+ && configureAwaitInvocation.Expression is MemberAccessExpressionSyntax { Name.Identifier.ValueText: nameof(Task.ConfigureAwait) } configureAwaitAccess
+ && IsSupportedConfigureAwaitInvocation(context, configureAwaitInvocation))
+ {
+ receiver = UnwrapParentheses(configureAwaitAccess.Expression);
+ }
+
+ return receiver;
+ }
+
+ private static bool IsSupportedGetAwaiterInvocation(
+ SyntaxNodeAnalysisContext context,
+ InvocationExpressionSyntax invocation,
+ ExpressionSyntax receiver)
+ {
+ if (invocation.ArgumentList.Arguments.Count != 0
+ || context.SemanticModel.GetSymbolInfo(invocation, context.CancellationToken).Symbol is not IMethodSymbol method
+ || method.ReducedFrom is object
+ || method.IsStatic
+ || !method.Parameters.IsEmpty)
{
return false;
}
- // Is the task variable passed as an argument to `Task.WhenAll`?
- return IsVariablePassedToInvocation(invocationExpr, taskVariableName, byRef: false);
+ if (IsTaskLike(method.ContainingType))
+ {
+ return true;
+ }
+
+ receiver = UnwrapParentheses(receiver);
+ return receiver is InvocationExpressionSyntax configureAwaitInvocation
+ && IsSupportedConfigureAwaitInvocation(context, configureAwaitInvocation);
}
- private static bool HasTaskCompleted(SyntaxNodeAnalysisContext context, MemberAccessExpressionSyntax memberAccessSyntax)
+ private static bool IsSupportedConfigureAwaitInvocation(SyntaxNodeAnalysisContext context, InvocationExpressionSyntax invocation)
+ => invocation.ArgumentList.Arguments.Count == 1
+ && context.SemanticModel.GetSymbolInfo(invocation, context.CancellationToken).Symbol is IMethodSymbol method
+ && method.ReducedFrom is null
+ && !method.IsStatic
+ && method.Parameters.Length == 1
+ && IsTaskLike(method.ContainingType);
+
+ private static bool IsTaskLike(ITypeSymbol? type)
+ => Utils.IsTask(type)
+ || (type?.Name == nameof(ValueTask) && type.BelongsToNamespace(Namespaces.SystemThreadingTasks));
+
+ private static bool HasTaskCompletedCore(
+ SyntaxNodeAnalysisContext context,
+ ExpressionSyntax taskReceiver,
+ SyntaxNode accessSyntax)
{
- SyntaxNode? enclosingBlock = GetEnclosingBlock(memberAccessSyntax);
- if (enclosingBlock is null)
+ taskReceiver = UnwrapParentheses(taskReceiver);
+ ITypeSymbol? taskType = context.SemanticModel.GetTypeInfo(taskReceiver, context.CancellationToken).Type;
+ if (!IsTaskLike(taskType))
+ {
+ return false;
+ }
+
+ ISymbol? taskSymbol = context.SemanticModel.GetSymbolInfo(taskReceiver, context.CancellationToken).Symbol;
+ if (taskSymbol is IParameterSymbol { RefKind: not RefKind.None }
+ || taskSymbol is not ILocalSymbol and not IParameterSymbol)
+ {
+ return false;
+ }
+
+ if (context.SemanticModel.GetEnclosingSymbol(accessSyntax.SpanStart, context.CancellationToken) is not IMethodSymbol enclosingMethod
+ || !SymbolEqualityComparer.Default.Equals(taskSymbol.ContainingSymbol, enclosingMethod))
+ {
+ return false;
+ }
+
+ (ImmutableHashSet taskSymbols, ImmutableHashSet potentialTaskSymbols) =
+ GetSymbolAndRefAliases(context, accessSyntax, taskSymbol);
+ if (taskSymbols.Any(symbol => symbol is IParameterSymbol { RefKind: not RefKind.None }))
+ {
+ return false;
+ }
+
+ if (NestedFunctionMayReassignTask(context, accessSyntax, potentialTaskSymbols))
+ {
+ return false;
+ }
+
+ if (ContainsPotentialControlFlowBypass(accessSyntax))
+ {
+ return false;
+ }
+
+ if (IsWithinCompletedTaskBranch(context, accessSyntax, taskSymbols, potentialTaskSymbols))
+ {
+ return Utils.IsTask(taskType) || !MayHaveConsumedValueTaskBefore(context, accessSyntax, taskSymbols);
+ }
+
+ // Awaiting an IValueTaskSource-backed ValueTask consumes it, so a later Result access is not safe.
+ if (!Utils.IsTask(taskType))
{
return false;
}
- // Get the task variable name from the problematic member access expression so that we can later try
- // and determine if it has been used in a `Task.WhenAll` invocation.
- // Examples:
- // task1.Result;
- // task2.GetAwaiter().GetResult();
- string? taskVariableName = null;
- ExpressionSyntax parentExpr = memberAccessSyntax.Expression;
- while (parentExpr is not null)
+ StatementSyntax? containingStatement = accessSyntax.FirstAncestorOrSelf();
+ if (containingStatement is null)
+ {
+ ArrowExpressionClauseSyntax? arrowExpression = accessSyntax.FirstAncestorOrSelf();
+ return arrowExpression is object
+ && TryGetAwaitExpression(context, arrowExpression, taskSymbols, accessSyntax.SpanStart, out AwaitExpressionSyntax? arrowPrecedingAwait)
+ && !MayReassignTask(context, arrowExpression, potentialTaskSymbols, arrowPrecedingAwait.Span.End, accessSyntax.SpanStart);
+ }
+
+ if (TryGetAwaitExpression(context, containingStatement, taskSymbols, accessSyntax.SpanStart, out AwaitExpressionSyntax? precedingAwait)
+ && !MayReassignTask(context, containingStatement, potentialTaskSymbols, precedingAwait.Span.End, accessSyntax.SpanStart))
+ {
+ return true;
+ }
+
+ while (true)
{
- if (parentExpr is IdentifierNameSyntax identifierExpr)
+ if (MayReassignTask(context, containingStatement, potentialTaskSymbols, containingStatement.SpanStart - 1, accessSyntax.SpanStart))
{
- taskVariableName = identifierExpr.Identifier.ValueText;
- break;
+ return false;
}
- else if (parentExpr is MemberAccessExpressionSyntax memberAccessExpr)
+
+ SyntaxList statements = containingStatement.Parent switch
{
- parentExpr = memberAccessExpr.Expression;
+ BlockSyntax block => block.Statements,
+ SwitchSectionSyntax switchSection => switchSection.Statements,
+ _ => default,
+ };
+ int statementIndex = statements.IndexOf(containingStatement);
+ for (int i = statementIndex - 1; i >= 0; i--)
+ {
+ StatementSyntax statement = statements[i];
+ if (StatementCompletesTask(context, statement, taskSymbols, potentialTaskSymbols))
+ {
+ return true;
+ }
+
+ if (MayReassignTask(context, statement, potentialTaskSymbols))
+ {
+ return false;
+ }
}
- else if (parentExpr is InvocationExpressionSyntax invocExpr)
+
+ StatementSyntax? outerStatement = containingStatement.Ancestors().OfType()
+ .FirstOrDefault(statement => statement.Parent is BlockSyntax or SwitchSectionSyntax);
+ if (outerStatement is null)
{
- parentExpr = invocExpr.Expression;
+ return false;
}
- else
+
+ if (outerStatement is WhileStatementSyntax
+ or DoStatementSyntax
+ or ForStatementSyntax
+ or ForEachStatementSyntax
+ or ForEachVariableStatementSyntax
+ && MayReassignTask(context, outerStatement, potentialTaskSymbols))
{
- break;
+ return false;
}
- }
- if (taskVariableName is null)
- {
- return false;
+ if (MayReassignTask(context, outerStatement, potentialTaskSymbols, outerStatement.SpanStart - 1, containingStatement.SpanStart))
+ {
+ return false;
+ }
+
+ containingStatement = outerStatement;
}
+ }
- // Find all `Task.WhenAll` invocations that precede the problematic member access, which are also in the same enclosing block.
- IEnumerable? taskWhenAllInvocationList =
- from invoc in enclosingBlock.DescendantNodes().OfType()
- where memberAccessSyntax.SpanStart > invoc.Span.End &&
- IsTaskCompletedWithWhenAll(context, invoc, taskVariableName)
- select invoc;
+ private static bool MayHaveConsumedValueTaskBefore(
+ SyntaxNodeAnalysisContext context,
+ SyntaxNode accessSyntax,
+ IImmutableSet taskSymbols)
+ {
+ SyntaxNode searchRoot = accessSyntax.AncestorsAndSelf().FirstOrDefault(
+ ancestor => ancestor is AnonymousFunctionExpressionSyntax
+ or LocalFunctionStatementSyntax
+ or BaseMethodDeclarationSyntax
+ or AccessorDeclarationSyntax)
+ ?? accessSyntax.FirstAncestorOrSelf()?.Parent
+ ?? accessSyntax;
+ bool DescendIntoChildren(SyntaxNode child) =>
+ child == searchRoot || child is not AnonymousFunctionExpressionSyntax and not LocalFunctionStatementSyntax;
- if (!taskWhenAllInvocationList.Any())
+ bool IsDefinitelyExecutedBefore(SyntaxNode candidate, SyntaxNode consumption)
{
- return false;
+ StatementSyntax? consumptionStatement = consumption.FirstAncestorOrSelf();
+ if (consumptionStatement?.Parent is not BlockSyntax block)
+ {
+ return false;
+ }
+
+ StatementSyntax? candidateStatement = candidate.FirstAncestorOrSelf();
+ while (candidateStatement is object && candidateStatement.Parent != block)
+ {
+ if (candidateStatement.Parent is not BlockSyntax containingBlock)
+ {
+ return false;
+ }
+
+ candidateStatement = containingBlock;
+ }
+
+ return candidateStatement is object
+ && block.Statements.IndexOf(candidateStatement) < block.Statements.IndexOf(consumptionStatement);
}
- // If a `Task.WhenAll` invocation precedes the problematic member access, and the task variable has not been
- // invalidated in between, then we consider the task to be completed.
- // Example:
- // await Task.WhenAll(task1, task2, task3);
- // task1 = Task.Run(...); // Invalidates `task1`
- // DoSomething(ref task2); // Invalidates `task2`
- // task1.Result; // Warn
- // task2.Result; // Warn
- // task3.Result; // No warning, task3 has not been invalidated in between WhenAll and this problematic member access
- foreach (InvocationExpressionSyntax? taskWhenAllInvocation in taskWhenAllInvocationList)
+ ImmutableHashSet GetTaskAndCopySymbolsBefore(SyntaxNode consumption)
{
- // Has the task variable been assigned to a new task?
- IEnumerable? assignmentList =
- from assign in enclosingBlock.DescendantNodes().OfType()
- where assign.SpanStart > taskWhenAllInvocation.Span.End &&
- assign.SpanStart < memberAccessSyntax.SpanStart &&
- ((IdentifierNameSyntax)assign.Left).Identifier.ValueText == taskVariableName
- select assign;
+ var taskAndCopySymbols = new HashSet(taskSymbols, SymbolEqualityComparer.Default);
+ bool IsTaskOrCopy(ExpressionSyntax expression)
+ {
+ expression = UnwrapParentheses(expression);
+ ISymbol? expressionSymbol = context.SemanticModel.GetSymbolInfo(expression, context.CancellationToken).Symbol;
+ if (expressionSymbol is object && taskAndCopySymbols.Contains(expressionSymbol))
+ {
+ return true;
+ }
+
+ return expression is InvocationExpressionSyntax configureAwaitInvocation
+ && configureAwaitInvocation.Expression is MemberAccessExpressionSyntax { Name.Identifier.ValueText: nameof(Task.ConfigureAwait) } configureAwaitAccess
+ && IsTaskOrCopy(configureAwaitAccess.Expression);
+ }
- if (assignmentList.Any())
+ IEnumerable copyOperations = searchRoot.DescendantNodes(DescendIntoChildren)
+ .Where(node => node.SpanStart < consumption.SpanStart
+ && node is VariableDeclaratorSyntax or AssignmentExpressionSyntax)
+ .OrderBy(node => node.SpanStart);
+ foreach (SyntaxNode copyOperation in copyOperations)
{
- return false;
+ if (copyOperation is VariableDeclaratorSyntax { Initializer: { } initializer } variable
+ && context.SemanticModel.GetDeclaredSymbol(variable, context.CancellationToken) is ILocalSymbol declaredLocal
+ && IsTaskOrCopy(initializer.Value))
+ {
+ taskAndCopySymbols.Add(declaredLocal);
+ }
+ else if (copyOperation is AssignmentExpressionSyntax assignment
+ && context.SemanticModel.GetSymbolInfo(assignment.Left, context.CancellationToken).Symbol is ISymbol assignedSymbol
+ && assignedSymbol is ILocalSymbol or IParameterSymbol)
+ {
+ if (IsTaskOrCopy(assignment.Right))
+ {
+ taskAndCopySymbols.Add(assignedSymbol);
+ }
+ else if (IsDefinitelyExecutedBefore(assignment, consumption))
+ {
+ taskAndCopySymbols.Remove(assignedSymbol);
+ }
+ }
}
- // Has the task variable been passed by ref to a method?
- // If so, we must assume the worst case that the method has assigned it to a new task.
- IEnumerable? invocationList =
- from invoc in enclosingBlock.DescendantNodes().OfType()
- where invoc.SpanStart > taskWhenAllInvocation.Span.End &&
- invoc.SpanStart < memberAccessSyntax.SpanStart &&
- IsVariablePassedToInvocation(invoc, taskVariableName, byRef: true)
- select invoc;
+ return taskAndCopySymbols.ToImmutableHashSet(SymbolEqualityComparer.Default);
+ }
- return !invocationList.Any();
+ foreach (AwaitExpressionSyntax awaitExpression in searchRoot.DescendantNodes(DescendIntoChildren)
+ .OfType()
+ .Where(awaitExpression => awaitExpression.SpanStart < accessSyntax.SpanStart)
+ .OrderBy(awaitExpression => awaitExpression.SpanStart))
+ {
+ ImmutableHashSet taskAndCopySymbols = GetTaskAndCopySymbolsBefore(awaitExpression);
+ if (AwaitCompletesTask(context, awaitExpression, taskAndCopySymbols)
+ || AwaitMayConsumeValueTask(context, awaitExpression, taskAndCopySymbols))
+ {
+ return true;
+ }
}
- return false;
+ foreach (MemberAccessExpressionSyntax blockingAccess in searchRoot.DescendantNodes(DescendIntoChildren)
+ .OfType()
+ .Where(memberAccess => memberAccess.SpanStart < accessSyntax.SpanStart
+ && IsValueTaskConsumption(memberAccess)))
+ {
+ ImmutableHashSet taskAndCopySymbols = GetTaskAndCopySymbolsBefore(blockingAccess);
+ if (IsOneOfSymbols(context, GetTaskReceiver(context, blockingAccess), taskAndCopySymbols))
+ {
+ return true;
+ }
+ }
+
+ foreach (LocalFunctionStatementSyntax localFunction in searchRoot.DescendantNodes()
+ .OfType()
+ .Where(localFunction => localFunction.SpanStart < accessSyntax.SpanStart))
+ {
+ if (context.SemanticModel.GetDeclaredSymbol(localFunction, context.CancellationToken) is not IMethodSymbol localFunctionSymbol)
+ {
+ continue;
+ }
+
+ if (searchRoot.DescendantNodes(DescendIntoChildren)
+ .OfType()
+ .Any(invocation => invocation.SpanStart < accessSyntax.SpanStart
+ && SymbolEqualityComparer.Default.Equals(
+ context.SemanticModel.GetSymbolInfo(invocation, context.CancellationToken).Symbol?.OriginalDefinition,
+ localFunctionSymbol.OriginalDefinition)
+ && NestedFunctionMayConsumeValueTask(localFunction, invocation)))
+ {
+ return true;
+ }
+ }
+
+ foreach (VariableDeclaratorSyntax delegateVariable in searchRoot.DescendantNodes(DescendIntoChildren)
+ .OfType()
+ .Where(variable => variable.SpanStart < accessSyntax.SpanStart
+ && variable.Initializer?.Value is AnonymousFunctionExpressionSyntax))
+ {
+ var anonymousFunction = (AnonymousFunctionExpressionSyntax)delegateVariable.Initializer!.Value;
+ if (context.SemanticModel.GetDeclaredSymbol(delegateVariable, context.CancellationToken) is not ILocalSymbol delegateSymbol)
+ {
+ continue;
+ }
+
+ if (searchRoot.DescendantNodes(DescendIntoChildren)
+ .OfType()
+ .Any(invocation => invocation.SpanStart < accessSyntax.SpanStart
+ && SymbolEqualityComparer.Default.Equals(
+ context.SemanticModel.GetSymbolInfo(invocation.Expression, context.CancellationToken).Symbol,
+ delegateSymbol)
+ && NestedFunctionMayConsumeValueTask(anonymousFunction, invocation)))
+ {
+ return true;
+ }
+ }
+
+ return false;
+
+ bool IsValueTaskConsumption(MemberAccessExpressionSyntax memberAccess)
+ {
+ IOperation? operation = context.SemanticModel.GetOperation(memberAccess, context.CancellationToken);
+ for (IOperation? ancestor = operation; ancestor is object; ancestor = ancestor.Parent)
+ {
+ if (ancestor is INameOfOperation)
+ {
+ return false;
+ }
+ }
+
+ return memberAccess.Name.Identifier.ValueText switch
+ {
+ nameof(Task