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
55 changes: 48 additions & 7 deletions TUnit.Analyzers.CodeFixers/NUnitExpectedResultRewriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@
if (isDirectReturn)
{
// Direct return - use the expression directly
returnExpression = returnStatements[0].Expression;

Check warning on line 168 in TUnit.Analyzers.CodeFixers/NUnitExpectedResultRewriter.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (windows-latest)

Converting null literal or possible null value to non-nullable type.

// Build new body with all statements except the return, plus assertion
var statementsWithoutReturn = new List<StatementSyntax>();
Expand All @@ -177,7 +177,7 @@
}
}

var assertStatement = CreateAssertStatement(returnExpression);

Check warning on line 180 in TUnit.Analyzers.CodeFixers/NUnitExpectedResultRewriter.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (windows-latest)

Possible null reference argument for parameter 'actualExpression' in 'ExpressionStatementSyntax NUnitExpectedResultRewriter.CreateAssertStatement(ExpressionSyntax actualExpression)'.
statementsWithoutReturn.Add(assertStatement);

return SyntaxFactory.Block(statementsWithoutReturn);
Expand Down Expand Up @@ -439,7 +439,11 @@
return attribute;
}

var newArgs = new List<AttributeArgumentSyntax>();
// Separate positional and named arguments to ensure correct ordering:
// Positional args → ExpectedResult → Named properties (DisplayName, Skip, Categories)
var positionalArgs = new List<AttributeArgumentSyntax>();
var namedArgs = new List<AttributeArgumentSyntax>();
var categories = new List<ExpressionSyntax>();
ExpressionSyntax? expectedValue = null;

foreach (var arg in attribute.ArgumentList.Arguments)
Expand All @@ -453,7 +457,7 @@
else if (namedProperty == null)
{
// Positional argument - keep it
newArgs.Add(arg);
positionalArgs.Add(arg);
}
else if (namedProperty == "Ignore" || namedProperty == "IgnoreReason")
{
Expand All @@ -462,27 +466,64 @@
SyntaxFactory.NameEquals(SyntaxFactory.IdentifierName("Skip")),
null,
arg.Expression);
newArgs.Add(skipArg);
namedArgs.Add(skipArg);
}
else if (namedProperty is "TestName" or "Category" or "Description" or "Author" or "Explicit" or "ExplicitReason")
else if (namedProperty == "TestName")
{
// Map NUnit's TestName to TUnit's DisplayName inline on [Arguments]
var displayNameArg = SyntaxFactory.AttributeArgument(
SyntaxFactory.NameEquals(SyntaxFactory.IdentifierName("DisplayName")),
null,
arg.Expression);
namedArgs.Add(displayNameArg);
}
else if (namedProperty == "Category")
{
// Collect categories to create a Categories array
categories.Add(arg.Expression);
}
else if (namedProperty is "Description" or "Author" or "Explicit" or "ExplicitReason")
{
// These properties are converted to separate TUnit attributes by NUnitTestCasePropertyRewriter:
// TestName → [DisplayName], Category → [Category], Description/Author → [Property], Explicit → [Explicit]
// Description/Author → [Property], Explicit → [Explicit]
// Skip them here - they don't belong in the [Arguments] attribute
}
// Other named arguments are preserved as-is (they might be TUnit-compatible)
else
{
newArgs.Add(arg);
namedArgs.Add(arg);
}
}

// Add expected value as last positional argument
// Build final argument list in correct order:
// 1. Positional arguments
// 2. ExpectedResult (as positional argument)
// 3. Named properties (DisplayName, Skip, Categories)
var newArgs = new List<AttributeArgumentSyntax>(positionalArgs);

// Add expected value as last positional argument (before named properties)
if (expectedValue != null)
{
newArgs.Add(SyntaxFactory.AttributeArgument(expectedValue));
}

// Add named arguments
newArgs.AddRange(namedArgs);

// Add Categories array if any categories were found
if (categories.Count > 0)
{
var categoriesArray = SyntaxFactory.CollectionExpression(
SyntaxFactory.SeparatedList(
categories.Select(c => (CollectionElementSyntax)SyntaxFactory.ExpressionElement(c))));

var categoriesArg = SyntaxFactory.AttributeArgument(
SyntaxFactory.NameEquals(SyntaxFactory.IdentifierName("Categories")),
null,
categoriesArray);
newArgs.Add(categoriesArg);
}

var newAttribute = attribute.WithArgumentList(
SyntaxFactory.AttributeArgumentList(SyntaxFactory.SeparatedList(newArgs)));

Expand Down
33 changes: 31 additions & 2 deletions TUnit.Analyzers.CodeFixers/NUnitMigrationCodeFixProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ protected override bool IsFrameworkAttribute(string attributeName)
private AttributeArgumentListSyntax ConvertTestCaseArguments(AttributeArgumentListSyntax argumentList)
{
var newArgs = new List<AttributeArgumentSyntax>();
var categories = new List<ExpressionSyntax>();

foreach (var arg in argumentList.Arguments)
{
Expand All @@ -103,10 +104,24 @@ private AttributeArgumentListSyntax ConvertTestCaseArguments(AttributeArgumentLi
arg.Expression);
newArgs.Add(skipArg);
}
else if (namedProperty is "TestName" or "Category" or "Description" or "Author" or "Explicit" or "ExplicitReason")
else if (namedProperty == "TestName")
{
// Map NUnit's TestName to TUnit's DisplayName inline on [Arguments]
var displayNameArg = SyntaxFactory.AttributeArgument(
SyntaxFactory.NameEquals(SyntaxFactory.IdentifierName("DisplayName")),
null,
arg.Expression);
newArgs.Add(displayNameArg);
}
else if (namedProperty == "Category")
{
// Collect categories to create a Categories array
categories.Add(arg.Expression);
}
else if (namedProperty is "Description" or "Author" or "Explicit" or "ExplicitReason")
{
// These properties are converted to separate TUnit attributes by NUnitTestCasePropertyRewriter:
// TestName → [DisplayName], Category → [Category], Description/Author → [Property], Explicit → [Explicit]
// Description/Author → [Property], Explicit → [Explicit]
// Skip them here - they don't belong in the [Arguments] attribute
}
else if (namedProperty == "ExpectedResult")
Expand All @@ -121,6 +136,20 @@ private AttributeArgumentListSyntax ConvertTestCaseArguments(AttributeArgumentLi
}
}

