diff --git a/docs/NunitAnalyzer.md b/docs/NunitAnalyzer.md index 877fa0e1..744effb9 100644 --- a/docs/NunitAnalyzer.md +++ b/docs/NunitAnalyzer.md @@ -23,6 +23,9 @@ var flag = true; // old assertion: Assert.IsTrue(flag); Assert.True(flag); +Assert.That(flag); +Assert.That(flag, Is.True); +Assert.That(flag, Is.Not.False); // new assertion: flag.Should().BeTrue(); @@ -40,6 +43,15 @@ Assert.True(flag); /* fail message: Expected: True Assert.IsTrue(flag); /* fail message: Expected: True But was: False */ +Assert.That(flag); /* fail message: Expected: True + But was: False + */ +Assert.That(flag, Is.True); /* fail message: Expected: True + But was: False + */ +Assert.That(flag, Is.Not.False); /* fail message: Expected: not False + But was: False + */ // new assertion: flag.Should().BeTrue(); /* fail message: Expected flag to be true, but found False. */ @@ -54,6 +66,8 @@ var flag = false; // old assertion: Assert.IsFalse(flag); Assert.False(flag); +Assert.That(flag, Is.False); +Assert.That(flag, Is.Not.True); // new assertion: flag.Should().BeFalse(); @@ -71,6 +85,12 @@ Assert.False(flag); /* fail message: Expected: False Assert.IsFalse(flag); /* fail message: Expected: False But was: True */ +Assert.That(flag, Is.False); /* fail message: Expected: False + But was: True + */ +Assert.That(flag, Is.Not.True); /* fail message: Expected: not True + But was: True + */ // new assertion: flag.Should().BeFalse(); /* fail message: Expected flag to be false, but found True. */ diff --git a/src/FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs/NunitAnalyzerTests.cs b/src/FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs/NunitAnalyzerTests.cs index f6de1159..0b64175c 100644 --- a/src/FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs/NunitAnalyzerTests.cs +++ b/src/FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs/NunitAnalyzerTests.cs @@ -1,7 +1,8 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Assert = NUnit.Framework.Assert; -using FluentAssertions; using System.Collections.Generic; +using NUnit.Framework; +using FluentAssertions; namespace FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs; @@ -17,6 +18,9 @@ public void BooleanAssertIsTrue() // old assertion: Assert.IsTrue(flag); Assert.True(flag); + Assert.That(flag); + Assert.That(flag, Is.True); + Assert.That(flag, Is.Not.False); // new assertion: flag.Should().BeTrue(); @@ -42,6 +46,36 @@ public void BooleanAssertIsTrue_Failure_OldAssertion_1() Assert.IsTrue(flag); } + [TestMethod, ExpectedTestFrameworkException] + public void BooleanAssertIsTrue_Failure_OldAssertion_2() + { + // arrange + var flag = false; + + // old assertion: + Assert.That(flag); + } + + [TestMethod, ExpectedTestFrameworkException] + public void BooleanAssertIsTrue_Failure_OldAssertion_3() + { + // arrange + var flag = false; + + // old assertion: + Assert.That(flag, Is.True); + } + + [TestMethod, ExpectedTestFrameworkException] + public void BooleanAssertIsTrue_Failure_OldAssertion_4() + { + // arrange + var flag = false; + + // old assertion: + Assert.That(flag, Is.Not.False); + } + [TestMethod, ExpectedTestFrameworkException] public void BooleanAssertIsTrue_Failure_NewAssertion() { @@ -61,6 +95,8 @@ public void BooleanAssertIsFalse() // old assertion: Assert.IsFalse(flag); Assert.False(flag); + Assert.That(flag, Is.False); + Assert.That(flag, Is.Not.True); // new assertion: flag.Should().BeFalse(); @@ -86,6 +122,26 @@ public void BooleanAssertIsFalse_Failure_OldAssertion_1() Assert.IsFalse(flag); } + [TestMethod, ExpectedTestFrameworkException] + public void BooleanAssertIsFalse_Failure_OldAssertion_2() + { + // arrange + var flag = true; + + // old assertion: + Assert.That(flag, Is.False); + } + + [TestMethod, ExpectedTestFrameworkException] + public void BooleanAssertIsFalse_Failure_OldAssertion_3() + { + // arrange + var flag = true; + + // old assertion: + Assert.That(flag, Is.Not.True); + } + [TestMethod, ExpectedTestFrameworkException] public void BooleanAssertIsFalse_Failure_NewAssertion() { diff --git a/src/FluentAssertions.Analyzers.Tests/Tips/NunitTests.cs b/src/FluentAssertions.Analyzers.Tests/Tips/NunitTests.cs index a5638929..5b46cb02 100644 --- a/src/FluentAssertions.Analyzers.Tests/Tips/NunitTests.cs +++ b/src/FluentAssertions.Analyzers.Tests/Tips/NunitTests.cs @@ -15,6 +15,9 @@ public class NunitTests [AssertionDiagnostic("Assert.True(bool.Parse(\"true\"){0});")] [AssertionDiagnostic("Assert.IsTrue(actual{0});")] [AssertionDiagnostic("Assert.IsTrue(bool.Parse(\"true\"){0});")] + [AssertionDiagnostic("Assert.That(actual{0});")] + [AssertionDiagnostic("Assert.That(actual, Is.True{0});")] + [AssertionDiagnostic("Assert.That(actual, Is.Not.False{0});")] [Implemented] public void Nunit3_AssertTrue_TestAnalyzer(string assertion) => Nunit3VerifyDiagnostic("bool actual", assertion); @@ -51,6 +54,15 @@ public class NunitTests [AssertionCodeFix( oldAssertion: "Assert.IsTrue(actual == false{0});", newAssertion: "(actual == false).Should().BeTrue({0});")] + [AssertionCodeFix( + oldAssertion: "Assert.That(actual{0});", + newAssertion: "actual.Should().BeTrue({0});")] + [AssertionCodeFix( + oldAssertion: "Assert.That(actual, Is.True{0});", + newAssertion: "actual.Should().BeTrue({0});")] + [AssertionCodeFix( + oldAssertion: "Assert.That(actual, Is.Not.False{0});", + newAssertion: "actual.Should().BeTrue({0});")] [Implemented] public void Nunit3_AssertTrue_TestCodeFix(string oldAssertion, string newAssertion) => Nunit3VerifyFix("bool actual", oldAssertion, newAssertion); diff --git a/src/FluentAssertions.Analyzers/Tips/NunitCodeFixProvider.cs b/src/FluentAssertions.Analyzers/Tips/NunitCodeFixProvider.cs index 8caf8e29..a683a688 100644 --- a/src/FluentAssertions.Analyzers/Tips/NunitCodeFixProvider.cs +++ b/src/FluentAssertions.Analyzers/Tips/NunitCodeFixProvider.cs @@ -11,10 +11,11 @@ namespace FluentAssertions.Analyzers; [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(NunitCodeFixProvider)), Shared] -public class NunitCodeFixProvider : TestingFrameworkCodeFixProvider +public class NunitCodeFixProvider : TestingFrameworkCodeFixProvider { public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(AssertAnalyzer.NUnitRule.Id); - protected override CreateChangedDocument TryComputeFixCore(IInvocationOperation invocation, CodeFixContext context, TestingFrameworkCodeFixContext t, Diagnostic diagnostic) + protected override NunitCodeFixContext CreateTestContext(SemanticModel semanticModel) => new(semanticModel.Compilation); + protected override CreateChangedDocument TryComputeFixCore(IInvocationOperation invocation, CodeFixContext context, NunitCodeFixContext t, Diagnostic diagnostic) { var assertType = invocation.TargetMethod.ContainingType; var nunitVersion = assertType.ContainingAssembly.Identity.Version; @@ -24,6 +25,7 @@ protected override CreateChangedDocument TryComputeFixCore(IInvocationOperation return assertType.Name switch { + "Assert" when invocation.TargetMethod.Name is "That" => TryComputeFixForNunitThat(invocation, context, t), "Assert" when isNunit3 => TryComputeFixForNunitClassicAssert(invocation, context, t), "ClassicAssert" when isNunit4 => TryComputeFixForNunitClassicAssert(invocation, context, t), //"StringAssert" => TryComputeFixForStringAssert(invocation, context, testContext), @@ -32,7 +34,7 @@ protected override CreateChangedDocument TryComputeFixCore(IInvocationOperation }; } - private CreateChangedDocument TryComputeFixForNunitClassicAssert(IInvocationOperation invocation, CodeFixContext context, TestingFrameworkCodeFixContext t) + private CreateChangedDocument TryComputeFixForNunitClassicAssert(IInvocationOperation invocation, CodeFixContext context, NunitCodeFixContext t) { switch (invocation.TargetMethod.Name) { @@ -228,4 +230,41 @@ private CreateChangedDocument TryComputeFixForNunitClassicAssert(IInvocationOper } return null; } + + private CreateChangedDocument TryComputeFixForNunitThat(IInvocationOperation invocation, CodeFixContext context, NunitCodeFixContext t) + { + if (invocation.Arguments.Length is 1 && invocation.Arguments[0].Value.Type.EqualsSymbol(t.Boolean) // Assert.That(subject) + || invocation.Arguments.Length > 2 && invocation.Arguments[0].Value.Type.EqualsSymbol(t.Boolean) && invocation.Arguments[1].Value.Type.EqualsSymbol(t.String)) // Assert.That(subject, message) + { + return DocumentEditorUtils.RenameMethodToSubjectShouldAssertion(invocation, context, "BeTrue", subjectIndex: 0, argumentsToRemove: []); + } + + if (invocation.Arguments[1].Value.UnwrapConversion() is not IPropertyReferenceOperation constraint) return null; + + switch (constraint.Property.Name) + { + case "True" when constraint.Property.ContainingType.EqualsSymbol(t.Is): // Assert.That(subject, Is.True) + case "False" when constraint.Instance is IPropertyReferenceOperation { Property.Name: "Not" } chainedReference && PropertyReferencedFromType(chainedReference, t.Is): // Assert.That(subject, Is.Not.False) + return DocumentEditorUtils.RenameMethodToSubjectShouldAssertion(invocation, context, "BeTrue", subjectIndex: 0, argumentsToRemove: [1]); + case "True" when constraint.Instance is IPropertyReferenceOperation { Property.Name: "Not" } chainedReference && PropertyReferencedFromType(chainedReference, t.Is): // Assert.That(subject, Is.Not.True) + case "False" when PropertyReferencedFromType(constraint, t.Is): // Assert.That(subject, Is.False) + return DocumentEditorUtils.RenameMethodToSubjectShouldAssertion(invocation, context, "BeFalse", subjectIndex: 0, argumentsToRemove: [1]); + + default: + return null; + } + + } + + private static bool PropertyReferencedFromType(IPropertyReferenceOperation propertyReference, INamedTypeSymbol type) => propertyReference.Property.ContainingType.EqualsSymbol(type); + + public class NunitCodeFixContext(Compilation compilation) : TestingFrameworkCodeFixProvider.TestingFrameworkCodeFixContext(compilation) + { + public INamedTypeSymbol Is { get; } = compilation.GetTypeByMetadataName("NUnit.Framework.Is"); + public INamedTypeSymbol Has { get; } = compilation.GetTypeByMetadataName("NUnit.Framework.Has"); + public INamedTypeSymbol Does { get; } = compilation.GetTypeByMetadataName("NUnit.Framework.Does"); + public INamedTypeSymbol Contains { get; } = compilation.GetTypeByMetadataName("NUnit.Framework.Contains"); + public INamedTypeSymbol Throws { get; } = compilation.GetTypeByMetadataName("NUnit.Framework.Throws"); + public INamedTypeSymbol ConstraintExpression { get; } = compilation.GetTypeByMetadataName("NUnit.Framework.Constraints.ConstraintExpression"); + } } \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/TestingFrameworkCodeFixProvider.cs b/src/FluentAssertions.Analyzers/Tips/TestingFrameworkCodeFixProvider.cs index ec314c92..91eae715 100644 --- a/src/FluentAssertions.Analyzers/Tips/TestingFrameworkCodeFixProvider.cs +++ b/src/FluentAssertions.Analyzers/Tips/TestingFrameworkCodeFixProvider.cs @@ -7,19 +7,49 @@ using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Operations; -using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Simplification; namespace FluentAssertions.Analyzers; -public abstract class TestingFrameworkCodeFixProvider : CodeFixProviderBase +public abstract class TestingFrameworkCodeFixProvider : CodeFixProviderBase where TTestContext : TestingFrameworkCodeFixProvider.TestingFrameworkCodeFixContext { protected override string Title => "Replace with FluentAssertions"; - protected override TestingFrameworkCodeFixContext CreateTestContext(SemanticModel semanticModel) => new TestingFrameworkCodeFixContext(semanticModel.Compilation); + protected override Func> TryComputeFix(IInvocationOperation invocation, CodeFixContext context, TTestContext t, Diagnostic diagnostic) + { + var fix = TryComputeFixCore(invocation, context, t, diagnostic); + if (fix is null) + { + return null; + } + + return async ctx => + { + const string fluentAssertionNamespace = "FluentAssertions"; + var document = await fix(ctx); + + var model = await document.GetSemanticModelAsync(); + var scopes = model.GetImportScopes(diagnostic.Location.SourceSpan.Start); + + var hasFluentAssertionImport = scopes.Any(scope => scope.Imports.Any(import => import.NamespaceOrType.ToString().Equals(fluentAssertionNamespace))); + if (hasFluentAssertionImport) + { + return document; + } + + var root = (CompilationUnitSyntax)await document.GetSyntaxRootAsync(); + root = root.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(fluentAssertionNamespace)).WithAdditionalAnnotations(Simplifier.AddImportsAnnotation)); + + document = document.WithSyntaxRoot(root); + document = await Formatter.OrganizeImportsAsync(document); + + return document; + }; + } + + protected abstract Func> TryComputeFixCore(IInvocationOperation invocation, CodeFixContext context, TTestContext t, Diagnostic diagnostic); protected static bool ArgumentsAreTypeOf(IInvocationOperation invocation, params ITypeSymbol[] types) => ArgumentsAreTypeOf(invocation, 0, types); protected static bool ArgumentsAreTypeOf(IInvocationOperation invocation, int startFromIndex, params ITypeSymbol[] types) @@ -82,42 +112,14 @@ protected static bool ArgumentsCount(IInvocationOperation invocation, int argume return invocation.TargetMethod.Parameters.Length == arguments; } - protected override Func> TryComputeFix(IInvocationOperation invocation, CodeFixContext context, TestingFrameworkCodeFixContext t, Diagnostic diagnostic) - { - var fix = TryComputeFixCore(invocation, context, t, diagnostic); - if (fix is null) - { - return null; - } - - return async ctx => - { - const string fluentAssertionNamespace = "FluentAssertions"; - var document = await fix(ctx); - - var model = await document.GetSemanticModelAsync(); - var scopes = model.GetImportScopes(diagnostic.Location.SourceSpan.Start); - - var hasFluentAssertionImport = scopes.Any(scope => scope.Imports.Any(import => import.NamespaceOrType.ToString().Equals(fluentAssertionNamespace))); - if (hasFluentAssertionImport) - { - return document; - } - - var root = (CompilationUnitSyntax) await document.GetSyntaxRootAsync(); - root = root.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(fluentAssertionNamespace)).WithAdditionalAnnotations(Simplifier.AddImportsAnnotation)); +} - document = document.WithSyntaxRoot(root); - document = await Formatter.OrganizeImportsAsync(document); - - return document; - }; - } - - protected abstract Func> TryComputeFixCore(IInvocationOperation invocation, CodeFixContext context, TestingFrameworkCodeFixContext t, Diagnostic diagnostic); +public abstract class TestingFrameworkCodeFixProvider : TestingFrameworkCodeFixProvider +{ + protected override TestingFrameworkCodeFixContext CreateTestContext(SemanticModel semanticModel) => new(semanticModel.Compilation); - public sealed class TestingFrameworkCodeFixContext(Compilation compilation) + public class TestingFrameworkCodeFixContext(Compilation compilation) { public INamedTypeSymbol Object { get; } = compilation.ObjectType; public INamedTypeSymbol String { get; } = compilation.GetTypeByMetadataName("System.String");