Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
9 changes: 8 additions & 1 deletion TUnit.Analyzers.CodeFixers/Base/TwoPhase/ConversionPlan.cs
Original file line number Diff line number Diff line change
Expand Up @@ -483,10 +483,17 @@ public class InvocationReplacement : ConversionTarget
public class TheoryDataConversion : ConversionTarget
{
/// <summary>
/// The element type(s) from TheoryData&lt;T&gt; (e.g., "TimeSpan" from TheoryData&lt;TimeSpan&gt;)
/// The element type(s) from TheoryData&lt;T&gt; (e.g., "TimeSpan" from TheoryData&lt;TimeSpan&gt;
/// or "(string, int)" from TheoryData&lt;string, int&gt;)
/// </summary>
public required string ElementType { get; init; }

/// <summary>
/// Whether the TheoryData has multiple type arguments (e.g., TheoryData&lt;string, int&gt;).
/// When true, initializer expressions { val1, val2 } must be converted to tuple expressions (val1, val2).
/// </summary>
public bool IsMultiType { get; init; }

/// <summary>
/// Annotation for the GenericName (TheoryData&lt;T&gt;) type syntax to convert to IEnumerable&lt;T&gt;
/// </summary>
Expand Down
60 changes: 55 additions & 5 deletions TUnit.Analyzers.CodeFixers/Base/TwoPhase/MigrationTransformer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@

if (objectCreation?.Initializer != null)
{
// Build array type: T[]
// Build array type: T[] or (T1, T2)[]
var arrayType = SyntaxFactory.ArrayType(
SyntaxFactory.ParseTypeName(conversion.ElementType),
SyntaxFactory.SingletonList(
Expand All @@ -251,11 +251,21 @@
SyntaxFactory.Whitespace(" "));
}

// For multi-type TheoryData, convert complex initializer expressions
// { val1, val2 } to tuple expressions (val1, val2)
var expressions = objectCreation.Initializer.Expressions;
if (conversion.IsMultiType)
{
expressions = SyntaxFactory.SeparatedList(
expressions.Select(expr => ConvertToTupleExpression(expr)),
expressions.GetSeparators());
}

// Create array initializer from the collection initializer
var newInitializer = SyntaxFactory.InitializerExpression(
SyntaxKind.ArrayInitializerExpression,
openBrace,
objectCreation.Initializer.Expressions,
expressions,
objectCreation.Initializer.CloseBraceToken);

// Build the array creation expression
Expand All @@ -273,7 +283,7 @@
}
}

// Then, transform the type declaration from TheoryData<T> to IEnumerable<T>
// Then, transform the type declaration from TheoryData<T> to IEnumerable<T> or IEnumerable<(T1, T2)>
if (conversion.TypeAnnotation != null)
{
var genericName = currentRoot.DescendantNodes()
Expand All @@ -282,10 +292,27 @@

if (genericName != null)
{
TypeArgumentListSyntax typeArgList;
if (conversion.IsMultiType)
{
// TheoryData<T1, T2, ...> → IEnumerable<(T1, T2, ...)>
var tupleType = SyntaxFactory.TupleType(
SyntaxFactory.SeparatedList(
genericName.TypeArgumentList.Arguments.Select(
arg => SyntaxFactory.TupleElement(arg))));
typeArgList = SyntaxFactory.TypeArgumentList(
SyntaxFactory.SingletonSeparatedList<TypeSyntax>(tupleType));
}
else
{
// TheoryData<T> → IEnumerable<T>
typeArgList = SyntaxFactory.TypeArgumentList(
SyntaxFactory.SeparatedList(genericName.TypeArgumentList.Arguments));
}

var enumerableType = SyntaxFactory.GenericName(
SyntaxFactory.Identifier("IEnumerable"),
SyntaxFactory.TypeArgumentList(
SyntaxFactory.SeparatedList(genericName.TypeArgumentList.Arguments)))
typeArgList)
.WithLeadingTrivia(genericName.GetLeadingTrivia())
.WithTrailingTrivia(genericName.GetTrailingTrivia());

Expand All @@ -308,6 +335,29 @@
return currentRoot;
}

