Skip to content

Commit 56f129c

Browse files
authored
Add codefix for MSTEST0022 (#3770)
1 parent bea492f commit 56f129c

17 files changed

+222
-5
lines changed

src/Analyzers/MSTest.Analyzers.CodeFixes/CodeFixResources.Designer.cs

+9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Analyzers/MSTest.Analyzers.CodeFixes/CodeFixResources.resx

+3
Original file line numberDiff line numberDiff line change
@@ -150,4 +150,7 @@
150150
<data name="ReplaceWithFailAssertionFix" xml:space="preserve">
151151
<value>Replace the assertion wit 'Assert.Fail()'</value>
152152
</data>
153+
<data name="ReplaceWithTestCleanuFix" xml:space="preserve">
154+
<value>Replace 'Dispose' with a TestCleanup method</value>
155+
</data>
153156
</root>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System.Collections.Immutable;
5+
using System.Composition;
6+
7+
using Analyzer.Utilities;
8+
9+
using Microsoft.CodeAnalysis;
10+
using Microsoft.CodeAnalysis.CodeActions;
11+
using Microsoft.CodeAnalysis.CodeFixes;
12+
using Microsoft.CodeAnalysis.CSharp;
13+
using Microsoft.CodeAnalysis.CSharp.Syntax;
14+
using Microsoft.CodeAnalysis.Editing;
15+
using Microsoft.CodeAnalysis.Text;
16+
17+
using MSTest.Analyzers.Helpers;
18+
19+
namespace MSTest.Analyzers;
20+
21+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ClassInitializeShouldBeValidFixer))]
22+
[Shared]
23+
public sealed class PreferTestCleanupOverDisposeFixer : CodeFixProvider
24+
{
25+
public override ImmutableArray<string> FixableDiagnosticIds { get; }
26+
= ImmutableArray.Create(DiagnosticIds.PreferTestCleanupOverDisposeRuleId);
27+
28+
public override FixAllProvider GetFixAllProvider()
29+
// See https://github.com/dotnet/roslyn/blob/main/docs/analyzers/FixAllProvider.md for more information on Fix All Providers
30+
=> WellKnownFixAllProviders.BatchFixer;
31+
32+
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
33+
{
34+
SyntaxNode root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
35+
36+
Diagnostic diagnostic = context.Diagnostics[0];
37+
TextSpan diagnosticSpan = diagnostic.Location.SourceSpan;
38+
39+
SyntaxToken syntaxToken = root.FindToken(diagnosticSpan.Start);
40+
if (syntaxToken.Parent is null)
41+
{
42+
return;
43+
}
44+
45+
// Find the method declaration identified by the diagnostic.
46+
MethodDeclarationSyntax methodDeclaration = syntaxToken.Parent.AncestorsAndSelf().OfType<MethodDeclarationSyntax>().First();
47+
48+
context.RegisterCodeFix(
49+
CodeAction.Create(
50+
CodeFixResources.ReplaceWithTestCleanuFix,
51+
c => ReplaceDisposeWithTestCleanupAsync(context.Document, methodDeclaration, c),
52+
nameof(TestMethodShouldBeValidCodeFixProvider)),
53+
diagnostic);
54+
}
55+
56+
private static async Task<Document> ReplaceDisposeWithTestCleanupAsync(Document document, MethodDeclarationSyntax methodDeclaration, CancellationToken cancellationToken)
57+
{
58+
DocumentEditor editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);
59+
60+
if (methodDeclaration.Parent is not TypeDeclarationSyntax newParent)
61+
{
62+
return editor.OriginalDocument;
63+
}
64+
65+
TypeDeclarationSyntax parentClass = newParent;
66+
67+
// Create a new method with the same body but named "TestCleanup"
68+
MethodDeclarationSyntax newMethodDeclaration = methodDeclaration
69+
.WithIdentifier(SyntaxFactory.Identifier("TestCleanup"))
70+
.WithAttributeLists(SyntaxFactory.SingletonList(CreateTestCleanupAttribute()));
71+
72+
newParent = newParent.ReplaceNode(methodDeclaration, newMethodDeclaration);
73+
74+
var newBaseTypes = newParent.BaseList?.Types
75+
.Where(type => !IsDisposableInterface(type))
76+
.ToList();
77+
78+
if (newBaseTypes?.Count != 0)
79+
{
80+
// If other interfaces remain, replace the base list with updated interfaces
81+
newParent = newParent.WithBaseList(SyntaxFactory.BaseList(SyntaxFactory.SeparatedList(newBaseTypes)));
82+
}
83+
else
84+
{
85+
// If no interfaces left, remove the base list entirely
86+
newParent = newParent.WithBaseList(null);
87+
}
88+
89+
editor.ReplaceNode(parentClass, newParent);
90+
91+
return editor.GetChangedDocument();
92+
}
93+
94+
private static bool IsDisposableInterface(BaseTypeSyntax baseTypeSyntax)
95+
{
96+
string typeName = baseTypeSyntax.Type.ToString();
97+
return typeName is "IDisposable" or "IAsyncDisposable";
98+
}
99+
100+
private static AttributeListSyntax CreateTestCleanupAttribute() =>
101+
// [TestCleanup] attribute
102+
SyntaxFactory.AttributeList(
103+
SyntaxFactory.SingletonSeparatedList(
104+
SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("TestCleanup"))));
105+
}

