Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MSTEST00012: AssemblyInitialize should be valid #2328

Merged
merged 17 commits into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ Rule ID | Category | Severity | Notes
MSTEST0007 | Usage | Info | UseAttributeOnTestMethodAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/core/testing/unit-testing-mstest-analyzers-MSTEST0007)
MSTEST0008 | Usage | Warning | TestInitializeShouldBeValidAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/core/testing/unit-testing-mstest-analyzers-MSTEST0008)
MSTEST0009 | Usage | Warning | TestCleanupShouldBeValidAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/core/testing/unit-testing-mstest-analyzers-MSTEST0009)
MSTEST0012 | Usage | Warning | AssemblyInitializeShouldBeValidAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/core/testing/unit-testing-mstest-analyzers-MSTEST0012)
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Collections.Immutable;

using Analyzer.Utilities.Extensions;

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

using MSTest.Analyzers.Helpers;

namespace MSTest.Analyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
public sealed class AssemblyInitializeShouldBeValidAnalyzer : DiagnosticAnalyzer
{
private static readonly LocalizableResourceString Title = new(nameof(Resources.AssemblyInitializeShouldBeValidTitle), Resources.ResourceManager, typeof(Resources));
private static readonly LocalizableResourceString Description = new(nameof(Resources.AssemblyInitializeShouldBeValidDescription), Resources.ResourceManager, typeof(Resources));
private static readonly LocalizableResourceString MessageFormat = new(nameof(Resources.AssemblyInitializeShouldBeValidMessageFormat_Public), Resources.ResourceManager, typeof(Resources));

internal static readonly DiagnosticDescriptor PublicRule = DiagnosticDescriptorHelper.Create(
DiagnosticIds.AssemblyInitializeShouldBeValidRuleId,
Title,
MessageFormat,
Description,
Category.Usage,
DiagnosticSeverity.Warning,
isEnabledByDefault: true);

internal static readonly DiagnosticDescriptor StaticRule = PublicRule.WithMessage(new(nameof(Resources.AssemblyInitializeShouldBeValidMessageFormat_Static), Resources.ResourceManager, typeof(Resources)));
internal static readonly DiagnosticDescriptor SingleContextParameterRule = PublicRule.WithMessage(new(nameof(Resources.AssemblyInitializeShouldBeValidMessageFormat_SingleContextParameter), Resources.ResourceManager, typeof(Resources)));
internal static readonly DiagnosticDescriptor ReturnTypeRule = PublicRule.WithMessage(new(nameof(Resources.AssemblyInitializeShouldBeValidMessageFormat_ReturnType), Resources.ResourceManager, typeof(Resources)));
internal static readonly DiagnosticDescriptor NotAsyncVoidRule = PublicRule.WithMessage(new(nameof(Resources.AssemblyInitializeShouldBeValidMessageFormat_NotAsyncVoid), Resources.ResourceManager, typeof(Resources)));
internal static readonly DiagnosticDescriptor NotGenericRule = PublicRule.WithMessage(new(nameof(Resources.AssemblyInitializeShouldBeValidMessageFormat_NotGeneric), Resources.ResourceManager, typeof(Resources)));
internal static readonly DiagnosticDescriptor OrdinaryRule = PublicRule.WithMessage(new(nameof(Resources.AssemblyInitializeShouldBeValidMessageFormat_Ordinary), Resources.ResourceManager, typeof(Resources)));

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
= ImmutableArray.Create(PublicRule);

public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();

context.RegisterCompilationStartAction(context =>
{
if (context.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingAssemblyInitializeAttribute, out var assemblyInitializeAttributeSymbol)
&& context.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingTestContext, out var testContextSymbol))
{
var taskSymbol = context.Compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingTasksTask);
var valueTaskSymbol = context.Compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingTasksValueTask);
bool canDiscoverInternals = context.Compilation.CanDiscoverInternals();
context.RegisterSymbolAction(
context => AnalyzeSymbol(context, assemblyInitializeAttributeSymbol, taskSymbol, valueTaskSymbol, testContextSymbol, canDiscoverInternals),
SymbolKind.Method);
}
});
}

private static void AnalyzeSymbol(SymbolAnalysisContext context, INamedTypeSymbol assemblyInitializeAttributeSymbol, INamedTypeSymbol? taskSymbol,
INamedTypeSymbol? valueTaskSymbol, INamedTypeSymbol? testContextSymbol, bool canDiscoverInternals)
{
var methodSymbol = (IMethodSymbol)context.Symbol;
if (!methodSymbol.GetAttributes().Any(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, assemblyInitializeAttributeSymbol)))
{
return;
}

