xunit/xunit#2849: Update xUnit1030 to handle ConfigureAwaitOptions

This commit is contained in:
Brad Wilson 2023-12-12 16:06:08 -08:00
Родитель 49afdc9427
Коммит 8aa39e4da2
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 0B7BD15AD1EC5FDE
6 изменённых файлов: 495 добавлений и 6 удалений

Просмотреть файл

@ -1,4 +1,6 @@
using System.Composition;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
@ -12,6 +14,7 @@ namespace Xunit.Analyzers.Fixes;
public class DoNotUseConfigureAwaitFixer : BatchedCodeFixProvider
{
public const string Key_RemoveConfigureAwait = "xUnit1030_RemoveConfigureAwait";
public const string Key_ReplaceArgumentValue = "xUnit1030_ReplaceArgumentValue";
public DoNotUseConfigureAwaitFixer() :
base(Descriptors.X1030_DoNotUseConfigureAwait.Id)
@ -23,6 +26,16 @@ public class DoNotUseConfigureAwaitFixer : BatchedCodeFixProvider
if (root is null)
return;
var diagnostic = context.Diagnostics.FirstOrDefault();
if (diagnostic is null)
return;
// Get the original and replacement values
if (!diagnostic.Properties.TryGetValue(Constants.Properties.ArgumentValue, out var original))
return;
if (!diagnostic.Properties.TryGetValue(Constants.Properties.Replacement, out var replacement))
return;
// The syntax node (the invocation) will include "(any preceding trivia)(any preceding code).ConfigureAwait(args)" despite
// the context.Span only covering "ConfigureAwait(args)". So we need to replace the whole invocation
// with an invocation that does not include the ConfigureAwait call.
@ -30,19 +43,40 @@ public class DoNotUseConfigureAwaitFixer : BatchedCodeFixProvider
var syntaxText = syntaxNode.ToFullString();
// Remove the context span (plus the preceding .)
var newSyntaxText = syntaxText.Substring(0, context.Span.Start - syntaxNode.FullSpan.Start - 1);
var newSyntaxNode = SyntaxFactory.ParseExpression(newSyntaxText);
var removeConfigureAwaitText = syntaxText.Substring(0, context.Span.Start - syntaxNode.FullSpan.Start - 1);
var removeConfigureAwaitNode = SyntaxFactory.ParseExpression(removeConfigureAwaitText);
// Only offer the removal fix if the replacement value is 'true', because anybody using ConfigureAwaitOptions
// will want to just add the extra value, not remove the call entirely.
if (replacement == "true")
context.RegisterCodeFix(
CodeAction.Create(
"Remove ConfigureAwait call",
async ct =>
{
var editor = await DocumentEditor.CreateAsync(context.Document, ct).ConfigureAwait(false);
editor.ReplaceNode(syntaxNode, removeConfigureAwaitNode);
return editor.GetChangedDocument();
},
Key_RemoveConfigureAwait
),
context.Diagnostics
);
// Offer the replacement fix
var replaceConfigureAwaitText = removeConfigureAwaitText + ".ConfigureAwait(" + replacement + ")";
var replaceConfigureAwaitNode = SyntaxFactory.ParseExpression(replaceConfigureAwaitText);
context.RegisterCodeFix(
CodeAction.Create(
"Remove ConfigureAwait call",
string.Format(CultureInfo.CurrentCulture, "Replace ConfigureAwait({0}) with ConfigureAwait({1})", original, replacement),
async ct =>
{
var editor = await DocumentEditor.CreateAsync(context.Document, ct).ConfigureAwait(false);
editor.ReplaceNode(syntaxNode, newSyntaxNode);
editor.ReplaceNode(syntaxNode, replaceConfigureAwaitNode);
return editor.GetChangedDocument();
},
Key_RemoveConfigureAwait
Key_ReplaceArgumentValue
),
context.Diagnostics
);

Просмотреть файл

@ -186,4 +186,130 @@ public class TestClass {{
await Verify.VerifyAnalyzer(source, expected);
}
}
#if NETCOREAPP
public class ConfigureAwait_ConfigureAwaitOptions
{
[Fact]
public async void NonTestMethod_DoesNotTrigger()
{
var source = @"
using System.Threading.Tasks;
using Xunit;
public class NonTestClass {
public async Task NonTestMethod() {
await Task.Delay(1).ConfigureAwait(ConfigureAwaitOptions.None);
}
}";
await Verify.VerifyAnalyzer(source);
}
[Theory]
[InlineData("ConfigureAwaitOptions.ContinueOnCapturedContext")]
[InlineData("ConfigureAwaitOptions.SuppressThrowing | ConfigureAwaitOptions.ContinueOnCapturedContext")]
[InlineData("ConfigureAwaitOptions.ForceYielding | ConfigureAwaitOptions.SuppressThrowing | ConfigureAwaitOptions.ContinueOnCapturedContext")]
public async void ValidValue_DoesNotTrigger(string enumValue)
{
var source = $@"
using System.Threading.Tasks;
using Xunit;
public class TestClass {{
[Fact]
public async Task TestMethod() {{
await Task.Delay(1).ConfigureAwait({enumValue});
}}
}}";
await Verify.VerifyAnalyzer(source);
}
public static TheoryData<string> InvalidValues = new()
{
// Literal values
"ConfigureAwaitOptions.None",
"ConfigureAwaitOptions.SuppressThrowing",
"ConfigureAwaitOptions.ForceYielding | ConfigureAwaitOptions.SuppressThrowing",
// Reference values (we don't do lookup)
"enumVar",
};
[Theory]
[MemberData(nameof(InvalidValues))]
public async void InvalidValue_TaskWithAwait_DoesNotTrigger(string enumValue)
{
var source = $@"
using System.Threading.Tasks;
using Xunit;
public class TestClass {{
[Fact]
public async Task TestMethod() {{
var enumVar = ConfigureAwaitOptions.ContinueOnCapturedContext;
await Task.Delay(1).ConfigureAwait({enumValue});
}}
}}";
var expected =
Verify
.Diagnostic()
.WithSpan(9, 29, 9, 45 + enumValue.Length)
.WithArguments(enumValue, "Ensure ConfigureAwaitOptions.ContinueOnCapturedContext in the flags.");
await Verify.VerifyAnalyzer(source, expected);
}
[Theory]
[MemberData(nameof(InvalidValues))]
public async void InvalidValue_TaskWithoutAwait_Triggers(string argumentValue)
{
var source = @$"
using System.Threading.Tasks;
using Xunit;
public class TestClass {{
[Fact]
public void TestMethod() {{
var enumVar = ConfigureAwaitOptions.ContinueOnCapturedContext;
Task.Delay(1).ConfigureAwait({argumentValue}).GetAwaiter().GetResult();
}}
}}";
var expected =
Verify
.Diagnostic()
.WithSpan(9, 23, 9, 39 + argumentValue.Length)
.WithArguments(argumentValue, "Ensure ConfigureAwaitOptions.ContinueOnCapturedContext in the flags.");
await Verify.VerifyAnalyzer(source, expected);
}
[Theory]
[MemberData(nameof(InvalidValues))]
public async void InvalidValue_TaskOfT_Triggers(string argumentValue)
{
var source = @$"
using System.Threading.Tasks;
using Xunit;
public class TestClass {{
[Fact]
public async Task TestMethod() {{
var enumVar = ConfigureAwaitOptions.ContinueOnCapturedContext;
var task = Task.FromResult(42);
await task.ConfigureAwait({argumentValue});
}}
}}";
var expected =
Verify
.Diagnostic()
.WithSpan(10, 20, 10, 36 + argumentValue.Length)
.WithArguments(argumentValue, "Ensure ConfigureAwaitOptions.ContinueOnCapturedContext in the flags.");
await Verify.VerifyAnalyzer(source, expected);
}
}
#endif
}