src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.cs.xlf

+5
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@
3737
<target state="new">Replace the assertion wit 'Assert.Fail()'</target>
3838
<note />
3939
</trans-unit>
40+
<trans-unit id="ReplaceWithTestCleanuFix">
41+
<source>Replace 'Dispose' with a TestCleanup method</source>
42+
<target state="new">Replace 'Dispose' with a TestCleanup method</target>
43+
<note />
44+
</trans-unit>
4045
<trans-unit id="TestClassShouldBeValidFix">
4146
<source>Fix test class signature</source>
4247
<target state="translated">Oprava podpisu testovací třídy</target>

src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.de.xlf

+5
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@
3737
<target state="new">Replace the assertion wit 'Assert.Fail()'</target>
3838
<note />
3939
</trans-unit>
40+
<trans-unit id="ReplaceWithTestCleanuFix">
41+
<source>Replace 'Dispose' with a TestCleanup method</source>
42+
<target state="new">Replace 'Dispose' with a TestCleanup method</target>
43+
<note />
44+
</trans-unit>
4045
<trans-unit id="TestClassShouldBeValidFix">
4146
<source>Fix test class signature</source>
4247
<target state="translated">Testklassensignatur korrigieren</target>

src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.es.xlf

+5
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@
3737
<target state="new">Replace the assertion wit 'Assert.Fail()'</target>
3838
<note />
3939
</trans-unit>
40+
<trans-unit id="ReplaceWithTestCleanuFix">
41+
<source>Replace 'Dispose' with a TestCleanup method</source>
42+
<target state="new">Replace 'Dispose' with a TestCleanup method</target>
43+
<note />
44+
</trans-unit>
4045
<trans-unit id="TestClassShouldBeValidFix">
4146
<source>Fix test class signature</source>
4247
<target state="translated">Corregir firma de clase de prueba</target>

src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.fr.xlf

+5
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@
3737
<target state="new">Replace the assertion wit 'Assert.Fail()'</target>
3838
<note />
3939
</trans-unit>
40+
<trans-unit id="ReplaceWithTestCleanuFix">
41+
<source>Replace 'Dispose' with a TestCleanup method</source>
42+
<target state="new">Replace 'Dispose' with a TestCleanup method</target>
43+
<note />
44+
</trans-unit>
4045
<trans-unit id="TestClassShouldBeValidFix">
4146
<source>Fix test class signature</source>
4247
<target state="translated">Correction de la signature de classe de test</target>

src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.it.xlf

