Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
24b7c83
feat: add NUnit and MSTest migration analyzers and code fixers
thomhurst Sep 23, 2025
841edef
docs: add NUnit and MSTest migration guides
thomhurst Sep 24, 2025
fbb4798
fix: add missing using directive for MigrationHelpers in code fixers
thomhurst Sep 24, 2025
ee4c39e
Merge branch 'main' into feature/nunit-migrate
thomhurst Sep 24, 2025
70c484c
fix: add missing diagnostic verifications for MSTest and NUnit migrat…
thomhurst Sep 24, 2025
0a9cee1
fix: address critical issues in migration analyzers
claude[bot] Sep 24, 2025
89f3daa
Merge branch 'main' into feature/nunit-migrate
thomhurst Oct 2, 2025
bea79c6
Merge branch 'main' into feature/nunit-migrate
thomhurst Oct 8, 2025
e42ba6c
Merge branch 'main' into feature/nunit-migrate
thomhurst Oct 10, 2025
f5936d8
refactor: enhance attribute analysis to return location for diagnostics
thomhurst Oct 10, 2025
30d8f60
refactor: include brackets location in attribute analysis
thomhurst Oct 11, 2025
7a80e69
refactor: reorganize migration process and enhance framework type det…
thomhurst Oct 11, 2025
368aaab
refactor: update assertion conversion methods to return ExpressionSyn…
thomhurst Oct 12, 2025
f7e8772
Merge branch 'main' into feature/nunit-migrate
thomhurst Oct 12, 2025
6fbf564
refactor: update assertion methods to return ExpressionSyntax for imp…
thomhurst Oct 12, 2025
0d6cf5c
refactor: enhance migration analyzers and tests for TUnit compatibility
thomhurst Oct 12, 2025
e1b5c54
refactor: add solution transform to normalize line endings for cross-…
thomhurst Oct 12, 2025
bcf7007
refactor: replace DefaultVerifier with LineEndingNormalizingVerifier …
thomhurst Oct 12, 2025
066c33f
refactor: update NormalizeLineEndings method to ensure platform-nativ…
thomhurst Oct 12, 2025
70ce7b6
refactor: update NormalizeLineEndings method to ensure CRLF line endi…
thomhurst Oct 12, 2025
1905377
refactor: format document in BaseMigrationCodeFixProvider to ensure c…
thomhurst Oct 12, 2025
00da093
refactor: remove unused NUnitMigrationAnalyzerTests methods for clean…
thomhurst Oct 12, 2025
e53508b
fix: normalize line endings to CRLF after formatting for cross-platfo…
thomhurst Oct 12, 2025
8468d0d
refactor: remove unnecessary whitespace in MSTestMigrationAnalyzerTes…
thomhurst Oct 12, 2025
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
Binary file modified .serena/cache/csharp/document_symbols_cache_v23-06-25.pkl
Binary file not shown.
79 changes: 79 additions & 0 deletions TUnit.Analyzers.CodeFixers/Base/AssertionRewriter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace TUnit.Analyzers.CodeFixers.Base;

public abstract class AssertionRewriter : CSharpSyntaxRewriter
{
protected readonly SemanticModel SemanticModel;
protected abstract string FrameworkName { get; }

protected AssertionRewriter(SemanticModel semanticModel)
{
SemanticModel = semanticModel;
}

public override SyntaxNode? VisitInvocationExpression(InvocationExpressionSyntax node)
{
var convertedAssertion = ConvertAssertionIfNeeded(node);
if (convertedAssertion != null)
{
return convertedAssertion;
}

return base.VisitInvocationExpression(node);
}

protected abstract ExpressionSyntax? ConvertAssertionIfNeeded(InvocationExpressionSyntax invocation);

protected ExpressionSyntax CreateTUnitAssertion(
string methodName,
ExpressionSyntax actualValue,
params ArgumentSyntax[] additionalArguments)
{
// Create Assert.That(actualValue)
var assertThatInvocation = SyntaxFactory.InvocationExpression(
SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
SyntaxFactory.IdentifierName("Assert"),
SyntaxFactory.IdentifierName("That")
),
SyntaxFactory.ArgumentList(
SyntaxFactory.SingletonSeparatedList(
SyntaxFactory.Argument(actualValue)
)
)
);

// Create Assert.That(actualValue).MethodName(args)
var methodAccess = SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
assertThatInvocation,
SyntaxFactory.IdentifierName(methodName)
);

var arguments = additionalArguments.Length > 0
? SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(additionalArguments))
: SyntaxFactory.ArgumentList();

var fullInvocation = SyntaxFactory.InvocationExpression(methodAccess, arguments);

// Now wrap the entire thing in await: await Assert.That(actualValue).MethodName(args)
return SyntaxFactory.AwaitExpression(fullInvocation);
}

protected bool IsFrameworkAssertion(InvocationExpressionSyntax invocation)
{
var symbol = SemanticModel.GetSymbolInfo(invocation).Symbol;
if (symbol is not IMethodSymbol methodSymbol)
{
return false;
}

var namespaceName = methodSymbol.ContainingNamespace?.ToDisplayString() ?? "";
return IsFrameworkAssertionNamespace(namespaceName);
}

protected abstract bool IsFrameworkAssertionNamespace(string namespaceName);
}
169 changes: 169 additions & 0 deletions TUnit.Analyzers.CodeFixers/Base/BaseMigrationCodeFixProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
using System.Collections.Immutable;
using System.Composition;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Formatting;
using TUnit.Analyzers.Migrators.Base;

namespace TUnit.Analyzers.CodeFixers.Base;

