Skip to content

Commit

Permalink
test fields should be readonly
Browse files Browse the repository at this point in the history
  • Loading branch information
fakefeik committed Jun 14, 2024
1 parent 2d3c2df commit 29c23bf
Show file tree
Hide file tree
Showing 11 changed files with 244 additions and 0 deletions.
7 changes: 7 additions & 0 deletions NUnit.Analyzers/Constants/AnalyzerIdentifiers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace NUnit.Analyzers.Constants
{
internal static class AnalyzerIdentifiers
{
internal const string TestFieldIsNotReadonly = "NU0001";
}
}
7 changes: 7 additions & 0 deletions NUnit.Analyzers/Constants/Categories.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace NUnit.Analyzers.Constants
{
internal static class Categories
{
internal const string ParallelExecution = nameof(ParallelExecution);
}
}
13 changes: 13 additions & 0 deletions NUnit.Analyzers/Constants/NUnitFrameworkConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace NUnit.Analyzers.Constants
{
public static class NUnitFrameworkConstants
{
public const string FullNameOfTypeITestBuilder = "NUnit.Framework.Interfaces.ITestBuilder";
public const string FullNameOfTypeISimpleTestBuilder = "NUnit.Framework.Interfaces.ISimpleTestBuilder";

public const string FullNameOfTypeOneTimeSetUpAttribute = "NUnit.Framework.OneTimeSetUpAttribute";
public const string FullNameOfTypeOneTimeTearDownAttribute = "NUnit.Framework.OneTimeTearDownAttribute";
public const string FullNameOfTypeSetUpAttribute = "NUnit.Framework.SetUpAttribute";
public const string FullNameOfTypeTearDownAttribute = "NUnit.Framework.TearDownAttribute";
}
}
51 changes: 51 additions & 0 deletions NUnit.Analyzers/Extensions/AttributeDataExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using Microsoft.CodeAnalysis;

using NUnit.Analyzers.Constants;

namespace NUnit.Analyzers.Extensions
{
public static class AttributeDataExtensions
{
public static bool DerivesFromISimpleTestBuilder(this AttributeData @this, Compilation compilation)
{
return DerivesFromInterface(compilation, @this, NUnitFrameworkConstants.FullNameOfTypeISimpleTestBuilder);
}

public static bool DerivesFromITestBuilder(this AttributeData @this, Compilation compilation)
{
return DerivesFromInterface(compilation, @this, NUnitFrameworkConstants.FullNameOfTypeITestBuilder);
}

public static bool IsTestMethodAttribute(this AttributeData @this, Compilation compilation)
{
return @this.DerivesFromITestBuilder(compilation) ||
@this.DerivesFromISimpleTestBuilder(compilation);
}

public static bool IsSetUpOrTearDownMethodAttribute(this AttributeData @this, Compilation compilation)
{
var attributeType = @this.AttributeClass;

if (attributeType is null)
return false;

return attributeType.IsType(NUnitFrameworkConstants.FullNameOfTypeOneTimeSetUpAttribute, compilation)
|| attributeType.IsType(NUnitFrameworkConstants.FullNameOfTypeOneTimeTearDownAttribute, compilation)
|| attributeType.IsType(NUnitFrameworkConstants.FullNameOfTypeSetUpAttribute, compilation)
|| attributeType.IsType(NUnitFrameworkConstants.FullNameOfTypeTearDownAttribute, compilation);
}

private static bool DerivesFromInterface(Compilation compilation, AttributeData attributeData, string interfaceTypeFullName)
{
if (attributeData.AttributeClass is null)
return false;

var interfaceType = compilation.GetTypeByMetadataName(interfaceTypeFullName);

if (interfaceType is null)
return false;

return attributeData.AttributeClass.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, interfaceType));
}
}
}
19 changes: 19 additions & 0 deletions NUnit.Analyzers/Extensions/MethodSymbolExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Microsoft.CodeAnalysis;

namespace NUnit.Analyzers.Extensions
{
internal static class MethodSymbolExtensions
{
internal static bool IsTestRelatedMethod(this IMethodSymbol methodSymbol, Compilation compilation)
{
return methodSymbol.HasTestRelatedAttributes(compilation) ||
(methodSymbol.OverriddenMethod is not null && methodSymbol.OverriddenMethod.IsTestRelatedMethod(compilation));
}

internal static bool HasTestRelatedAttributes(this IMethodSymbol methodSymbol, Compilation compilation)
{
return methodSymbol.GetAttributes().Any(
a => a.IsTestMethodAttribute(compilation) || a.IsSetUpOrTearDownMethodAttribute(compilation));
}
}
}
16 changes: 16 additions & 0 deletions NUnit.Analyzers/Extensions/TypeSymbolExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Diagnostics.CodeAnalysis;

using Microsoft.CodeAnalysis;

