diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/CodeFixResources.resx b/src/Analyzers/MSTest.Analyzers.CodeFixes/CodeFixResources.resx index 6258c66e21..1d11cc494a 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/CodeFixResources.resx +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/CodeFixResources.resx @@ -216,4 +216,7 @@ Remove duplicate 'DataRow' + + Use MSTest 'Description' attribute instead + \ No newline at end of file diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/DoNotUseSystemDescriptionAttributeFixer.cs b/src/Analyzers/MSTest.Analyzers.CodeFixes/DoNotUseSystemDescriptionAttributeFixer.cs new file mode 100644 index 0000000000..a0f223ffb3 --- /dev/null +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/DoNotUseSystemDescriptionAttributeFixer.cs @@ -0,0 +1,112 @@ +// 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.Composition; + +using Analyzer.Utilities; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Simplification; + +using MSTest.Analyzers.Helpers; + +namespace MSTest.Analyzers; + +/// +/// Code fixer for . +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(DoNotUseSystemDescriptionAttributeFixer))] +[Shared] +public sealed class DoNotUseSystemDescriptionAttributeFixer : CodeFixProvider +{ + /// + public override ImmutableArray FixableDiagnosticIds { get; } + = ImmutableArray.Create(DiagnosticIds.DoNotUseSystemDescriptionAttributeRuleId); + + /// + public override FixAllProvider GetFixAllProvider() + // See https://github.com/dotnet/roslyn/blob/main/docs/analyzers/FixAllProvider.md for more information on Fix All Providers + => WellKnownFixAllProviders.BatchFixer; + + /// + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + SyntaxNode root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + + Diagnostic diagnostic = context.Diagnostics[0]; + SyntaxToken syntaxToken = root.FindToken(diagnostic.Location.SourceSpan.Start); + if (syntaxToken.Parent is null) + { + return; + } + + MethodDeclarationSyntax? methodDeclaration = syntaxToken.Parent.AncestorsAndSelf().OfType().FirstOrDefault(); + if (methodDeclaration is null) + { + return; + } + + context.RegisterCodeFix( + CodeAction.Create( + title: CodeFixResources.UseMSTestDescriptionAttributeInsteadFix, + createChangedDocument: c => ReplaceWithMSTestDescriptionAttributeAsync(context.Document, methodDeclaration, c), + equivalenceKey: nameof(DoNotUseSystemDescriptionAttributeFixer)), + diagnostic); + } + + private static async Task ReplaceWithMSTestDescriptionAttributeAsync(Document document, MethodDeclarationSyntax methodDeclaration, CancellationToken cancellationToken) + { + SemanticModel semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + INamedTypeSymbol? systemDescriptionAttributeSymbol = semanticModel.Compilation.GetTypeByMetadataName(WellKnownTypeNames.SystemDescriptionAttribute); + + if (systemDescriptionAttributeSymbol is null) + { + return document; + } + + AttributeSyntax? systemDescriptionAttribute = null; + + foreach (AttributeListSyntax attributeList in methodDeclaration.AttributeLists) + { + foreach (AttributeSyntax attribute in attributeList.Attributes) + { + if (semanticModel.GetSymbolInfo(attribute, cancellationToken).Symbol is IMethodSymbol { ContainingType: { } containingType } + && SymbolEqualityComparer.Default.Equals(containingType, systemDescriptionAttributeSymbol)) + { + systemDescriptionAttribute = attribute; + break; + } + } + + if (systemDescriptionAttribute is not null) + { + break; + } + } + + if (systemDescriptionAttribute is null) + { + return document; + } + + // Replace the System.ComponentModel.Description attribute name with the fully-qualified MSTest Description + // attribute name, annotated for simplification. The Simplifier will reduce it to the simple name if the + // MSTest namespace is already in scope and there is no ambiguity; otherwise it keeps the fully-qualified form. + NameSyntax msTestDescriptionName = SyntaxFactory.ParseName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingDescriptionAttribute) + .WithTriviaFrom(systemDescriptionAttribute.Name) + .WithAdditionalAnnotations(Simplifier.Annotation); + + AttributeSyntax newAttribute = systemDescriptionAttribute.WithName(msTestDescriptionName); + + SyntaxNode root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + Document updatedDocument = document.WithSyntaxRoot(root.ReplaceNode(systemDescriptionAttribute, newAttribute)); + + return await Simplifier.ReduceAsync(updatedDocument, cancellationToken: cancellationToken).ConfigureAwait(false); + } +} diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.cs.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.cs.xlf index 80dd2445d2..722afbbd15 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.cs.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.cs.xlf @@ -157,6 +157,11 @@ Místo řetězcového argumentu použijte vlastnost DisplayName + + Use MSTest 'Description' attribute instead + Use MSTest 'Description' attribute instead + + Use '{0}' Použít {0} diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.de.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.de.xlf index 9535d2a0dd..0a5a4b1fea 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.de.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.de.xlf @@ -157,6 +157,11 @@ Verwenden Sie die Eigenschaft „DisplayName“ anstelle eines Zeichenfolgenarguments. + + Use MSTest 'Description' attribute instead + Use MSTest 'Description' attribute instead + + Use '{0}' "{0}" verwenden diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.es.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.es.xlf index 798ff759f0..bd6aaeae95 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.es.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.es.xlf @@ -157,6 +157,11 @@ Usar la propiedad "DisplayName" en lugar del argumento de cadena + + Use MSTest 'Description' attribute instead + Use MSTest 'Description' attribute instead + + Use '{0}' Usar "{0}" diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.fr.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.fr.xlf index 0e6304383a..3925d6c798 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.fr.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.fr.xlf @@ -157,6 +157,11 @@ Utilisez la propriété « DisplayName » au lieu d’un argument de type chaîne + + Use MSTest 'Description' attribute instead + Use MSTest 'Description' attribute instead + + Use '{0}' Utiliser « {0} » diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.it.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.it.xlf index 16c7156765..30882dc38e 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.it.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.it.xlf @@ -157,6 +157,11 @@ Usare la proprietà 'DisplayName' invece di un argomento stringa + + Use MSTest 'Description' attribute instead + Use MSTest 'Description' attribute instead + + Use '{0}' Usa '{0}' diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ja.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ja.xlf index d954cfb4b6..a334f49ac1 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ja.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ja.xlf @@ -157,6 +157,11 @@ 文字列引数の代わりに 'DisplayName' プロパティを使用する + + Use MSTest 'Description' attribute instead + Use MSTest 'Description' attribute instead + + Use '{0}' '{0}' を使用します diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ko.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ko.xlf index 8124039f0f..4da44421c0 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ko.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ko.xlf @@ -157,6 +157,11 @@ 문자열 인수 대신 'DisplayName' 속성 사용 + + Use MSTest 'Description' attribute instead + Use MSTest 'Description' attribute instead + + Use '{0}' '{0}' 사용 diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pl.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pl.xlf index 4989c1549d..cf68f15258 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pl.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pl.xlf @@ -157,6 +157,11 @@ Użyj właściwości „DisplayName” zamiast argumentu ciągu + + Use MSTest 'Description' attribute instead + Use MSTest 'Description' attribute instead + + Use '{0}' Użyj „{0}” diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pt-BR.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pt-BR.xlf index adcaf792ad..a293b3c41e 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pt-BR.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.pt-BR.xlf @@ -157,6 +157,11 @@ Usar a propriedade "DisplayName" em vez do argumento de cadeia de caracteres + + Use MSTest 'Description' attribute instead + Use MSTest 'Description' attribute instead + + Use '{0}' Usar '{0}' diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ru.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ru.xlf index 1d407ee96e..47374accfd 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ru.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.ru.xlf @@ -157,6 +157,11 @@ Использовать свойство "DisplayName" вместо строкового аргумента + + Use MSTest 'Description' attribute instead + Use MSTest 'Description' attribute instead + + Use '{0}' Использовать "{0}" diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.tr.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.tr.xlf index 5dde45c0a7..4774553d44 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.tr.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.tr.xlf @@ -157,6 +157,11 @@ Dize bağımsız değişkeni yerine 'DisplayName' özelliğini kullanın + + Use MSTest 'Description' attribute instead + Use MSTest 'Description' attribute instead + + Use '{0}' '{0}' kullan diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hans.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hans.xlf index 8b7e0ad15d..1464045a21 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hans.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hans.xlf @@ -157,6 +157,11 @@ 使用属性‘DisplayName’替代字符串参数 + + Use MSTest 'Description' attribute instead + Use MSTest 'Description' attribute instead + + Use '{0}' 使用“{0}” diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hant.xlf b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hant.xlf index 4968d57da8..6c2e650c45 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hant.xlf +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/xlf/CodeFixResources.zh-Hant.xlf @@ -157,6 +157,11 @@ 使用 'DisplayName' 屬性取代字串引數 + + Use MSTest 'Description' attribute instead + Use MSTest 'Description' attribute instead + + Use '{0}' 使用 '{0}' diff --git a/test/UnitTests/MSTest.Analyzers.UnitTests/DoNotUseSystemDescriptionAttributeAnalyzerTests.cs b/test/UnitTests/MSTest.Analyzers.UnitTests/DoNotUseSystemDescriptionAttributeAnalyzerTests.cs index f927d76c1c..b8e88b2e19 100644 --- a/test/UnitTests/MSTest.Analyzers.UnitTests/DoNotUseSystemDescriptionAttributeAnalyzerTests.cs +++ b/test/UnitTests/MSTest.Analyzers.UnitTests/DoNotUseSystemDescriptionAttributeAnalyzerTests.cs @@ -1,9 +1,9 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using VerifyCS = MSTest.Analyzers.Test.CSharpCodeFixVerifier< MSTest.Analyzers.DoNotUseSystemDescriptionAttributeAnalyzer, - Microsoft.CodeAnalysis.Testing.EmptyCodeFixProvider>; + MSTest.Analyzers.DoNotUseSystemDescriptionAttributeFixer>; namespace MSTest.Analyzers.Test; @@ -11,7 +11,7 @@ namespace MSTest.Analyzers.Test; public sealed class DoNotUseSystemDescriptionAttributeAnalyzerTests { [TestMethod] - public async Task WhenTestMethodHasSystemDescriptionAttribute_Diagnostic() + public async Task WhenTestMethodHasFullyQualifiedSystemDescriptionAttribute_Diagnostic() { string code = """ using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -27,7 +27,87 @@ public class MyTestClass } """; - await VerifyCS.VerifyAnalyzerAsync(code); + string fixedCode = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + [Description("Description")] + public void MyTestMethod() + { + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, fixedCode); + } + + [TestMethod] + public async Task WhenTestMethodHasSystemDescriptionAttributeWithSystemComponentModelUsing_UsesFullyQualifiedMSTestDescription() + { + string code = """ + using System.ComponentModel; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + [System.ComponentModel.Description("Description")] + public void [|MyTestMethod|]() + { + } + } + """; + + string fixedCode = """ + using System.ComponentModel; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [TestMethod] + [Microsoft.VisualStudio.TestTools.UnitTesting.Description("Description")] + public void MyTestMethod() + { + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, fixedCode); + } + + [TestMethod] + public async Task WhenTestMethodHasSystemDescriptionAttributeWithoutUnitTestingUsing_UsesFullyQualifiedMSTestDescription() + { + string code = """ + [Microsoft.VisualStudio.TestTools.UnitTesting.TestClass] + public class MyTestClass + { + [Microsoft.VisualStudio.TestTools.UnitTesting.TestMethod] + [System.ComponentModel.Description("Description")] + public void [|MyTestMethod|]() + { + } + } + """; + + string fixedCode = """ + [Microsoft.VisualStudio.TestTools.UnitTesting.TestClass] + public class MyTestClass + { + [Microsoft.VisualStudio.TestTools.UnitTesting.TestMethod] + [Microsoft.VisualStudio.TestTools.UnitTesting.Description("Description")] + public void MyTestMethod() + { + } + } + """; + + await VerifyCS.VerifyCodeFixAsync(code, fixedCode); } [TestMethod]