Skip to content
100 changes: 93 additions & 7 deletions src/Analyzers/MSTest.Analyzers.CodeFixes/AddTestClassFixer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,36 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
// Find the type declaration identified by the diagnostic.
TypeDeclarationSyntax declaration = syntaxToken.Parent.AncestorsAndSelf().OfType<TypeDeclarationSyntax>().First();

// Register a code action that will invoke the fix.
context.RegisterCodeFix(
CodeAction.Create(
title: CodeFixResources.AddTestClassFix,
createChangedDocument: c => AddTestClassAttributeAsync(context.Document, declaration, c),
equivalenceKey: $"{nameof(AddTestClassFixer)}_{diagnostic.Id}"),
diagnostic);
// For structs and record structs, we need to change them to classes/record classes since [TestClass] cannot be applied to structs
if (declaration is StructDeclarationSyntax)
{
context.RegisterCodeFix(
CodeAction.Create(
title: CodeFixResources.ChangeStructToClassAndAddTestClassFix,
createChangedDocument: c => ChangeStructToClassAndAddTestClassAttributeAsync(context.Document, declaration, c),
equivalenceKey: $"{nameof(AddTestClassFixer)}_ChangeStructToClass_{diagnostic.Id}"),
diagnostic);
}
else if (declaration is RecordDeclarationSyntax recordDeclaration
&& recordDeclaration.ClassOrStructKeyword.IsKind(SyntaxKind.StructKeyword))
{
context.RegisterCodeFix(
CodeAction.Create(
title: CodeFixResources.ChangeStructToClassAndAddTestClassFix,
createChangedDocument: c => ChangeRecordStructToRecordClassAndAddTestClassAttributeAsync(context.Document, recordDeclaration, c),
equivalenceKey: $"{nameof(AddTestClassFixer)}_ChangeRecordStructToClass_{diagnostic.Id}"),
diagnostic);
}
else
{
// For classes and record classes, just add the [TestClass] attribute
context.RegisterCodeFix(
CodeAction.Create(
title: CodeFixResources.AddTestClassFix,
createChangedDocument: c => AddTestClassAttributeAsync(context.Document, declaration, c),
equivalenceKey: $"{nameof(AddTestClassFixer)}_{diagnostic.Id}"),
diagnostic);
}
}

private static async Task<Document> AddTestClassAttributeAsync(Document document, TypeDeclarationSyntax typeDeclaration, CancellationToken cancellationToken)
Expand All @@ -75,4 +98,67 @@ private static async Task<Document> AddTestClassAttributeAsync(Document document
SyntaxNode newRoot = editor.GetChangedRoot();
return document.WithSyntaxRoot(newRoot);
}

private static async Task<Document> ChangeStructToClassAndAddTestClassAttributeAsync(Document document, TypeDeclarationSyntax structDeclaration, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
DocumentEditor editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);

// Create the [TestClass] attribute
AttributeSyntax testClassAttribute = SyntaxFactory.Attribute(SyntaxFactory.ParseName("TestClass"));
AttributeListSyntax attributeList = SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(testClassAttribute));

// Convert struct to class
ClassDeclarationSyntax classDeclaration = SyntaxFactory.ClassDeclaration(structDeclaration.Identifier)
.WithModifiers(structDeclaration.Modifiers)
.WithTypeParameterList(structDeclaration.TypeParameterList)
.WithConstraintClauses(structDeclaration.ConstraintClauses)
.WithBaseList(structDeclaration.BaseList)
.WithMembers(structDeclaration.Members)
.WithAttributeLists(structDeclaration.AttributeLists.Add(attributeList))
.WithLeadingTrivia(structDeclaration.GetLeadingTrivia())
.WithTrailingTrivia(structDeclaration.GetTrailingTrivia());

editor.ReplaceNode(structDeclaration, classDeclaration);

SyntaxNode newRoot = editor.GetChangedRoot();
return document.WithSyntaxRoot(newRoot);
}

private static async Task<Document> ChangeRecordStructToRecordClassAndAddTestClassAttributeAsync(Document document, RecordDeclarationSyntax recordStructDeclaration, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
DocumentEditor editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);

