diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/AssertToAssertFixerHelpers.cs b/src/Analyzers/MSTest.Analyzers.CodeFixes/AssertToAssertFixerHelpers.cs new file mode 100644 index 0000000000..69db23bcec --- /dev/null +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/AssertToAssertFixerHelpers.cs @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Analyzer.Utilities; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace MSTest.Analyzers.CodeFixes; + +/// +/// Shared helpers for code fixers that migrate legacy MSTest assert types to Assert. +/// +internal static class AssertToAssertFixerHelpers +{ + /// + /// Runs the common diagnostic-property/invocation-shape validation and, when applicable, registers a + /// CodeAction that delegates to . + /// + /// The code-fix context. + /// Diagnostic property key holding the replacement Assert method name. + /// Localized format string used to build the code action title. + /// Optional diagnostic property key holding an additional fix discriminator. When , no fixKind is read. + /// Callback that rewrites the invocation. + internal static async Task RegisterCodeFixAsync( + CodeFixContext context, + string properAssertMethodNamePropertyKey, + string codeActionTitleFormat, + string? fixKindPropertyKey, + Func> fixAssertAsync) + { + SyntaxNode root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + + Diagnostic diagnostic = context.Diagnostics[0]; + if (!diagnostic.Properties.TryGetValue(properAssertMethodNamePropertyKey, out string? properAssertMethodName) + || properAssertMethodName is null) + { + return; + } + + string? fixKind = null; + if (fixKindPropertyKey is not null + && (!diagnostic.Properties.TryGetValue(fixKindPropertyKey, out fixKind) || fixKind is null)) + { + return; + } + + if (root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true) is not InvocationExpressionSyntax invocationExpr) + { + return; + } + + // We only know how to rewrite `.(...)`-shaped invocations. `using static` and similar + // shapes fall through without a fix; the diagnostic still surfaces so the user can migrate manually. + if (invocationExpr.Expression is not MemberAccessExpressionSyntax) + { + return; + } + + string title = string.Format(CultureInfo.InvariantCulture, codeActionTitleFormat, properAssertMethodName); + var action = CodeAction.Create( + title: title, + createChangedDocument: ct => fixAssertAsync(context.Document, invocationExpr, properAssertMethodName, fixKind, ct), + equivalenceKey: title); + + context.RegisterCodeFix(action, diagnostic); + } + + /// + /// Replaces an invocation node in the document root. + /// + internal static async Task ReplaceInvocationAsync( + Document document, + InvocationExpressionSyntax invocationExpr, + InvocationExpressionSyntax newInvocationExpr, + CancellationToken cancellationToken) + { + SyntaxNode root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + return document.WithSyntaxRoot(root.ReplaceNode(invocationExpr, newInvocationExpr)); + } +} diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/CollectionAssertToAssertFixer.cs b/src/Analyzers/MSTest.Analyzers.CodeFixes/CollectionAssertToAssertFixer.cs index 439c75c3a1..66c247d0e5 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/CollectionAssertToAssertFixer.cs +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/CollectionAssertToAssertFixer.cs @@ -7,7 +7,6 @@ using Analyzer.Utilities; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -35,39 +34,23 @@ public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; /// - public override async Task RegisterCodeFixesAsync(CodeFixContext context) - { - SyntaxNode root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); - - Diagnostic diagnostic = context.Diagnostics[0]; - if (!diagnostic.Properties.TryGetValue(AssertToAssertAnalyzerHelpers.ProperAssertMethodNameKey, out string? properAssertMethodName) - || properAssertMethodName is null - || !diagnostic.Properties.TryGetValue(CollectionAssertToAssertAnalyzer.FixKindKey, out string? fixKind) - || fixKind is null) - { - return; - } - - if (root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true) is not InvocationExpressionSyntax invocationExpr) - { - return; - } - - // We only know how to rewrite `.(...)`-shaped invocations. `using static` and similar - // shapes fall through without a fix; the diagnostic still surfaces so the user can migrate manually. - if (invocationExpr.Expression is not MemberAccessExpressionSyntax) - { - return; - } - - string title = string.Format(CultureInfo.InvariantCulture, CodeFixResources.CollectionAssertToAssertTitle, properAssertMethodName); - var action = CodeAction.Create( - title: title, - createChangedDocument: ct => FixCollectionAssertAsync(context.Document, invocationExpr, properAssertMethodName, fixKind, ct), - equivalenceKey: title); - - context.RegisterCodeFix(action, diagnostic); - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + => AssertToAssertFixerHelpers.RegisterCodeFixAsync( + context, + AssertToAssertAnalyzerHelpers.ProperAssertMethodNameKey, + CodeFixResources.CollectionAssertToAssertTitle, + CollectionAssertToAssertAnalyzer.FixKindKey, + FixAssertAsync); + + private static Task FixAssertAsync( + Document document, + InvocationExpressionSyntax invocationExpr, + string properAssertMethodName, + string? fixKind, + CancellationToken cancellationToken) + => fixKind is null + ? Task.FromResult(document) + : FixCollectionAssertAsync(document, invocationExpr, properAssertMethodName, fixKind, cancellationToken); private static async Task FixCollectionAssertAsync( Document document, @@ -95,8 +78,6 @@ private static async Task FixCollectionAssertAsync( return document; } - SyntaxNode root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - // FixKindInstanceOfType prefers the generic `Assert.AreAllOfType(coll, ...)` overload // when the `expectedType` argument is a `typeof(T)` literal. When it isn't (e.g. a // runtime `Type` expression like `GetType()` or a local variable), we fall back to the @@ -149,7 +130,7 @@ private static async Task FixCollectionAssertAsync( .WithLeadingTrivia(invocationExpr.GetLeadingTrivia()) .WithAdditionalAnnotations(Formatter.Annotation); - return document.WithSyntaxRoot(root.ReplaceNode(invocationExpr, newInvocationExpr)); + return await AssertToAssertFixerHelpers.ReplaceInvocationAsync(document, invocationExpr, newInvocationExpr, cancellationToken).ConfigureAwait(false); } private static bool TryGetArgumentsByOrdinal(IInvocationOperation invocationOperation, out ArgumentSyntax[]? orderedArguments) diff --git a/src/Analyzers/MSTest.Analyzers.CodeFixes/StringAssertToAssertFixer.cs b/src/Analyzers/MSTest.Analyzers.CodeFixes/StringAssertToAssertFixer.cs index f5e03f22b0..c21d7592ac 100644 --- a/src/Analyzers/MSTest.Analyzers.CodeFixes/StringAssertToAssertFixer.cs +++ b/src/Analyzers/MSTest.Analyzers.CodeFixes/StringAssertToAssertFixer.cs @@ -4,10 +4,7 @@ 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; @@ -33,31 +30,21 @@ public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; /// - public override async Task RegisterCodeFixesAsync(CodeFixContext context) - { - SyntaxNode? root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); - - Diagnostic diagnostic = context.Diagnostics[0]; - if (!diagnostic.Properties.TryGetValue(AssertToAssertAnalyzerHelpers.ProperAssertMethodNameKey, out string? properAssertMethodName) - || properAssertMethodName == null) - { - return; - } - - if (root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true) is not InvocationExpressionSyntax invocationExpressionSyntax) - { - return; - } - - // Register a code fix that will invoke the fix operation. - string title = string.Format(CultureInfo.InvariantCulture, CodeFixResources.StringAssertToAssertTitle, properAssertMethodName); - var action = CodeAction.Create( - title: title, - createChangedDocument: ct => FixStringAssertAsync(context.Document, invocationExpressionSyntax, properAssertMethodName, ct), - equivalenceKey: title); - - context.RegisterCodeFix(action, diagnostic); - } + public override Task RegisterCodeFixesAsync(CodeFixContext context) + => AssertToAssertFixerHelpers.RegisterCodeFixAsync( + context, + AssertToAssertAnalyzerHelpers.ProperAssertMethodNameKey, + CodeFixResources.StringAssertToAssertTitle, + fixKindPropertyKey: null, + FixAssertAsync); + + private static Task FixAssertAsync( + Document document, + InvocationExpressionSyntax invocationExpr, + string properAssertMethodName, + string? fixKind, + CancellationToken cancellationToken) + => FixStringAssertAsync(document, invocationExpr, properAssertMethodName, cancellationToken); private static async Task FixStringAssertAsync( Document document, @@ -77,8 +64,6 @@ private static async Task FixStringAssertAsync( return document; } - SyntaxNode root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - // Create new argument list with swapped first two arguments // We keep the existing separators in case there is trivia attached to them. var newArguments = arguments.GetWithSeparators().ToList(); @@ -97,6 +82,6 @@ private static async Task FixStringAssertAsync( // Preserve leading trivia (including empty lines) from the original invocation newInvocationExpr = newInvocationExpr.WithLeadingTrivia(invocationExpr.GetLeadingTrivia()); - return document.WithSyntaxRoot(root.ReplaceNode(invocationExpr, newInvocationExpr)); + return await AssertToAssertFixerHelpers.ReplaceInvocationAsync(document, invocationExpr, newInvocationExpr, cancellationToken).ConfigureAwait(false); } }