diff --git a/TUnit.Core.SourceGenerator.Tests/ConstantArgumentsTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConstantArgumentsTests.Test.verified.txt index e60cdb2198..3688cce703 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConstantArgumentsTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConstantArgumentsTests.Test.verified.txt @@ -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(), PropertyDataSources = global::System.Array.Empty(), diff --git a/TUnit.Core.SourceGenerator.Tests/NumberArgumentTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/NumberArgumentTests.Test.verified.txt index 28c087324f..c8fb6132f4 100644 --- a/TUnit.Core.SourceGenerator.Tests/NumberArgumentTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/NumberArgumentTests.Test.verified.txt @@ -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(), PropertyDataSources = global::System.Array.Empty(), diff --git a/TUnit.Core.SourceGenerator.Tests/NumberArgumentTests.TestDE.verified.txt b/TUnit.Core.SourceGenerator.Tests/NumberArgumentTests.TestDE.verified.txt index 0b19ba30e1..c8fb6132f4 100644 --- a/TUnit.Core.SourceGenerator.Tests/NumberArgumentTests.TestDE.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/NumberArgumentTests.TestDE.verified.txt @@ -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(), PropertyDataSources = global::System.Array.Empty(), @@ -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(), PropertyDataSources = global::System.Array.Empty(), diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Formatting/TypedConstantFormatter.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Formatting/TypedConstantFormatter.cs index 5b83eaada6..f73177db97 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Formatting/TypedConstantFormatter.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Formatting/TypedConstantFormatter.cs @@ -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) @@ -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) @@ -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"; } @@ -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"); } @@ -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}"; } @@ -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"; } } @@ -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)} }}"; @@ -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"; } } @@ -340,4 +341,4 @@ private static string EscapeForTestId(string str) .Replace("\t", "\\t") .Replace("\"", "\\\""); } -} \ No newline at end of file +} diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/AotConversionHelper.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/AotConversionHelper.cs index 0827e97121..9f12f20641 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/AotConversionHelper.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/AotConversionHelper.cs @@ -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); } @@ -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); diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/FullyQualifiedWithGlobalPrefixRewriter.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/FullyQualifiedWithGlobalPrefixRewriter.cs index 61a89d4683..1bd9c2f1b1 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/FullyQualifiedWithGlobalPrefixRewriter.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/FullyQualifiedWithGlobalPrefixRewriter.cs @@ -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) diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SpecialFloatingPointValuesHelper.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SpecialFloatingPointValuesHelper.cs index 24192da6c1..ef6aa350e9 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SpecialFloatingPointValuesHelper.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/SpecialFloatingPointValuesHelper.cs @@ -10,37 +10,24 @@ namespace TUnit.Core.SourceGenerator.CodeGenerators.Helpers; internal static class SpecialFloatingPointValuesHelper { /// - /// 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. /// 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 + }; } /// @@ -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 @@ -76,4 +63,4 @@ private static MemberAccessExpressionSyntax CreateDoubleMemberAccess(string memb SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.DoubleKeyword)), SyntaxFactory.IdentifierName(memberName)); } -} \ No newline at end of file +} diff --git a/TUnit.Core.SourceGenerator/Extensions/ObjectExtensions.cs b/TUnit.Core.SourceGenerator/Extensions/ObjectExtensions.cs new file mode 100644 index 0000000000..a81be4ef94 --- /dev/null +++ b/TUnit.Core.SourceGenerator/Extensions/ObjectExtensions.cs @@ -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(); + } +} diff --git a/TUnit.Core.SourceGenerator/Generators/HookMetadataGenerator.cs b/TUnit.Core.SourceGenerator/Generators/HookMetadataGenerator.cs index 77dc7fb29b..e380fd36ce 100644 --- a/TUnit.Core.SourceGenerator/Generators/HookMetadataGenerator.cs +++ b/TUnit.Core.SourceGenerator/Generators/HookMetadataGenerator.cs @@ -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; diff --git a/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs b/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs index fea33a6426..9ded27cb94 100644 --- a/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs +++ b/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs @@ -1092,7 +1092,15 @@ private static void WriteTypedConstant(CodeWriter writer, TypedConstant constant } 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;