Fix bugs with generation for ctor param default values (#62798) (#63493)
authorLayomi Akinrinade <laakinri@microsoft.com>
Sat, 8 Jan 2022 03:32:08 +0000 (22:32 -0500)
committerGitHub <noreply@github.com>
Sat, 8 Jan 2022 03:32:08 +0000 (20:32 -0700)
* Fix bugs with generation for ctor param default values

* Address review feedback

* Address feedback

src/libraries/System.Text.Json/Common/JsonConstants.cs
src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs
src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs
src/libraries/System.Text.Json/gen/SourceGenerationSpec.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Double.cs
src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.Float.cs
src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.ParameterMatching.cs
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/ConstructorTests.cs

index a97b11c..0d121d9 100644 (file)
@@ -7,5 +7,9 @@ namespace System.Text.Json
     {
         // The maximum number of parameters a constructor can have where it can be supported by the serializer.
         public const int MaxParameterCount = 64;
+
+        // Standard format for double and single on non-inbox frameworks.
+        public const string DoubleFormatString = "G17";
+        public const string SingleFormatString = "G9";
     }
 }
index 087f3e2..7cfda7f 100644 (file)
@@ -4,7 +4,9 @@
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
 using System.Reflection;
+using System.Reflection.Metadata;
 using System.Text.Json;
 using System.Text.Json.Reflection;
 using System.Text.Json.Serialization;
