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

MSTEST0013: AssemblyCleanup should be valid #2353

Merged
merged 3 commits into from
Feb 16, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -8,3 +8,4 @@ MSTEST0007 | Usage | Info | UseAttributeOnTestMethodAnalyzer, [Documentation](ht
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)
MSTEST0013 | Usage | Warning | AssemblyCleanupShouldBeValidAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/core/testing/unit-testing-mstest-analyzers-MSTEST0013)
117 changes: 117 additions & 0 deletions src/Analyzers/MSTest.Analyzers/AssemblyCleanupShouldBeValidAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// 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 AssemblyCleanupShouldBeValidAnalyzer : DiagnosticAnalyzer
{
private static readonly LocalizableResourceString Title = new(nameof(Resources.AssemblyCleanupShouldBeValidTitle), Resources.ResourceManager, typeof(Resources));
private static readonly LocalizableResourceString Description = new(nameof(Resources.AssemblyCleanupShouldBeValidDescription), Resources.ResourceManager, typeof(Resources));
private static readonly LocalizableResourceString MessageFormat = new(nameof(Resources.AssemblyCleanupShouldBeValidMessageFormat_Public), Resources.ResourceManager, typeof(Resources));

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

internal static readonly DiagnosticDescriptor NotStaticRule = PublicRule.WithMessage(new(nameof(Resources.AssemblyCleanupShouldBeValidMessageFormat_NotStatic), Resources.ResourceManager, typeof(Resources)));
internal static readonly DiagnosticDescriptor NoParametersRule = PublicRule.WithMessage(new(nameof(Resources.AssemblyCleanupShouldBeValidMessageFormat_NoParameters), Resources.ResourceManager, typeof(Resources)));
internal static readonly DiagnosticDescriptor ReturnTypeRule = PublicRule.WithMessage(new(nameof(Resources.AssemblyCleanupShouldBeValidMessageFormat_ReturnType), Resources.ResourceManager, typeof(Resources)));
internal static readonly DiagnosticDescriptor NotAsyncVoidRule = PublicRule.WithMessage(new(nameof(Resources.AssemblyCleanupShouldBeValidMessageFormat_NotAsyncVoid), Resources.ResourceManager, typeof(Resources)));
internal static readonly DiagnosticDescriptor NotGenericRule = PublicRule.WithMessage(new(nameof(Resources.AssemblyCleanupShouldBeValidMessageFormat_NotGeneric), Resources.ResourceManager, typeof(Resources)));
internal static readonly DiagnosticDescriptor OrdinaryRule = PublicRule.WithMessage(new(nameof(Resources.AssemblyCleanupShouldBeValidMessageFormat_Ordinary), Resources.ResourceManager, typeof(Resources)));
internal static readonly DiagnosticDescriptor NotAbstractRule = PublicRule.WithMessage(new(nameof(Resources.TestInitializeShouldBeValidMessageFormat_NotAbstract), 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.MicrosoftVisualStudioTestToolsUnitTestingAssemblyCleanupAttribute, out var assemblyCleanupAttributeSymbol))
{
var taskSymbol = context.Compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingTasksTask);
var valueTaskSymbol = context.Compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingTasksValueTask);
bool canDiscoverInternals = context.Compilation.CanDiscoverInternals();
context.RegisterSymbolAction(
context => AnalyzeSymbol(context, assemblyCleanupAttributeSymbol, taskSymbol, valueTaskSymbol, canDiscoverInternals),
SymbolKind.Method);
}
});
}

private static void AnalyzeSymbol(SymbolAnalysisContext context, INamedTypeSymbol assemblyCleanupAttributeSymbol, INamedTypeSymbol? taskSymbol,
INamedTypeSymbol? valueTaskSymbol, bool canDiscoverInternals)
{
var methodSymbol = (IMethodSymbol)context.Symbol;
if (!methodSymbol.GetAttributes().Any(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, assemblyCleanupAttributeSymbol)))
{
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 > 0)
{
context.ReportDiagnostic(methodSymbol.CreateDiagnostic(NoParametersRule, methodSymbol.Name));
}

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