namespace NUnit.Analyzers.Extensions
{
public static class TypeSymbolExtensions
{
internal static bool IsType([NotNullWhen(true)] this ITypeSymbol? @this, string fullMetadataName, Compilation compilation)
{
var typeSymbol = compilation.GetTypeByMetadataName(fullMetadataName);

return SymbolEqualityComparer.Default.Equals(typeSymbol, @this);
}
}
}
13 changes: 13 additions & 0 deletions NUnit.Analyzers/NUnit.Analyzers.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.10.0" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System.Collections.Immutable;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

using NUnit.Analyzers.Constants;
using NUnit.Analyzers.Extensions;

namespace NUnit.Analyzers.TestFieldIsNotReadonly
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class TestFieldIsNotReadonlyAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor testFieldIsNotReadonly = new DiagnosticDescriptor(
AnalyzerIdentifiers.TestFieldIsNotReadonly,
TestFieldIsNotReadonlyConstants.TestFieldIsNotReadonlyTitle,
TestFieldIsNotReadonlyConstants.TestFieldIsNotReadonlyMessage,
Categories.ParallelExecution,
DiagnosticSeverity.Warning,
true,
TestFieldIsNotReadonlyConstants.TestFieldIsNotReadonlyDescription
);

public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSymbolAction(AnalyzeType, SymbolKind.NamedType);
}

private static void AnalyzeType(SymbolAnalysisContext context)
{
var typeSymbol = (INamedTypeSymbol)context.Symbol;

var hasTests = typeSymbol
.GetMembers()
.OfType<IMethodSymbol>()
.Any(x => x.MethodKind == MethodKind.Ordinary && x.IsTestRelatedMethod(context.Compilation));

if (!hasTests)
return;

var fields = typeSymbol
.GetMembers()
.OfType<IFieldSymbol>()
.Where(x => !x.IsRequired && !x.IsConst);

foreach (var field in fields)
context.ReportDiagnostic(Diagnostic.Create(testFieldIsNotReadonly, field.Locations[0]));
}

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
ImmutableArray.Create(testFieldIsNotReadonly);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System.Collections.Immutable;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

using NUnit.Analyzers.Constants;

namespace NUnit.Analyzers.TestFieldIsNotReadonly
{
[ExportCodeFixProvider(LanguageNames.CSharp)]
public class TestFieldIsNotReadonlyCodeFix : CodeFixProvider
{
public override sealed FixAllProvider GetFixAllProvider()
{
return WellKnownFixAllProviders.BatchFixer;
}

public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);

if (root is null)
{
return;
}

context.CancellationToken.ThrowIfCancellationRequested();

var node = root.FindNode(context.Span);
if (node is not FieldDeclarationSyntax originalExpression)
return;

if (originalExpression.Modifiers.Any(IsReadonlyModifier))
return;

// var newExpression = originalExpression.WithModifiers(originalExpression.Modifiers.Add(Synra))
}

private static bool IsReadonlyModifier(SyntaxToken syntaxToken) =>
syntaxToken.IsKind(SyntaxKind.ConstKeyword) ||
syntaxToken.IsKind(SyntaxKind.ReadOnlyKeyword);

public override ImmutableArray<string> FixableDiagnosticIds
=> ImmutableArray.Create(AnalyzerIdentifiers.TestFieldIsNotReadonly);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace NUnit.Analyzers.TestFieldIsNotReadonly
{
internal class TestFieldIsNotReadonlyConstants
{
internal const string TestFieldIsNotReadonlyTitle = "The field in test class is not readonly";
internal const string TestFieldIsNotReadonlyMessage = "Fields in test classes should be readonly";
internal const string TestFieldIsNotReadonlyDescription = "A test fixture should not contain any modifiable shared state to simplify tests parallel execution.";
}
}
6 changes: 6 additions & 0 deletions nunit-extensions.sln
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NUnit.Extensions.Tests", "N
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NUnit.Retries", "NUnit.Retries\NUnit.Retries.csproj", "{7EEE6F2E-F282-410D-A82A-6A5C4B9A9ECF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NUnit.Analyzers", "NUnit.Analyzers\NUnit.Analyzers.csproj", "{5BEC009E-0FB1-4747-A011-A8B20CBE1EFC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -24,5 +26,9 @@ Global
{7EEE6F2E-F282-410D-A82A-6A5C4B9A9ECF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7EEE6F2E-F282-410D-A82A-6A5C4B9A9ECF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7EEE6F2E-F282-410D-A82A-6A5C4B9A9ECF}.Release|Any CPU.Build.0 = Release|Any CPU
{5BEC009E-0FB1-4747-A011-A8B20CBE1EFC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5BEC009E-0FB1-4747-A011-A8B20CBE1EFC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5BEC009E-0FB1-4747-A011-A8B20CBE1EFC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5BEC009E-0FB1-4747-A011-A8B20CBE1EFC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

0 comments on commit 29c23bf

Please sign in to comment.