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

Feat: Add code suppressor for CS8618 on TestContext property #3271

Merged
merged 1 commit into from
Jul 16, 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
8 changes: 8 additions & 0 deletions samples/Playground/Playground.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@
<ItemGroup>
<ProjectReference Include="$(RepoRoot)src\Platform\Microsoft.Testing.Platform\Microsoft.Testing.Platform.csproj" />
<ProjectReference Include="$(RepoRoot)src\Adapter\MSTest.TestAdapter\MSTest.TestAdapter.csproj" />
<ProjectReference Include="$(RepoRoot)src\Analyzers\MSTest.Analyzers.CodeFixes\MSTest.Analyzers.CodeFixes.csproj"
PrivateAssets="all"
ReferenceOutputAssembly="false"
OutputItemType="Analyzer" />
<ProjectReference Include="$(RepoRoot)src\Analyzers\MSTest.Analyzers\MSTest.Analyzers.csproj"
PrivateAssets="all"
ReferenceOutputAssembly="false"
OutputItemType="Analyzer" />
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 2 additions & 0 deletions samples/Playground/Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ namespace Playground;
[TestClass]
public class TestClass
{
public TestContext TestContext { get; set; }

[TestMethod]
public void Test()
{
Expand Down
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 @@ -36,4 +36,5 @@ internal static class DiagnosticIds
public const string TypeContainingTestMethodShouldBeATestClassRuleId = "MSTEST0030";
public const string DoNotUseSystemDescriptionAttributeRuleId = "MSTEST0031";
public const string ReviewAlwaysTrueAssertConditionAnalyzerRuleId = "MSTEST0032";
public const string NonNullableReferenceNotInitializedSuppressorRuleId = "MSTEST0033";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// 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;

/// <summary>
/// MSTEST0028: <inheritdoc cref="Resources.UseAsyncSuffixTestFixtureMethodSuppressorJustification"/>.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
public sealed class NonNullableReferenceNotInitializedSuppressor : DiagnosticSuppressor
{
// CS8618: Non-nullable variable must contain a non-null value when exiting constructor. Consider declaring it as nullable.
// https://learn.microsoft.com/dotnet/csharp/language-reference/compiler-messages/nullable-warnings?f1url=%3FappId%3Droslyn%26k%3Dk(CS8618)#nonnullable-reference-not-initialized
private const string SuppressedDiagnosticId = "CS8618";

internal static readonly SuppressionDescriptor Rule =
new(DiagnosticIds.NonNullableReferenceNotInitializedSuppressorRuleId, SuppressedDiagnosticId, Resources.UseAsyncSuffixTestFixtureMethodSuppressorJustification);

public override ImmutableArray<SuppressionDescriptor> SupportedSuppressions { get; } = ImmutableArray.Create(Rule);

public override void ReportSuppressions(SuppressionAnalysisContext context)
{
if (!context.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingTestContext, out INamedTypeSymbol? testContextSymbol)
|| !context.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingTestClassAttribute, out INamedTypeSymbol? testClassAttributeSymbol))
{
return;
}

foreach (Diagnostic diagnostic in context.ReportedDiagnostics)
{
// The diagnostic is reported on the test method
if (diagnostic.Location.SourceTree is not { } tree)
{
continue;
}

SyntaxNode root = tree.GetRoot(context.CancellationToken);
SyntaxNode node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true);

SemanticModel semanticModel = context.GetSemanticModel(tree);
ISymbol? declaredSymbol = semanticModel.GetDeclaredSymbol(node, context.CancellationToken);
if (declaredSymbol is IPropertySymbol property
&& string.Equals(property.Name, "TestContext", StringComparison.OrdinalIgnoreCase)
&& SymbolEqualityComparer.Default.Equals(testContextSymbol, property.GetMethod?.ReturnType)
&& property.ContainingType.GetAttributes().Any(attr => attr.AttributeClass.Inherits(testClassAttributeSymbol)))
{
context.ReportSuppression(Suppression.Create(Rule, diagnostic));
}
}
}
}
4 changes: 4 additions & 0 deletions src/Analyzers/MSTest.Analyzers/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
#nullable enable
MSTest.Analyzers.NonNullableReferenceNotInitializedSuppressor
MSTest.Analyzers.NonNullableReferenceNotInitializedSuppressor.NonNullableReferenceNotInitializedSuppressor() -> void
override MSTest.Analyzers.NonNullableReferenceNotInitializedSuppressor.ReportSuppressions(Microsoft.CodeAnalysis.Diagnostics.SuppressionAnalysisContext context) -> void
override MSTest.Analyzers.NonNullableReferenceNotInitializedSuppressor.SupportedSuppressions.get -> System.Collections.Immutable.ImmutableArray<Microsoft.CodeAnalysis.SuppressionDescriptor!>
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// 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 System.Diagnostics.CodeAnalysis;

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