// Create the [TestClass] attribute
AttributeSyntax testClassAttribute = SyntaxFactory.Attribute(SyntaxFactory.ParseName("TestClass"));
AttributeListSyntax attributeList = SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(testClassAttribute));

// Filter out readonly modifier since it's not valid for record classes
SyntaxTokenList filteredModifiers = SyntaxFactory.TokenList(
recordStructDeclaration.Modifiers.Where(modifier => !modifier.IsKind(SyntaxKind.ReadOnlyKeyword)));

// Convert record struct to record class by creating a new RecordDeclarationSyntax
// We need to create a new record declaration instead of just changing the keyword
RecordDeclarationSyntax recordClassDeclaration = SyntaxFactory.RecordDeclaration(
recordStructDeclaration.Keyword,
recordStructDeclaration.Identifier)
.WithModifiers(filteredModifiers)
.WithTypeParameterList(recordStructDeclaration.TypeParameterList)
.WithParameterList(recordStructDeclaration.ParameterList)
.WithBaseList(recordStructDeclaration.BaseList)
.WithConstraintClauses(recordStructDeclaration.ConstraintClauses)
.WithOpenBraceToken(recordStructDeclaration.OpenBraceToken)
.WithMembers(recordStructDeclaration.Members)
.WithCloseBraceToken(recordStructDeclaration.CloseBraceToken)
.WithSemicolonToken(recordStructDeclaration.SemicolonToken)
.WithAttributeLists(recordStructDeclaration.AttributeLists.Add(attributeList))
.WithLeadingTrivia(recordStructDeclaration.GetLeadingTrivia())
.WithTrailingTrivia(recordStructDeclaration.GetTrailingTrivia());

editor.ReplaceNode(recordStructDeclaration, recordClassDeclaration);

