Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,151 @@ internal static bool IsApplicableAsyncAlternative(
return SymbolEqualityComparer.Default.Equals(applicableDefinition, candidateDefinition);
}

/// <summary>
/// Determines whether a method carries async-compatible values from its parameters into its return value.
/// </summary>
/// <remarks>
/// Synchronous higher-order methods may compose asynchronous work without synchronously blocking on it.
/// For example, <c>Enumerable.Select</c> can project values into an <c>IEnumerable&lt;Task&gt;</c>.
/// In such cases, an Async-suffixed method is not necessarily a preferable alternative.
/// </remarks>
internal static bool ReturnsAsyncCompatibleValuesFromParameters(IMethodSymbol method)
{
IMethodSymbol constructedMethod = method;
IMethodSymbol methodDefinition = constructedMethod.OriginalDefinition;
foreach (ITypeParameterSymbol typeParameter in methodDefinition.TypeParameters)
{
if (IsAsyncCompatibleTypeParameterFlow(typeParameter))
{
return true;
}
}

for (INamedTypeSymbol? containingType = methodDefinition.ContainingType; containingType is object; containingType = containingType.ContainingType)
{
foreach (ITypeParameterSymbol typeParameter in containingType.TypeParameters)
{
if (IsAsyncCompatibleTypeParameterFlow(typeParameter))
{
return true;
}
}
}

return false;

bool IsAsyncCompatibleTypeParameterFlow(ITypeParameterSymbol typeParameter)
=> ContainsTypeParameterInOutputPosition(methodDefinition.ReturnType, typeParameter)
&& IsUsedByParameter(typeParameter)
&& GetConstructedTypeArgument(constructedMethod, typeParameter) is { } typeArgument
&& ContainsAsyncCompatibleType(typeArgument);

bool IsUsedByParameter(ITypeParameterSymbol typeParameter)
{
if (methodDefinition.Parameters.Any(
parameter => parameter.RefKind != RefKind.Out
&& ContainsTypeParameterInOutputPosition(parameter.Type, typeParameter)))
{
return true;
}

IMethodSymbol? unreducedDefinition = method.ReducedFrom?.OriginalDefinition;
return unreducedDefinition is object
&& typeParameter.TypeParameterKind == TypeParameterKind.Method
&& unreducedDefinition.Parameters.Length > 0
&& ContainsTypeParameterInOutputPosition(
unreducedDefinition.Parameters[0].Type,
unreducedDefinition.TypeParameters[typeParameter.Ordinal]);
}

static bool ContainsAsyncCompatibleType(ITypeSymbol type, VarianceKind variance = VarianceKind.Out)
{
if (variance != VarianceKind.In && type.IsAsyncCompatibleReturnType())
{
return true;
}

if (type is IArrayTypeSymbol arrayType)
{
return ContainsAsyncCompatibleType(arrayType.ElementType, variance);
}

if (type is INamedTypeSymbol namedType)
{
for (int i = 0; i < namedType.TypeArguments.Length; i++)
{
VarianceKind typeArgumentVariance = i < namedType.OriginalDefinition.TypeParameters.Length
? namedType.OriginalDefinition.TypeParameters[i].Variance
: VarianceKind.None;
if (ContainsAsyncCompatibleType(namedType.TypeArguments[i], ComposeVariance(variance, typeArgumentVariance)))
{
return true;
}
}
}

return false;
}

static bool ContainsTypeParameterInOutputPosition(
ITypeSymbol type,
ITypeParameterSymbol typeParameter,
VarianceKind variance = VarianceKind.Out)
{
if (variance != VarianceKind.In && SymbolEqualityComparer.Default.Equals(type, typeParameter))
{
return true;
}

if (type is IArrayTypeSymbol arrayType)
{
return ContainsTypeParameterInOutputPosition(arrayType.ElementType, typeParameter, variance);
}

if (type is INamedTypeSymbol namedType)
{
for (int i = 0; i < namedType.TypeArguments.Length; i++)
{
VarianceKind typeArgumentVariance = i < namedType.OriginalDefinition.TypeParameters.Length
? namedType.OriginalDefinition.TypeParameters[i].Variance
: VarianceKind.None;
if (ContainsTypeParameterInOutputPosition(
namedType.TypeArguments[i],
typeParameter,
ComposeVariance(variance, typeArgumentVariance)))
{
return true;
}
}
}

return false;
}

static VarianceKind ComposeVariance(VarianceKind outer, VarianceKind inner)
=> outer == VarianceKind.None || inner == VarianceKind.None
? VarianceKind.None
: outer == inner ? VarianceKind.Out : VarianceKind.In;

static ITypeSymbol? GetConstructedTypeArgument(IMethodSymbol constructedMethod, ITypeParameterSymbol typeParameter)
{
if (typeParameter.TypeParameterKind == TypeParameterKind.Method)
{
return constructedMethod.TypeArguments[typeParameter.Ordinal];
}

for (INamedTypeSymbol? containingType = constructedMethod.ContainingType; containingType is object; containingType = containingType.ContainingType)
{
if (SymbolEqualityComparer.Default.Equals(containingType.OriginalDefinition, typeParameter.ContainingSymbol))
{
return containingType.TypeArguments[typeParameter.Ordinal];
}
}

return null;
}
}