/// <summary>
/// Converts a complex initializer expression { val1, val2 } to a tuple expression (val1, val2).
/// For simple expressions (single-value), returns the expression unchanged.
/// </summary>
private static ExpressionSyntax ConvertToTupleExpression(ExpressionSyntax expression)
{
if (expression is not InitializerExpressionSyntax initializer
|| !initializer.IsKind(SyntaxKind.ComplexElementInitializerExpression))
{
return expression;
}

var arguments = initializer.Expressions.Select(
expr => SyntaxFactory.Argument(expr.WithoutLeadingTrivia().WithoutTrailingTrivia()));

var tupleExpression = SyntaxFactory.TupleExpression(
SyntaxFactory.SeparatedList(arguments))
.WithLeadingTrivia(initializer.GetLeadingTrivia())
.WithTrailingTrivia(initializer.GetTrailingTrivia());

return tupleExpression;
}

private CompilationUnitSyntax TransformAssertions(CompilationUnitSyntax root)
{
var currentRoot = root;
Expand Down Expand Up @@ -346,7 +396,7 @@
{
todoTrivia.Add(indentationTrivia);
}
todoTrivia.Add(SyntaxFactory.Comment(assertion.TodoComment));

Check warning on line 399 in TUnit.Analyzers.CodeFixers/Base/TwoPhase/MigrationTransformer.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

Possible null reference argument for parameter 'text' in 'SyntaxTrivia SyntaxFactory.Comment(string text)'.

Check warning on line 399 in TUnit.Analyzers.CodeFixers/Base/TwoPhase/MigrationTransformer.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (windows-latest)

Possible null reference argument for parameter 'text' in 'SyntaxTrivia SyntaxFactory.Comment(string text)'.

Check warning on line 399 in TUnit.Analyzers.CodeFixers/Base/TwoPhase/MigrationTransformer.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

Possible null reference argument for parameter 'text' in 'SyntaxTrivia SyntaxFactory.Comment(string text)'.
todoTrivia.Add(SyntaxFactory.EndOfLine("\n"));

// Combine TODO comment with existing leading trivia
Expand Down Expand Up @@ -426,7 +476,7 @@
SyntaxFactory.Identifier("Task"),
SyntaxFactory.TypeArgumentList(
SyntaxFactory.SingletonSeparatedList(
SyntaxFactory.ParseTypeName(change.OriginalReturnType))))

Check warning on line 479 in TUnit.Analyzers.CodeFixers/Base/TwoPhase/MigrationTransformer.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

Possible null reference argument for parameter 'text' in 'TypeSyntax SyntaxFactory.ParseTypeName(string text, int offset = 0, ParseOptions? options = null, bool consumeFullText = true)'.

Check warning on line 479 in TUnit.Analyzers.CodeFixers/Base/TwoPhase/MigrationTransformer.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

Possible null reference argument for parameter 'text' in 'TypeSyntax SyntaxFactory.ParseTypeName(string text, int offset = 0, ParseOptions? options = null, bool consumeFullText = true)'.
.WithTrailingTrivia(SyntaxFactory.Space);
newMethod = newMethod.WithReturnType(taskGenericType);
}
Expand Down Expand Up @@ -572,7 +622,7 @@
if (!string.IsNullOrEmpty(additional.Arguments))
{
additionalAttr = additionalAttr.WithArgumentList(
SyntaxFactory.ParseAttributeArgumentList(additional.Arguments));

Check warning on line 625 in TUnit.Analyzers.CodeFixers/Base/TwoPhase/MigrationTransformer.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

Possible null reference argument for parameter 'text' in 'AttributeArgumentListSyntax? SyntaxFactory.ParseAttributeArgumentList(string text, int offset = 0, ParseOptions? options = null, bool consumeFullText = true)'.

Check warning on line 625 in TUnit.Analyzers.CodeFixers/Base/TwoPhase/MigrationTransformer.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

Possible null reference argument for parameter 'text' in 'AttributeArgumentListSyntax? SyntaxFactory.ParseAttributeArgumentList(string text, int offset = 0, ParseOptions? options = null, bool consumeFullText = true)'.
}