SyntaxNode newRoot = editor.GetChangedRoot();
return document.WithSyntaxRoot(newRoot);
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@
<data name="AddTestClassFix" xml:space="preserve">
<value>Add '[TestClass]'</value>
</data>
<data name="ChangeStructToClassAndAddTestClassFix" xml:space="preserve">
<value>Change to 'class' and add '[TestClass]'</value>
</data>
<data name="TestMethodShouldBeValidFix" xml:space="preserve">
<value>Fix test method signature</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
<target state="translated">Změnit přístupnost metody na private</target>
<note />
</trans-unit>
<trans-unit id="ChangeStructToClassAndAddTestClassFix">
<source>Change to 'class' and add '[TestClass]'</source>
<target state="new">Change to 'class' and add '[TestClass]'</target>
<note />
</trans-unit>
<trans-unit id="FixAssertionArgsOrder">
<source>Fix actual/expected arguments order</source>
<target state="translated">Opravit pořadí skutečných/očekávaných argumentů</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
<target state="translated">Methodenzugriff auf „privat“ ändern</target>
<note />
</trans-unit>
<trans-unit id="ChangeStructToClassAndAddTestClassFix">
<source>Change to 'class' and add '[TestClass]'</source>
<target state="new">Change to 'class' and add '[TestClass]'</target>
<note />
</trans-unit>
<trans-unit id="FixAssertionArgsOrder">
<source>Fix actual/expected arguments order</source>
<target state="translated">Reihenfolge der tatsächlichen/erwarteten Argumente korrigieren</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
<target state="translated">Cambiar la accesibilidad del método a "private"</target>
<note />
</trans-unit>
<trans-unit id="ChangeStructToClassAndAddTestClassFix">
<source>Change to 'class' and add '[TestClass]'</source>
<target state="new">Change to 'class' and add '[TestClass]'</target>
<note />
</trans-unit>
<trans-unit id="FixAssertionArgsOrder">
<source>Fix actual/expected arguments order</source>
<target state="translated">Corregir el orden de los argumentos reales o esperados</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
<target state="translated">Remplacer l’accessibilité de la méthode par « privé »</target>
<note />
</trans-unit>
<trans-unit id="ChangeStructToClassAndAddTestClassFix">
<source>Change to 'class' and add '[TestClass]'</source>
<target state="new">Change to 'class' and add '[TestClass]'</target>
<note />
</trans-unit>
<trans-unit id="FixAssertionArgsOrder">
<source>Fix actual/expected arguments order</source>
<target state="translated">Corriger l’ordre des arguments réels/attendus</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
<target state="translated">Modifica l'accessibilità del metodo in 'privato'</target>
<note />
</trans-unit>
<trans-unit id="ChangeStructToClassAndAddTestClassFix">
<source>Change to 'class' and add '[TestClass]'</source>
<target state="new">Change to 'class' and add '[TestClass]'</target>
<note />
</trans-unit>
<trans-unit id="FixAssertionArgsOrder">
<source>Fix actual/expected arguments order</source>
<target state="translated">Correggi l'ordine degli argomenti effettivi/previsti</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
<target state="translated">メソッドのアクセシビリティを 'private' に変更する</target>
<note />
</trans-unit>
<trans-unit id="ChangeStructToClassAndAddTestClassFix">
<source>Change to 'class' and add '[TestClass]'</source>
<target state="new">Change to 'class' and add '[TestClass]'</target>
<note />
</trans-unit>
<trans-unit id="FixAssertionArgsOrder">
<source>Fix actual/expected arguments order</source>
<target state="translated">実際の引数と予想される引数の順序を修正する</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
<target state="translated">메서드 접근성 '비공개'로 변경하기</target>
<note />
</trans-unit>
<trans-unit id="ChangeStructToClassAndAddTestClassFix">
<source>Change to 'class' and add '[TestClass]'</source>
<target state="new">Change to 'class' and add '[TestClass]'</target>
<note />
</trans-unit>
<trans-unit id="FixAssertionArgsOrder">
<source>Fix actual/expected arguments order</source>
<target state="translated">실제/예상 인수 순서 수정</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
<target state="translated">Zmień dostępność metody na „private” (prywatna)</target>
<note />
</trans-unit>
<trans-unit id="ChangeStructToClassAndAddTestClassFix">
<source>Change to 'class' and add '[TestClass]'</source>
<target state="new">Change to 'class' and add '[TestClass]'</target>
<note />
</trans-unit>
<trans-unit id="FixAssertionArgsOrder">
<source>Fix actual/expected arguments order</source>
<target state="translated">Napraw rzeczywistą/oczekiwaną kolejność argumentów</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
<target state="translated">Alterar a acessibilidade do método para 'privado'</target>
<note />
</trans-unit>
<trans-unit id="ChangeStructToClassAndAddTestClassFix">
<source>Change to 'class' and add '[TestClass]'</source>
<target state="new">Change to 'class' and add '[TestClass]'</target>
<note />
</trans-unit>
<trans-unit id="FixAssertionArgsOrder">
<source>Fix actual/expected arguments order</source>
<target state="translated">Corrigir ordem de argumentos real/esperada</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
<target state="translated">Изменить доступность метода на "private"</target>
<note />
</trans-unit>
<trans-unit id="ChangeStructToClassAndAddTestClassFix">
<source>Change to 'class' and add '[TestClass]'</source>
<target state="new">Change to 'class' and add '[TestClass]'</target>
<note />
</trans-unit>
<trans-unit id="FixAssertionArgsOrder">
<source>Fix actual/expected arguments order</source>
<target state="translated">Исправить порядок фактических и ожидаемых аргументов</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
<target state="translated">Yöntem erişilebilirliğini ‘özel’ olarak değiştir</target>
<note />
</trans-unit>
<trans-unit id="ChangeStructToClassAndAddTestClassFix">
<source>Change to 'class' and add '[TestClass]'</source>
<target state="new">Change to 'class' and add '[TestClass]'</target>
<note />
</trans-unit>
<trans-unit id="FixAssertionArgsOrder">
<source>Fix actual/expected arguments order</source>
<target state="translated">Fiili/beklenen bağımsız değişken sırasını düzelt</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
<target state="translated">将方法可访问性更改为“private”</target>
<note />
</trans-unit>
<trans-unit id="ChangeStructToClassAndAddTestClassFix">
<source>Change to 'class' and add '[TestClass]'</source>
<target state="new">Change to 'class' and add '[TestClass]'</target>
<note />
</trans-unit>
<trans-unit id="FixAssertionArgsOrder">
<source>Fix actual/expected arguments order</source>
<target state="translated">修复实际/预期参数顺序</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
<target state="translated">將方法協助工具變更為 'private'</target>
<note />
</trans-unit>
<trans-unit id="ChangeStructToClassAndAddTestClassFix">
<source>Change to 'class' and add '[TestClass]'</source>
<target state="new">Change to 'class' and add '[TestClass]'</target>
<note />
</trans-unit>
<trans-unit id="FixAssertionArgsOrder">
<source>Fix actual/expected arguments order</source>
<target state="translated">修正實際/預期的引數順序</target>
Expand Down
4 changes: 2 additions & 2 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.

4 changes: 2 additions & 2 deletions src/Analyzers/MSTest.Analyzers/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -448,10 +448,10 @@ The type declaring these methods should also respect the following rules:
<value>Type containing '[TestMethod]' should be marked with '[TestClass]'</value>
</data>
<data name="TypeContainingTestMethodShouldBeATestClassDescription" xml:space="preserve">
<value>Type contaning '[TestMethod]' should be marked with '[TestClass]', otherwise the test method will be silently ignored.</value>
<value>Type containing '[TestMethod]' should be marked with '[TestClass]', otherwise the test method will be silently ignored.</value>
</data>
<data name="TypeContainingTestMethodShouldBeATestClassMessageFormat" xml:space="preserve">
<value>Class '{0}' contains test methods and should be marked with '[TestClass]'</value>
<value>Type '{0}' contains test methods and should be marked with '[TestClass]'</value>
</data>
<data name="DoNotUseSystemDescriptionAttributeTitle" xml:space="preserve">
<value>'System.ComponentModel.DescriptionAttribute' has no effect on test methods</value>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public override void Initialize(AnalysisContext context)
private static void AnalyzeSymbol(SymbolAnalysisContext context, INamedTypeSymbol testClassAttributeSymbol, INamedTypeSymbol testMethodAttributeSymbol)
{
var namedTypeSymbol = (INamedTypeSymbol)context.Symbol;
if (namedTypeSymbol.TypeKind != TypeKind.Class
if ((namedTypeSymbol.TypeKind != TypeKind.Class && namedTypeSymbol.TypeKind != TypeKind.Struct)
|| namedTypeSymbol.IsAbstract)
{
return;
Expand Down
8 changes: 4 additions & 4 deletions src/Analyzers/MSTest.Analyzers/xlf/Resources.cs.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -750,13 +750,13 @@ Typ deklarující tyto metody by měl také respektovat následující pravidla:
<note />
</trans-unit>
<trans-unit id="TypeContainingTestMethodShouldBeATestClassDescription">
<source>Type contaning '[TestMethod]' should be marked with '[TestClass]', otherwise the test method will be silently ignored.</source>
<target state="translated">Typ obsahující [TestMethod] by měl mít označení [TestClass], jinak bude testovací metoda bez jakéhokoli upozornění ignorována.</target>
<source>Type containing '[TestMethod]' should be marked with '[TestClass]', otherwise the test method will be silently ignored.</source>
<target state="needs-review-translation">Typ obsahující [TestMethod] by měl mít označení [TestClass], jinak bude testovací metoda bez jakéhokoli upozornění ignorována.</target>
<note />
</trans-unit>
<trans-unit id="TypeContainingTestMethodShouldBeATestClassMessageFormat">
<source>Class '{0}' contains test methods and should be marked with '[TestClass]'</source>
<target state="translated">Třída {0} obsahuje testovací metody a měla by mít označení [TestClass].</target>
<source>Type '{0}' contains test methods and should be marked with '[TestClass]'</source>
<target state="translated">Typ {0} obsahuje testovací metody a měl by mít označení [TestClass].</target>
<note />
</trans-unit>
<trans-unit id="TypeContainingTestMethodShouldBeATestClassTitle">
Expand Down
Loading