From c00354de7ecea1aa8ea297e3f92f1cb39eaa5a45 Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Thu, 6 Feb 2020 13:31:48 -0800 Subject: [PATCH 1/2] Implement MSML_RelaxTestNaming suppressor for VSTHRD200 Allow asynchronous test methods to omit the 'Async' suffix. --- .../RelaxTestNamingSuppressor.cs | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 tools-local/Microsoft.ML.InternalCodeAnalyzer/RelaxTestNamingSuppressor.cs diff --git a/tools-local/Microsoft.ML.InternalCodeAnalyzer/RelaxTestNamingSuppressor.cs b/tools-local/Microsoft.ML.InternalCodeAnalyzer/RelaxTestNamingSuppressor.cs new file mode 100644 index 0000000000..b9a4533bc9 --- /dev/null +++ b/tools-local/Microsoft.ML.InternalCodeAnalyzer/RelaxTestNamingSuppressor.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Concurrent; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.ML.InternalCodeAnalyzer +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class RelaxTestNamingSuppressor : DiagnosticSuppressor + { + private const string Id = "MSML_RelaxTestNaming"; + private const string SuppressedDiagnosticId = "VSTHRD200"; + private const string Justification = "Asynchronous test methods do not require the 'Async' suffix."; + + private static readonly SuppressionDescriptor Rule = + new SuppressionDescriptor(Id, SuppressedDiagnosticId, Justification); + + public override ImmutableArray SupportedSuppressions { get; } = ImmutableArray.Create(Rule); + + public override void ReportSuppressions(SuppressionAnalysisContext context) + { + if (!(context.Compilation.GetTypeByMetadataName("Xunit.FactAttribute") is { } factAttribute)) + { + return; + } + + var knownTestAttributes = new ConcurrentDictionary(); + + foreach (var diagnostic in context.ReportedDiagnostics) + { + // The diagnostic is reported on the test method + if (!(diagnostic.Location.SourceTree is { } tree)) + { + continue; + } + + var root = tree.GetRoot(context.CancellationToken); + var node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); + + var semanticModel = context.GetSemanticModel(tree); + var declaredSymbol = semanticModel.GetDeclaredSymbol(node, context.CancellationToken); + if (declaredSymbol is IMethodSymbol method + && method.IsTestMethod(knownTestAttributes, factAttribute)) + { + context.ReportSuppression(Suppression.Create(Rule, diagnostic)); + } + } + } + } +} From cfa2dc094339b7dfee9e7cd410c48a30f8f8aee4 Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Tue, 11 Feb 2020 13:26:16 -0800 Subject: [PATCH 2/2] Add tests for MSML_RelaxTestNaming --- .../Code/BaseTestClassTest.cs | 2 +- .../Code/RelaxTestNamingTest.cs | 148 ++++++++++++++++++ .../RelaxTestNamingSuppressor.cs | 2 +- 3 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 test/Microsoft.ML.CodeAnalyzer.Tests/Code/RelaxTestNamingTest.cs diff --git a/test/Microsoft.ML.CodeAnalyzer.Tests/Code/BaseTestClassTest.cs b/test/Microsoft.ML.CodeAnalyzer.Tests/Code/BaseTestClassTest.cs index a51abfa3ab..4bbfaf2542 100644 --- a/test/Microsoft.ML.CodeAnalyzer.Tests/Code/BaseTestClassTest.cs +++ b/test/Microsoft.ML.CodeAnalyzer.Tests/Code/BaseTestClassTest.cs @@ -14,7 +14,7 @@ namespace Microsoft.ML.CodeAnalyzer.Tests.Code { public class BaseTestClassTest { - private static readonly ReferenceAssemblies ReferenceAssemblies = ReferenceAssemblies.Default + internal static readonly ReferenceAssemblies ReferenceAssemblies = ReferenceAssemblies.Default .AddPackages(ImmutableArray.Create(new PackageIdentity("xunit", "2.4.0"))); [Fact] diff --git a/test/Microsoft.ML.CodeAnalyzer.Tests/Code/RelaxTestNamingTest.cs b/test/Microsoft.ML.CodeAnalyzer.Tests/Code/RelaxTestNamingTest.cs new file mode 100644 index 0000000000..53a929d5ce --- /dev/null +++ b/test/Microsoft.ML.CodeAnalyzer.Tests/Code/RelaxTestNamingTest.cs @@ -0,0 +1,148 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.ML.InternalCodeAnalyzer; +using Xunit; +using VerifyCS = Microsoft.ML.CodeAnalyzer.Tests.Helpers.CSharpCodeFixVerifier< + Microsoft.ML.CodeAnalyzer.Tests.Code.RelaxTestNamingTest.WarnForMissingAsyncSuffix, + Microsoft.CodeAnalysis.Testing.EmptyCodeFixProvider>; + +namespace Microsoft.ML.CodeAnalyzer.Tests.Code +{ + public class RelaxTestNamingTest + { + private static Solution WithoutSuppressedDiagnosticsTransform(Solution solution, ProjectId projectId) + { + var compilationOptions = solution.GetProject(projectId).CompilationOptions; + return solution.WithProjectCompilationOptions(projectId, compilationOptions.WithReportSuppressedDiagnostics(false)); + } + + [Fact(Skip = "https://github.com/dotnet/roslyn/issues/41584")] + public async Task TestClassWithFact() + { + var code = @" +using System.Threading.Tasks; +using Xunit; + +public class SomeClass { +[Fact] +public async Task [|TestMethod|]() { } +} +"; + + await new VerifyCS.Test + { + ReferenceAssemblies = BaseTestClassTest.ReferenceAssemblies, + TestState = { Sources = { code } }, + SolutionTransforms = { WithoutSuppressedDiagnosticsTransform }, + }.RunAsync(); + + await new TestWithSuppressor + { + ReferenceAssemblies = BaseTestClassTest.ReferenceAssemblies, + TestState = { Sources = { code }, MarkupHandling = MarkupMode.Ignore, }, + SolutionTransforms = { WithoutSuppressedDiagnosticsTransform }, + }.RunAsync(); + } + + [Fact(Skip = "https://github.com/dotnet/roslyn/issues/41584")] + public async Task TestClassWithTheory() + { + var code = @" +using Xunit; + +public class [|SomeClass|] { +[Theory, InlineData(0)] +public void TestMethod(int arg) { } +} +"; + + await new VerifyCS.Test + { + ReferenceAssemblies = BaseTestClassTest.ReferenceAssemblies, + TestState = { Sources = { code } }, + SolutionTransforms = { WithoutSuppressedDiagnosticsTransform }, + }.RunAsync(); + + await new TestWithSuppressor + { + ReferenceAssemblies = BaseTestClassTest.ReferenceAssemblies, + TestState = { Sources = { code }, MarkupHandling = MarkupMode.Ignore, }, + SolutionTransforms = { WithoutSuppressedDiagnosticsTransform }, + }.RunAsync(); + } + + [Fact] + public async Task TestAlreadyHasAsyncSuffix() + { + var code = @" +using System.Threading.Tasks; +using Xunit; + +public class SomeClass { +[Fact] +public async Task TestMethodAsync() { } +} +"; + + await new VerifyCS.Test + { + ReferenceAssemblies = BaseTestClassTest.ReferenceAssemblies, + TestState = { Sources = { code } }, + }.RunAsync(); + } + + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class WarnForMissingAsyncSuffix : DiagnosticAnalyzer + { + [SuppressMessage("MicrosoftCodeAnalysisDesign", "RS1017:DiagnosticId for analyzers must be a non-null constant.", Justification = "For suppression test only.")] + public static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(RelaxTestNamingSuppressor.Rule.SuppressedDiagnosticId, "Title", "Message", "Category", DiagnosticSeverity.Warning, isEnabledByDefault: true); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.Method); + } + + private void AnalyzeSymbol(SymbolAnalysisContext context) + { + var method = (IMethodSymbol)context.Symbol; + if (method.Name.EndsWith("Async")) + { + return; + } + + if (method.ReturnType.MetadataName != "Task") + { + // Not asynchronous (incomplete checking is sufficient for this test) + return; + } + + context.ReportDiagnostic(Diagnostic.Create(Rule, method.Locations[0])); + } + } + + internal class TestWithSuppressor : VerifyCS.Test + { + protected override IEnumerable GetDiagnosticAnalyzers() + { + foreach (var analyzer in base.GetDiagnosticAnalyzers()) + yield return analyzer; + + yield return new RelaxTestNamingSuppressor(); + } + } + } +} diff --git a/tools-local/Microsoft.ML.InternalCodeAnalyzer/RelaxTestNamingSuppressor.cs b/tools-local/Microsoft.ML.InternalCodeAnalyzer/RelaxTestNamingSuppressor.cs index b9a4533bc9..cdd4d6bd2b 100644 --- a/tools-local/Microsoft.ML.InternalCodeAnalyzer/RelaxTestNamingSuppressor.cs +++ b/tools-local/Microsoft.ML.InternalCodeAnalyzer/RelaxTestNamingSuppressor.cs @@ -16,7 +16,7 @@ public sealed class RelaxTestNamingSuppressor : DiagnosticSuppressor private const string SuppressedDiagnosticId = "VSTHRD200"; private const string Justification = "Asynchronous test methods do not require the 'Async' suffix."; - private static readonly SuppressionDescriptor Rule = + internal static readonly SuppressionDescriptor Rule = new SuppressionDescriptor(Id, SuppressedDiagnosticId, Justification); public override ImmutableArray SupportedSuppressions { get; } = ImmutableArray.Create(Rule);