+5
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@
3737
<target state="new">Replace the assertion wit 'Assert.Fail()'</target>
3838
<note />
3939
</trans-unit>
40+
<trans-unit id="ReplaceWithTestCleanuFix">
41+
<source>Replace 'Dispose' with a TestCleanup method</source>
42+
<target state="new">Replace 'Dispose' with a TestCleanup method</target>
43+
<note />
44+
</trans-unit>
4045
<trans-unit id="TestClassShouldBeValidFix">
4146
<source>Fix test class signature</source>
4247
<target state="translated">Correggi la firma della classe di test</target>

src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ja.xlf

+5
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@
3737
<target state="new">Replace the assertion wit 'Assert.Fail()'</target>
3838
<note />
3939
</trans-unit>
40+
<trans-unit id="ReplaceWithTestCleanuFix">
41+
<source>Replace 'Dispose' with a TestCleanup method</source>
42+
<target state="new">Replace 'Dispose' with a TestCleanup method</target>
43+
<note />
44+
</trans-unit>
4045
<trans-unit id="TestClassShouldBeValidFix">
4146
<source>Fix test class signature</source>
4247
<target state="translated">テスト クラスのシグネチャの修正</target>

src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ko.xlf

+5
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@
3737
<target state="new">Replace the assertion wit 'Assert.Fail()'</target>
3838
<note />
3939
</trans-unit>
40+
<trans-unit id="ReplaceWithTestCleanuFix">
41+
<source>Replace 'Dispose' with a TestCleanup method</source>
42+
<target state="new">Replace 'Dispose' with a TestCleanup method</target>
43+
<note />
44+
</trans-unit>
4045
<trans-unit id="TestClassShouldBeValidFix">
4146
<source>Fix test class signature</source>
4247
<target state="translated">테스트 클래스 서명 수정</target>

src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pl.xlf

+5
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@
3737
<target state="new">Replace the assertion wit 'Assert.Fail()'</target>
3838
<note />
3939
</trans-unit>
40+
<trans-unit id="ReplaceWithTestCleanuFix">
41+
<source>Replace 'Dispose' with a TestCleanup method</source>
42+
<target state="new">Replace 'Dispose' with a TestCleanup method</target>
43+
<note />
44+
</trans-unit>
4045
<trans-unit id="TestClassShouldBeValidFix">
4146
<source>Fix test class signature</source>
4247
<target state="translated">Napraw podpis klasy testowej</target>

src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pt-BR.xlf

+5
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@
3737
<target state="new">Replace the assertion wit 'Assert.Fail()'</target>
3838
<note />
3939
</trans-unit>
40+
<trans-unit id="ReplaceWithTestCleanuFix">
41+
<source>Replace 'Dispose' with a TestCleanup method</source>
42+
<target state="new">Replace 'Dispose' with a TestCleanup method</target>
43+
<note />
44+
</trans-unit>
4045
<trans-unit id="TestClassShouldBeValidFix">
4146
<source>Fix test class signature</source>
4247
<target state="translated">Correção da assinatura de classe do teste</target>

src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ru.xlf

+5
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@
3737
<target state="new">Replace the assertion wit 'Assert.Fail()'</target>
3838
<note />
3939
</trans-unit>
40+
<trans-unit id="ReplaceWithTestCleanuFix">
41+
<source>Replace 'Dispose' with a TestCleanup method</source>
42+
<target state="new">Replace 'Dispose' with a TestCleanup method</target>
43+
<note />
44+
</trans-unit>
4045
<trans-unit id="TestClassShouldBeValidFix">
4146
<source>Fix test class signature</source>
4247
<target state="translated">Исправить подпись класса теста</target>

src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.tr.xlf

+5
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@
3737
<target state="new">Replace the assertion wit 'Assert.Fail()'</target>
3838
<note />
3939
</trans-unit>
40+
<trans-unit id="ReplaceWithTestCleanuFix">
41+
<source>Replace 'Dispose' with a TestCleanup method</source>
42+
<target state="new">Replace 'Dispose' with a TestCleanup method</target>
43+
<note />
44+
</trans-unit>
4045
<trans-unit id="TestClassShouldBeValidFix">
4146
<source>Fix test class signature</source>
4247
<target state="translated">Test sınıfı imzasını düzeltme</target>

src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hans.xlf