Просмотреть файл

@ -177,5 +177,281 @@ public class TestClass {
await Verify.VerifyCodeFix(before, after, DoNotUseConfigureAwaitFixer.Key_RemoveConfigureAwait);
}
}
public class ReplaceConfigureAwait
{
[Theory]
[MemberData(nameof(InvalidValues), MemberType = typeof(ConfigureAwait_Boolean))]
public async void Task_Async(string argumentValue)
{
var before = @$"
using System.Threading.Tasks;
using Xunit;
public class TestClass {{
[Fact]
public async Task TestMethod() {{
var booleanVar = true;
await Task.Delay(1).[|ConfigureAwait({argumentValue})|];
}}
}}";
var after = @"
using System.Threading.Tasks;
using Xunit;
public class TestClass {
[Fact]
public async Task TestMethod() {
var booleanVar = true;
await Task.Delay(1).ConfigureAwait(true);
}
}";
await Verify.VerifyCodeFix(before, after, DoNotUseConfigureAwaitFixer.Key_ReplaceArgumentValue);
}
[Theory]
[MemberData(nameof(InvalidValues), MemberType = typeof(ConfigureAwait_Boolean))]
public async void Task_NonAsync(string argumentValue)
{
var before = @$"
using System.Threading.Tasks;
using Xunit;
public class TestClass {{
[Fact]
public void TestMethod() {{
var booleanVar = true;
Task.Delay(1).[|ConfigureAwait({argumentValue})|].GetAwaiter().GetResult();
}}
}}";
var after = @"
using System.Threading.Tasks;
using Xunit;
public class TestClass {
[Fact]
public void TestMethod() {
var booleanVar = true;
Task.Delay(1).ConfigureAwait(true).GetAwaiter().GetResult();
}
}";
await Verify.VerifyCodeFix(before, after, DoNotUseConfigureAwaitFixer.Key_ReplaceArgumentValue);
}
[Theory]
[MemberData(nameof(InvalidValues), MemberType = typeof(ConfigureAwait_Boolean))]
public async void TaskOfT(string argumentValue)
{
var before = @$"
using System.Threading.Tasks;
using Xunit;
public class TestClass {{
[Fact]
public async Task TestMethod() {{
var booleanVar = true;
var task = Task.FromResult(42);
await task.[|ConfigureAwait({argumentValue})|];
}}
}}";
var after = @"
using System.Threading.Tasks;
using Xunit;
public class TestClass {
[Fact]
public async Task TestMethod() {
var booleanVar = true;
var task = Task.FromResult(42);
await task.ConfigureAwait(true);
}
}";
await Verify.VerifyCodeFix(before, after, DoNotUseConfigureAwaitFixer.Key_ReplaceArgumentValue);
}
[Theory]
[MemberData(nameof(InvalidValues), MemberType = typeof(ConfigureAwait_Boolean))]
public async void ValueTask(string argumentValue)
{
var before = @$"
using System.Threading.Tasks;
using Xunit;
public class TestClass {{
[Fact]
public async Task TestMethod() {{
var booleanVar = true;
var valueTask = default(ValueTask);
await valueTask.[|ConfigureAwait({argumentValue})|];
}}
}}";
var after = @"
using System.Threading.Tasks;
using Xunit;
public class TestClass {
[Fact]
public async Task TestMethod() {
var booleanVar = true;
var valueTask = default(ValueTask);
await valueTask.ConfigureAwait(true);
}
}";
await Verify.VerifyCodeFix(before, after, DoNotUseConfigureAwaitFixer.Key_ReplaceArgumentValue);
}
[Theory]
[MemberData(nameof(InvalidValues), MemberType = typeof(ConfigureAwait_Boolean))]
public async void ValueTaskOfT(string argumentValue)
{
var before = @$"
using System.Threading.Tasks;
using Xunit;
public class TestClass {{
[Fact]
public async Task TestMethod() {{
var booleanVar = true;
var valueTask = default(ValueTask<object>);
await valueTask.[|ConfigureAwait({argumentValue})|];
}}
}}";
var after = @"
using System.Threading.Tasks;
using Xunit;
public class TestClass {
[Fact]
public async Task TestMethod() {
var booleanVar = true;
var valueTask = default(ValueTask<object>);
await valueTask.ConfigureAwait(true);
}
}";
await Verify.VerifyCodeFix(before, after, DoNotUseConfigureAwaitFixer.Key_ReplaceArgumentValue);
}
}
}
#if NETCOREAPP
public class ConfigureAwait_ConfigureAwaitOptions
{
public static TheoryData<string> InvalidValues = new()
{
// Literal values
"ConfigureAwaitOptions.None",
"ConfigureAwaitOptions.SuppressThrowing",
"ConfigureAwaitOptions.ForceYielding | ConfigureAwaitOptions.SuppressThrowing",
// Reference values (we don't do lookup)
"enumVar",
};
[Theory]
[MemberData(nameof(InvalidValues))]
public async void Task_Async(string argumentValue)
{
var before = @$"
using System.Threading.Tasks;
using Xunit;
public class TestClass {{
[Fact]
public async Task TestMethod() {{
var enumVar = ConfigureAwaitOptions.ContinueOnCapturedContext;
await Task.Delay(1).[|ConfigureAwait({argumentValue})|];
}}
}}";
var after = @$"
using System.Threading.Tasks;
using Xunit;
public class TestClass {{
[Fact]
public async Task TestMethod() {{
var enumVar = ConfigureAwaitOptions.ContinueOnCapturedContext;
await Task.Delay(1).ConfigureAwait({argumentValue} | ConfigureAwaitOptions.ContinueOnCapturedContext);
}}
}}";
await Verify.VerifyCodeFix(before, after, DoNotUseConfigureAwaitFixer.Key_ReplaceArgumentValue);
}
[Theory]
[MemberData(nameof(InvalidValues))]
public async void Task_NonAsync(string argumentValue)
{
var before = @$"
using System.Threading.Tasks;
using Xunit;
public class TestClass {{
[Fact]
public void TestMethod() {{
var enumVar = ConfigureAwaitOptions.ContinueOnCapturedContext;
Task.Delay(1).[|ConfigureAwait({argumentValue})|].GetAwaiter().GetResult();
}}
}}";
var after = @$"
using System.Threading.Tasks;
using Xunit;
public class TestClass {{
[Fact]
public void TestMethod() {{
var enumVar = ConfigureAwaitOptions.ContinueOnCapturedContext;
Task.Delay(1).ConfigureAwait({argumentValue} | ConfigureAwaitOptions.ContinueOnCapturedContext).GetAwaiter().GetResult();
}}
}}";
await Verify.VerifyCodeFix(before, after, DoNotUseConfigureAwaitFixer.Key_ReplaceArgumentValue);
}
[Theory]
[MemberData(nameof(InvalidValues))]
public async void TaskOfT(string argumentValue)
{
var before = @$"
using System.Threading.Tasks;
using Xunit;
public class TestClass {{
[Fact]
public async Task TestMethod() {{
var enumVar = ConfigureAwaitOptions.ContinueOnCapturedContext;
var task = Task.FromResult(42);
await task.[|ConfigureAwait({argumentValue})|];
}}
}}";
var after = @$"
using System.Threading.Tasks;
using Xunit;
public class TestClass {{
[Fact]
public async Task TestMethod() {{
var enumVar = ConfigureAwaitOptions.ContinueOnCapturedContext;
var task = Task.FromResult(42);
await task.ConfigureAwait({argumentValue} | ConfigureAwaitOptions.ContinueOnCapturedContext);
}}
}}";
await Verify.VerifyCodeFix(before, after, DoNotUseConfigureAwaitFixer.Key_ReplaceArgumentValue);
}
}
#endif
}

