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
7 changes: 7 additions & 0 deletions src/TUnit.Core.SourceGenerator/CodeGenerationHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,13 @@ public static string GenerateTypeInfo(ITypeSymbol typeSymbol)
// Fall through to regular typeof() handling
}

if (typeSymbol is IArrayTypeSymbol && ContainsTypeParameter(typeSymbol))
{
// An array of a type parameter (e.g. `params T[]`) has no TypeInfo variant, and typeof(T[])
// does not compile outside T's scope. Erase it to object, as the parameter's Type already is.
return "new global::TUnit.Core.ConcreteType(typeof(object))";
}

// Regular concrete type (including fully closed generic types like List<int>)
var typeOfExpression = GetTypeOfExpression(typeSymbol);
return $@"new global::TUnit.Core.ConcreteType({typeOfExpression})";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@

namespace TUnit.Core.SourceGenerator.CodeGenerators.Helpers;

/// <summary>
/// An argument of a generated method call. <see cref="Name"/> is set when the argument must be
/// passed by name (e.g. a trailing array after omitted optional parameters).
/// </summary>
public readonly record struct InvocationArgument(string Expression, string? Name = null)
{
public bool IsNamed => Name is not null;

public override string ToString() => Name is null ? Expression : $"@{Name}: {Expression}";
}

