Skip to content

Commit

Permalink
Add suppressor for CA1515 (#182)
Browse files Browse the repository at this point in the history
  • Loading branch information
CollinAlpert authored Mar 2, 2024
1 parent e622842 commit 6d907a0
Show file tree
Hide file tree
Showing 10 changed files with 194 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -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<Xunit.Suppressors.MakeTypesInternalSuppressor>;

#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<DiagnosticResult>();
#endif

await Verify.VerifySuppressor(code, CodeAnalysisNetAnalyzers.CA1515(), expected);
}
}

#endif
46 changes: 46 additions & 0 deletions src/xunit.analyzers.tests/Utility/CodeAnalysisNetAnalyzers.cs
Original file line number Diff line number Diff line change
@@ -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> assembly = new(LoadAssembly, isThreadSafe: true);
public static Lazy<Type> 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
4 changes: 4 additions & 0 deletions src/xunit.analyzers.tests/xunit.analyzers.tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
<PackageDownload Include="xunit.v3.common" Version="[0.1.1-pre.350]" />
<PackageDownload Include="xunit.v3.extensibility.core" Version="[0.1.1-pre.350]" />
<PackageDownload Include="xunit.v3.runner.utility" Version="[0.1.1-pre.350]" />

<!-- Download packages referenced by CodeAnalysisNetAnalyzers -->
<PackageDownload Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="[9.0.0-preview.24072.1]" />
<PackageDownload Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="[3.11.0]" />
</ItemGroup>

<ItemGroup>
Expand Down
31 changes: 31 additions & 0 deletions src/xunit.analyzers/Suppressors/MakeTypesInternalSuppressor.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
39 changes: 36 additions & 3 deletions src/xunit.analyzers/Utility/CodeAnalysisExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IMethodSymbol>()
.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)
Expand Down
4 changes: 4 additions & 0 deletions src/xunit.analyzers/Utility/Descriptors.Suppressors.cs
Original file line number Diff line number Diff line change
@@ -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.");
}
3 changes: 2 additions & 1 deletion src/xunit.analyzers/Utility/XunitDiagnosticSuppressor.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Xunit.Analyzers;

namespace Xunit.Analyzers;
namespace Xunit.Suppressors;

/// <summary>
/// Base class for diagnostic suppressors which support xUnit.net v2 and v3.
Expand Down
3 changes: 2 additions & 1 deletion src/xunit.analyzers/Utility/XunitV2DiagnosticSuppressor.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Microsoft.CodeAnalysis;
using Xunit.Analyzers;

namespace Xunit.Analyzers;
namespace Xunit.Suppressors;

/// <summary>
/// Base class for diagnostic suppressors which support xUnit.net v2 only.
Expand Down
3 changes: 2 additions & 1 deletion src/xunit.analyzers/Utility/XunitV3DiagnosticSuppressor.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Microsoft.CodeAnalysis;
using Xunit.Analyzers;

namespace Xunit.Analyzers;
namespace Xunit.Suppressors;

/// <summary>
/// Base class for diagnostic suppressors which support xUnit.net v3 only.
Expand Down
2 changes: 1 addition & 1 deletion src/xunit.analyzers/X1000/EnsureFixturesHaveASource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 6d907a0

Please sign in to comment.