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
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ internal sealed class ConstantArgumentsTests_Double_TestSource_GUID : global::TU
],
DataSources = new global::TUnit.Core.IDataSourceAttribute[]
{
new global::TUnit.Core.ArgumentsAttribute(1.23),
new global::TUnit.Core.ArgumentsAttribute(1.23d),
},
ClassDataSources = global::System.Array.Empty<global::TUnit.Core.IDataSourceAttribute>(),
PropertyDataSources = global::System.Array.Empty<global::TUnit.Core.PropertyDataSource>(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ internal sealed class NumberArgumentTests_Double_TestSource_GUID : global::TUnit
],
DataSources = new global::TUnit.Core.IDataSourceAttribute[]
{
new global::TUnit.Core.ArgumentsAttribute(1.1),
new global::TUnit.Core.ArgumentsAttribute(1.1d),
},
ClassDataSources = global::System.Array.Empty<global::TUnit.Core.IDataSourceAttribute>(),
PropertyDataSources = global::System.Array.Empty<global::TUnit.Core.PropertyDataSource>(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ internal sealed class NumberArgumentTests_Double_TestSource_GUID : global::TUnit
],
DataSources = new global::TUnit.Core.IDataSourceAttribute[]
{
new global::TUnit.Core.ArgumentsAttribute(1,1),
new global::TUnit.Core.ArgumentsAttribute(1.1d),
},
ClassDataSources = global::System.Array.Empty<global::TUnit.Core.IDataSourceAttribute>(),
PropertyDataSources = global::System.Array.Empty<global::TUnit.Core.PropertyDataSource>(),
Expand Down Expand Up @@ -249,7 +249,7 @@ internal sealed class NumberArgumentTests_Float_TestSource_GUID : global::TUnit.
],
DataSources = new global::TUnit.Core.IDataSourceAttribute[]
{
new global::TUnit.Core.ArgumentsAttribute(1,1f),
new global::TUnit.Core.ArgumentsAttribute(1.1f),
},
ClassDataSources = global::System.Array.Empty<global::TUnit.Core.IDataSourceAttribute>(),
PropertyDataSources = global::System.Array.Empty<global::TUnit.Core.PropertyDataSource>(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace TUnit.Core.SourceGenerator.CodeGenerators.Formatting;

public class TypedConstantFormatter : ITypedConstantFormatter
{

public string FormatForCode(TypedConstant constant, ITypeSymbol? targetType = null)
{
if (constant.IsNull)
Expand All @@ -18,8 +18,7 @@ public string FormatForCode(TypedConstant constant, ITypeSymbol? targetType = nu
{
case TypedConstantKind.Primitive:
// Check for special floating-point values first using the TypedConstant's type info
if (constant.Type?.SpecialType == SpecialType.System_Single ||
constant.Type?.SpecialType == SpecialType.System_Double)
if (constant.Type?.SpecialType is SpecialType.System_Single or SpecialType.System_Double)
{
var specialValue = Helpers.SpecialFloatingPointValuesHelper.TryFormatSpecialFloatingPointValue(constant.Value);
if (specialValue != null)
Expand All @@ -28,20 +27,20 @@ public string FormatForCode(TypedConstant constant, ITypeSymbol? targetType = nu
}
}
return FormatPrimitiveForCode(constant.Value, targetType);

case TypedConstantKind.Enum:
return FormatEnumForCode(constant, targetType);

case TypedConstantKind.Type:
var type = (ITypeSymbol)constant.Value!;
return $"typeof({type.GloballyQualified()})";

case TypedConstantKind.Array:
return FormatArrayForCode(constant, targetType);

case TypedConstantKind.Error:
return "default";

default:
return constant.Value?.ToString() ?? "null";
}
Expand All @@ -58,21 +57,21 @@ public string FormatForTestId(TypedConstant constant)
{
case TypedConstantKind.Primitive:
return EscapeForTestId(constant.Value?.ToString() ?? "null");

case TypedConstantKind.Enum:
// For test IDs, use the numeric value or member name
var enumType = constant.Type as INamedTypeSymbol;
var memberName = GetEnumMemberName(enumType, constant.Value);
return memberName ?? constant.Value?.ToString() ?? "null";

case TypedConstantKind.Type:
var type = (ITypeSymbol)constant.Value!;
return type.GloballyQualified();

case TypedConstantKind.Array:
var elements = constant.Values.Select(v => FormatForTestId(v));
return $"[{string.Join(", ", elements)}]";

default:
return EscapeForTestId(constant.Value?.ToString() ?? "null");
}
Expand Down Expand Up @@ -113,10 +112,10 @@ private string FormatPrimitiveForCode(object? value, ITypeSymbol? targetType)
var enumTypeName = targetType.GloballyQualified();
return $"{enumTypeName}.{memberName}";
}

// Fallback to cast for non-member values
var formattedValue = FormatPrimitive(value);
return formattedValue != null && formattedValue.StartsWith("-")
return formattedValue != null && formattedValue.StartsWith("-")
? $"({targetType.GloballyQualified()})({formattedValue})"
: $"({targetType.GloballyQualified()}){formattedValue}";
}
Expand Down Expand Up @@ -155,56 +154,56 @@ private string FormatPrimitiveForCode(object? value, ITypeSymbol? targetType)
switch (targetType.SpecialType)
{
case SpecialType.System_Byte:
return $"(byte){value}";
return $"(byte){value.ToInvariantString()}";
case SpecialType.System_SByte:
return $"(sbyte){value}";
return $"(sbyte){value.ToInvariantString()}";
case SpecialType.System_Int16:
return $"(short){value}";
return $"(short){value.ToInvariantString()}";
case SpecialType.System_UInt16:
return $"(ushort){value}";
return $"(ushort){value.ToInvariantString()}";
case SpecialType.System_Int32:
// Int32 is the default for integer literals, no cast needed unless value is not int32
return value is int ? value.ToString()! : $"(int){value}";
case SpecialType.System_UInt32:
return $"{value}u";
return $"{value.ToInvariantString()}u";
case SpecialType.System_Int64:
return $"{value}L";
return $"{value.ToInvariantString()}L";
case SpecialType.System_UInt64:
return $"{value}UL";
return $"{value.ToInvariantString()}UL";
case SpecialType.System_Single:
return $"{value}f";
return $"{value.ToInvariantString()}f";
case SpecialType.System_Double:
// Double is default for floating-point literals
return value.ToString()!;
return $"{value.ToInvariantString()}d";
case SpecialType.System_Decimal:
// Handle string to decimal conversion for values that can't be expressed as literals
if (value is string s)
{
// Generate code that parses the string at runtime
// This allows for maximum precision decimal values
return $"decimal.Parse(\"{s}\", System.Globalization.CultureInfo.InvariantCulture)";
return $"decimal.Parse(\"{s.ToInvariantString()}\", global::System.Globalization.CultureInfo.InvariantCulture)";
}
// When target is decimal but value is double/float/int, convert and format with m suffix
else if (value is double d)
if (value is double d)
{
// Use the full precision by formatting with sufficient digits
// The 'G29' format gives us the maximum precision for decimal
var decimalValue = (decimal)d;
return $"{decimalValue.ToString("G29", System.Globalization.CultureInfo.InvariantCulture)}m";
}
else if (value is float f)
if (value is float f)
{
var decimalValue = (decimal)f;
return $"{decimalValue.ToString("G29", System.Globalization.CultureInfo.InvariantCulture)}m";
}
else if (value is int || value is long || value is short || value is byte ||
value is uint || value is ulong || value is ushort || value is sbyte)

if (value is int or long or short or byte or uint or ulong or ushort or sbyte)
{
// For integer types, convert to decimal
var decimalValue = Convert.ToDecimal(value);
return $"{decimalValue.ToString(System.Globalization.CultureInfo.InvariantCulture)}m";
}
return $"{value}m";

return $"{value.ToInvariantString()}m";
}
}

Expand Down Expand Up @@ -244,7 +243,7 @@ private string FormatArrayForCode(TypedConstant constant, ITypeSymbol? targetTyp
{
elementType = (constant.Type as IArrayTypeSymbol)?.ElementType;
}

var elements = constant.Values.Select(v => FormatForCode(v, elementType));
var elementTypeString = elementType?.GloballyQualified() ?? "object";
return $"new {elementTypeString}[] {{ {string.Join(", ", elements)} }}";
Expand Down Expand Up @@ -288,6 +287,8 @@ private static string FormatPrimitive(object? value)
{
return formattable.ToString(null, System.Globalization.CultureInfo.InvariantCulture);
}
// For non-IFormattable types, fallback to ToString()
// This should be safe as we've handled all numeric types above
return value.ToString() ?? "null";
}
}
Expand Down Expand Up @@ -340,4 +341,4 @@ private static string EscapeForTestId(string str)
.Replace("\t", "\\t")
.Replace("\"", "\\\"");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public static bool HasConversionOperators(ITypeSymbol type)
{
var members = type.GetMembers();
return members.Any(m => m is IMethodSymbol method &&
(method.Name == "op_Implicit" || method.Name == "op_Explicit") &&
method.Name is "op_Implicit" or "op_Explicit" &&
method.IsStatic);
}