+5
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@
3737
<target state="new">Replace the assertion wit 'Assert.Fail()'</target>
3838
<note />
3939
</trans-unit>
40+
<trans-unit id="ReplaceWithTestCleanuFix">
41+
<source>Replace 'Dispose' with a TestCleanup method</source>
42+
<target state="new">Replace 'Dispose' with a TestCleanup method</target>
43+
<note />
44+
</trans-unit>
4045
<trans-unit id="TestClassShouldBeValidFix">
4146
<source>Fix test class signature</source>
4247
<target state="translated">修复测试类签名</target>

src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hant.xlf

+5
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@
3737
<target state="new">Replace the assertion wit 'Assert.Fail()'</target>
3838
<note />
3939
</trans-unit>
40+
<trans-unit id="ReplaceWithTestCleanuFix">
41+
<source>Replace 'Dispose' with a TestCleanup method</source>
42+
<target state="new">Replace 'Dispose' with a TestCleanup method</target>
43+
<note />
44+
</trans-unit>
4045
<trans-unit id="TestClassShouldBeValidFix">
4146
<source>Fix test class signature</source>
4247
<target state="translated">修正測試類別簽章</target>

test/UnitTests/MSTest.Analyzers.UnitTests/PreferTestCleanupOverDisposeAnalyzerTests.cs

+40-5
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
using VerifyCS = MSTest.Analyzers.Test.CSharpCodeFixVerifier<
55
MSTest.Analyzers.PreferTestCleanupOverDisposeAnalyzer,
6-
Microsoft.CodeAnalysis.Testing.EmptyCodeFixProvider>;
6+
MSTest.Analyzers.PreferTestCleanupOverDisposeFixer>;
77

88
namespace MSTest.Analyzers.Test;
99

@@ -17,15 +17,36 @@ public async Task WhenTestClassHasDispose_Diagnostic()
1717
using Microsoft.VisualStudio.TestTools.UnitTesting;
1818
1919
[TestClass]
20-
public class MyTestClass : IDisposable
20+
public class MyTestClass : IDisposable, IMyInterface
2121
{
2222
public void [|Dispose|]()
2323
{
2424
}
25+
26+
[TestMethod]
27+
public void Test() {}
28+
}
29+
public interface IMyInterface { }
30+
""";
31+
string fixedCode = """
32+
using System;
33+
using Microsoft.VisualStudio.TestTools.UnitTesting;
34+
35+
[TestClass]
36+
public class MyTestClass : IMyInterface
37+
{
38+
[TestCleanup]
39+
public void TestCleanup()
40+
{
41+
}
42+
43+
[TestMethod]
44+
public void Test() {}
2545
}
46+
public interface IMyInterface { }
2647
""";
2748

28-
await VerifyCS.VerifyAnalyzerAsync(code);
49+
await VerifyCS.VerifyCodeFixAsync(code, fixedCode);
2950
}
3051

3152
public async Task WhenTestClassHasDisposeAsync_Diagnostic()
@@ -44,8 +65,22 @@ public class MyTestClass : IAsyncDisposable
4465
}
4566
}
4667
""";
68+
string fixedCode = """
69+
using System;
70+
using System.Threading.Tasks;
71+
using Microsoft.VisualStudio.TestTools.UnitTesting;
72+
73+
[TestClass]
74+
public class MyTestClass {
75+
[TestCleanup]
76+
public ValueTask TestCleanup()
77+
{
78+
return ValueTask.CompletedTask;
79+
}
80+
}
81+
""";
4782

48-
await VerifyCS.VerifyAnalyzerAsync(code);
83+
await VerifyCS.VerifyCodeFixAsync(code, fixedCode);
4984
}
5085

5186
public async Task WhenTestClassHasTestCleanup_NoDiagnostic()
@@ -84,6 +119,6 @@ public ValueTask MyTestCleanup()
84119
}
85120
""";
86121

87-
await VerifyCS.VerifyAnalyzerAsync(code);
122+
await VerifyCS.VerifyCodeFixAsync(code, code);
88123
}
89124
}

0 commit comments

Comments
 (0)