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);
}
}