-
-
Notifications
You must be signed in to change notification settings - Fork 108
feat: properly migrate NUnit TestCase properties to TUnit attributes #4209
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,298 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System.Linq; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using Microsoft.CodeAnalysis; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using Microsoft.CodeAnalysis.CSharp; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using Microsoft.CodeAnalysis.CSharp.Syntax; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public class NUnitTestCasePropertyRewriter : CSharpSyntaxRewriter | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public override SyntaxNode? VisitMethodDeclaration(MethodDeclarationSyntax node) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Get all TestCase attributes on this method | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var testCaseAttributes = GetTestCaseAttributes(node); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (testCaseAttributes.Count == 0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return base.VisitMethodDeclaration(node); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Extract properties from all TestCase attributes | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var properties = ExtractProperties(testCaseAttributes); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Generate new attribute lists for the extracted properties | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var newAttributeLists = GeneratePropertyAttributes(properties, node.AttributeLists); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // If we generated new attributes, add them to the method | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (newAttributeLists.Count > node.AttributeLists.Count) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return node.WithAttributeLists(newAttributeLists); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return base.VisitMethodDeclaration(node); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private List<AttributeSyntax> GetTestCaseAttributes(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 "TestCase" or "NUnit.Framework.TestCase" or "TestCaseAttribute" or "NUnit.Framework.TestCaseAttribute") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result.Add(attribute); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return result; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private TestCaseProperties ExtractProperties(List<AttributeSyntax> testCaseAttributes) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Early return if no TestCase has named arguments (performance optimization) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bool hasNamedArguments = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| foreach (var attribute in testCaseAttributes) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (attribute.ArgumentList?.Arguments.Any(a => a.NameEquals != null) == true) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hasNamedArguments = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!hasNamedArguments) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return new TestCaseProperties(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var properties = new TestCaseProperties(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| foreach (var attribute in testCaseAttributes) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (attribute.ArgumentList == null) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| foreach (var arg in attribute.ArgumentList.Arguments) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var propertyName = arg.NameEquals?.Name.Identifier.Text; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case "Explicit": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (arg.Expression is LiteralExpressionSyntax literal && | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| literal.IsKind(SyntaxKind.TrueLiteralExpression)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| properties.IsExplicit = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case "ExplicitReason": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var explicitReason = GetStringValue(arg.Expression); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (explicitReason != null) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| properties.ExplicitReasons.Add(explicitReason); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| properties.IsExplicit = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return properties; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private string? GetStringValue(ExpressionSyntax expression) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Only handle string literals - interpolated strings, concatenation, and const references | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // cannot be reliably migrated and should be handled manually by the user | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (expression is LiteralExpressionSyntax literal && | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| literal.IsKind(SyntaxKind.StringLiteralExpression)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return literal.Token.ValueText; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private SyntaxList<AttributeListSyntax> GeneratePropertyAttributes( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TestCaseProperties properties, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SyntaxList<AttributeListSyntax> existingAttributeLists) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var newLists = new List<AttributeListSyntax>(existingAttributeLists); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var leadingTrivia = existingAttributeLists.Count > 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? existingAttributeLists[0].GetLeadingTrivia() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : SyntaxTriviaList.Empty; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 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)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+182
to
+193
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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)); | |
| } | |
| var categoryAttributeLists = properties.Categories | |
| .Distinct() | |
| .Select(category => | |
| CreateAttributeList( | |
| SyntaxFactory.Attribute( | |
| SyntaxFactory.IdentifierName("Category"), | |
| SyntaxFactory.AttributeArgumentList( | |
| SyntaxFactory.SingletonSeparatedList( | |
| SyntaxFactory.AttributeArgument( | |
| SyntaxFactory.LiteralExpression( | |
| SyntaxKind.StringLiteralExpression, | |
| SyntaxFactory.Literal(category)))))), | |
| indentation)); | |
| newLists = newLists.AddRange(categoryAttributeLists); |
Copilot
AI
Jan 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.
| foreach (var trivia in triviaList) | |
| { | |
| if (trivia.IsKind(SyntaxKind.WhitespaceTrivia)) | |
| { | |
| return trivia; | |
| } | |
| var whitespaceTrivia = triviaList.FirstOrDefault(t => t.IsKind(SyntaxKind.WhitespaceTrivia)); | |
| if (whitespaceTrivia.IsKind(SyntaxKind.WhitespaceTrivia)) | |
| { | |
| return whitespaceTrivia; |
Copilot
AI
Jan 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to TUnit coding guidelines, use collection expressions (C# 12+). These HashSet initializations in the TestCaseProperties class should use collection expression syntax:
public HashSet<string> TestNames { get; } = new();should be:
public HashSet<string> TestNames { get; } = [];Apply the same change to all other HashSet properties (Categories, Descriptions, Authors, ExplicitReasons).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to TUnit coding guidelines, use collection expressions (C# 12+). This should be:
should be: