Skip to content
Merged
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
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);
}
}
}