Skip to content

Commit 2e2d14e

Browse files
AArnottCopilot
andcommitted
Harden VSTHRD002 caller conversion
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent bdb808f commit 2e2d14e

2 files changed

Lines changed: 76 additions & 6 deletions

File tree

src/Microsoft.VisualStudio.Threading.Analyzers.CodeFixes/VSTHRD002UseJtfRunCodeFixWithAwait.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,9 @@ private static bool HasAsyncNameCollision(IMethodSymbol method)
192192
return method.ContainingType.GetMembers(asyncName)
193193
.OfType<IMethodSymbol>()
194194
.Any(candidate => candidate.Arity == method.Arity
195-
&& candidate.Parameters.Length == method.Parameters.Length
196-
&& candidate.Parameters.Zip(method.Parameters, ParametersHaveEquivalentSignatures).All(match => match));
195+
&& candidate.Parameters.Length >= method.Parameters.Length
196+
&& candidate.Parameters.Take(method.Parameters.Length).Zip(method.Parameters, ParametersHaveEquivalentSignatures).All(match => match)
197+
&& candidate.Parameters.Skip(method.Parameters.Length).All(parameter => parameter.IsOptional));
197198
}
198199

199200
private static bool ParametersHaveEquivalentSignatures(IParameterSymbol left, IParameterSymbol right)
@@ -275,10 +276,7 @@ or LocalFunctionStatementSyntax
275276
&& callerReturnType.IsAsyncCompatibleReturnType()
276277
&& ((invocation.FirstAncestorOrSelf<ReturnStatementSyntax>() is { Expression: { } returnExpression }
277278
&& returnExpression.FullSpan.Contains(invocation.Span))
278-
|| (callingMethod.ExpressionBody?.Expression.FullSpan.Contains(invocation.Span) is true))
279-
&& (callerReturnType.Arity == 0
280-
|| !Utils.IsTask(callerReturnType)
281-
|| !semanticModel.Compilation.ClassifyConversion(method.ReturnType, callerReturnType.TypeArguments[0]).IsImplicit))
279+
|| (callingMethod.ExpressionBody?.Expression.FullSpan.Contains(invocation.Span) is true)))
282280
{
283281
return false;
284282
}

test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD002UseJtfRunAnalyzerTests.cs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1926,6 +1926,55 @@ static DerivedTask<int> GetValue(Task<int> task)
19261926
await CSVerify.VerifyCodeFixAsync(test, test);
19271927
}
19281928

1929+
[Fact]
1930+
public async Task CodeFixIsNotOfferedForDirectReturnFromAsyncCompatibleCaller()
1931+
{
1932+
string test = """
1933+
using System;
1934+
using System.Runtime.CompilerServices;
1935+
using System.Threading.Tasks;
1936+
1937+
[AsyncMethodBuilder(typeof(CustomTaskMethodBuilder<>))]
1938+
class CustomTask<T>
1939+
{
1940+
public TaskAwaiter<T> GetAwaiter() => Task.FromResult(default(T)).GetAwaiter();
1941+
public static implicit operator CustomTask<T>(T value) => new();
1942+
}
1943+
1944+
struct CustomTaskMethodBuilder<T>
1945+
{
1946+
public static CustomTaskMethodBuilder<T> Create() => default;
1947+
public CustomTask<T> Task => new();
1948+
public void SetResult(T result) { }
1949+
public void SetException(Exception exception) { }
1950+
public void SetStateMachine(IAsyncStateMachine stateMachine) { }
1951+
public void Start<TStateMachine>(ref TStateMachine stateMachine)
1952+
where TStateMachine : IAsyncStateMachine => stateMachine.MoveNext();
1953+
public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
1954+
where TAwaiter : INotifyCompletion
1955+
where TStateMachine : IAsyncStateMachine { }
1956+
public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
1957+
where TAwaiter : ICriticalNotifyCompletion
1958+
where TStateMachine : IAsyncStateMachine { }
1959+
}
1960+
1961+
class Test
1962+
{
1963+
static int GetValue(Task<int> task)
1964+
{
1965+
return task.[|Result|];
1966+
}
1967+
1968+
static CustomTask<int> Caller(Task<int> task)
1969+
{
1970+
return GetValue(task);
1971+
}
1972+
}
1973+
""";
1974+
1975+
await CSVerify.VerifyCodeFixAsync(test, test);
1976+
}
1977+
19291978
[Fact]
19301979
public async Task CodeFixIsNotOfferedWhenCallerUsesConditionalAccess()
19311980
{
@@ -1973,6 +2022,29 @@ Task<int> GetValueAsync(Task<int> task)
19732022
await CSVerify.VerifyCodeFixAsync(test, test);
19742023
}
19752024

2025+
[Fact]
2026+
public async Task CodeFixIsNotOfferedWhenAsyncNameHasApplicableOptionalOverload()
2027+
{
2028+
string test = """
2029+
using System.Threading.Tasks;
2030+
2031+
class Test
2032+
{
2033+
int GetValue(Task<int> task)
2034+
{
2035+
return task.[|Result|];
2036+
}
2037+
2038+
Task<int> GetValueAsync(Task<int> task, bool optional = false)
2039+
{
2040+
return task;
2041+
}
2042+
}
2043+
""";
2044+
2045+
await CSVerify.VerifyCodeFixAsync(test, test);
2046+
}
2047+
19762048
[Fact]
19772049
public async Task CodeFixIsNotOfferedWhenChangingMethodContract()
19782050
{

0 commit comments

Comments
 (0)