Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Shared helpers for code fixers that migrate legacy MSTest assert types to <c>Assert</c>.
/// </summary>
internal static class AssertToAssertFixerHelpers
{
/// <summary>
/// Runs the common diagnostic-property/invocation-shape validation and, when applicable, registers a
/// <c>CodeAction</c> that delegates to <paramref name="fixAssertAsync"/>.
/// </summary>
/// <param name="context">The code-fix context.</param>
/// <param name="properAssertMethodNamePropertyKey">Diagnostic property key holding the replacement <c>Assert</c> method name.</param>
/// <param name="codeActionTitleFormat">Localized format string used to build the code action title.</param>
/// <param name="fixKindPropertyKey">Optional diagnostic property key holding an additional fix discriminator. When <see langword="null"/>, no <c>fixKind</c> is read.</param>
/// <param name="fixAssertAsync">Callback that rewrites the invocation.</param>
internal static async Task RegisterCodeFixAsync(
CodeFixContext context,
string properAssertMethodNamePropertyKey,
string codeActionTitleFormat,
string? fixKindPropertyKey,
Func<Document, InvocationExpressionSyntax, string, string?, CancellationToken, Task<Document>> 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 `<expr>.<member>(...)`-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);
}

/// <summary>
/// Replaces an invocation node in the document root.
/// </summary>
internal static async Task<Document> ReplaceInvocationAsync(
Document document,
InvocationExpressionSyntax invocationExpr,
InvocationExpressionSyntax newInvocationExpr,
CancellationToken cancellationToken)
{
SyntaxNode root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
return document.WithSyntaxRoot(root.ReplaceNode(invocationExpr, newInvocationExpr));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -35,39 +34,23 @@ public sealed override FixAllProvider GetFixAllProvider()
=> WellKnownFixAllProviders.BatchFixer;

/// <inheritdoc />
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 `<expr>.<member>(...)`-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<Document> 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<Document> FixCollectionAssertAsync(
Document document,
Expand Down Expand Up @@ -95,8 +78,6 @@ private static async Task<Document> FixCollectionAssertAsync(
return document;
}

SyntaxNode root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);

// FixKindInstanceOfType prefers the generic `Assert.AreAllOfType<T>(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
Expand Down Expand Up @@ -149,7 +130,7 @@ private static async Task<Document> 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -33,31 +30,21 @@ public sealed override FixAllProvider GetFixAllProvider()
=> WellKnownFixAllProviders.BatchFixer;

/// <inheritdoc />
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<Document> FixAssertAsync(
Document document,
InvocationExpressionSyntax invocationExpr,
string properAssertMethodName,
string? fixKind,
CancellationToken cancellationToken)
=> FixStringAssertAsync(document, invocationExpr, properAssertMethodName, cancellationToken);

private static async Task<Document> FixStringAssertAsync(
Document document,
Expand All @@ -77,8 +64,6 @@ private static async Task<Document> 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();
Expand All @@ -97,6 +82,6 @@ private static async Task<Document> 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);
}
}
Loading