using VerifyCS = MSTest.Analyzers.Test.CSharpCodeFixVerifier<
MSTest.Analyzers.UnitTests.NonNullableReferenceNotInitializedSuppressorTests.DoNothingAnalyzer,
Microsoft.CodeAnalysis.Testing.EmptyCodeFixProvider>;

namespace MSTest.Analyzers.UnitTests;

[TestGroup]
public sealed class NonNullableReferenceNotInitializedSuppressorTests(ITestExecutionContext testExecutionContext) : TestBase(testExecutionContext)
{
public async Task TestContextPropertyOnTestClass_DiagnosticIsSuppressed()
{
string code = @"
#nullable enable

using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class SomeClass
{
public TestContext [|TestContext|] { get; set; }
}
";

// Verify issues are reported
await new VerifyCS.Test
{
TestState = { Sources = { code } },
}.RunAsync();

await new TestWithSuppressor
{
TestState = { Sources = { code } },
}.RunAsync();
}

public async Task TestContextPropertyOnNonTestClass_DiagnosticIsNotSuppressed()
{
string code = @"
#nullable enable

using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;

public class SomeClass
{
public TestContext [|TestContext|] { get; set; }
}
";

// Verify issues are reported
await new VerifyCS.Test
{
TestState = { Sources = { code } },
}.RunAsync();

await new TestWithSuppressor
{
TestState = { Sources = { code } },
}.RunAsync();
}

[DiagnosticAnalyzer(LanguageNames.CSharp)]
[SuppressMessage("MicrosoftCodeAnalysisCorrectness", "RS1038:Compiler extensions should be implemented in assemblies with compiler-provided references", Justification = "For suppression test only.")]
[SuppressMessage("MicrosoftCodeAnalysisCorrectness", "RS1036:Specify analyzer banned API enforcement setting", Justification = "For suppression test only.")]
[SuppressMessage("MicrosoftCodeAnalysisCorrectness", "RS1041:Compiler extensions should be implemented in assemblies targeting netstandard2.0", Justification = "For suppression test only.")]
public class DoNothingAnalyzer : DiagnosticAnalyzer
{
[SuppressMessage("MicrosoftCodeAnalysisDesign", "RS1017:DiagnosticId for analyzers must be a non-null constant.", Justification = "For suppression test only.")]
public static readonly DiagnosticDescriptor Rule = new(NonNullableReferenceNotInitializedSuppressor.Rule.SuppressedDiagnosticId, "Title", "Message", "Category", DiagnosticSeverity.Warning, isEnabledByDefault: true);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);

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

context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.Property);
}

private void AnalyzeSymbol(SymbolAnalysisContext context)
{
}
}

internal sealed class TestWithSuppressor : VerifyCS.Test
{
protected override IEnumerable<DiagnosticAnalyzer> GetDiagnosticAnalyzers()
{
foreach (DiagnosticAnalyzer analyzer in base.GetDiagnosticAnalyzers())
{
yield return analyzer;
}

yield return new NonNullableReferenceNotInitializedSuppressor();
}
}
}