Skip to content

Commit

Permalink
MSTEST0005: TestContext should be valid (#2019)
Browse files Browse the repository at this point in the history
  • Loading branch information
Evangelink authored Dec 30, 2023
1 parent 580a088 commit 78efbdc
Show file tree
Hide file tree
Showing 27 changed files with 1,028 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ MSTEST0001 | Performance | Warning | UseParallelizeAttributeAnalyzer, [Documenta
MSTEST0002 | Usage | Warning | TestClassShouldBeValidAnalyzer, [Documentation](https://github.com/microsoft/testfx/blob/main/docs/analyzers/MSTEST0002.md)
MSTEST0003 | Usage | Warning | TestMethodShouldBeValidAnalyzer, [Documentation](https://github.com/microsoft/testfx/blob/main/docs/analyzers/MSTEST0003.md)
MSTEST0004 | Design | Disabled | PublicTypeShouldBeTestClassAnalyzer, [Documentation](https://github.com/microsoft/testfx/blob/main/docs/analyzers/MSTEST0004.md)
MSTEST0005 | Usage | Warning | TestContextShouldBeValidAnalyzer, [Documentation](https://github.com/microsoft/testfx/blob/main/docs/analyzers/MSTEST0005.md)
12 changes: 12 additions & 0 deletions src/Analyzers/MSTest.Analyzers/Helpers/ApplicationStateGuard.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// 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.Runtime.CompilerServices;

namespace MSTest.Analyzers.Helpers;

internal static class ApplicationStateGuard
{
public static InvalidOperationException Unreachable([CallerFilePath] string? path = null, [CallerLineNumber] int line = 0)
=> new($"This program location is thought to be unreachable. File='{path}' Line={line}");
}
12 changes: 12 additions & 0 deletions src/Analyzers/MSTest.Analyzers/Helpers/CompilationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ namespace MSTest.Analyzers.Helpers;

internal static class CompilationExtensions
{
private static readonly BoundedCacheWithFactory<Compilation, bool> CanDiscoverInternalsCache = new();

/// <summary>
/// Gets a type by its full type name and cache it at the compilation level.
/// </summary>
Expand All @@ -28,4 +30,14 @@ internal static class CompilationExtensions
/// <returns>The <see cref="INamedTypeSymbol"/> if found, null otherwise.</returns>
internal static bool TryGetOrCreateTypeByMetadataName(this Compilation compilation, string fullTypeName, [NotNullWhen(returnValue: true)] out INamedTypeSymbol? namedTypeSymbol)
=> WellKnownTypeProvider.GetOrCreate(compilation).TryGetOrCreateTypeByMetadataName(fullTypeName, out namedTypeSymbol);

internal static bool CanDiscoverInternals(this Compilation compilation)
{
return CanDiscoverInternalsCache.GetOrCreateValue(compilation, GetCanDiscoverInternals);

// Local functions
static bool GetCanDiscoverInternals(Compilation compilation)
=> compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingDiscoverInternalsAttribute, out var discoverInternalsAttributeSymbol)
&& compilation.Assembly.GetAttributes().Any(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, discoverInternalsAttributeSymbol));
}
}
1 change: 1 addition & 0 deletions src/Analyzers/MSTest.Analyzers/Helpers/DiagnosticIds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ internal static class DiagnosticIds
public const string TestClassShouldBeValidRuleId = "MSTEST0002";
public const string TestMethodShouldBeValidRuleId = "MSTEST0003";
public const string PublicTypeShouldBeTestClassRuleId = "MSTEST0004";
public const string TestContextShouldBeValidRuleId = "MSTEST0005";
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ internal static class WellKnownTypeNames
public const string MicrosoftVisualStudioTestToolsUnitTestingDoNotParallelizeAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.DoNotParallelizeAttribute";
public const string MicrosoftVisualStudioTestToolsUnitTestingParallelizeAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.ParallelizeAttribute";
public const string MicrosoftVisualStudioTestToolsUnitTestingTestClassAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.TestClassAttribute";
public const string MicrosoftVisualStudioTestToolsUnitTestingTestContext = "Microsoft.VisualStudio.TestTools.UnitTesting.TestContext";
public const string MicrosoftVisualStudioTestToolsUnitTestingTestMethodAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.TestMethodAttribute";

public const string SystemThreadingTasksTask = "System.Threading.Tasks.Task";
Expand Down
67 changes: 67 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.

25 changes: 25 additions & 0 deletions src/Analyzers/MSTest.Analyzers/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,31 @@
<data name="TestClassShouldBeValidTitle" xml:space="preserve">
<value>Test classes should have valid layout</value>
</data>
<data name="TestContextShouldBeValidDescription" xml:space="preserve">
<value>TestContext property should follow the following layout to be valid:
- it should be a property
- it should be 'public' (or 'internal' if '[assembly: DiscoverInternals]' attribute is set)
- it should not be 'static'
- it should not be readonly.</value>
</data>
<data name="TestContextShouldBeValidMessageFormat_NotField" xml:space="preserve">
<value>Member 'TestContext' should be a property and not a field</value>
</data>
<data name="TestContextShouldBeValidMessageFormat_NotReadonly" xml:space="preserve">
<value>Property 'TestContext' should be settable</value>
</data>
<data name="TestContextShouldBeValidMessageFormat_NotStatic" xml:space="preserve">
<value>Property 'TestContext' should not be 'static'</value>
</data>
<data name="TestContextShouldBeValidMessageFormat_Public" xml:space="preserve">
<value>Property 'TestContext' should be 'public'</value>
</data>
<data name="TestContextShouldBeValidMessageFormat_PublicOrInternal" xml:space="preserve">
<value>Property 'TestContext' should be 'public' or 'internal'</value>
</data>
<data name="TestContextShouldBeValidTitle" xml:space="preserve">
<value>Test context property should have valid layout</value>
</data>
<data name="TestMethodShouldBeValidDescription" xml:space="preserve">
<value>Test methods, methods marked with the '[TestMethod]' attribute, should respect the following layout to be considered valid by MSTest:
- it should be 'public' (or 'internal' if '[assembly: DiscoverInternals]' attribute is set)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,7 @@ public override void Initialize(AnalysisContext context)
{
if (context.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingTestClassAttribute, out var testClassAttributeSymbol))
{
bool canDiscoverInternals = context.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingDiscoverInternalsAttribute, out var discoverInternalsAttributeSymbol)
&& context.Compilation.Assembly.GetAttributes().Any(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, discoverInternalsAttributeSymbol));

bool canDiscoverInternals = context.Compilation.CanDiscoverInternals();
context.RegisterSymbolAction(context => AnalyzeSymbol(context, testClassAttributeSymbol, canDiscoverInternals), SymbolKind.NamedType);
}
});
Expand Down
151 changes: 151 additions & 0 deletions src/Analyzers/MSTest.Analyzers/TestContextShouldBeValidAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// 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 TestContextShouldBeValidAnalyzer : DiagnosticAnalyzer
{
private static readonly LocalizableResourceString Title = new(nameof(Resources.TestContextShouldBeValidTitle), Resources.ResourceManager, typeof(Resources));
private static readonly LocalizableResourceString Description = new(nameof(Resources.TestContextShouldBeValidDescription), Resources.ResourceManager, typeof(Resources));

private static readonly LocalizableResourceString NotFieldMessageFormat = new(nameof(Resources.TestContextShouldBeValidMessageFormat_NotField), Resources.ResourceManager, typeof(Resources));
internal static readonly DiagnosticDescriptor NotFieldRule = new(
DiagnosticIds.TestContextShouldBeValidRuleId,
Title,
NotFieldMessageFormat,
Categories.Usage,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
Description,
$"https://github.com/microsoft/testfx/blob/main/docs/analyzers/{DiagnosticIds.TestContextShouldBeValidRuleId}.md");

private static readonly LocalizableResourceString PublicMessageFormat = new(nameof(Resources.TestContextShouldBeValidMessageFormat_Public), Resources.ResourceManager, typeof(Resources));
internal static readonly DiagnosticDescriptor PublicRule = new(
DiagnosticIds.TestContextShouldBeValidRuleId,
Title,
PublicMessageFormat,
Categories.Usage,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
Description,
$"https://github.com/microsoft/testfx/blob/main/docs/analyzers/{DiagnosticIds.TestContextShouldBeValidRuleId}.md");

private static readonly LocalizableResourceString PublicOrInternalMessageFormat = new(nameof(Resources.TestContextShouldBeValidMessageFormat_PublicOrInternal), Resources.ResourceManager, typeof(Resources));
internal static readonly DiagnosticDescriptor PublicOrInternalRule = new(
DiagnosticIds.TestContextShouldBeValidRuleId,
Title,
PublicOrInternalMessageFormat,
Categories.Usage,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
Description,
$"https://github.com/microsoft/testfx/blob/main/docs/analyzers/{DiagnosticIds.TestContextShouldBeValidRuleId}.md");

private static readonly LocalizableResourceString NotStaticMessageFormat = new(nameof(Resources.TestContextShouldBeValidMessageFormat_NotStatic), Resources.ResourceManager, typeof(Resources));
internal static readonly DiagnosticDescriptor NotStaticRule = new(
DiagnosticIds.TestContextShouldBeValidRuleId,
Title,
NotStaticMessageFormat,
Categories.Usage,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
Description,
$"https://github.com/microsoft/testfx/blob/main/docs/analyzers/{DiagnosticIds.TestContextShouldBeValidRuleId}.md");

private static readonly LocalizableResourceString NotReadonlyMessageFormat = new(nameof(Resources.TestContextShouldBeValidMessageFormat_NotReadonly), Resources.ResourceManager, typeof(Resources));
internal static readonly DiagnosticDescriptor NotReadonlyRule = new(
DiagnosticIds.TestContextShouldBeValidRuleId,
Title,
NotReadonlyMessageFormat,
Categories.Usage,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
Description,
$"https://github.com/microsoft/testfx/blob/main/docs/analyzers/{DiagnosticIds.TestContextShouldBeValidRuleId}.md");

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.MicrosoftVisualStudioTestToolsUnitTestingTestContext, out var testContextSymbol))
{
bool canDiscoverInternals = context.Compilation.CanDiscoverInternals();
context.RegisterSymbolAction(context => AnalyzeSymbol(context, testContextSymbol, canDiscoverInternals), SymbolKind.Field, SymbolKind.Property);
}
});
}

