diff --git a/src/libraries/Common/src/SourceGenerators/CSharpSyntaxUtilities.cs b/src/libraries/Common/src/SourceGenerators/CSharpSyntaxUtilities.cs new file mode 100644 index 00000000000000..8cd2f1850b718a --- /dev/null +++ b/src/libraries/Common/src/SourceGenerators/CSharpSyntaxUtilities.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Globalization; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace SourceGenerators; + +internal static class CSharpSyntaxUtilities +{ + // Standard format for double and single on non-inbox frameworks to ensure value is round-trippable. + public const string DoubleFormatString = "G17"; + public const string SingleFormatString = "G9"; + + // Format a literal in C# format -- works around https://github.com/dotnet/roslyn/issues/58705 + public static string FormatLiteral(object? value, TypeRef type) + { + if (value == null) + { + return $"default({type.FullyQualifiedName})"; + } + + 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 $"{@double.ToString(DoubleFormatString, CultureInfo.InvariantCulture)}D"; + case float.NegativeInfinity: + return "float.NegativeInfinity"; + case float.PositiveInfinity: + return "float.PositiveInfinity"; + case float.NaN: + return "float.NaN"; + case float @float: + return $"{@float.ToString(SingleFormatString, CultureInfo.InvariantCulture)}F"; + case decimal @decimal: + // we do not need to specify a format string for decimal as it's default is round-trippable on all frameworks. + return $"{@decimal.ToString(CultureInfo.InvariantCulture)}M"; + case bool @bool: + return @bool ? "true" : "false"; + default: + // Assume this is a number. + return FormatNumber(); + } + + string FormatNumber() => $"({type.FullyQualifiedName})({Convert.ToString(value, CultureInfo.InvariantCulture)})"; + } +} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Parser.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Parser.cs index d01c5dbae13f3c..3b5d460087751d 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Parser.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Parser.cs @@ -583,9 +583,8 @@ private ObjectSpec CreateObjectSpec(TypeParseInfo typeParseInfo) AttributeData? attributeData = property.GetAttributes().FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, _typeSymbols.ConfigurationKeyNameAttribute)); string configKeyName = attributeData?.ConstructorArguments.FirstOrDefault().Value as string ?? propertyName; - PropertySpec spec = new(property) + PropertySpec spec = new(property, propertyTypeRef) { - TypeRef = propertyTypeRef, ConfigurationKeyName = configKeyName }; @@ -617,9 +616,8 @@ private ObjectSpec CreateObjectSpec(TypeParseInfo typeParseInfo) } else { - ParameterSpec paramSpec = new ParameterSpec(parameter) + ParameterSpec paramSpec = new ParameterSpec(parameter, propertySpec.TypeRef) { - TypeRef = propertySpec.TypeRef, ConfigurationKeyName = propertySpec.ConfigurationKeyName, }; diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Microsoft.Extensions.Configuration.Binder.SourceGeneration.csproj b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Microsoft.Extensions.Configuration.Binder.SourceGeneration.csproj index b66741549b4584..d3fa37da6a2f6d 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Microsoft.Extensions.Configuration.Binder.SourceGeneration.csproj +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Microsoft.Extensions.Configuration.Binder.SourceGeneration.csproj @@ -28,6 +28,7 @@ + diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Members/MemberSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Members/MemberSpec.cs index dc5b03087ac87a..cbc205ec2976ff 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Members/MemberSpec.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Members/MemberSpec.cs @@ -9,17 +9,18 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { public abstract record MemberSpec { - public MemberSpec(ISymbol member) + public MemberSpec(ISymbol member, TypeRef typeRef) { Debug.Assert(member is IPropertySymbol or IParameterSymbol); Name = member.Name; DefaultValueExpr = "default"; + TypeRef = typeRef; } public string Name { get; } public string DefaultValueExpr { get; protected set; } - public required TypeRef TypeRef { get; init; } + public TypeRef TypeRef { get; } public required string ConfigurationKeyName { get; init; } public abstract bool CanGet { get; } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Members/ParameterSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Members/ParameterSpec.cs index 62c781e1f1631f..53ca14ae7eedec 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Members/ParameterSpec.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Members/ParameterSpec.cs @@ -1,24 +1,23 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Globalization; +using System; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using SourceGenerators; namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { public sealed record ParameterSpec : MemberSpec { - public ParameterSpec(IParameterSymbol parameter) : base(parameter) + public ParameterSpec(IParameterSymbol parameter, TypeRef typeRef) : base(parameter, typeRef) { RefKind = parameter.RefKind; if (parameter.HasExplicitDefaultValue) { - string formatted = SymbolDisplay.FormatPrimitive(parameter.ExplicitDefaultValue!, quoteStrings: true, useHexadecimalNumbers: false); - if (formatted is not "null") - { - DefaultValueExpr = formatted; - } + DefaultValueExpr = CSharpSyntaxUtilities.FormatLiteral(parameter.ExplicitDefaultValue, TypeRef); } else { diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Members/PropertySpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Members/PropertySpec.cs index 443e39d32e4933..66257d06cab891 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Members/PropertySpec.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Members/PropertySpec.cs @@ -2,12 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.CodeAnalysis; +using SourceGenerators; namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { public sealed record PropertySpec : MemberSpec { - public PropertySpec(IPropertySymbol property) : base(property) + public PropertySpec(IPropertySymbol property, TypeRef typeRef) : base(property, typeRef) { IMethodSymbol? setMethod = property.SetMethod; bool setterIsPublic = setMethod?.DeclaredAccessibility is Accessibility.Public; diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.cs index 7d10f66c822fc0..cc34f80055dc83 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.cs @@ -112,20 +112,45 @@ public ClassWhereParametersMatchPropertiesAndFields(string name, string address, public record RecordWhereParametersHaveDefaultValue(string Name, string Address, int Age = 42); - public record ClassWhereParametersHaveDefaultValue + public class ClassWhereParametersHaveDefaultValue { public string? Name { get; } public string Address { get; } public int Age { get; } - - public ClassWhereParametersHaveDefaultValue(string? name, string address, int age = 42) + public float F { get; } + public double D { get; } + public decimal M { get; } + public StringComparison SC { get; } + public char C { get; } + public int? NAge { get; } + public float? NF { get; } + public double? ND { get; } + public decimal? NM { get; } + public StringComparison? NSC { get; } + public char? NC { get; } + + public ClassWhereParametersHaveDefaultValue(string? name, string address, + int age = 42, float f = 42.0f, double d = 3.14159, decimal m = 3.1415926535897932384626433M, StringComparison sc = StringComparison.Ordinal, char c = 'q', + int? nage = 42, float? nf = 42.0f, double? nd = 3.14159, decimal? nm = 3.1415926535897932384626433M, StringComparison? nsc = StringComparison.Ordinal, char? nc = 'q') { Name = name; Address = address; Age = age; + F = f; + D = d; + M = m; + SC = sc; + C = c; + NAge = nage; + NF = nf; + ND = nd; + NM = nm; + NSC = nsc; + NC = nc; } } + public class ClassWithPrimaryCtor(string color, int length) { public string Color { get; } = color; diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs index 296cb790c22ba5..ab372b8ec814ee 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs @@ -1064,6 +1064,17 @@ public void BindsToClassConstructorParametersWithDefaultValues() Assert.Equal("John", testOptions.ClassWhereParametersHaveDefaultValueProperty.Name); Assert.Equal("123, Abc St.", testOptions.ClassWhereParametersHaveDefaultValueProperty.Address); Assert.Equal(42, testOptions.ClassWhereParametersHaveDefaultValueProperty.Age); + Assert.Equal(42.0f, testOptions.ClassWhereParametersHaveDefaultValueProperty.F); + Assert.Equal(3.14159, testOptions.ClassWhereParametersHaveDefaultValueProperty.D); + Assert.Equal(3.1415926535897932384626433M, testOptions.ClassWhereParametersHaveDefaultValueProperty.M); + Assert.Equal(StringComparison.Ordinal, testOptions.ClassWhereParametersHaveDefaultValueProperty.SC); + Assert.Equal('q', testOptions.ClassWhereParametersHaveDefaultValueProperty.C); + Assert.Equal(42, testOptions.ClassWhereParametersHaveDefaultValueProperty.NAge); + Assert.Equal(42.0f, testOptions.ClassWhereParametersHaveDefaultValueProperty.NF); + Assert.Equal(3.14159, testOptions.ClassWhereParametersHaveDefaultValueProperty.ND); + Assert.Equal(3.1415926535897932384626433M, testOptions.ClassWhereParametersHaveDefaultValueProperty.NM); + Assert.Equal(StringComparison.Ordinal, testOptions.ClassWhereParametersHaveDefaultValueProperty.NSC); + Assert.Equal('q', testOptions.ClassWhereParametersHaveDefaultValueProperty.NC); } [Fact] diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index 5109227a9b12d6..90575721c507d7 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -707,7 +707,7 @@ private static void GenerateCtorParamMetadataInitFunc(SourceWriter writer, strin ParameterType = typeof({{spec.ParameterType.FullyQualifiedName}}), Position = {{spec.ParameterIndex}}, HasDefaultValue = {{FormatBool(spec.HasDefaultValue)}}, - DefaultValue = {{FormatDefaultConstructorParameter(spec.DefaultValue, spec.ParameterType)}} + DefaultValue = {{CSharpSyntaxUtilities.FormatLiteral(spec.DefaultValue, spec.ParameterType)}} }; """); @@ -1351,56 +1351,6 @@ private static string FormatJsonSerializerDefaults(JsonSerializerDefaults defaul private static string CreateTypeInfoMethodName(TypeGenerationSpec typeSpec) => $"Create_{typeSpec.TypeInfoPropertyName}"; - private static string FormatDefaultConstructorParameter(object? value, TypeRef type) - { - if (value == null) - { - return $"default({type.FullyQualifiedName})"; - } - - if (type.TypeKind is TypeKind.Enum) - { - // 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 $"({type.FullyQualifiedName})({@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 $"({type.FullyQualifiedName})({@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() => $"({type.FullyQualifiedName})({Convert.ToString(value, CultureInfo.InvariantCulture)})"; - } private static string FormatDefaultConstructorExpr(TypeGenerationSpec typeSpec) { diff --git a/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.targets b/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.targets index 23add6278d7c07..9b6b9fc77aea52 100644 --- a/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.targets +++ b/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.targets @@ -30,6 +30,7 @@ + diff --git a/src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.ParameterMatching.cs b/src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.ParameterMatching.cs index b06f0ad9bbae16..0cf16797cd463f 100644 --- a/src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.ParameterMatching.cs +++ b/src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.ParameterMatching.cs @@ -1602,6 +1602,14 @@ public class ClassWithIgnoredPropertyDefaultParam public ClassWithIgnoredPropertyDefaultParam(int x, int y = 5) => (X, Y) = (x, y); } + [Fact] + public async Task TestClassWithManyDefaultParams() + { + string json = "{}"; + Class_With_Parameters_Default_Values result = await Serializer.DeserializeWrapper(json); + result.Verify(); + } + [Fact] public async Task TestClassWithCustomConverterOnCtorParameter_ShouldPassCorrectTypeToConvertParameter() { diff --git a/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.Constructor.cs b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.Constructor.cs index f12cf89e41de59..c2bf9e06a9a335 100644 --- a/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.Constructor.cs +++ b/src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.Constructor.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Collections; using System.Collections.Generic; using System.Collections.Immutable; @@ -1972,6 +1973,61 @@ public void VerifyMinimal() MyTuple.Item4.Verify(); } } + public class Class_With_Parameters_Default_Values + { + public int I { get; } + public float F { get; } + public double D { get; } + public decimal M { get; } + public StringComparison SC { get; } + public char C { get; } + public int? NI { get; } + public float? NF { get; } + public double? ND { get; } + public decimal? NM { get; } + public StringComparison? NSC { get; } + public char? NC { get; } + + public Class_With_Parameters_Default_Values( + int i = 21, float f = 42.0f, double d = 3.14159, decimal m = 3.1415926535897932384626433M, StringComparison sc = StringComparison.Ordinal, char c = 'q', + int? ni = 21, float? nf = 42.0f, double? nd = 3.14159, decimal? nm = 3.1415926535897932384626433M, StringComparison? nsc = StringComparison.Ordinal, char? nc = 'q') + { + I = i; + F = f; + D = d; + M = m; + SC = sc; + C = c; + NI = ni; + NF = nf; + ND = nd; + NM = nm; + NSC = nsc; + NC = nc; + } + + public void Initialize() { } + + public static readonly string s_json = @"{}"; + + public static readonly byte[] s_data = Encoding.UTF8.GetBytes(s_json); + + public void Verify() + { + Assert.Equal(21, I); + Assert.Equal(42.0f, F); + Assert.Equal(3.14159, D); + Assert.Equal(3.1415926535897932384626433M, M); + Assert.Equal(StringComparison.Ordinal, SC); + Assert.Equal('q', C); + Assert.Equal(21, NI); + Assert.Equal(42.0f, NF); + Assert.Equal(3.14159, ND); + Assert.Equal(3.1415926535897932384626433M, NM); + Assert.Equal(StringComparison.Ordinal, NSC); + Assert.Equal('q', NC); + } + } public class Point_MembersHave_JsonPropertyName : ITestClass { diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/ConstructorTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/ConstructorTests.cs index d1769f491104db..1f2fe901f456ea 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/ConstructorTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/ConstructorTests.cs @@ -112,6 +112,7 @@ protected ConstructorTests_Metadata(JsonSerializerWrapper stringWrapper) [JsonSerializable(typeof(SimpleClassWithParameterizedCtor_Derived_GenericIDictionary_ObjectExt))] [JsonSerializable(typeof(Point_MembersHave_JsonInclude))] [JsonSerializable(typeof(ClassWithFiveArgs_MembersHave_JsonNumberHandlingAttributes))] + [JsonSerializable(typeof(Class_With_Parameters_Default_Values))] [JsonSerializable(typeof(Point_MembersHave_JsonPropertyName))] [JsonSerializable(typeof(Point_MembersHave_JsonConverter))] [JsonSerializable(typeof(Point_MembersHave_JsonIgnore))] @@ -260,6 +261,7 @@ public ConstructorTests_Default(JsonSerializerWrapper jsonSerializer) : base(jso [JsonSerializable(typeof(SimpleClassWithParameterizedCtor_Derived_GenericIDictionary_ObjectExt))] [JsonSerializable(typeof(Point_MembersHave_JsonInclude))] [JsonSerializable(typeof(ClassWithFiveArgs_MembersHave_JsonNumberHandlingAttributes))] + [JsonSerializable(typeof(Class_With_Parameters_Default_Values))] [JsonSerializable(typeof(Point_MembersHave_JsonPropertyName))] [JsonSerializable(typeof(Point_MembersHave_JsonConverter))] [JsonSerializable(typeof(Point_MembersHave_JsonIgnore))]