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
28 changes: 28 additions & 0 deletions TUnit.Analyzers.CodeFixers/NUnitMigrationCodeFixProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -437,13 +437,41 @@ private AttributeSyntax ConvertValueSourceAttribute(AttributeSyntax attribute)
{
return attributeName switch
{
"Test" or "Theory" => ConvertTestArguments(argumentList),
"TestCase" => ConvertTestCaseArguments(argumentList),
"TestCaseSource" => ConvertTestCaseSourceArguments(argumentList),
"Category" => ConvertCategoryArguments(argumentList),
_ => argumentList
};
}

private AttributeArgumentListSyntax? ConvertTestArguments(AttributeArgumentListSyntax argumentList)
{
// NUnit's [Test] attribute supports Description, Author, ExpectedResult, TestOf, etc.
// TUnit's [Test] attribute does not support these named properties.
// Strip all named arguments - they will be converted to [Property] attributes
// by NUnitTestCasePropertyRewriter (for Description and Author).
// ExpectedResult is not applicable for [Test] without parameters.

// If all arguments are named properties to strip, return null to remove argument list entirely
if (argumentList.Arguments.All(arg => arg.NameEquals != null))
{
return null;
}

// Keep only positional arguments (shouldn't normally exist for [Test])
var positionalArgs = argumentList.Arguments
.Where(arg => arg.NameEquals == null)
.ToList();

if (positionalArgs.Count == 0)
{
return null;
}

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

private AttributeArgumentListSyntax ConvertTestCaseArguments(AttributeArgumentListSyntax argumentList)
{
var newArgs = new List<AttributeArgumentSyntax>();
Expand Down
112 changes: 105 additions & 7 deletions TUnit.Analyzers.CodeFixers/NUnitTestCasePropertyRewriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,30 @@
namespace TUnit.Analyzers.CodeFixers;

/// <summary>
/// Extracts NUnit TestCase properties and converts them to TUnit attributes.
/// Maps: Description/Author Property, Explicit Explicit
/// Note: TestName DisplayName and Category Categories are now handled inline on [Arguments]
/// Extracts NUnit TestCase and Test properties and converts them to TUnit attributes.
/// 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
{
public override SyntaxNode? VisitMethodDeclaration(MethodDeclarationSyntax node)
{
// Get all TestCase attributes on this method
// Get all TestCase and Test attributes on this method
var testCaseAttributes = GetTestCaseAttributes(node);
var testAttributes = GetTestAttributes(node);

if (testCaseAttributes.Count == 0)
if (testCaseAttributes.Count == 0 && testAttributes.Count == 0)
{
return base.VisitMethodDeclaration(node);
}

// Extract properties from all TestCase attributes
// Extract properties from all TestCase and Test attributes
var properties = ExtractProperties(testCaseAttributes);
var testProperties = ExtractTestProperties(testAttributes);

// Merge Test properties into TestCase properties (Test properties take precedence if both exist)
MergeProperties(properties, testProperties);

// Generate new attribute lists for the extracted properties
var newAttributeLists = GeneratePropertyAttributes(properties, node.AttributeLists);
Expand Down Expand Up @@ -57,6 +62,26 @@ private List<AttributeSyntax> GetTestCaseAttributes(MethodDeclarationSyntax meth
return result;
}

private List<AttributeSyntax> GetTestAttributes(MethodDeclarationSyntax method)
{
var result = new List<AttributeSyntax>();

foreach (var attributeList in method.AttributeLists)
{
foreach (var attribute in attributeList.Attributes)
{
var name = attribute.Name.ToString();
if (name is "Test" or "NUnit.Framework.Test" or "TestAttribute" or "NUnit.Framework.TestAttribute"
or "Theory" or "NUnit.Framework.Theory" or "TheoryAttribute" or "NUnit.Framework.TheoryAttribute")
{
result.Add(attribute);
}
}
}

return result;
}

private TestCaseProperties ExtractProperties(List<AttributeSyntax> testCaseAttributes)
{
// Early return if no TestCase has named arguments (performance optimization)
Expand Down Expand Up @@ -132,6 +157,79 @@ private TestCaseProperties ExtractProperties(List<AttributeSyntax> testCaseAttri
return properties;
}

private TestCaseProperties ExtractTestProperties(List<AttributeSyntax> testAttributes)
{
// Extract Description, Author, etc. from [Test] attributes
// NUnit's [Test] supports: Description, Author, ExpectedResult, TestOf
var properties = new TestCaseProperties();

foreach (var attribute in testAttributes)
{
if (attribute.ArgumentList == null)
{
continue;
}

foreach (var arg in attribute.ArgumentList.Arguments)
{
var propertyName = arg.NameEquals?.Name.Identifier.Text;

switch (propertyName)
{
case "Description":
var descValue = GetStringValue(arg.Expression);
if (descValue != null)
{
properties.Descriptions.Add(descValue);
}
break;

case "Author":
var authorValue = GetStringValue(arg.Expression);
if (authorValue != null)
{
properties.Authors.Add(authorValue);
}
break;

// ExpectedResult on [Test] is used with return values, but TUnit doesn't support this
// Just skip it - the user will need to handle this manually or use assertions
case "ExpectedResult":
break;

// TestOf specifies the type being tested - convert to Property
case "TestOf":
// TestOf is a Type, not a string, so we need to handle it differently
// For now, skip it as it's rarely used
break;
}
}
}

return properties;
}

private void MergeProperties(TestCaseProperties target, TestCaseProperties source)
{
// Merge source properties into target
foreach (var desc in source.Descriptions)
{
target.Descriptions.Add(desc);
}
foreach (var author in source.Authors)
{
target.Authors.Add(author);
}
if (source.IsExplicit)
{
target.IsExplicit = true;
}
foreach (var reason in source.ExplicitReasons)
{
target.ExplicitReasons.Add(reason);
}
}

private string? GetStringValue(ExpressionSyntax expression)
{
// Only handle string literals - interpolated strings, concatenation, and const references
Expand All @@ -157,7 +255,7 @@ private SyntaxList<AttributeListSyntax> GeneratePropertyAttributes(
// Get indentation from existing attributes
var indentation = GetIndentation(leadingTrivia);

// Note: TestName DisplayName and Category Categories are now handled inline on [Arguments]
// 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
Expand Down
155 changes: 154 additions & 1 deletion TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,63 @@ public void MyMethod() { }
ConfigureNUnitTest
);
}


[Test]
public async Task NUnit_Static_Using_Directive_Removed()
{
// Issue #4422: Static using directives should also be removed
await CodeFixer.VerifyCodeFixAsync(
"""
using NUnit.Framework;
using static NUnit.Framework.Assert;

public class MyClass
{
{|#0:[Test]|}
public void MyMethod() { }
}
""",
Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0),
"""

public class MyClass
{
[Test]
public void MyMethod() { }
}
""",
ConfigureNUnitTest
);
}

[Test]
public async Task NUnit_Subnamespace_Using_Directive_Removed()
{
// Issue #4422: Subnamespace using directives should also be removed
await CodeFixer.VerifyCodeFixAsync(
"""
using NUnit.Framework;
using NUnit.Framework.Internal;

public class MyClass
{
{|#0:[Test]|}
public void MyMethod() { }
}
""",
Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0),
"""

public class MyClass
{
[Test]
public void MyMethod() { }
}
""",
ConfigureNUnitTest
);
}

[Test]
public async Task NUnit_SetUp_TearDown_Converted()
{
Expand Down Expand Up @@ -2711,6 +2767,103 @@ public void TestMethod()
);
}

[Test]
public async Task NUnit_Test_Attribute_With_Description_Converted()
{
// Issue #4426: [Test(Description = "...")] should be converted properly
await CodeFixer.VerifyCodeFixAsync(
"""
using NUnit.Framework;

public class MyClass
{
{|#0:[Test(Description = "This is a test description")]|}
public void TestMethod()
{
}
}
""",
Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0),
"""

public class MyClass
{
[Test]
[Property("Description", "This is a test description")]
public void TestMethod()
{
}
}
""",
ConfigureNUnitTest
);
}

[Test]
public async Task NUnit_Test_Attribute_With_Author_Converted()
{
// [Test(Author = "...")] should be converted to [Test] + [Property("Author", "...")]
await CodeFixer.VerifyCodeFixAsync(
"""
using NUnit.Framework;

public class MyClass
{
{|#0:[Test(Author = "John Doe")]|}
public void TestMethod()
{
}
}
""",
Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0),
"""

public class MyClass
{
[Test]
[Property("Author", "John Doe")]
public void TestMethod()
{
}
}
""",
ConfigureNUnitTest
);
}

[Test]
public async Task NUnit_Test_Attribute_With_Multiple_Properties_Converted()
{
// [Test(Description = "...", Author = "...")] should be converted properly
await CodeFixer.VerifyCodeFixAsync(
"""
using NUnit.Framework;

public class MyClass
{
{|#0:[Test(Description = "Test description", Author = "Jane Doe")]|}
public void TestMethod()
{
}
}
""",
Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0),
"""

public class MyClass
{
[Test]
[Property("Description", "Test description")]
[Property("Author", "Jane Doe")]
public void TestMethod()
{
}
}
""",
ConfigureNUnitTest
);
}

[Test]
public async Task NUnit_Method_With_Ref_Parameter_Not_Converted_To_Async()
{
Expand Down
Loading