/// <summary>
/// Determines whether a blocking member access has a receiver that is provably complete.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ private static void AnalyzeInvocation(
&& !methodsExcludedFromVSTHRD103.Contains(methodDefinition)
&& !invokedMethod.Name.EndsWith(VSTHRD200UseAsyncNamingConventionAnalyzer.MandatoryAsyncSuffix, StringComparison.CurrentCulture)
&& !invokedMethod.HasAsyncCompatibleReturnType()
&& !CSharpCommonInterest.ReturnsAsyncCompatibleValuesFromParameters(invokedMethod)
&& IsInTaskReturningMethodOrDelegate(context)
&& HasAsyncAlternative(context, invocationExpressionSyntax, invokedMethod);
if (!isBuiltInSyncBlockingMethod
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,8 @@ MemberBindingExpressionSyntax memberBinding when invocationExpressionSyntax.Firs
// Also consider all method calls to check for Async-suffixed alternatives.
SymbolInfo symbolInfo = context.SemanticModel.GetSymbolInfo(invocationExpressionSyntax, context.CancellationToken);
if (symbolInfo.Symbol is IMethodSymbol methodSymbol && !methodSymbol.Name.EndsWith(VSTHRD200UseAsyncNamingConventionAnalyzer.MandatoryAsyncSuffix, StringComparison.CurrentCulture) &&
!methodSymbol.HasAsyncCompatibleReturnType())
!methodSymbol.HasAsyncCompatibleReturnType() &&
!CSharpCommonInterest.ReturnsAsyncCompatibleValuesFromParameters(methodSymbol))
{
string asyncMethodName = methodSymbol.Name + VSTHRD200UseAsyncNamingConventionAnalyzer.MandatoryAsyncSuffix;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1345,6 +1345,44 @@ Task<int> FAsync(Task<int> task) {
await verifyTest.RunAsync();
}

[Fact]
public async Task ConfiguredTaskFlowMethodInTaskReturningMethodReports()
{
string test = """
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

static class Helpers
{
internal static IEnumerable<TResult> Project<TSource, TResult>(
IEnumerable<TSource> source,
Func<TSource, TResult> selector) => throw null;

internal static Task<IEnumerable<TResult>> ProjectAsync<TSource, TResult>(
IEnumerable<TSource> source,
Func<TSource, TResult> selector) => throw null;
}

class Test
{
Task FAsync(IEnumerable<int> values)
{
IEnumerable<Task> tasks = Helpers.[|Project|](values, value => Task.CompletedTask);
return Task.WhenAll(tasks);
}
}
""";

var verifyTest = new CSVerify.Test
{
TestCode = test,
FixedCode = test,
};
verifyTest.TestState.AdditionalFiles.Add(("vs-threading.SyncBlockingMethods.txt", "[Helpers]::Project"));
await verifyTest.RunAsync();
}

[Fact]
public async Task ConditionalTaskWaitInSynchronousMethodReports()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1983,6 +1983,135 @@ await CSVerify.VerifyAnalyzerAsync(
CSVerify.Diagnostic(Descriptor).WithLocation(0).WithArguments("Join", "JoinAsync"));
}