public abstract class BaseMigrationCodeFixProvider : CodeFixProvider
{
protected abstract string FrameworkName { get; }
protected abstract string DiagnosticId { get; }
protected abstract string CodeFixTitle { get; }

public sealed override ImmutableArray<string> FixableDiagnosticIds =>
ImmutableArray.Create(DiagnosticId);

public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;

public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
var diagnostic = context.Diagnostics.First();

context.RegisterCodeFix(
CodeAction.Create(
title: CodeFixTitle,
createChangedDocument: async c => await ConvertCodeAsync(context.Document, root, c),
equivalenceKey: CodeFixTitle),
diagnostic);
}

protected async Task<Document> ConvertCodeAsync(Document document, SyntaxNode? root, CancellationToken cancellationToken)
{
if (root is not CompilationUnitSyntax compilationUnit)
{
return document;
}

var semanticModel = await document.GetSemanticModelAsync(cancellationToken);
if (semanticModel == null)
{
return document;
}

try
{
// Convert assertions FIRST (while semantic model still matches the syntax tree)
var assertionRewriter = CreateAssertionRewriter(semanticModel);
compilationUnit = (CompilationUnitSyntax)assertionRewriter.Visit(compilationUnit);

// Framework-specific conversions (also use semantic model while it still matches)
compilationUnit = ApplyFrameworkSpecificConversions(compilationUnit, semanticModel);

// Remove unnecessary base classes and interfaces
var baseTypeRewriter = CreateBaseTypeRewriter(semanticModel);
compilationUnit = (CompilationUnitSyntax)baseTypeRewriter.Visit(compilationUnit);

// Update lifecycle methods
var lifecycleRewriter = CreateLifecycleRewriter();
compilationUnit = (CompilationUnitSyntax)lifecycleRewriter.Visit(compilationUnit);

// Convert attributes
var attributeRewriter = CreateAttributeRewriter();
compilationUnit = (CompilationUnitSyntax)attributeRewriter.Visit(compilationUnit);

// Remove framework usings and add TUnit usings (do this LAST)
compilationUnit = MigrationHelpers.RemoveFrameworkUsings(compilationUnit, FrameworkName);
compilationUnit = MigrationHelpers.AddTUnitUsings(compilationUnit);

// Format the document first
var documentWithNewRoot = document.WithSyntaxRoot(compilationUnit);
var formattedDocument = await Formatter.FormatAsync(documentWithNewRoot, options: null, cancellationToken).ConfigureAwait(false);

// Normalize all line endings to CRLF for cross-platform consistency
var text = await formattedDocument.GetTextAsync(cancellationToken).ConfigureAwait(false);
var normalizedContent = text.ToString().Replace("\r\n", "\n").Replace("\n", "\r\n");
var normalizedText = Microsoft.CodeAnalysis.Text.SourceText.From(normalizedContent, text.Encoding);

return formattedDocument.WithText(normalizedText);
}
catch
{
// If any transformation fails, return the original document unchanged
return document;
}
}

protected abstract AttributeRewriter CreateAttributeRewriter();
protected abstract CSharpSyntaxRewriter CreateAssertionRewriter(SemanticModel semanticModel);
protected abstract CSharpSyntaxRewriter CreateBaseTypeRewriter(SemanticModel semanticModel);
protected abstract CSharpSyntaxRewriter CreateLifecycleRewriter();
protected abstract CompilationUnitSyntax ApplyFrameworkSpecificConversions(CompilationUnitSyntax compilationUnit, SemanticModel semanticModel);
}

public abstract class AttributeRewriter : CSharpSyntaxRewriter
{
protected abstract string FrameworkName { get; }

public override SyntaxNode? VisitAttributeList(AttributeListSyntax node)
{
var attributes = new List<AttributeSyntax>();

foreach (var attribute in node.Attributes)
{
var attributeName = MigrationHelpers.GetAttributeName(attribute);

if (MigrationHelpers.ShouldRemoveAttribute(attributeName, FrameworkName))
{
continue;
}

if (MigrationHelpers.IsHookAttribute(attributeName, FrameworkName))
{
var hookAttributeList = MigrationHelpers.ConvertHookAttribute(attribute, FrameworkName);
if (hookAttributeList != null)
{
// Preserve only the leading trivia (indentation) from the original node
// and strip any trailing trivia to prevent extra blank lines
return hookAttributeList
.WithLeadingTrivia(node.GetLeadingTrivia())
.WithTrailingTrivia(node.GetTrailingTrivia());
}
}

var convertedAttribute = ConvertAttribute(attribute);
if (convertedAttribute != null)
{
attributes.Add(convertedAttribute);
}
}

return attributes.Count > 0
? node.WithAttributes(SyntaxFactory.SeparatedList(attributes))
: null;
}

protected virtual AttributeSyntax? ConvertAttribute(AttributeSyntax attribute)
{
var attributeName = MigrationHelpers.GetAttributeName(attribute);
var newName = MigrationHelpers.ConvertTestAttributeName(attributeName, FrameworkName);

if (newName == null)
{
return null;
}

if (newName == attributeName && !IsFrameworkAttribute(attributeName))
{
return attribute;
}

var newAttribute = SyntaxFactory.Attribute(SyntaxFactory.IdentifierName(newName));

if (attribute.ArgumentList != null && attribute.ArgumentList.Arguments.Count > 0)
{
newAttribute = newAttribute.WithArgumentList(ConvertAttributeArguments(attribute.ArgumentList, attributeName));
}

return newAttribute;
}

protected abstract bool IsFrameworkAttribute(string attributeName);
protected abstract AttributeArgumentListSyntax? ConvertAttributeArguments(AttributeArgumentListSyntax argumentList, string attributeName);
}
Loading
Loading