@@ -759,17 +761,17 @@ private static {JsonPropertyInfoTypeRef}[] {propInitMethodName}({JsonSerializerC
                     sb.Append($@"
     {JsonPropertyInfoValuesTypeRef}<{memberTypeCompilableName}> {infoVarName} = new {JsonPropertyInfoValuesTypeRef}<{memberTypeCompilableName}>()
     {{
-        IsProperty = {ToCSharpKeyword(memberMetadata.IsProperty)},
-        IsPublic = {ToCSharpKeyword(memberMetadata.IsPublic)},
-        IsVirtual = {ToCSharpKeyword(memberMetadata.IsVirtual)},
+        IsProperty = {FormatBool(memberMetadata.IsProperty)},
+        IsPublic = {FormatBool(memberMetadata.IsPublic)},
+        IsVirtual = {FormatBool(memberMetadata.IsVirtual)},
         DeclaringType = typeof({memberMetadata.DeclaringTypeRef}),
         PropertyTypeInfo = {memberTypeFriendlyName},
         Converter = {converterValue},
         Getter = {getterValue},
         Setter = {setterValue},
         IgnoreCondition = {ignoreConditionNamedArg},
-        HasJsonInclude = {ToCSharpKeyword(memberMetadata.HasJsonInclude)},
-        IsExtensionData = {ToCSharpKeyword(memberMetadata.IsExtensionData)},
+        HasJsonInclude = {FormatBool(memberMetadata.HasJsonInclude)},
+        IsExtensionData = {FormatBool(memberMetadata.IsExtensionData)},
         NumberHandling = {GetNumberHandlingAsStr(memberMetadata.NumberHandling)},
         PropertyName = ""{clrPropertyName}"",
         JsonPropertyName = {jsonPropertyNameValue}
@@ -805,11 +807,11 @@ private static {JsonParameterInfoValuesTypeRef}[] {typeGenerationSpec.TypeInfoPr
                 for (int i = 0; i < paramCount; i++)
                 {
                     ParameterInfo reflectionInfo = parameters[i].ParameterInfo;
-
-                    string parameterTypeRef = reflectionInfo.ParameterType.GetCompilableName();
+                    Type parameterType = reflectionInfo.ParameterType;
+                    string parameterTypeRef = parameterType.GetCompilableName();
 
                     object? defaultValue = reflectionInfo.GetDefaultValue();
-                    string defaultValueAsStr = GetParamDefaultValueAsString(defaultValue, parameterTypeRef);
+                    string defaultValueAsStr = GetParamDefaultValueAsString(defaultValue, parameterType, parameterTypeRef);
 
                     sb.Append(@$"
     {InfoVarName} = new()
@@ -817,7 +819,7 @@ private static {JsonParameterInfoValuesTypeRef}[] {typeGenerationSpec.TypeInfoPr
         Name = ""{reflectionInfo.Name!}"",
         ParameterType = typeof({parameterTypeRef}),
         Position = {reflectionInfo.Position},
-        HasDefaultValue = {ToCSharpKeyword(reflectionInfo.HasDefaultValue)},
+        HasDefaultValue = {FormatBool(reflectionInfo.HasDefaultValue)},
         DefaultValue = {defaultValueAsStr}
     }};
     {parametersVarName}[{i}] = {InfoVarName};
@@ -1192,10 +1194,10 @@ public {contextTypeName}({JsonSerializerOptionsTypeRef} options) : base(options)
 private static {JsonSerializerOptionsTypeRef} {DefaultOptionsStaticVarName} {{ get; }} = new {JsonSerializerOptionsTypeRef}()
 {{
     DefaultIgnoreCondition = {JsonIgnoreConditionTypeRef}.{options.DefaultIgnoreCondition},
-    IgnoreReadOnlyFields = {ToCSharpKeyword(options.IgnoreReadOnlyFields)},
-    IgnoreReadOnlyProperties = {ToCSharpKeyword(options.IgnoreReadOnlyProperties)},
-    IncludeFields = {ToCSharpKeyword(options.IncludeFields)},
-    WriteIndented = {ToCSharpKeyword(options.WriteIndented)},{namingPolicyInit}
+    IgnoreReadOnlyFields = {FormatBool(options.IgnoreReadOnlyFields)},
+    IgnoreReadOnlyProperties = {FormatBool(options.IgnoreReadOnlyProperties)},
+    IncludeFields = {FormatBool(options.IncludeFields)},
+    WriteIndented = {FormatBool(options.WriteIndented)},{namingPolicyInit}
 }};";
             }
 
@@ -1316,20 +1318,64 @@ private static readonly {JsonEncodedTextTypeRef} {name_varName_pair.Value} = {Js
                     : "default";
 
             private static string GetCreateValueInfoMethodRef(string typeCompilableName) => $"{CreateValueInfoMethodName}<{typeCompilableName}>";
-        }
 
-        private static string ToCSharpKeyword(bool value) => value.ToString().ToLowerInvariant();
+            private static string FormatBool(bool value) => value ? "true" : "false";
 
-        private static string GetParamDefaultValueAsString(object? value, string objectTypeAsStr)
-        {
-            switch (value)
+            private string GetParamDefaultValueAsString(object? value, Type type, string typeRef)
             {
-                case null:
-                    return $"default({objectTypeAsStr})";
-                case bool boolVal:
-                    return ToCSharpKeyword(boolVal);
-                default:
-                    return value!.ToString();
+                if (value == null)
+                {
+                    return $"default({typeRef})";
+                }
+
+                if (type.IsEnum)
+                {
+                    // Roslyn gives us an instance of the underlying type, which is numerical.
+#if DEBUG
+                    Type runtimeType = _generationSpec.MetadataLoadContext.Resolve(value.GetType());
+                    Debug.Assert(_generationSpec.IsNumberType(runtimeType));
+#endif
+
+                    // Return the numeric value.
+                    return FormatNumber();
+                }
+
+                switch (value)
+                {
+                    case string @string:
+                        return SymbolDisplay.FormatLiteral(@string, quote: true); ;
+                    case char @char:
+                        return SymbolDisplay.FormatLiteral(@char, quote: true);
+                    case double.NegativeInfinity:
+                        return "double.NegativeInfinity";
+                    case double.PositiveInfinity:
+                        return "double.PositiveInfinity";
+                    case double.NaN:
+                        return "double.NaN";
+                    case double @double:
+                        return $"({typeRef})({@double.ToString(JsonConstants.DoubleFormatString, CultureInfo.InvariantCulture)})";
+                    case float.NegativeInfinity:
+                        return "float.NegativeInfinity";
+                    case float.PositiveInfinity:
+                        return "float.PositiveInfinity";
+                    case float.NaN:
+                        return "float.NaN";
+                    case float @float:
+                        return $"({typeRef})({@float.ToString(JsonConstants.SingleFormatString, CultureInfo.InvariantCulture)})";
+                    case decimal.MaxValue:
+                        return "decimal.MaxValue";
+                    case decimal.MinValue:
+                        return "decimal.MinValue";
+                    case decimal @decimal:
+                        return @decimal.ToString(CultureInfo.InvariantCulture);
+                    case bool @bool:
+                        return FormatBool(@bool);
+                    default:
+                        // Assume this is a number.
+                        return FormatNumber();
+                }
+
+                string FormatNumber() => $"({typeRef})({Convert.ToString(value, CultureInfo.InvariantCulture)})";
             }
         }
     }
index fd19045..bc4b626 100644 (file)
@@ -375,6 +375,9 @@ namespace System.Text.Json.SourceGeneration
                     GuidType = _guidType,
                     StringType = _stringType,
                     NumberTypes = _numberTypes,
+#if DEBUG
+                    MetadataLoadContext = _metadataLoadContext,
+#endif
                 };
             }
 
index 0019dc5..09865cd 100644 (file)
@@ -4,6 +4,8 @@
 using System;
 using System.Collections.Generic;
 using System.Text;
+using System.Text.Json.Reflection;
+using Microsoft.CodeAnalysis;
 
 namespace System.Text.Json.SourceGeneration
 {
@@ -11,6 +13,9 @@ namespace System.Text.Json.SourceGeneration
     {
         public List<ContextGenerationSpec> ContextGenerationSpecList { get; init; }
 
+#if DEBUG
+        public MetadataLoadContextInternal MetadataLoadContext { get; init; }
+#endif
         public Type BooleanType { get; init; }
         public Type ByteArrayType { get; init; }
         public Type CharType { get; init; }
index f8a46a3..bf16e32 100644 (file)
@@ -109,9 +109,7 @@ namespace System.Text.Json
 #if BUILDING_INBOX_LIBRARY
             return Utf8Formatter.TryFormat(value, destination, out bytesWritten);
 #else
-            const string FormatString = "G17";
-
-            string utf16Text = value.ToString(FormatString, CultureInfo.InvariantCulture);
+            string utf16Text = value.ToString(JsonConstants.DoubleFormatString, CultureInfo.InvariantCulture);
 
             // Copy the value to the destination, if it's large enough.
 
index 2f046d8..067971e 100644 (file)
@@ -109,9 +109,7 @@ namespace System.Text.Json
 #if BUILDING_INBOX_LIBRARY
             return Utf8Formatter.TryFormat(value, destination, out bytesWritten);
 #else
-            const string FormatString = "G9";
-
-            string utf16Text = value.ToString(FormatString, CultureInfo.InvariantCulture);
+            string utf16Text = value.ToString(JsonConstants.SingleFormatString, CultureInfo.InvariantCulture);
 
             // Copy the value to the destination, if it's large enough.
 
index 93aff74..d56a64a 100644 (file)
@@ -4,6 +4,7 @@
 using System.Collections.Generic;
 using System.Collections.Specialized;
 using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
 using System.Threading.Tasks;
 using Xunit;
 
@@ -1322,5 +1323,142 @@ namespace System.Text.Json.Serialization.Tests
 
             public ClassWithIgnoredSameType(ClassWithIgnoredSameType prop) { }
         }
+
+        public async Task TestClassWithDefaultCtorParams()
+        {
+            ClassWithDefaultCtorParams obj = new ClassWithDefaultCtorParams(
+                new Point_2D_Struct_WithAttribute(1, 2),
+                DayOfWeek.Sunday,
+                (DayOfWeek)(-2),
+                (DayOfWeek)19,
+                BindingFlags.CreateInstance | BindingFlags.FlattenHierarchy,
+                SampleEnumUInt32.MinZero,
+                "Hello world!",
+                null,
+                "xzy",
+                'c',
+                ' ',
+                23,
+                4,
+                -40,
+                double.Epsilon,
+                23,
+                4,
+                -40,
+                float.MinValue,
+                1,
+                2,
+                3,
+                4,
+                5,
+                6,
+                7,
+                8,
+                9,
+                10);
+
+            string json = await JsonSerializerWrapperForString.SerializeWrapper(obj);
+            obj = await JsonSerializerWrapperForString.DeserializeWrapper<ClassWithDefaultCtorParams>(json);
+            JsonTestHelper.AssertJsonEqual(json, await JsonSerializerWrapperForString.SerializeWrapper(obj));
+        }
+
+        public class ClassWithDefaultCtorParams
+        {
+            public Point_2D_Struct_WithAttribute Struct { get; }
+            public DayOfWeek Enum1 { get; }
+            public DayOfWeek Enum2 { get; }
+            public DayOfWeek Enum3 { get; }
+            public BindingFlags Enum4 { get; }
+            public SampleEnumUInt32 Enum5 { get; }
+            public string Str1 { get; }
+            public string Str2 { get; }
+            public string Str3 { get; }
+            public char Char1 { get; }
+            public char Char2 { get; }
+            public double Double1 { get; }
+            public double Double2 { get; }
+            public double Double3 { get; }
+            public double Double4 { get; }
+            public double Double5 { get; }
+            public float Float1 { get; }
+            public float Float2 { get; }
+            public float Float3 { get; }
+            public float Float4 { get; }
+            public float Float5 { get; }
+            public byte Byte { get; }
+            public decimal Decimal1 { get; }
+            public decimal Decimal2 { get; }
+            public short Short { get; }
+            public sbyte Sbyte { get; }
+            public int Int { get; }
+            public long Long { get; }
+            public ushort UShort { get; }
+            public uint UInt { get; }
+            public ulong ULong { get; }
+
+            public ClassWithDefaultCtorParams(
+                Point_2D_Struct_WithAttribute @struct = default,
+                DayOfWeek enum1 = default,
+                DayOfWeek enum2 = DayOfWeek.Sunday,
+                DayOfWeek enum3 = DayOfWeek.Sunday | DayOfWeek.Monday,
+                BindingFlags enum4 = BindingFlags.CreateInstance | BindingFlags.ExactBinding,
+                SampleEnumUInt32 enum5 = SampleEnumUInt32.MinZero,
+                string str1 = "abc",
+                string str2 = "",
+                string str3 = "\n\r⁉️\'\"\u200D\f\t\v\0\a\b\\\'\"",
+                char char1 = 'a',
+                char char2 = '\u200D',
+                double double1 = double.NegativeInfinity,
+                double double2 = double.PositiveInfinity,
+                double double3 = double.NaN,
+                double double4 = double.MaxValue,
+                double double5 = double.Epsilon,
+                float float1 = float.NegativeInfinity,
+                float float2 = float.PositiveInfinity,
+                float float3 = float.NaN,
+                float float4 = float.MinValue,
+                float float5 = float.Epsilon,
+                byte @byte = byte.MinValue,
+                decimal @decimal1 = decimal.MinValue,
+                decimal @decimal2 = decimal.MaxValue,
+                short @short = short.MinValue,
+                sbyte @sbyte = sbyte.MaxValue,
+                int @int = int.MinValue,
+                long @long = long.MaxValue,
+                ushort @ushort = ushort.MinValue,
+                uint @uint = uint.MaxValue,
+                ulong @ulong = ulong.MinValue)
+            {
+                Struct = @struct;
+                Enum1 = enum1;
+                Enum2 = enum2;
+                Enum3 = enum3;
+                Enum4 = enum4;
+                Enum5 = enum5;
+                Str1 = str1;
+                Str2 = str2;
+                Str3 = str3;
+                Char1 = char1;
+                Char2 = char2;
+                Double1 = double1;
+                Double2 = double2;
+                Double3 = double3;
+                Double4 = double4;
+                Float1 = float1;
+                Float2 = float2;
+                Float3 = float3;
+                Float4 = float4;
+                Byte = @byte;
+                Decimal1 = @decimal1;
+                Decimal2 = @decimal2;
+                Short = @short;
+                Sbyte = @sbyte;
+                Int = @int;
+                Long = @long;
+                UShort = @ushort;
+                UInt = @uint;
+                ULong = @ulong;
+            }
+        }
     }
 }
index deba915..3f65e03 100644 (file)
@@ -130,6 +130,7 @@ namespace System.Text.Json.SourceGeneration.Tests
         [JsonSerializable(typeof(LargeType_IgnoredProp_Bind_ParamWithDefaultValue))]
         [JsonSerializable(typeof(LargeType_IgnoredProp_Bind_Param))]
         [JsonSerializable(typeof(ClassWithIgnoredSameType))]
+        [JsonSerializable(typeof(ClassWithDefaultCtorParams))]
         internal sealed partial class ConstructorTestsContext_Metadata : JsonSerializerContext
         {
         }
@@ -251,6 +252,7 @@ namespace System.Text.Json.SourceGeneration.Tests
         [JsonSerializable(typeof(LargeType_IgnoredProp_Bind_ParamWithDefaultValue))]
         [JsonSerializable(typeof(LargeType_IgnoredProp_Bind_Param))]
         [JsonSerializable(typeof(ClassWithIgnoredSameType))]
+        [JsonSerializable(typeof(ClassWithDefaultCtorParams))]
         internal sealed partial class ConstructorTestsContext_Default : JsonSerializerContext
         {
         }