if (methodSymbol.IsAbstract)
{
context.ReportDiagnostic(methodSymbol.CreateDiagnostic(NotAbstractRule, methodSymbol.Name));
}

if (methodSymbol.IsStatic)
{
context.ReportDiagnostic(methodSymbol.CreateDiagnostic(NotStaticRule, 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))
Comment on lines +98 to +99
Copy link
Member

@nohwnd nohwnd Feb 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will not run.

The declared accessibility of the method should always be public. The DiscoverInternals should only apply to the class (when disabled only public classes with [TestClass] should be scanned, if enabled public and internal classes should be scanned).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nohwnd Yeah that's the correct case here, I added tests that test all those scenarios correctly

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nohwnd DeclaredAccesibility is what is declared on the method and is the first check Engy's doing here. The other checks are using GetResultantVisibility that walks up tree (class + outer classes) to get the final accessibility. We had offline chat with Engy and the code she wrotes LGTM.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Evangelink this resource, and other resources don't reflect the changes made for Task and ValueTask https://github.com/microsoft/testfx/blob/main/src/Adapter/MSTest.TestAdapter/Resources/Resource.resx#L212

Thanks! I see some other resources are missing edit. I'll do a follow-up

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, and I am wrong. :) (For others: The first check makes sure that the class always has public on the member, the other two check the visibility due to placement in a internal or private class (or public class in internal class etc.)).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@engyebrahim Would you mind doing a follow-up PR where you add a comment on top of the check with what Jakub wrotes for the various analyzers doing the same trick? It will be helpful for other devs.