Просмотреть файл

@ -75,6 +75,7 @@ public static class Constants
/// </summary>
public static class Properties
{
public const string ArgumentValue = "ArgumentValue";
public const string AssertMethodName = "AssertMethodName";
public const string DeclaringType = "DeclaringType";
public const string IgnoreCase = "IgnoreCase";

Просмотреть файл

@ -20,6 +20,9 @@ public static class TypeSymbolFactory
public static INamedTypeSymbol? CollectionDefinitionAttribute(Compilation compilation) =>
Guard.ArgumentNotNull(compilation).GetTypeByMetadataName("Xunit.CollectionDefinitionAttribute");
public static INamedTypeSymbol? ConfigureAwaitOptions(Compilation compilation) =>
Guard.ArgumentNotNull(compilation).GetTypeByMetadataName("System.Threading.Tasks.ConfigureAwaitOptions");
public static INamedTypeSymbol? ConfiguredTaskAwaitable(Compilation compilation) =>
Guard.ArgumentNotNull(compilation).GetTypeByMetadataName("System.Runtime.CompilerServices.ConfiguredTaskAwaitable");

Просмотреть файл

@ -1,4 +1,6 @@
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
@ -74,7 +76,10 @@ public class DoNotUseConfigureAwait : XunitDiagnosticAnalyzer
// Determine the invocation type and resolution
var parameterType = invocation.TargetMethod.Parameters[0].Type;
var configureAwaitOptions = TypeSymbolFactory.ConfigureAwaitOptions(context.Compilation);
var argumentValue = argumentSyntax.ToFullString();
string resolution;
string replacement;
// We want to exempt calls with "(true)" because of CA2007
if (SymbolEqualityComparer.Default.Equals(parameterType, context.Compilation.GetSpecialType(SpecialType.System_Boolean)))
@ -83,6 +88,18 @@ public class DoNotUseConfigureAwait : XunitDiagnosticAnalyzer
return;
resolution = "Omit ConfigureAwait, or use ConfigureAwait(true) to avoid CA2007.";
replacement = "true";
}
// We want to exempt calls which include ConfigureAwaitOptions.ContinueOnCapturedContext
else if (SymbolEqualityComparer.Default.Equals(parameterType, configureAwaitOptions))
{
if (invocation.SemanticModel is null)
return;
if (ContainsContinueOnCapturedContext(argumentSyntax.Expression, invocation.SemanticModel, configureAwaitOptions, context.CancellationToken))
return;
resolution = "Ensure ConfigureAwaitOptions.ContinueOnCapturedContext in the flags.";
replacement = argumentValue + " | ConfigureAwaitOptions.ContinueOnCapturedContext";
}
else
return;
@ -97,7 +114,39 @@ public class DoNotUseConfigureAwait : XunitDiagnosticAnalyzer
var textSpan = new TextSpan(methodCallChildren[2].SpanStart, length);
var location = Location.Create(invocation.Syntax.SyntaxTree, textSpan);
context.ReportDiagnostic(Diagnostic.Create(Descriptors.X1030_DoNotUseConfigureAwait, location, argumentSyntax.ToFullString(), resolution));
// Provide the original value and replacement value to the fixer
var builder = ImmutableDictionary.CreateBuilder<string, string?>();
builder[Constants.Properties.ArgumentValue] = argumentValue;
builder[Constants.Properties.Replacement] = replacement;
context.ReportDiagnostic(
Diagnostic.Create(
Descriptors.X1030_DoNotUseConfigureAwait,
location,
builder.ToImmutable(),
argumentValue,
resolution
)
);
}, OperationKind.Invocation);
}
static bool ContainsContinueOnCapturedContext(
ExpressionSyntax expression,
SemanticModel semanticModel,
INamedTypeSymbol configureAwaitOptions,
CancellationToken cancellationToken)
{
// If we have a binary expression of bitwise OR, we evaluate both sides of the expression
if (expression is BinaryExpressionSyntax binaryExpression && binaryExpression.Kind() == SyntaxKind.BitwiseOrExpression)
return ContainsContinueOnCapturedContext(binaryExpression.Left, semanticModel, configureAwaitOptions, cancellationToken)
|| ContainsContinueOnCapturedContext(binaryExpression.Right, semanticModel, configureAwaitOptions, cancellationToken);
// Look for constant value of enum type
var symbol = semanticModel.GetSymbolInfo(expression, cancellationToken).Symbol;
if (symbol is not null && SymbolEqualityComparer.Default.Equals(symbol.ContainingType, configureAwaitOptions) && symbol.Name == "ContinueOnCapturedContext")
return true;
return false;
}
}