if (methodSymbol.MethodKind != MethodKind.Ordinary)
{
context.ReportDiagnostic(methodSymbol.CreateDiagnostic(OrdinaryRule, methodSymbol.Name));

// Do not check the other criteria, users should fix the method kind first.
return;
}

if (methodSymbol.Parameters.Length != 1 || testContextSymbol is null ||
!SymbolEqualityComparer.Default.Equals(methodSymbol.Parameters[0].Type, testContextSymbol))
{
context.ReportDiagnostic(methodSymbol.CreateDiagnostic(SingleContextParameterRule, methodSymbol.Name));
}

if (methodSymbol.IsGenericMethod)
{
context.ReportDiagnostic(methodSymbol.CreateDiagnostic(NotGenericRule, methodSymbol.Name));
}

if (!methodSymbol.IsStatic)
{
context.ReportDiagnostic(methodSymbol.CreateDiagnostic(StaticRule, methodSymbol.Name));
}

if (methodSymbol.ReturnsVoid && methodSymbol.IsAsync)
{
context.ReportDiagnostic(methodSymbol.CreateDiagnostic(NotAsyncVoidRule, methodSymbol.Name));
}

if (methodSymbol.DeclaredAccessibility != Accessibility.Public
|| (!canDiscoverInternals && methodSymbol.GetResultantVisibility() != SymbolVisibility.Public)
|| (canDiscoverInternals && methodSymbol.GetResultantVisibility() == SymbolVisibility.Private))
{
context.ReportDiagnostic(methodSymbol.CreateDiagnostic(PublicRule, methodSymbol.Name));
}