{
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 @@ -9,6 +9,7 @@ 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 MicrosoftVisualStudioTestToolsUnitTestingAssemblyCleanupAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.AssemblyCleanupAttribute";
public const string MicrosoftVisualStudioTestToolsUnitTestingCssIterationAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.CssIterationAttribute";
public const string MicrosoftVisualStudioTestToolsUnitTestingCssProjectStructureAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.CssProjectStructureAttribute";
public const string MicrosoftVisualStudioTestToolsUnitTestingDescriptionAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.DescriptionAttribute";
Expand Down
98 changes: 98 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.

38 changes: 38 additions & 0 deletions src/Analyzers/MSTest.Analyzers/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,44 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="AssemblyCleanupShouldBeValidDescription" xml:space="preserve">
<value>Methods marked with [AssemblyCleanup] should follow the following layout to be valid:
- it should be 'public'
- it should not be 'static'
- it should not be generic
- it should not be 'abstract'
- it should not take any parameter
- 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="AssemblyCleanupShouldBeValidMessageFormat_NoParameters" xml:space="preserve">
<value>AssemblyCleanup method '{0}' should not take any parameter</value>
</data>
<data name="AssemblyCleanupShouldBeValidMessageFormat_NotAbstract" xml:space="preserve">
<value>AssemblyCleanup '{0}' should not be 'abstract'</value>
</data>
<data name="AssemblyCleanupShouldBeValidMessageFormat_NotAsyncVoid" xml:space="preserve">
<value>AssemblyCleanup method '{0}' should return 'void', 'Task' or 'ValueTask'</value>
</data>
<data name="AssemblyCleanupShouldBeValidMessageFormat_NotGeneric" xml:space="preserve">
<value>AssemblyCleanup method '{0}' should not be generic</value>
</data>
<data name="AssemblyCleanupShouldBeValidMessageFormat_NotStatic" xml:space="preserve">
<value>AssemblyCleanup method '{0}' should not be 'static'</value>
</data>
<data name="AssemblyCleanupShouldBeValidMessageFormat_Ordinary" xml:space="preserve">
<value>AssemblyCleanup method '{0}' should be an 'ordinary' method</value>
</data>
<data name="AssemblyCleanupShouldBeValidMessageFormat_Public" xml:space="preserve">
<value>AssemblyCleanup method '{0}' should be 'public'</value>
</data>
<data name="AssemblyCleanupShouldBeValidMessageFormat_ReturnType" xml:space="preserve">
<value>AssemblyCleanup method '{0}' should return 'void', 'Task' or 'ValueTask'</value>
</data>
<data name="AssemblyCleanupShouldBeValidTitle" xml:space="preserve">
<value>AssemblyCleanup methods should have valid layout</value>
</data>
<data name="AssemblyInitializeShouldBeValidDescription" xml:space="preserve">
<value>Methods marked with [AssemblyInitialize] should follow the following layout to be valid:
- it should be 'public'
Expand Down
66 changes: 66 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,72 @@
<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="AssemblyCleanupShouldBeValidDescription">
<source>Methods marked with [AssemblyCleanup] should follow the following layout to be valid:
- it should be 'public'
- it should not be 'static'
- it should not be generic
- it should not be 'abstract'
- it should not take any parameter
- 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 [AssemblyCleanup] should follow the following layout to be valid:
- it should be 'public'
- it should not be 'static'
- it should not be generic
- it should not be 'abstract'
- it should not take any parameter
- 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="AssemblyCleanupShouldBeValidMessageFormat_NoParameters">
<source>AssemblyCleanup method '{0}' should not take any parameter</source>
<target state="new">AssemblyCleanup method '{0}' should not take any parameter</target>
<note />
</trans-unit>
<trans-unit id="AssemblyCleanupShouldBeValidMessageFormat_NotAbstract">
<source>AssemblyCleanup '{0}' should not be 'abstract'</source>
<target state="new">AssemblyCleanup '{0}' should not be 'abstract'</target>
<note />
</trans-unit>
<trans-unit id="AssemblyCleanupShouldBeValidMessageFormat_NotAsyncVoid">
<source>AssemblyCleanup method '{0}' should return 'void', 'Task' or 'ValueTask'</source>
<target state="new">AssemblyCleanup method '{0}' should return 'void', 'Task' or 'ValueTask'</target>
<note />
</trans-unit>
<trans-unit id="AssemblyCleanupShouldBeValidMessageFormat_NotGeneric">
<source>AssemblyCleanup method '{0}' should not be generic</source>
<target state="new">AssemblyCleanup method '{0}' should not be generic</target>
<note />
</trans-unit>
<trans-unit id="AssemblyCleanupShouldBeValidMessageFormat_NotStatic">
<source>AssemblyCleanup method '{0}' should not be 'static'</source>
<target state="new">AssemblyCleanup method '{0}' should not be 'static'</target>
<note />
</trans-unit>
<trans-unit id="AssemblyCleanupShouldBeValidMessageFormat_Ordinary">
<source>AssemblyCleanup method '{0}' should be an 'ordinary' method</source>
<target state="new">AssemblyCleanup method '{0}' should be an 'ordinary' method</target>
<note />
</trans-unit>
<trans-unit id="AssemblyCleanupShouldBeValidMessageFormat_Public">
<source>AssemblyCleanup method '{0}' should be 'public'</source>
<target state="new">AssemblyCleanup method '{0}' should be 'public'</target>
<note />
</trans-unit>
<trans-unit id="AssemblyCleanupShouldBeValidMessageFormat_ReturnType">
<source>AssemblyCleanup method '{0}' should return 'void', 'Task' or 'ValueTask'</source>
<target state="new">AssemblyCleanup method '{0}' should return 'void', 'Task' or 'ValueTask'</target>
<note />
</trans-unit>
<trans-unit id="AssemblyCleanupShouldBeValidTitle">
<source>AssemblyCleanup methods should have valid layout</source>
<target state="new">AssemblyCleanup methods should have valid layout</target>
<note />
</trans-unit>
<trans-unit id="AssemblyInitializeShouldBeValidDescription">
<source>Methods marked with [AssemblyInitialize] should follow the following layout to be valid:
- it should be 'public'
Expand Down
Loading
Loading