private static void AnalyzeSymbol(SymbolAnalysisContext context, INamedTypeSymbol testContextSymbol, bool canDiscoverInternals)
{
if (context.Symbol is IFieldSymbol fieldSymbol)
{
AnalyzeFieldSymbol(context, fieldSymbol, testContextSymbol);
return;
}

if (context.Symbol is IPropertySymbol propertySymbol)
{
AnalyzePropertySymbol(context, testContextSymbol, canDiscoverInternals, propertySymbol);
return;
}

throw ApplicationStateGuard.Unreachable();
}

private static void AnalyzePropertySymbol(SymbolAnalysisContext context, INamedTypeSymbol testContextSymbol, bool canDiscoverInternals, IPropertySymbol propertySymbol)
{
if (propertySymbol.GetMethod is null
|| !string.Equals(propertySymbol.Name, "TestContext", StringComparison.OrdinalIgnoreCase)
|| !SymbolEqualityComparer.Default.Equals(testContextSymbol, propertySymbol.GetMethod.ReturnType))
{
return;
}

if (propertySymbol.GetResultantVisibility() is { } resultantVisibility)
{
if (!canDiscoverInternals && resultantVisibility != SymbolVisibility.Public)
{
context.ReportDiagnostic(propertySymbol.CreateDiagnostic(PublicRule));
}
else if (canDiscoverInternals && resultantVisibility == SymbolVisibility.Private)
{
context.ReportDiagnostic(propertySymbol.CreateDiagnostic(PublicOrInternalRule));
}
}

if (propertySymbol.IsStatic)
{
context.ReportDiagnostic(propertySymbol.CreateDiagnostic(NotStaticRule));
}

if (propertySymbol.SetMethod is null)
{
context.ReportDiagnostic(propertySymbol.CreateDiagnostic(NotReadonlyRule));
}
}

private static void AnalyzeFieldSymbol(SymbolAnalysisContext context, IFieldSymbol fieldSymbol, INamedTypeSymbol testContextSymbol)
{
if (string.Equals(fieldSymbol.Name, "TestContext", StringComparison.OrdinalIgnoreCase)
&& SymbolEqualityComparer.Default.Equals(testContextSymbol, fieldSymbol.Type))
{
context.ReportDiagnostic(fieldSymbol.CreateDiagnostic(NotFieldRule));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,7 @@ public override void Initialize(AnalysisContext context)
if (context.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingTestMethodAttribute, out var testMethodAttributeSymbol))
{
var taskSymbol = context.Compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingTasksTask);
bool canDiscoverInternals = context.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingDiscoverInternalsAttribute, out var discoverInternalsAttributeSymbol)
&& context.Compilation.Assembly.GetAttributes().Any(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, discoverInternalsAttributeSymbol));

bool canDiscoverInternals = context.Compilation.CanDiscoverInternals();
context.RegisterSymbolAction(context => AnalyzeSymbol(context, testMethodAttributeSymbol, taskSymbol, canDiscoverInternals), SymbolKind.Method);
}
});
Expand Down
Loading

0 comments on commit 78efbdc

Please sign in to comment.