// Add Categories array if any categories were found
if (categories.Count > 0)
{
var categoriesArray = SyntaxFactory.CollectionExpression(
SyntaxFactory.SeparatedList(
categories.Select(c => (CollectionElementSyntax)SyntaxFactory.ExpressionElement(c))));

var categoriesArg = SyntaxFactory.AttributeArgument(
SyntaxFactory.NameEquals(SyntaxFactory.IdentifierName("Categories")),
null,
categoriesArray);
newArgs.Add(categoriesArg);
}

return SyntaxFactory.AttributeArgumentList(SyntaxFactory.SeparatedList(newArgs));
}

Expand Down
70 changes: 8 additions & 62 deletions TUnit.Analyzers.CodeFixers/NUnitTestCasePropertyRewriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ namespace TUnit.Analyzers.CodeFixers;

/// <summary>
/// Extracts NUnit TestCase properties and converts them to TUnit attributes.
/// Maps: TestName → DisplayName, Category → Category, Description/Author → Property, Explicit → Explicit
/// Maps: Description/Author → Property, Explicit → Explicit
/// Note: TestName → DisplayName and Category → Categories are now handled inline on [Arguments]
/// by NUnitAttributeRewriter, so we don't generate separate attributes for those.
/// </summary>
public class NUnitTestCasePropertyRewriter : CSharpSyntaxRewriter
{
Expand Down Expand Up @@ -88,21 +90,8 @@ private TestCaseProperties ExtractProperties(List<AttributeSyntax> testCaseAttri

switch (propertyName)
{
case "TestName":
var testNameValue = GetStringValue(arg.Expression);
if (testNameValue != null)
{
properties.TestNames.Add(testNameValue);
}
break;

case "Category":
var categoryValue = GetStringValue(arg.Expression);
if (categoryValue != null)
{
properties.Categories.Add(categoryValue);
}
break;
// Note: TestName and Category are now handled inline on [Arguments] by NUnitAttributeRewriter
// so we don't need to extract them here anymore.

case "Description":
var descValue = GetStringValue(arg.Expression);
Expand Down Expand Up @@ -168,29 +157,8 @@ private SyntaxList<AttributeListSyntax> GeneratePropertyAttributes(
// Get indentation from existing attributes
var indentation = GetIndentation(leadingTrivia);

// DisplayName from TestName (use first if multiple, or try to create pattern)
if (properties.TestNames.Count > 0)
{
var displayNameAttr = CreateDisplayNameAttribute(properties.TestNames);
if (displayNameAttr != null)
{
newLists.Add(CreateAttributeList(displayNameAttr, indentation));
}
}

// Category - add all unique categories
foreach (var category in properties.Categories.Distinct())
{
var categoryAttr = SyntaxFactory.Attribute(
SyntaxFactory.IdentifierName("Category"),
SyntaxFactory.AttributeArgumentList(
SyntaxFactory.SingletonSeparatedList(
SyntaxFactory.AttributeArgument(
SyntaxFactory.LiteralExpression(
SyntaxKind.StringLiteralExpression,
SyntaxFactory.Literal(category))))));
newLists.Add(CreateAttributeList(categoryAttr, indentation));
}
// Note: TestName → DisplayName and Category → Categories are now handled inline on [Arguments]
// by NUnitAttributeRewriter.ConvertTestCaseArguments, so we don't generate separate attributes here.

// Description - use Property attribute
if (properties.Descriptions.Count > 0)
Expand Down Expand Up @@ -226,27 +194,6 @@ private SyntaxList<AttributeListSyntax> GeneratePropertyAttributes(
return SyntaxFactory.List(newLists);
}

private AttributeSyntax? CreateDisplayNameAttribute(HashSet<string> testNames)
{
// Only generate DisplayName if there's exactly one unique TestName
// Multiple different TestNames cannot be represented by a single DisplayName attribute
if (testNames.Count != 1)
{
return null;
}

var displayName = testNames.First();

return SyntaxFactory.Attribute(
SyntaxFactory.IdentifierName("DisplayName"),
SyntaxFactory.AttributeArgumentList(
SyntaxFactory.SingletonSeparatedList(
SyntaxFactory.AttributeArgument(
SyntaxFactory.LiteralExpression(
SyntaxKind.StringLiteralExpression,
SyntaxFactory.Literal(displayName))))));
}

private AttributeSyntax CreatePropertyAttribute(string name, string value)
{
return SyntaxFactory.Attribute(
Expand Down Expand Up @@ -288,8 +235,7 @@ private SyntaxTrivia GetIndentation(SyntaxTriviaList triviaList)

private class TestCaseProperties
{
public HashSet<string> TestNames { get; } = new();
public HashSet<string> Categories { get; } = new();
// Note: TestNames and Categories are now handled inline on [Arguments] by NUnitAttributeRewriter
public HashSet<string> Descriptions { get; } = new();
public HashSet<string> Authors { get; } = new();
public bool IsExplicit { get; set; }
Expand Down
Loading
Loading