Skip to content

Commit

Permalink
137 xunit define codefixes for exceptionasserts assert.throws (#284)
Browse files Browse the repository at this point in the history
* feat: add xunit Assert.Throws & Assert.ThrowsAsync

* feat: add xunit Assert.ThrowsAny & Assert.ThrowsAnyAsync

* feat: add xunit Assert.Throws*
  • Loading branch information
Meir017 committed Jan 8, 2024
1 parent 8af10d5 commit f7a5c04
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 40 deletions.
75 changes: 75 additions & 0 deletions src/FluentAssertions.Analyzers.Tests/Tips/XunitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,81 @@ public void AssertEquivalent_TestAnalyzer(string arguments, string assertion) =>
public void AssertEquivalent_TestCodeFix(string oldAssertion, string newAssertion)
=> VerifyCSharpFix("object actual, object expected", oldAssertion, newAssertion);

[DataTestMethod]
[DataRow("Action action", "Assert.Throws(typeof(ArgumentException), action);")]
[DataRow("Action action, Type exceptionType", "Assert.Throws(exceptionType, action);")]
[DataRow("Action action", "Assert.Throws<NullReferenceException>(action);")]
[DataRow("Action action", "Assert.Throws<ArgumentException>(\"propertyName\", action);")]
[Implemented]
public void AssertThrows_TestAnalyzer(string arguments, string assertion)
=> VerifyCSharpDiagnostic(arguments, assertion);

[DataTestMethod]
[DataRow("Action action",
/* oldAssertion */ "Assert.Throws(typeof(ArgumentException), action);",
/* newAssertion */ "action.Should().ThrowExactly<ArgumentException>();")]
[DataRow("Action action",
/* oldAssertion */ "Assert.Throws<NullReferenceException>(action);",
/* newAssertion */ "action.Should().ThrowExactly<NullReferenceException>();")]
[DataRow("Action action",
/* oldAssertion */ "Assert.Throws<ArgumentException>(\"propertyName\", action);",
/* newAssertion */ "action.Should().ThrowExactly<ArgumentException>().WithParameterName(\"propertyName\");")]
[Implemented]
public void AssertThrows_TestCodeFix(string arguments, string oldAssertion, string newAssertion)
=> VerifyCSharpFix(arguments, oldAssertion, newAssertion);

[DataTestMethod]
[DataRow("Func<Task> action", "Assert.ThrowsAsync(typeof(ArgumentException), action);")]
[DataRow("Func<Task> action, Type exceptionType", "Assert.ThrowsAsync(exceptionType, action);")]
[DataRow("Func<Task> action", "Assert.ThrowsAsync<NullReferenceException>(action);")]
[DataRow("Func<Task> action", "Assert.ThrowsAsync<ArgumentException>(\"propertyName\", action);")]
[Implemented]
public void AssertThrowsAsync_TestAnalyzer(string arguments, string assertion)
=> VerifyCSharpDiagnostic(arguments, assertion);

[DataTestMethod]
[DataRow("Func<Task> action",
/* oldAssertion */ "Assert.ThrowsAsync(typeof(ArgumentException), action);",
/* newAssertion */ "action.Should().ThrowExactlyAsync<ArgumentException>();")]
[DataRow("Func<Task> action",
/* oldAssertion */ "Assert.ThrowsAsync<NullReferenceException>(action);",
/* newAssertion */ "action.Should().ThrowExactlyAsync<NullReferenceException>();")]
[DataRow("Func<Task> action",
/* oldAssertion */ "Assert.ThrowsAsync<ArgumentException>(\"propertyName\", action);",
/* newAssertion */ "action.Should().ThrowExactlyAsync<ArgumentException>().WithParameterName(\"propertyName\");")]
[Implemented]
public void AssertThrowsAsync_TestCodeFix(string arguments, string oldAssertion, string newAssertion)
=> VerifyCSharpFix(arguments, oldAssertion, newAssertion);

[DataTestMethod]
[DataRow("Action action", "Assert.ThrowsAny<NullReferenceException>(action);")]
[Implemented]
public void AssertThrowsAny_TestAnalyzer(string arguments, string assertion)
=> VerifyCSharpDiagnostic(arguments, assertion);

[DataTestMethod]
[DataRow("Action action",
/* oldAssertion */ "Assert.ThrowsAny<NullReferenceException>(action);",
/* newAssertion */ "action.Should().Throw<NullReferenceException>();")]
[Implemented]
public void AssertThrowsAny_TestCodeFix(string arguments, string oldAssertion, string newAssertion)
=> VerifyCSharpFix(arguments, oldAssertion, newAssertion);

[DataTestMethod]
[DataRow("Func<Task> action", "Assert.ThrowsAnyAsync<NullReferenceException>(action);")]
[Implemented]
public void AssertThrowsAnyAsync_TestAnalyzer(string arguments, string assertion)
=> VerifyCSharpDiagnostic(arguments, assertion);

[DataTestMethod]
[DataRow("Func<Task> action",
/* oldAssertion */ "Assert.ThrowsAnyAsync<NullReferenceException>(action);",
/* newAssertion */ "action.Should().ThrowAsync<NullReferenceException>();")]
[Implemented]
public void AssertThrowsAnyAsync_TestCodeFix(string arguments, string oldAssertion, string newAssertion)
=> VerifyCSharpFix(arguments, oldAssertion, newAssertion);


private void VerifyCSharpDiagnostic(string methodArguments, string assertion)
{
var source = GenerateCode.XunitAssertion(methodArguments, assertion);
Expand Down
32 changes: 18 additions & 14 deletions src/FluentAssertions.Analyzers/Tips/DocumentEditorUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,8 @@ public class DocumentEditorUtils
{
public static CreateChangedDocument RenameMethodToSubjectShouldAssertion(IInvocationOperation invocation, CodeFixContext context, string newName, int subjectIndex, int[] argumentsToRemove)
{
var invocationExpression = (InvocationExpressionSyntax)invocation.Syntax;

return async ctx => await RewriteExpression(invocationExpression, [
..Array.ConvertAll(argumentsToRemove, arg => EditAction.RemoveNode(invocationExpression.ArgumentList.Arguments[arg])),
return async ctx => await RewriteExpression(invocation, [
..Array.ConvertAll(argumentsToRemove, arg => EditAction.RemoveInvocationArgument(arg)),
EditAction.SubjectShouldAssertion(subjectIndex, newName)
], context, ctx);
}
Expand All @@ -27,33 +25,39 @@ public static CreateChangedDocument RenameGenericMethodToSubjectShouldGenericAss
=> RenameMethodToSubjectShouldGenericAssertion(invocation, invocation.TargetMethod.TypeArguments, context, newName, subjectIndex, argumentsToRemove);
public static CreateChangedDocument RenameMethodToSubjectShouldGenericAssertion(IInvocationOperation invocation, ImmutableArray<ITypeSymbol> genericTypes, CodeFixContext context, string newName, int subjectIndex, int[] argumentsToRemove)
{
var invocationExpression = (InvocationExpressionSyntax)invocation.Syntax;

return async ctx => await RewriteExpression(invocationExpression, [
..Array.ConvertAll(argumentsToRemove, arg => EditAction.RemoveNode(invocationExpression.ArgumentList.Arguments[arg])),
EditAction.SubjectShouldGenericAssertion(subjectIndex, newName, genericTypes)
return async ctx => await RewriteExpression(invocation, [
..Array.ConvertAll(argumentsToRemove, arg => EditAction.RemoveInvocationArgument(arg)),
EditAction.SubjectShouldGenericAssertion(subjectIndex, newName, genericTypes)
], context, ctx);
}

public static CreateChangedDocument RenameMethodToSubjectShouldAssertionWithOptionsLambda(IInvocationOperation invocation, CodeFixContext context, string newName, int subjectIndex, int optionsIndex)
{
var invocationExpression = (InvocationExpressionSyntax)invocation.Syntax;

return async ctx => await RewriteExpression(invocationExpression, [
return async ctx => await RewriteExpression(invocation, [
EditAction.SubjectShouldAssertion(subjectIndex, newName),
EditAction.CreateEquivalencyAssertionOptionsLambda(optionsIndex)
], context, ctx);
}

private static async Task<Document> RewriteExpression(InvocationExpressionSyntax invocationExpression, Action<DocumentEditor, InvocationExpressionSyntax>[] actions, CodeFixContext context, CancellationToken cancellationToken)
public static async Task<Document> RewriteExpression(IInvocationOperation invocation, Action<EditActionContext>[] actions, CodeFixContext context, CancellationToken cancellationToken)
{
var invocationExpression = (InvocationExpressionSyntax)invocation.Syntax;

var editor = await DocumentEditor.CreateAsync(context.Document, cancellationToken);
var editActionContext = new EditActionContext(editor, invocationExpression);

foreach (var action in actions)
{
action(editor, invocationExpression);
action(editActionContext);
}

return editor.GetChangedDocument();
}
}

public class EditActionContext(DocumentEditor editor, InvocationExpressionSyntax invocationExpression) {
public DocumentEditor Editor { get; } = editor;
public InvocationExpressionSyntax InvocationExpression { get; } = invocationExpression;

public InvocationExpressionSyntax FluentAssertion { get; set; }
}
19 changes: 11 additions & 8 deletions src/FluentAssertions.Analyzers/Tips/Editing/EditAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,18 @@ namespace FluentAssertions.Analyzers;

public static class EditAction
{
public static Action<DocumentEditor, InvocationExpressionSyntax> RemoveNode(SyntaxNode node)
=> (editor, invocationExpression) => editor.RemoveNode(node);
public static Action<EditActionContext> RemoveNode(SyntaxNode node)
=> context => context.Editor.RemoveNode(node);

public static Action<DocumentEditor, InvocationExpressionSyntax> SubjectShouldAssertion(int argumentIndex, string assertion)
=> (editor, invocationExpression) => new SubjectShouldAssertionAction(argumentIndex, assertion).Apply(editor, invocationExpression);
public static Action<EditActionContext> RemoveInvocationArgument(int argumentIndex)
=> context => context.Editor.RemoveNode(context.InvocationExpression.ArgumentList.Arguments[argumentIndex]);

public static Action<DocumentEditor, InvocationExpressionSyntax> SubjectShouldGenericAssertion(int argumentIndex, string assertion, ImmutableArray<ITypeSymbol> genericTypes)
=> (editor, invocationExpression) => new SubjectShouldGenericAssertionAction(argumentIndex, assertion, genericTypes).Apply(editor, invocationExpression);
public static Action<EditActionContext> SubjectShouldAssertion(int argumentIndex, string assertion)
=> context => new SubjectShouldAssertionAction(argumentIndex, assertion).Apply(context);

public static Action<DocumentEditor, InvocationExpressionSyntax> CreateEquivalencyAssertionOptionsLambda(int optionsIndex)
=> (editor, invocationExpression) => new CreateEquivalencyAssertionOptionsLambdaAction(optionsIndex).Apply(editor, invocationExpression);
public static Action<EditActionContext> SubjectShouldGenericAssertion(int argumentIndex, string assertion, ImmutableArray<ITypeSymbol> genericTypes)
=> context => new SubjectShouldGenericAssertionAction(argumentIndex, assertion, genericTypes).Apply(context);

public static Action<EditActionContext> CreateEquivalencyAssertionOptionsLambda(int optionsIndex)
=> context => new CreateEquivalencyAssertionOptionsLambdaAction(optionsIndex).Apply(context.Editor, context.InvocationExpression);
}
10 changes: 0 additions & 10 deletions src/FluentAssertions.Analyzers/Tips/Editing/RemoveNodeAction.cs

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;

namespace FluentAssertions.Analyzers;

public class SubjectShouldAssertionAction : IEditAction
public class SubjectShouldAssertionAction
{
private readonly int _argumentIndex;
protected readonly string _assertion;
Expand All @@ -15,13 +16,21 @@ public SubjectShouldAssertionAction(int argumentIndex, string assertion)
_assertion = assertion;
}

public void Apply(DocumentEditor editor, InvocationExpressionSyntax invocationExpression)
public void Apply(EditActionContext context)
{
var generator = editor.Generator;
var subject = invocationExpression.ArgumentList.Arguments[_argumentIndex];
var generator = context.Editor.Generator;
var arguments = context.InvocationExpression.ArgumentList.Arguments;

var subject = arguments[_argumentIndex];
var should = generator.InvocationExpression(generator.MemberAccessExpression(subject.Expression, "Should"));
editor.RemoveNode(subject);
editor.ReplaceNode(invocationExpression.Expression, generator.MemberAccessExpression(should, GenerateAssertion(generator)).WithTriviaFrom(invocationExpression.Expression));
context.Editor.RemoveNode(subject);

var memberAccess = (MemberAccessExpressionSyntax) generator.MemberAccessExpression(should, GenerateAssertion(generator)).WithTriviaFrom(context.InvocationExpression.Expression);

context.Editor.ReplaceNode(context.InvocationExpression.Expression, memberAccess);
context.FluentAssertion = context.InvocationExpression
.WithExpression(memberAccess)
.WithArgumentList(SyntaxFactory.ArgumentList(arguments.RemoveAt(_argumentIndex)));
}

protected virtual SyntaxNode GenerateAssertion(SyntaxGenerator generator) => generator.IdentifierName(_assertion);
Expand Down
50 changes: 48 additions & 2 deletions src/FluentAssertions.Analyzers/Tips/XunitCodeFixProvider.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
using System.Collections.Immutable;
using System.Composition;
using System.Runtime.InteropServices.ComTypes;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Operations;
using CreateChangedDocument = System.Func<System.Threading.CancellationToken, System.Threading.Tasks.Task<Microsoft.CodeAnalysis.Document>>;
using SF = Microsoft.CodeAnalysis.CSharp.SyntaxFactory;

namespace FluentAssertions.Analyzers;

Expand Down Expand Up @@ -33,7 +33,8 @@ protected override CreateChangedDocument TryComputeFix(IInvocationOperation invo
{
if (invocation.Arguments[2].Value is ILiteralOperation literal)
{
return literal.ConstantValue.Value switch {
return literal.ConstantValue.Value switch
{
false => DocumentEditorUtils.RenameMethodToSubjectShouldAssertion(invocation, context, "BeEquivalentTo", subjectIndex: 1, argumentsToRemove: literal.IsImplicit ? [] : [2]),
_ => null
};
Expand Down Expand Up @@ -150,7 +151,52 @@ protected override CreateChangedDocument TryComputeFix(IInvocationOperation invo
return DocumentEditorUtils.RenameMethodToSubjectShouldAssertion(invocation, context, "BeInRange", subjectIndex: 0, argumentsToRemove: []);
case "NotInRange" when ArgumentsCount(invocation, 3): // Assert.NotInRange<T>(T actual, T low, T high)
return DocumentEditorUtils.RenameMethodToSubjectShouldAssertion(invocation, context, "NotBeInRange", subjectIndex: 0, argumentsToRemove: []);
case "Throws" when ArgumentsCount(invocation, 1): // Assert.Throws<T>(Action testCode) where T : Exception
return DocumentEditorUtils.RenameGenericMethodToSubjectShouldGenericAssertion(invocation, context, "ThrowExactly", subjectIndex: 0, argumentsToRemove: []);
case "Throws" when ArgumentsAreTypeOf(invocation, t.Type, t.Action): // Assert.Throws(Type exceptionType, Action testCode)
{
if (invocation.Arguments[0].Value is not ITypeOfOperation typeOf)
{
return null; // no fix for this
}

return DocumentEditorUtils.RenameMethodToSubjectShouldGenericAssertion(invocation, ImmutableArray.Create(typeOf.TypeOperand), context, "ThrowExactly", subjectIndex: 1, argumentsToRemove: [0]);
}
case "Throws" when ArgumentsAreTypeOf(invocation, t.String, t.Action): // Assert.Throws(string paramName, Action testCode)
return RewriteThrowArgumentExceptionAssertion("ThrowExactly");
case "ThrowsAsync" when ArgumentsCount(invocation, 1): // Assert.ThrowsAsync<T>(Func<Task> testCode) where T : Exception
return DocumentEditorUtils.RenameGenericMethodToSubjectShouldGenericAssertion(invocation, context, "ThrowExactlyAsync", subjectIndex: 0, argumentsToRemove: []);
case "ThrowsAsync" when ArgumentsAreTypeOf(invocation, t.Type, t.FuncOfTask): // Assert.ThrowsAsync(Type exceptionType, Func<Task> testCode)
{
if (invocation.Arguments[0].Value is not ITypeOfOperation typeOf)
{
return null; // no fix for this
}

return DocumentEditorUtils.RenameMethodToSubjectShouldGenericAssertion(invocation, ImmutableArray.Create(typeOf.TypeOperand), context, "ThrowExactlyAsync", subjectIndex: 1, argumentsToRemove: [0]);
}
case "ThrowsAsync" when ArgumentsAreTypeOf(invocation, t.String, t.FuncOfTask): // Assert.ThrowsAsync(string paramName, Func<Task> testCode)
return RewriteThrowArgumentExceptionAssertion("ThrowExactlyAsync");
case "ThrowsAny" when ArgumentsCount(invocation, 1): // Assert.ThrowsAny<T>(Action testCode) where T : Exception
return DocumentEditorUtils.RenameGenericMethodToSubjectShouldGenericAssertion(invocation, context, "Throw", subjectIndex: 0, argumentsToRemove: []);
case "ThrowsAnyAsync" when ArgumentsCount(invocation, 1): // Assert.ThrowsAnyAsync<T>(Func<Task> testCode) where T : Exception
return DocumentEditorUtils.RenameGenericMethodToSubjectShouldGenericAssertion(invocation, context, "ThrowAsync", subjectIndex: 0, argumentsToRemove: []);
}
return null;

CreateChangedDocument RewriteThrowArgumentExceptionAssertion(string newName)
{
return ctx => DocumentEditorUtils.RewriteExpression(invocation, [
EditAction.SubjectShouldGenericAssertion(argumentIndex: 1, newName, invocation.TargetMethod.TypeArguments),
(editActionContext) =>
{
var generator = editActionContext.Editor.Generator;
var withParameterName = generator.MemberAccessExpression(editActionContext.FluentAssertion.WithArgumentList(SF.ArgumentList()), "WithParameterName");
var chainedAssertion = generator.InvocationExpression(withParameterName, editActionContext.InvocationExpression.ArgumentList.Arguments[0]);
editActionContext.Editor.ReplaceNode(editActionContext.InvocationExpression, chainedAssertion);
}
], context, ctx);
}
}
}

0 comments on commit f7a5c04

Please sign in to comment.