public static class TupleArgumentHelper
{
/// <summary>
Expand Down Expand Up @@ -32,10 +43,10 @@ public static string GenerateMethodInvocationArguments(IList<IParameterSymbol> p
/// <param name="parameters">The method parameters</param>
/// <param name="argumentsArrayName">The name of the arguments array</param>
/// <param name="argumentCount">The actual number of arguments provided</param>
/// <returns>List of argument expressions for method invocation</returns>
public static List<string> GenerateArgumentAccessWithParams(IList<IParameterSymbol> parameters, string argumentsArrayName, object argumentCount)
/// <returns>The arguments for the method invocation, in declaration order</returns>
public static List<InvocationArgument> GenerateArgumentAccessWithParams(IList<IParameterSymbol> parameters, string argumentsArrayName, object argumentCount)
{
var argumentExpressions = new List<string>();
var argumentExpressions = new List<InvocationArgument>();

// Parse argument count - can be an int or a string expression
int argCount;
Expand Down Expand Up @@ -69,7 +80,7 @@ public static List<string> GenerateArgumentAccessWithParams(IList<IParameterSymb
{
var param = parameters[i];
var castExpression = $"global::TUnit.Core.Helpers.CastHelper.Cast<{param.Type.GloballyQualified()}>({argumentsArrayName}[{i.ToString(CultureInfo.InvariantCulture)}])";
argumentExpressions.Add(castExpression);
argumentExpressions.Add(new InvocationArgument(castExpression));
}
}
else
Expand All @@ -78,7 +89,7 @@ public static List<string> GenerateArgumentAccessWithParams(IList<IParameterSymb
{
var param = parameters[i];
var castExpression = $"global::TUnit.Core.Helpers.CastHelper.Cast<{param.Type.GloballyQualified()}>({argumentsArrayName}[{i.ToString(CultureInfo.InvariantCulture)}])";
argumentExpressions.Add(castExpression);
argumentExpressions.Add(new InvocationArgument(castExpression));
}
}
}
Expand All @@ -95,7 +106,7 @@ public static List<string> GenerateArgumentAccessWithParams(IList<IParameterSymb
{
var param = parameters[i];
var castExpression = $"global::TUnit.Core.Helpers.CastHelper.Cast<{param.Type.GloballyQualified()}>({argumentsArrayName}[{i.ToString(CultureInfo.InvariantCulture)}])";
argumentExpressions.Add(castExpression);
argumentExpressions.Add(new InvocationArgument(castExpression));
}
}
else
Expand All @@ -104,7 +115,7 @@ public static List<string> GenerateArgumentAccessWithParams(IList<IParameterSymb
{
var param = parameters[i];
var castExpression = $"global::TUnit.Core.Helpers.CastHelper.Cast<{param.Type.GloballyQualified()}>({argumentsArrayName}[{i.ToString(CultureInfo.InvariantCulture)}])";
argumentExpressions.Add(castExpression);
argumentExpressions.Add(new InvocationArgument(castExpression));
}
}

Expand All @@ -126,19 +137,33 @@ public static List<string> GenerateArgumentAccessWithParams(IList<IParameterSymb
// extension-method chain — generated files don't import System.Linq unless the
// project has ImplicitUsings enabled, so the extension form fails to compile (CS1061).
var arrayInit = $"({argumentsArrayName}.Length > {regularParamCount.ToString(CultureInfo.InvariantCulture)} ? global::System.Linq.Enumerable.ToArray(global::System.Linq.Enumerable.Select(global::System.Linq.Enumerable.Range({regularParamCount.ToString(CultureInfo.InvariantCulture)}, {rangeCount}), i => global::TUnit.Core.Helpers.CastHelper.Cast<{elementType.GloballyQualified()}>({argumentsArrayName}[i]))) : global::System.Array.Empty<{elementType.GloballyQualified()}>())";
argumentExpressions.Add(arrayInit);
argumentExpressions.Add(new InvocationArgument(arrayInit));
}
else
{
var remainingArgCount = Math.Max(0, argCount - regularParamCount);

if (remainingArgCount == 0)
// When optional regular parameters were omitted (argCount < regularParamCount),
// a positional array would bind to the first omitted parameter, so name it.
var arrayName = remainingArgCount == 0 && argCount < regularParamCount
? paramsParam.Name
: null;

if (remainingArgCount == 0 && !paramsParam.IsParams && paramsParam.HasExplicitDefaultValue)
{
// An omitted optional plain array keeps its declared default (as C# and
// reflection mode do). The only default an array parameter can declare is
// null, so pass a typed null rather than leaving the argument out: omitting
// it could bind the call to another overload (e.g. a parameterless M()).
argumentExpressions.Add(new InvocationArgument($"default({paramsParam.Type.GloballyQualified()})", arrayName));
}
Comment thread
thomhurst marked this conversation as resolved.
Resolved
else if (remainingArgCount == 0)
{
// No arguments for params array - pass empty array. Use Array.Empty<T>()
// rather than `new T[0]`: when T is itself an array (jagged param like
// byte[][], element type byte[]) the rank-specifier form `new byte[][0]`
// is invalid C# (CS1586/CS0178). See issue #6150.
argumentExpressions.Add($"global::System.Array.Empty<{elementType.GloballyQualified()}>()");
argumentExpressions.Add(new InvocationArgument($"global::System.Array.Empty<{elementType.GloballyQualified()}>()", arrayName));
}
else if (remainingArgCount == 1)
{
Expand All @@ -152,7 +177,7 @@ public static List<string> GenerateArgumentAccessWithParams(IList<IParameterSymb
// MatrixAttribute subclass for a MyEnum[] parameter — issue #6678).
var singleArg = $"{argumentsArrayName}[{regularParamCount.ToString(CultureInfo.InvariantCulture)}]";
var checkAndCast = $"({singleArg} is null ? null : {singleArg} is {paramsParam.Type.GloballyQualified()} arr ? arr : global::TUnit.Core.Helpers.CastHelper.ToTrailingArray<{elementType.GloballyQualified()}>({singleArg}))";
argumentExpressions.Add(checkAndCast);
argumentExpressions.Add(new InvocationArgument(checkAndCast));
}
else
{
Expand All @@ -162,15 +187,21 @@ public static List<string> GenerateArgumentAccessWithParams(IList<IParameterSymb
{
arrayElements.Add($"global::TUnit.Core.Helpers.CastHelper.Cast<{elementType.GloballyQualified()}>({argumentsArrayName}[{i.ToString(CultureInfo.InvariantCulture)}])");
}
argumentExpressions.Add($"new {elementType.GloballyQualified()}[] {{ {string.Join(", ", arrayElements)} }}");
// Name the full array type rather than appending [] to the element type:
// for a multidimensional element (params int[][,]) `new int[,][]` would
// reverse the ranks and not compile. Collection interfaces still need T[].
var arrayType = paramsParam.Type is IArrayTypeSymbol
? paramsParam.Type.GloballyQualified()
: $"{elementType.GloballyQualified()}[]";
argumentExpressions.Add(new InvocationArgument($"new {arrayType} {{ {string.Join(", ", arrayElements)} }}"));
}
}
}
else
{
// Fallback if we can't determine element type
var castExpression = $"global::TUnit.Core.Helpers.CastHelper.Cast<{paramsParam.Type.GloballyQualified()}>({argumentsArrayName}[{regularParamCount.ToString(CultureInfo.InvariantCulture)}])";
argumentExpressions.Add(castExpression);
argumentExpressions.Add(new InvocationArgument(castExpression));
}
}

Expand All @@ -185,7 +216,7 @@ public static List<string> GenerateArgumentAccessWithParams(IList<IParameterSymb
/// </summary>
private static ITypeSymbol? GetTrailingElementType(ITypeSymbol parameterType)
{
if (parameterType is IArrayTypeSymbol array)
if (parameterType is IArrayTypeSymbol { IsSZArray: true } array)
{
return array.ElementType;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,19 @@ internal static class TypeSymbolExtensions
/// on the final parameter), since a non-trailing array binds value-by-value, not collected.
/// </remarks>
public static bool CollectsTrailingArguments(this IParameterSymbol parameter)
=> parameter.IsParams || parameter.Type is IArrayTypeSymbol;
// Only single-dimensional arrays can be built from loose values; a multidimensional
// array such as int[,] binds as a single value.
=> parameter.IsParams || parameter.Type is IArrayTypeSymbol { IsSZArray: true };

/// <summary>
/// Determines whether the type is <c>System.Threading.CancellationToken</c>.
/// </summary>
public static bool IsCancellationToken(this ITypeSymbol type)
=> type is INamedTypeSymbol
{
Name: "CancellationToken",
ContainingNamespace: { Name: "Threading", ContainingNamespace: { Name: "System", ContainingNamespace.IsGlobalNamespace: true } }
};

/// <summary>
/// Determines if the type is a nullable value type (e.g., int?, bool?, MyEnum?)
Expand Down
Loading
Loading