From 6d907a090c914931b546d4c1e7531c1ac8bfcb52 Mon Sep 17 00:00:00 2001 From: Collin Alpert Date: Sat, 2 Mar 2024 20:52:13 +0100 Subject: [PATCH] Add suppressor for CA1515 (#182) --- .../MakeTypesInternalSuppressorTests.cs | 66 +++++++++++++++++++ .../Utility/CodeAnalysisNetAnalyzers.cs | 46 +++++++++++++ .../xunit.analyzers.tests.csproj | 4 ++ .../MakeTypesInternalSuppressor.cs | 31 +++++++++ .../Utility/CodeAnalysisExtensions.cs | 39 ++++++++++- .../Utility/Descriptors.Suppressors.cs | 4 ++ .../Utility/XunitDiagnosticSuppressor.cs | 3 +- .../Utility/XunitV2DiagnosticSuppressor.cs | 3 +- .../Utility/XunitV3DiagnosticSuppressor.cs | 3 +- .../X1000/EnsureFixturesHaveASource.cs | 2 +- 10 files changed, 194 insertions(+), 7 deletions(-) create mode 100644 src/xunit.analyzers.tests/Suppressors/MakeTypesInternalSuppressorTests.cs create mode 100644 src/xunit.analyzers.tests/Utility/CodeAnalysisNetAnalyzers.cs create mode 100644 src/xunit.analyzers/Suppressors/MakeTypesInternalSuppressor.cs diff --git a/src/xunit.analyzers.tests/Suppressors/MakeTypesInternalSuppressorTests.cs b/src/xunit.analyzers.tests/Suppressors/MakeTypesInternalSuppressorTests.cs new file mode 100644 index 00000000..a510bf27 --- /dev/null +++ b/src/xunit.analyzers.tests/Suppressors/MakeTypesInternalSuppressorTests.cs @@ -0,0 +1,66 @@ +#if NETCOREAPP + +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Testing; +using Xunit; +using Xunit.Analyzers; +using Verify = CSharpVerifier; + +#if !ROSLYN_3_11 +using System; +#endif + +public sealed class MakeTypesInternalSuppressorTests +{ + [Fact] + public async Task NonTestClass_DoesNotSuppress() + { + var code = @" +public class NonTestClass { + public void NonTestMethod() { } +}"; + + var expected = + new DiagnosticResult("CA1515", DiagnosticSeverity.Warning) + .WithSpan(2, 14, 2, 26) + .WithIsSuppressed(false); + + await Verify.VerifySuppressor(code, CodeAnalysisNetAnalyzers.CA1515(), expected); + } + + [Theory] + [InlineData("Fact")] + [InlineData("FactAttribute")] + [InlineData("Theory")] + [InlineData("TheoryAttribute")] + [InlineData("CustomFact")] + [InlineData("CustomFactAttribute")] + public async Task TestClass_Suppresses(string attribute) + { + var code = @$" +using Xunit; + +internal class CustomFactAttribute : FactAttribute {{ }} + +public class TestClass {{ + [{attribute}] + public void TestMethod() {{ }} +}}"; + +#if ROSLYN_3_11 + // Roslyn 3.11 still surfaces the diagnostic that has been suppressed + var expected = + new DiagnosticResult("CA1515", DiagnosticSeverity.Warning) + .WithSpan(6, 14, 6, 23) + .WithIsSuppressed(true); +#else + // Later versions of Roslyn just removes it from the results + var expected = Array.Empty(); +#endif + + await Verify.VerifySuppressor(code, CodeAnalysisNetAnalyzers.CA1515(), expected); + } +} + +#endif diff --git a/src/xunit.analyzers.tests/Utility/CodeAnalysisNetAnalyzers.cs b/src/xunit.analyzers.tests/Utility/CodeAnalysisNetAnalyzers.cs new file mode 100644 index 00000000..4095934c --- /dev/null +++ b/src/xunit.analyzers.tests/Utility/CodeAnalysisNetAnalyzers.cs @@ -0,0 +1,46 @@ +#if NETCOREAPP + +using System; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Runtime.Loader; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Xunit.Analyzers; + +public static class CodeAnalysisNetAnalyzers +{ + public static Lazy assembly = new(LoadAssembly, isThreadSafe: true); + public static Lazy typeCA1515 = new(() => FindType("Microsoft.CodeQuality.CSharp.Analyzers.Maintainability.CSharpMakeTypesInternal"), isThreadSafe: true); + + public static DiagnosticAnalyzer CA1515() => + Activator.CreateInstance(typeCA1515.Value) as DiagnosticAnalyzer ?? throw new InvalidOperationException($"Could not create instance of '{typeCA1515.Value.FullName}'"); + + static Type FindType(string typeName) => + assembly.Value.GetType(typeName) ?? throw new InvalidOperationException($"Could not locate type '{typeName}' from Microsoft.CodeAnalysis.NetAnalyzers"); + + static Assembly LoadAssembly() + { + var nugetPackagesFolder = Environment.GetEnvironmentVariable("NUGET_PACKAGES"); + if (nugetPackagesFolder is null) + { + var homeFolder = + RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? Environment.GetEnvironmentVariable("USERPROFILE") + : Environment.GetEnvironmentVariable("HOME"); + + nugetPackagesFolder = Path.Combine(homeFolder ?? throw new InvalidOperationException("Could not determine home folder"), ".nuget", "packages"); + } + + if (!Directory.Exists(nugetPackagesFolder)) + throw new InvalidOperationException($"NuGet package cache folder '{nugetPackagesFolder}' does not exist"); + + var loadContext = AssemblyLoadContext.Default; + loadContext.LoadFromAssemblyPath(Path.Combine(nugetPackagesFolder, "microsoft.codeanalysis.workspaces.common", "3.11.0", "lib", "netcoreapp3.1", "Microsoft.CodeAnalysis.Workspaces.dll")); + loadContext.LoadFromAssemblyPath(Path.Combine(nugetPackagesFolder, "microsoft.codeanalysis.netanalyzers", "9.0.0-preview.24072.1", "analyzers", "dotnet", "cs", "Microsoft.CodeAnalysis.NetAnalyzers.dll")); + return loadContext.LoadFromAssemblyPath(Path.Combine(nugetPackagesFolder, "microsoft.codeanalysis.netanalyzers", "9.0.0-preview.24072.1", "analyzers", "dotnet", "cs", "Microsoft.CodeAnalysis.CSharp.NetAnalyzers.dll")); + } +} + +#endif diff --git a/src/xunit.analyzers.tests/xunit.analyzers.tests.csproj b/src/xunit.analyzers.tests/xunit.analyzers.tests.csproj index ba25247b..a6d47caf 100644 --- a/src/xunit.analyzers.tests/xunit.analyzers.tests.csproj +++ b/src/xunit.analyzers.tests/xunit.analyzers.tests.csproj @@ -18,6 +18,10 @@ + + + + diff --git a/src/xunit.analyzers/Suppressors/MakeTypesInternalSuppressor.cs b/src/xunit.analyzers/Suppressors/MakeTypesInternalSuppressor.cs new file mode 100644 index 00000000..0af56de3 --- /dev/null +++ b/src/xunit.analyzers/Suppressors/MakeTypesInternalSuppressor.cs @@ -0,0 +1,31 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Xunit.Analyzers; + +namespace Xunit.Suppressors; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class MakeTypesInternalSuppressor : XunitDiagnosticSuppressor +{ + public MakeTypesInternalSuppressor() : + base(Descriptors.CA1515_Suppression) + { } + + protected override bool ShouldSuppress( + Diagnostic diagnostic, + SuppressionAnalysisContext context, + XunitContext xunitContext) + { + if (diagnostic.Location.SourceTree is null) + return false; + + var root = diagnostic.Location.SourceTree.GetRoot(context.CancellationToken); + if (root?.FindNode(diagnostic.Location.SourceSpan) is not ClassDeclarationSyntax classDeclaration) + return false; + + var semanticModel = context.GetSemanticModel(diagnostic.Location.SourceTree); + var classSymbol = semanticModel.GetDeclaredSymbol(classDeclaration) as ITypeSymbol; + return classSymbol.IsTestClass(xunitContext, strict: false); + } +} diff --git a/src/xunit.analyzers/Utility/CodeAnalysisExtensions.cs b/src/xunit.analyzers/Utility/CodeAnalysisExtensions.cs index 705289aa..c98f048d 100644 --- a/src/xunit.analyzers/Utility/CodeAnalysisExtensions.cs +++ b/src/xunit.analyzers/Utility/CodeAnalysisExtensions.cs @@ -73,12 +73,45 @@ public static bool IsInTestMethod( } public static bool IsTestClass( - this ITypeSymbol type, - XunitContext xunitContext) + this ITypeSymbol? type, + XunitContext xunitContext, + bool strict) { - Guard.ArgumentNotNull(type); Guard.ArgumentNotNull(xunitContext); + if (type is null) + return false; + + if (strict) + return IsTestClassStrict(type, xunitContext); + else + return IsTestClassNonStrict(type, xunitContext); + } + + static bool IsTestClassNonStrict( + ITypeSymbol type, + XunitContext xunitContext) + { + var factAttributeType = xunitContext.Core.FactAttributeType; + if (factAttributeType is null) + return false; + + return + type + .GetMembers() + .OfType() + .Any(method => + method + .GetAttributes() + .Select(a => a.AttributeClass) + .Any(t => factAttributeType.IsAssignableFrom(t)) + ); + } + + static bool IsTestClassStrict( + ITypeSymbol type, + XunitContext xunitContext) + { var factAttributeType = xunitContext.Core.FactAttributeType; var theoryAttributeType = xunitContext.Core.TheoryAttributeType; if (factAttributeType is null || theoryAttributeType is null) diff --git a/src/xunit.analyzers/Utility/Descriptors.Suppressors.cs b/src/xunit.analyzers/Utility/Descriptors.Suppressors.cs index 1ec0d740..04fed0eb 100644 --- a/src/xunit.analyzers/Utility/Descriptors.Suppressors.cs +++ b/src/xunit.analyzers/Utility/Descriptors.Suppressors.cs @@ -1,5 +1,9 @@ +using Microsoft.CodeAnalysis; + namespace Xunit.Analyzers; public static partial class Descriptors { + public static SuppressionDescriptor CA1515_Suppression { get; } = + Suppression("CA1515", "xUnit.net's test classes must be public."); } diff --git a/src/xunit.analyzers/Utility/XunitDiagnosticSuppressor.cs b/src/xunit.analyzers/Utility/XunitDiagnosticSuppressor.cs index 08bf80d1..e8610980 100644 --- a/src/xunit.analyzers/Utility/XunitDiagnosticSuppressor.cs +++ b/src/xunit.analyzers/Utility/XunitDiagnosticSuppressor.cs @@ -1,8 +1,9 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; +using Xunit.Analyzers; -namespace Xunit.Analyzers; +namespace Xunit.Suppressors; /// /// Base class for diagnostic suppressors which support xUnit.net v2 and v3. diff --git a/src/xunit.analyzers/Utility/XunitV2DiagnosticSuppressor.cs b/src/xunit.analyzers/Utility/XunitV2DiagnosticSuppressor.cs index 4ed60126..982ff0cf 100644 --- a/src/xunit.analyzers/Utility/XunitV2DiagnosticSuppressor.cs +++ b/src/xunit.analyzers/Utility/XunitV2DiagnosticSuppressor.cs @@ -1,6 +1,7 @@ using Microsoft.CodeAnalysis; +using Xunit.Analyzers; -namespace Xunit.Analyzers; +namespace Xunit.Suppressors; /// /// Base class for diagnostic suppressors which support xUnit.net v2 only. diff --git a/src/xunit.analyzers/Utility/XunitV3DiagnosticSuppressor.cs b/src/xunit.analyzers/Utility/XunitV3DiagnosticSuppressor.cs index 581b81ef..8ee05603 100644 --- a/src/xunit.analyzers/Utility/XunitV3DiagnosticSuppressor.cs +++ b/src/xunit.analyzers/Utility/XunitV3DiagnosticSuppressor.cs @@ -1,6 +1,7 @@ using Microsoft.CodeAnalysis; +using Xunit.Analyzers; -namespace Xunit.Analyzers; +namespace Xunit.Suppressors; /// /// Base class for diagnostic suppressors which support xUnit.net v3 only. diff --git a/src/xunit.analyzers/X1000/EnsureFixturesHaveASource.cs b/src/xunit.analyzers/X1000/EnsureFixturesHaveASource.cs index b17f2cc7..d26dba3c 100644 --- a/src/xunit.analyzers/X1000/EnsureFixturesHaveASource.cs +++ b/src/xunit.analyzers/X1000/EnsureFixturesHaveASource.cs @@ -30,7 +30,7 @@ public override void AnalyzeCompilation( return; if (namedType.IsAbstract) return; - if (!namedType.IsTestClass(xunitContext)) + if (!namedType.IsTestClass(xunitContext, strict: true)) return; // Only evaluate if there's a single public constructor