// Use only indentation for additional attributes (no blank lines)
Expand Down Expand Up @@ -1030,7 +1080,7 @@
if (!string.IsNullOrEmpty(addition.NewReturnType))
{
newMethod = newMethod.WithReturnType(
SyntaxFactory.ParseTypeName(addition.NewReturnType)

Check warning on line 1083 in TUnit.Analyzers.CodeFixers/Base/TwoPhase/MigrationTransformer.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

Possible null reference argument for parameter 'text' in 'TypeSyntax SyntaxFactory.ParseTypeName(string text, int offset = 0, ParseOptions? options = null, bool consumeFullText = true)'.

Check warning on line 1083 in TUnit.Analyzers.CodeFixers/Base/TwoPhase/MigrationTransformer.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

Possible null reference argument for parameter 'text' in 'TypeSyntax SyntaxFactory.ParseTypeName(string text, int offset = 0, ParseOptions? options = null, bool consumeFullText = true)'.
.WithTrailingTrivia(SyntaxFactory.Space));
}

Expand Down
14 changes: 10 additions & 4 deletions TUnit.Analyzers.CodeFixers/TwoPhase/XUnitTwoPhaseAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1833,11 +1833,16 @@ protected override CompilationUnitSyntax AnalyzeTheoryData(CompilationUnitSyntax
{
try
{
// Get the type argument
var typeArg = originalGeneric.TypeArgumentList.Arguments.FirstOrDefault();
if (typeArg == null) continue;
// Get the type arguments
var typeArgs = originalGeneric.TypeArgumentList.Arguments;
if (typeArgs.Count == 0) continue;

var elementType = typeArg.ToString();
// For single type: TheoryData<T> → IEnumerable<T>, element type is T
// For multi type: TheoryData<T1, T2> → IEnumerable<(T1, T2)>, element type is (T1, T2)
var isMultiType = typeArgs.Count > 1;
var elementType = isMultiType
? $"({string.Join(", ", typeArgs.Select(t => t.ToString()))})"
: typeArgs[0].ToString();

// Create annotations for both the type and the object creation
var typeAnnotation = new SyntaxAnnotation("TUnitMigration", Guid.NewGuid().ToString());
Expand All @@ -1846,6 +1851,7 @@ protected override CompilationUnitSyntax AnalyzeTheoryData(CompilationUnitSyntax
var conversion = new TheoryDataConversion
{
ElementType = elementType,
IsMultiType = isMultiType,
TypeAnnotation = typeAnnotation,
CreationAnnotation = creationAnnotation,
OriginalText = originalGeneric.ToString()
Expand Down
95 changes: 95 additions & 0 deletions TUnit.Analyzers.Tests/XUnitMigrationAnalyzerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,101 @@ public class MyClass
);
}

[Test]
public async Task TheoryData_MultiType_Is_Flagged()
{
await Verifier
.VerifyAnalyzerAsync(
"""
{|#0:using System;

public class MyClass
{
public static TheoryData<string, int> MyData
{
get
{
var data = new TheoryData<string, int>
{
{ "a", 1 },
{ "b", 2 }
};
return data;
}
}
}|}
""",
ConfigureXUnitTest,
Verifier.Diagnostic(Rules.XunitMigration).WithLocation(0)
);
}

[Test]
public async Task TheoryData_MultiType_Can_Be_Converted()
{
await CodeFixer
.VerifyCodeFixAsync(
"""
{|#0:using Xunit;

public class MyClass
{
public static readonly TheoryData<string, int> Items = new()
{
{ "a", 1 },
{ "b", 2 }
};
}|}
""",
Verifier.Diagnostic(Rules.XunitMigration).WithLocation(0),
"""

public class MyClass
{
public static readonly IEnumerable<(string, int)> Items = new (string, int)[]
{
("a", 1),
("b", 2)
};
}
""",
ConfigureXUnitTest
);
}

[Test]
public async Task TheoryData_ThreeTypes_Can_Be_Converted()
{
await CodeFixer
.VerifyCodeFixAsync(
"""
{|#0:using Xunit;

public class MyClass
{
public static readonly TheoryData<string, int, bool> Items = new()
{
{ "a", 1, true },
{ "b", 2, false }
};
}|}
""",
Verifier.Diagnostic(Rules.XunitMigration).WithLocation(0),
"""

public class MyClass
{
public static readonly IEnumerable<(string, int, bool)> Items = new (string, int, bool)[]
{
("a", 1, true),
("b", 2, false)
};
}
""",
ConfigureXUnitTest
);
}

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