[Fact]
public async Task TaskProducingProjectionDoesNotGenerateWarning()
{
string test = """
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

class Item { }

class Test
{
async Task ProcessAllAsync(IEnumerable<Item> items)
{
await Task.WhenAll(items.Select(ProcessAsync));

IEnumerable<Task> tasks = items.Select(ProcessAsync);
await Task.WhenAll(tasks);
}

Task ProcessAsync(Item item) => Task.CompletedTask;
}

static class AsyncEnumerableExtensions
{
public static Task<IEnumerable<TResult>> SelectAsync<TSource, TResult>(
this IEnumerable<TSource> source,
Func<TSource, TResult> selector) => Task.FromResult(source.Select(selector));
}
""";

await CSVerify.VerifyAnalyzerAsync(test);
}

[Fact]
public async Task TaskLikeValuesFlowingThroughContainersDoNotGenerateWarning()
{
string test = """
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

class Test
{
Task ProcessAllAsync(IEnumerable<int> items, Task existingTask)
{
_ = Map(items, ProcessAsync);
_ = Wrap(existingTask);
_ = WrapInArray(existingTask);
_ = existingTask.WrapInArray();
return Task.CompletedTask;
}

ValueTask<int> ProcessAsync(int item) => new ValueTask<int>(item);

static IReadOnlyList<TResult> Map<TSource, TResult>(
IEnumerable<TSource> source,
Func<TSource, TResult> selector) => throw null;

static Task<IReadOnlyList<TResult>> MapAsync<TSource, TResult>(
IEnumerable<TSource> source,
Func<TSource, TResult> selector) => throw null;

static (T, int) Wrap<T>(T value) => (value, 0);

static Task<(T, int)> WrapAsync<T>(T value) => Task.FromResult((value, 0));

static T[] WrapInArray<T>(T value) => new[] { value };

static Task<T[]> WrapInArrayAsync<T>(T value) => Task.FromResult(new[] { value });
}

static class TaskCarryingExtensions
{
internal static T[] WrapInArray<T>(this T value) => new[] { value };

internal static Task<T[]> WrapInArrayAsync<T>(this T value) => Task.FromResult(new[] { value });
}
""";

await CSVerify.VerifyAnalyzerAsync(test);
}

[Fact]
public async Task AsyncAlternativeStillWarnsWhenTaskLikeValuesDoNotFlowToReturnValue()
{
string test = """
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

class Test
{
Task ProcessAllAsync()
{
_ = {|#0:GetTasks|}();
{|#1:Run|}(() => Task.CompletedTask);
_ = {|#2:Wrap|}(Task.CompletedTask);
_ = {|#3:Load<Task>|}(task => { });
return Task.CompletedTask;
}

static IEnumerable<Task> GetTasks() => throw null;

static Task<IEnumerable<Task>> GetTasksAsync() => throw null;

static void Run(Func<Task> action) { }

static Task RunAsync(Func<Task> action) => action();

static (Task, int) Wrap(Task task) => (task, 0);

static Task<(Task, int)> WrapAsync(Task task) => Task.FromResult((task, 0));

static IEnumerable<T> Load<T>(Action<T> consumer) => throw null;

static Task<IEnumerable<T>> LoadAsync<T>(Action<T> consumer) => throw null;
}
""";

await CSVerify.VerifyAnalyzerAsync(
test,
CSVerify.Diagnostic(Descriptor).WithLocation(0).WithArguments("GetTasks", "GetTasksAsync"),
CSVerify.Diagnostic(Descriptor).WithLocation(1).WithArguments("Run", "RunAsync"),
CSVerify.Diagnostic(Descriptor).WithLocation(2).WithArguments("Wrap", "WrapAsync"),
CSVerify.Diagnostic(Descriptor).WithLocation(3).WithArguments("Load<Task>", "LoadAsync"));
}

[Fact]
public async Task TaskGetAwaiterGetResultInTaskReturningMethodGeneratesWarning()
{
Expand Down
Loading