if (!methodSymbol.ReturnsVoid
&& (taskSymbol is null || !SymbolEqualityComparer.Default.Equals(methodSymbol.ReturnType, taskSymbol))
&& (valueTaskSymbol is null || !SymbolEqualityComparer.Default.Equals(methodSymbol.ReturnType, valueTaskSymbol)))
{
context.ReportDiagnostic(methodSymbol.CreateDiagnostic(ReturnTypeRule, methodSymbol.Name));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace MSTest.Analyzers;
// IMPORTANT: Keep this file sorted alphabetically.
internal static class WellKnownTypeNames
{
public const string MicrosoftVisualStudioTestToolsUnitTestingAssemblyInitializeAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.AssemblyInitializeAttribute";
public const string MicrosoftVisualStudioTestToolsUnitTestingClassCleanupAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.ClassCleanupAttribute";
public const string MicrosoftVisualStudioTestToolsUnitTestingClassInitializeAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.ClassInitializeAttribute";
public const string MicrosoftVisualStudioTestToolsUnitTestingCssIterationAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.CssIterationAttribute";
Expand Down
88 changes: 88 additions & 0 deletions src/Analyzers/MSTest.Analyzers/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions src/Analyzers/MSTest.Analyzers/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,40 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="AssemblyInitializeShouldBeValidDescription" xml:space="preserve">
<value>Methods marked with [AssemblyInitialize] should follow the following layout to be valid:
- it should be 'public'
- it should be 'static'
- it should not be generic
- it should take one parameter of type 'TestContext'
- return type should be 'void', 'Task' or 'ValueTask'
- it should not be 'async void'
- it should not be a special method (finalizer, operator...).</value>
</data>
<data name="AssemblyInitializeShouldBeValidMessageFormat_NotAsyncVoid" xml:space="preserve">
<value>AssemblyInitialize method '{0}' should return 'void', 'Task' or 'ValueTask'</value>
</data>
<data name="AssemblyInitializeShouldBeValidMessageFormat_NotGeneric" xml:space="preserve">
<value>AssemblyInitialize method '{0}' should not be generic</value>
</data>
<data name="AssemblyInitializeShouldBeValidMessageFormat_Ordinary" xml:space="preserve">
<value>AssemblyInitialize method '{0}' should be an 'ordinary' method</value>
</data>
<data name="AssemblyInitializeShouldBeValidMessageFormat_Public" xml:space="preserve">
<value>AssemblyInitialize method '{0}' should be 'public'</value>
</data>
<data name="AssemblyInitializeShouldBeValidMessageFormat_ReturnType" xml:space="preserve">
<value>AssemblyInitialize method '{0}' should return 'void', 'Task' or 'ValueTask'</value>
</data>
<data name="AssemblyInitializeShouldBeValidMessageFormat_SingleContextParameter" xml:space="preserve">
<value>AssemblyInitialize method '{0}' should take a single parameter of type 'TestContext'</value>
</data>
<data name="AssemblyInitializeShouldBeValidMessageFormat_Static" xml:space="preserve">
<value>AssemblyInitialize method '{0}' should be 'static'</value>
</data>
<data name="AssemblyInitializeShouldBeValidTitle" xml:space="preserve">
<value>AssemblyInitialize methods should have valid layout</value>
</data>
<data name="AvoidExpectedExceptionAttributeDescription" xml:space="preserve">
<value>Prefer 'Assert.ThrowsException' or 'Assert.ThrowsExceptionAsync' over '[ExpectedException]' as it ensures that only the expected call throws the expected exception. The assert APIs also provide more flexibility and allow you to assert extra properties of the exeption.</value>
</data>
Expand Down
59 changes: 59 additions & 0 deletions src/Analyzers/MSTest.Analyzers/xlf/Resources.cs.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,65 @@
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd">
<file datatype="xml" source-language="en" target-language="cs" original="../Resources.resx">
<body>
<trans-unit id="AssemblyInitializeShouldBeValidDescription">
<source>Methods marked with [AssemblyInitialize] should follow the following layout to be valid:
- it should be 'public'
- it should be 'static'
- it should not be generic
- it should take one parameter of type 'TestContext'
- return type should be 'void', 'Task' or 'ValueTask'
- it should not be 'async void'
- it should not be a special method (finalizer, operator...).</source>
<target state="new">Methods marked with [AssemblyInitialize] should follow the following layout to be valid:
- it should be 'public'
- it should be 'static'
- it should not be generic
- it should take one parameter of type 'TestContext'
- return type should be 'void', 'Task' or 'ValueTask'
- it should not be 'async void'
- it should not be a special method (finalizer, operator...).</target>
<note />
</trans-unit>
<trans-unit id="AssemblyInitializeShouldBeValidMessageFormat_NotAsyncVoid">
<source>AssemblyInitialize method '{0}' should return 'void', 'Task' or 'ValueTask'</source>
<target state="new">AssemblyInitialize method '{0}' should return 'void', 'Task' or 'ValueTask'</target>
<note />
</trans-unit>
<trans-unit id="AssemblyInitializeShouldBeValidMessageFormat_NotGeneric">
<source>AssemblyInitialize method '{0}' should not be generic</source>
<target state="new">AssemblyInitialize method '{0}' should not be generic</target>
<note />
</trans-unit>
<trans-unit id="AssemblyInitializeShouldBeValidMessageFormat_Ordinary">
<source>AssemblyInitialize method '{0}' should be an 'ordinary' method</source>
<target state="new">AssemblyInitialize method '{0}' should be an 'ordinary' method</target>
<note />
</trans-unit>
<trans-unit id="AssemblyInitializeShouldBeValidMessageFormat_Public">
<source>AssemblyInitialize method '{0}' should be 'public'</source>
<target state="new">AssemblyInitialize method '{0}' should be 'public'</target>
<note />
</trans-unit>
<trans-unit id="AssemblyInitializeShouldBeValidMessageFormat_ReturnType">
<source>AssemblyInitialize method '{0}' should return 'void', 'Task' or 'ValueTask'</source>
<target state="new">AssemblyInitialize method '{0}' should return 'void', 'Task' or 'ValueTask'</target>
<note />
</trans-unit>
<trans-unit id="AssemblyInitializeShouldBeValidMessageFormat_SingleContextParameter">
<source>AssemblyInitialize method '{0}' should take a single parameter of type 'TestContext'</source>
<target state="new">AssemblyInitialize method '{0}' should take a single parameter of type 'TestContext'</target>
<note />
</trans-unit>
<trans-unit id="AssemblyInitializeShouldBeValidMessageFormat_Static">
<source>AssemblyInitialize method '{0}' should be 'static'</source>
<target state="new">AssemblyInitialize method '{0}' should be 'static'</target>
<note />
</trans-unit>
<trans-unit id="AssemblyInitializeShouldBeValidTitle">
<source>AssemblyInitialize methods should have valid layout</source>
<target state="new">AssemblyInitialize methods should have valid layout</target>
<note />
</trans-unit>
<trans-unit id="AvoidExpectedExceptionAttributeDescription">
<source>Prefer 'Assert.ThrowsException' or 'Assert.ThrowsExceptionAsync' over '[ExpectedException]' as it ensures that only the expected call throws the expected exception. The assert APIs also provide more flexibility and allow you to assert extra properties of the exeption.</source>
<target state="translated">Preferujte Assert.ThrowsException nebo Assert.ThrowsExceptionAsync před [ExpectedException], protože zajišťuje, že očekávanou výjimku vyvolá pouze očekávané volání. Rozhraní API assert také poskytují větší flexibilitu a umožňují vyhodnocovat další vlastnosti výjimky.</target>
Expand Down
Loading
Loading