From 7d1c8b29915b0bb1f1e895655e32287439457c6d Mon Sep 17 00:00:00 2001 From: Meir Blachman Date: Mon, 8 Jan 2024 22:31:40 +0200 Subject: [PATCH] feat: Add Using directive for FluentAssertion if missing (#285) --- .../Tips/FluentAssertionsTests.cs | 142 ++++++++++++++++++ .../Tips/MsTestCodeFixProvider.cs | 2 +- .../Tips/TestingFrameworkCodeFixProvider.cs | 46 ++++++ .../Tips/XunitCodeFixProvider.cs | 2 +- 4 files changed, 190 insertions(+), 2 deletions(-) diff --git a/src/FluentAssertions.Analyzers.Tests/Tips/FluentAssertionsTests.cs b/src/FluentAssertions.Analyzers.Tests/Tips/FluentAssertionsTests.cs index 672d4180..7da5f0f6 100644 --- a/src/FluentAssertions.Analyzers.Tests/Tips/FluentAssertionsTests.cs +++ b/src/FluentAssertions.Analyzers.Tests/Tips/FluentAssertionsTests.cs @@ -1,3 +1,4 @@ +using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace FluentAssertions.Analyzers.Tests @@ -55,5 +56,146 @@ public class TestClassB .WithSources(source) ); } + + [TestMethod] + [Implemented] + public void ShouldAddFluentAssertionsUsing_WhenFluentAssertionIsNotInScope_ForXunit() + => ShouldAddFluentAssertionsUsing_WhenFluentAssertionIsNotInScope("True", "using Xunit;", PackageReference.XunitAssert_2_5_1); + + [TestMethod] + [Implemented] + public void ShouldNotAddFluentAssertionsUsing_WhenFluentAssertionIsInGlobalScope_ForXunit() + => ShouldNotAddFluentAssertionsUsing_WhenFluentAssertionIsInGlobalScope("True", "using Xunit;", PackageReference.XunitAssert_2_5_1); + + [TestMethod] + [Implemented] + public void ShouldNotAddFluentAssertionsUsing_WhenFluentAssertionIsInAnyScope_ForXunit() + => ShouldNotAddFluentAssertionsUsing_WhenFluentAssertionIsInAnyScope("True", "using Xunit;", PackageReference.XunitAssert_2_5_1); + + [TestMethod] + [Implemented] + public void ShouldAddFluentAssertionsUsing_WhenFluentAssertionIsNotInScope_ForMsTest() + => ShouldAddFluentAssertionsUsing_WhenFluentAssertionIsNotInScope("IsTrue", "using Microsoft.VisualStudio.TestTools.UnitTesting;", PackageReference.MSTestTestFramework_3_1_1); + + [TestMethod] + [Implemented] + public void ShouldNotAddFluentAssertionsUsing_WhenFluentAssertionIsInGlobalScope_ForMsTest() + => ShouldNotAddFluentAssertionsUsing_WhenFluentAssertionIsInGlobalScope("IsTrue", "using Microsoft.VisualStudio.TestTools.UnitTesting;", PackageReference.MSTestTestFramework_3_1_1); + + [TestMethod] + [Implemented] + public void ShouldNotAddFluentAssertionsUsing_WhenFluentAssertionIsInAnyScope_ForMsTest() + => ShouldNotAddFluentAssertionsUsing_WhenFluentAssertionIsInAnyScope("IsTrue", "using Microsoft.VisualStudio.TestTools.UnitTesting;", PackageReference.MSTestTestFramework_3_1_1); + + private void ShouldAddFluentAssertionsUsing_WhenFluentAssertionIsNotInScope(string assertTrue, string usingDirective, PackageReference testingLibraryReference) where TCodeFixProvider : CodeFixProvider, new() + { + string source = $@" +{usingDirective} +namespace TestProject +{{ + public class TestClass + {{ + public void TestMethod(bool subject) + {{ + Assert.{assertTrue}(subject); + }} + }} +}}"; + string newSource = @$" +using FluentAssertions; +{usingDirective} +namespace TestProject +{{ + public class TestClass + {{ + public void TestMethod(bool subject) + {{ + subject.Should().BeTrue(); + }} + }} +}}"; + DiagnosticVerifier.VerifyFix(new CodeFixVerifierArguments() + .WithDiagnosticAnalyzer() + .WithCodeFixProvider() + .WithPackageReferences(PackageReference.FluentAssertions_6_12_0, testingLibraryReference) + .WithSources(source) + .WithFixedSources(newSource) + ); + } + + private void ShouldNotAddFluentAssertionsUsing_WhenFluentAssertionIsInGlobalScope(string assertTrue, string usingDirective, PackageReference testingLibraryReference) where TCodeFixProvider : CodeFixProvider, new() + { + string source = $@" +{usingDirective} +namespace TestProject +{{ + public class TestClass + {{ + public void TestMethod(bool subject) + {{ + Assert.{assertTrue}(subject); + }} + }} +}}"; + const string globalUsings = "global using FluentAssertions;"; + string newSource = @$" +{usingDirective} +namespace TestProject +{{ + public class TestClass + {{ + public void TestMethod(bool subject) + {{ + subject.Should().BeTrue(); + }} + }} +}}"; + + DiagnosticVerifier.VerifyFix(new CodeFixVerifierArguments() + .WithDiagnosticAnalyzer() + .WithCodeFixProvider() + .WithPackageReferences(PackageReference.FluentAssertions_6_12_0, testingLibraryReference) + .WithSources(source, globalUsings) + .WithFixedSources(newSource) + ); + } + + private void ShouldNotAddFluentAssertionsUsing_WhenFluentAssertionIsInAnyScope(string assertTrue, string usingDirective, PackageReference testingLibraryReference) where TCodeFixProvider : CodeFixProvider, new() + { + string source = $@" +{usingDirective} +namespace TestProject +{{ + using FluentAssertions; + public class TestClass + {{ + public void TestMethod(bool subject) + {{ + Assert.{assertTrue}(subject); + }} + }} +}}"; + string newSource = @$" +{usingDirective} +namespace TestProject +{{ + using FluentAssertions; + public class TestClass + {{ + public void TestMethod(bool subject) + {{ + subject.Should().BeTrue(); + }} + }} +}}"; + + DiagnosticVerifier.VerifyFix(new CodeFixVerifierArguments() + .WithDiagnosticAnalyzer() + .WithCodeFixProvider() + .WithPackageReferences(PackageReference.FluentAssertions_6_12_0, testingLibraryReference) + .WithSources(source) + .WithFixedSources(newSource) + ); + } } } \ No newline at end of file diff --git a/src/FluentAssertions.Analyzers/Tips/MsTestCodeFixProvider.cs b/src/FluentAssertions.Analyzers/Tips/MsTestCodeFixProvider.cs index 9f5bf0a9..a9cf92a8 100644 --- a/src/FluentAssertions.Analyzers/Tips/MsTestCodeFixProvider.cs +++ b/src/FluentAssertions.Analyzers/Tips/MsTestCodeFixProvider.cs @@ -12,7 +12,7 @@ public class MsTestCodeFixProvider : TestingFrameworkCodeFixProvider { public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(AssertAnalyzer.MSTestsRule.Id); - protected override CreateChangedDocument TryComputeFix(IInvocationOperation invocation, CodeFixContext context, TestingFrameworkCodeFixContext testContext, Diagnostic diagnostic) + protected override CreateChangedDocument TryComputeFixCore(IInvocationOperation invocation, CodeFixContext context, TestingFrameworkCodeFixContext testContext, Diagnostic diagnostic) { var assertType = invocation.TargetMethod.ContainingType; return assertType.Name switch diff --git a/src/FluentAssertions.Analyzers/Tips/TestingFrameworkCodeFixProvider.cs b/src/FluentAssertions.Analyzers/Tips/TestingFrameworkCodeFixProvider.cs index 74082bf4..e05170df 100644 --- a/src/FluentAssertions.Analyzers/Tips/TestingFrameworkCodeFixProvider.cs +++ b/src/FluentAssertions.Analyzers/Tips/TestingFrameworkCodeFixProvider.cs @@ -1,6 +1,17 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; using FluentAssertions.Analyzers.Utilities; using Microsoft.CodeAnalysis; +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; @@ -71,6 +82,41 @@ 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 sealed class TestingFrameworkCodeFixContext(Compilation compilation) { public INamedTypeSymbol Object { get; } = compilation.ObjectType; diff --git a/src/FluentAssertions.Analyzers/Tips/XunitCodeFixProvider.cs b/src/FluentAssertions.Analyzers/Tips/XunitCodeFixProvider.cs index 19032fdb..1a00d0b0 100644 --- a/src/FluentAssertions.Analyzers/Tips/XunitCodeFixProvider.cs +++ b/src/FluentAssertions.Analyzers/Tips/XunitCodeFixProvider.cs @@ -13,7 +13,7 @@ public class XunitCodeFixProvider : TestingFrameworkCodeFixProvider { public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(AssertAnalyzer.XunitRule.Id); - protected override CreateChangedDocument TryComputeFix(IInvocationOperation invocation, CodeFixContext context, TestingFrameworkCodeFixContext t, Diagnostic diagnostic) + protected override CreateChangedDocument TryComputeFixCore(IInvocationOperation invocation, CodeFixContext context, TestingFrameworkCodeFixContext t, Diagnostic diagnostic) { switch (invocation.TargetMethod.Name) {