Expand All @@ -63,7 +63,7 @@ public static bool HasConversionOperators(ITypeSymbol type)
foreach (var member in members)
{
if (member is IMethodSymbol method &&
(method.Name == "op_Implicit" || method.Name == "op_Explicit") &&
method.Name is "op_Implicit" or "op_Explicit" &&
method is { IsStatic: true, Parameters.Length: 1 })
{
yield return (method, method.ReturnType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ public override SyntaxNode VisitNamespaceDeclaration(NamespaceDeclarationSyntax
if (symbol is IFieldSymbol fieldSymbol && fieldSymbol.IsConst)
{
var containingType = fieldSymbol.ContainingType;
if (containingType?.SpecialType == SpecialType.System_Double ||
containingType?.SpecialType == SpecialType.System_Single)
if (containingType?.SpecialType is SpecialType.System_Double or SpecialType.System_Single)
{
// Get the constant value and use the helper to create the appropriate syntax
if (fieldSymbol.HasConstantValue)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,37 +10,24 @@ namespace TUnit.Core.SourceGenerator.CodeGenerators.Helpers;
internal static class SpecialFloatingPointValuesHelper
{
/// <summary>
/// Formats a floating-point value as a string for code generation,
/// Formats a floating-point value as a string for code generation,
/// handling special values like NaN and Infinity.
/// </summary>
public static string? TryFormatSpecialFloatingPointValue(object? value)
{
if (value == null)
return null;

// Handle float values
if (value is float floatValue)
{
if (float.IsNaN(floatValue))
return "float.NaN";
if (float.IsPositiveInfinity(floatValue))
return "float.PositiveInfinity";
if (float.IsNegativeInfinity(floatValue))
return "float.NegativeInfinity";
}

// Handle double values
if (value is double doubleValue)
return value switch
{
if (double.IsNaN(doubleValue))
return "double.NaN";
if (double.IsPositiveInfinity(doubleValue))
return "double.PositiveInfinity";
if (double.IsNegativeInfinity(doubleValue))
return "double.NegativeInfinity";
}

return null;
null => null,
// Handle float values
float.NaN => "float.NaN",
float floatValue when float.IsPositiveInfinity(floatValue) => "float.PositiveInfinity",
float floatValue when float.IsNegativeInfinity(floatValue) => "float.NegativeInfinity",
// Handle double values
double.NaN => "double.NaN",
double doubleValue when double.IsPositiveInfinity(doubleValue) => "double.PositiveInfinity",
double doubleValue when double.IsNegativeInfinity(doubleValue) => "double.NegativeInfinity",
_ => null
};
}

/// <summary>
Expand All @@ -51,10 +38,10 @@ internal static class SpecialFloatingPointValuesHelper
{
return value switch
{
float f when float.IsNaN(f) => CreateFloatMemberAccess("NaN"),
float.NaN => CreateFloatMemberAccess("NaN"),
float f when float.IsPositiveInfinity(f) => CreateFloatMemberAccess("PositiveInfinity"),
float f when float.IsNegativeInfinity(f) => CreateFloatMemberAccess("NegativeInfinity"),
double d when double.IsNaN(d) => CreateDoubleMemberAccess("NaN"),
double.NaN => CreateDoubleMemberAccess("NaN"),
double d when double.IsPositiveInfinity(d) => CreateDoubleMemberAccess("PositiveInfinity"),
double d when double.IsNegativeInfinity(d) => CreateDoubleMemberAccess("NegativeInfinity"),
_ => null
Expand All @@ -76,4 +63,4 @@ private static MemberAccessExpressionSyntax CreateDoubleMemberAccess(string memb
SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.DoubleKeyword)),
SyntaxFactory.IdentifierName(memberName));
}
}
}
14 changes: 14 additions & 0 deletions TUnit.Core.SourceGenerator/Extensions/ObjectExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace TUnit.Core.SourceGenerator.Extensions;

public static class ObjectExtensions
{
public static string? ToInvariantString(this object? obj)
{
if(obj is IFormattable formattable)
{
return formattable.ToString(null, System.Globalization.CultureInfo.InvariantCulture);
}

return obj?.ToString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -829,7 +829,7 @@ private static string GetContextTypeForBody(string hookType, string hookKind)

private static string GetHookIndexMethodName(HookMethodMetadata hook)
{
var prefix = hook.HookKind == "Before" || hook.HookKind == "BeforeEvery" ? "Before" : "After";
var prefix = hook.HookKind is "Before" or "BeforeEvery" ? "Before" : "After";
var suffix = hook.HookKind.Contains("Every") && hook.HookType != "TestSession" && hook.HookType != "TestDiscovery" ? "Every" : "";
var hookType = hook.HookType;

Expand Down
10 changes: 9 additions & 1 deletion TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -764,7 +764,7 @@
}

// Find the data source method
var dataSourceMethod = targetType.GetMembers(methodName)

Check warning on line 767 in TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (windows-latest)

Possible null reference argument for parameter 'name' in 'ImmutableArray<ISymbol> INamespaceOrTypeSymbol.GetMembers(string name)'.

Check warning on line 767 in TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (windows-latest)

Possible null reference argument for parameter 'name' in 'ImmutableArray<ISymbol> INamespaceOrTypeSymbol.GetMembers(string name)'.

Check warning on line 767 in TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

Possible null reference argument for parameter 'name' in 'ImmutableArray<ISymbol> INamespaceOrTypeSymbol.GetMembers(string name)'.

Check warning on line 767 in TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

Possible null reference argument for parameter 'name' in 'ImmutableArray<ISymbol> INamespaceOrTypeSymbol.GetMembers(string name)'.

Check warning on line 767 in TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

Possible null reference argument for parameter 'name' in 'ImmutableArray<ISymbol> INamespaceOrTypeSymbol.GetMembers(string name)'.

Check warning on line 767 in TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

Possible null reference argument for parameter 'name' in 'ImmutableArray<ISymbol> INamespaceOrTypeSymbol.GetMembers(string name)'.
.OfType<IMethodSymbol>()
.FirstOrDefault();

Expand Down Expand Up @@ -1092,7 +1092,15 @@
}
else
{
writer.Append(constant.Value?.ToString() ?? "null");
// Handle numeric types with InvariantCulture to ensure consistent formatting
if (constant.Value is IFormattable formattable)
{
writer.Append(formattable.ToString(null, System.Globalization.CultureInfo.InvariantCulture));
}
else
{
writer.Append(constant.Value?.ToString() ?? "null");
}
}
break;

Expand Down
Loading