Skip to content

Commit e041633

Browse files
authored
Fix Decimal literal formatting in source generators (#93160)
* Fix Decimal literal formatting in source generators Both ConfigurationBinder and Json were not handling decimal values correctly. This shares and updates our workaround method for formatting these. * Address feedback
1 parent 1d760d6 commit e041633

File tree

13 files changed

+179
-67
lines changed

13 files changed

+179
-67
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Globalization;
6+
using Microsoft.CodeAnalysis;
7+
using Microsoft.CodeAnalysis.CSharp;
8+
9+
namespace SourceGenerators;
10+
11+
internal static class CSharpSyntaxUtilities
12+
{
13+
// Standard format for double and single on non-inbox frameworks to ensure value is round-trippable.
14+
public const string DoubleFormatString = "G17";
15+
public const string SingleFormatString = "G9";
16+
17+
// Format a literal in C# format -- works around https://github.com/dotnet/roslyn/issues/58705
18+
public static string FormatLiteral(object? value, TypeRef type)
19+
{
20+
if (value == null)
21+
{
22+
return $"default({type.FullyQualifiedName})";
23+
}
24+
25+
switch (value)
26+
{
27+
case string @string:
28+
return SymbolDisplay.FormatLiteral(@string, quote: true); ;
29+
case char @char:
30+
return SymbolDisplay.FormatLiteral(@char, quote: true);
31+
case double.NegativeInfinity:
32+
return "double.NegativeInfinity";
33+
case double.PositiveInfinity:
34+
return "double.PositiveInfinity";
35+
case double.NaN:
36+
return "double.NaN";
37+
case double @double:
38+
return $"{@double.ToString(DoubleFormatString, CultureInfo.InvariantCulture)}D";
39+
case float.NegativeInfinity:
40+
return "float.NegativeInfinity";
41+
case float.PositiveInfinity:
42+
return "float.PositiveInfinity";
43+
case float.NaN:
44+
return "float.NaN";
45+
case float @float:
46+
return $"{@float.ToString(SingleFormatString, CultureInfo.InvariantCulture)}F";
47+
case decimal @decimal:
48+
// we do not need to specify a format string for decimal as it's default is round-trippable on all frameworks.
49+
return $"{@decimal.ToString(CultureInfo.InvariantCulture)}M";
50+
case bool @bool:
51+
return @bool ? "true" : "false";
52+
default:
53+
// Assume this is a number.
54+
return FormatNumber();
55+
}
56+
57+
string FormatNumber() => $"({type.FullyQualifiedName})({Convert.ToString(value, CultureInfo.InvariantCulture)})";
58+
}
59+
}

src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Parser.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -583,9 +583,8 @@ private ObjectSpec CreateObjectSpec(TypeParseInfo typeParseInfo)
583583
AttributeData? attributeData = property.GetAttributes().FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, _typeSymbols.ConfigurationKeyNameAttribute));
584584
string configKeyName = attributeData?.ConstructorArguments.FirstOrDefault().Value as string ?? propertyName;
585585

586-
PropertySpec spec = new(property)
586+
PropertySpec spec = new(property, propertyTypeRef)
587587
{
588-
TypeRef = propertyTypeRef,
589588
ConfigurationKeyName = configKeyName
590589
};
591590

@@ -617,9 +616,8 @@ private ObjectSpec CreateObjectSpec(TypeParseInfo typeParseInfo)
617616
}
618617
else
619618
{
620-
ParameterSpec paramSpec = new ParameterSpec(parameter)
619+
ParameterSpec paramSpec = new ParameterSpec(parameter, propertySpec.TypeRef)
621620
{
622-
TypeRef = propertySpec.TypeRef,
623621
ConfigurationKeyName = propertySpec.ConfigurationKeyName,
624622
};
625623

src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Microsoft.Extensions.Configuration.Binder.SourceGeneration.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
<Compile Include="$(CoreLibSharedDir)System\Runtime\CompilerServices\RequiredMemberAttribute.cs" Link="Common\System\Runtime\CompilerServices\RequiredMemberAttribute.cs" />
2929
<Compile Include="$(CommonPath)\Roslyn\DiagnosticDescriptorHelper.cs" Link="Common\Roslyn\DiagnosticDescriptorHelper.cs" />
3030
<Compile Include="$(CommonPath)\Roslyn\GetBestTypeByMetadataName.cs" Link="Common\Roslyn\GetBestTypeByMetadataName.cs" />
31+
<Compile Include="$(CommonPath)\SourceGenerators\CSharpSyntaxUtilities.cs" Link="Common\SourceGenerators\CSharpSyntaxUtilities.cs" />
3132
<Compile Include="$(CommonPath)\SourceGenerators\DiagnosticInfo.cs" Link="Common\SourceGenerators\DiagnosticInfo.cs" />
3233
<Compile Include="$(CommonPath)\SourceGenerators\ImmutableEquatableArray.cs" Link="Common\SourceGenerators\ImmutableEquatableArray.cs" />
3334
<Compile Include="$(CommonPath)\SourceGenerators\SourceWriter.cs" Link="Common\SourceGenerators\SourceWriter.cs" />

src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Members/MemberSpec.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,18 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
99
{
1010
public abstract record MemberSpec
1111
{
12-
public MemberSpec(ISymbol member)
12+
public MemberSpec(ISymbol member, TypeRef typeRef)
1313
{
1414
Debug.Assert(member is IPropertySymbol or IParameterSymbol);
1515
Name = member.Name;
1616
DefaultValueExpr = "default";
17+
TypeRef = typeRef;
1718
}
1819

1920
public string Name { get; }
2021
public string DefaultValueExpr { get; protected set; }
2122

22-
public required TypeRef TypeRef { get; init; }
23+
public TypeRef TypeRef { get; }
2324
public required string ConfigurationKeyName { get; init; }
2425

2526
public abstract bool CanGet { get; }

src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Members/ParameterSpec.cs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,23 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Globalization;
5+
using System;
46
using Microsoft.CodeAnalysis;
57
using Microsoft.CodeAnalysis.CSharp;
8+
using SourceGenerators;
69

710
namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
811
{
912
public sealed record ParameterSpec : MemberSpec
1013
{
11-
public ParameterSpec(IParameterSymbol parameter) : base(parameter)
14+
public ParameterSpec(IParameterSymbol parameter, TypeRef typeRef) : base(parameter, typeRef)
1215
{
1316
RefKind = parameter.RefKind;
1417

1518
if (parameter.HasExplicitDefaultValue)
1619
{
17-
string formatted = SymbolDisplay.FormatPrimitive(parameter.ExplicitDefaultValue!, quoteStrings: true, useHexadecimalNumbers: false);
18-
if (formatted is not "null")
19-
{
20-
DefaultValueExpr = formatted;
21-
}
20+
DefaultValueExpr = CSharpSyntaxUtilities.FormatLiteral(parameter.ExplicitDefaultValue, TypeRef);
2221
}
2322
else
2423
{

src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Members/PropertySpec.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using Microsoft.CodeAnalysis;
5+
using SourceGenerators;
56

67
namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
78
{
89
public sealed record PropertySpec : MemberSpec
910
{
10-
public PropertySpec(IPropertySymbol property) : base(property)
11+
public PropertySpec(IPropertySymbol property, TypeRef typeRef) : base(property, typeRef)
1112
{
1213
IMethodSymbol? setMethod = property.SetMethod;
1314
bool setterIsPublic = setMethod?.DeclaredAccessibility is Accessibility.Public;

src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.cs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,20 +112,45 @@ public ClassWhereParametersMatchPropertiesAndFields(string name, string address,
112112

113113
public record RecordWhereParametersHaveDefaultValue(string Name, string Address, int Age = 42);
114114

115-
public record ClassWhereParametersHaveDefaultValue
115+
public class ClassWhereParametersHaveDefaultValue
116116
{
117117
public string? Name { get; }
118118
public string Address { get; }
119119
public int Age { get; }
120-
121-
public ClassWhereParametersHaveDefaultValue(string? name, string address, int age = 42)
120+
public float F { get; }
121+
public double D { get; }
122+
public decimal M { get; }
123+
public StringComparison SC { get; }
124+
public char C { get; }
125+
public int? NAge { get; }
126+
public float? NF { get; }
127+
public double? ND { get; }
128+
public decimal? NM { get; }
129+
public StringComparison? NSC { get; }
130+
public char? NC { get; }
131+
132+
public ClassWhereParametersHaveDefaultValue(string? name, string address,
133+
int age = 42, float f = 42.0f, double d = 3.14159, decimal m = 3.1415926535897932384626433M, StringComparison sc = StringComparison.Ordinal, char c = 'q',
134+
int? nage = 42, float? nf = 42.0f, double? nd = 3.14159, decimal? nm = 3.1415926535897932384626433M, StringComparison? nsc = StringComparison.Ordinal, char? nc = 'q')
122135
{
123136
Name = name;
124137
Address = address;
125138
Age = age;
139+
F = f;
140+
D = d;
141+
M = m;
142+
SC = sc;
143+
C = c;
144+
NAge = nage;
145+
NF = nf;
146+
ND = nd;
147+
NM = nm;
148+
NSC = nsc;
149+
NC = nc;
126150
}
127151
}
128152

153+
129154
public class ClassWithPrimaryCtor(string color, int length)
130155
{
131156
public string Color { get; } = color;

src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,6 +1064,17 @@ public void BindsToClassConstructorParametersWithDefaultValues()
10641064
Assert.Equal("John", testOptions.ClassWhereParametersHaveDefaultValueProperty.Name);
10651065
Assert.Equal("123, Abc St.", testOptions.ClassWhereParametersHaveDefaultValueProperty.Address);
10661066
Assert.Equal(42, testOptions.ClassWhereParametersHaveDefaultValueProperty.Age);
1067+
Assert.Equal(42.0f, testOptions.ClassWhereParametersHaveDefaultValueProperty.F);
1068+
Assert.Equal(3.14159, testOptions.ClassWhereParametersHaveDefaultValueProperty.D);
1069+
Assert.Equal(3.1415926535897932384626433M, testOptions.ClassWhereParametersHaveDefaultValueProperty.M);
1070+
Assert.Equal(StringComparison.Ordinal, testOptions.ClassWhereParametersHaveDefaultValueProperty.SC);
1071+
Assert.Equal('q', testOptions.ClassWhereParametersHaveDefaultValueProperty.C);
1072+
Assert.Equal(42, testOptions.ClassWhereParametersHaveDefaultValueProperty.NAge);
1073+
Assert.Equal(42.0f, testOptions.ClassWhereParametersHaveDefaultValueProperty.NF);
1074+
Assert.Equal(3.14159, testOptions.ClassWhereParametersHaveDefaultValueProperty.ND);
1075+
Assert.Equal(3.1415926535897932384626433M, testOptions.ClassWhereParametersHaveDefaultValueProperty.NM);
1076+
Assert.Equal(StringComparison.Ordinal, testOptions.ClassWhereParametersHaveDefaultValueProperty.NSC);
1077+
Assert.Equal('q', testOptions.ClassWhereParametersHaveDefaultValueProperty.NC);
10671078
}
10681079

10691080
[Fact]

src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs

Lines changed: 1 addition & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -707,7 +707,7 @@ private static void GenerateCtorParamMetadataInitFunc(SourceWriter writer, strin
707707
ParameterType = typeof({{spec.ParameterType.FullyQualifiedName}}),
708708
Position = {{spec.ParameterIndex}},
709709
HasDefaultValue = {{FormatBool(spec.HasDefaultValue)}},
710-
DefaultValue = {{FormatDefaultConstructorParameter(spec.DefaultValue, spec.ParameterType)}}
710+
DefaultValue = {{CSharpSyntaxUtilities.FormatLiteral(spec.DefaultValue, spec.ParameterType)}}
711711
};
712712
713713
""");
@@ -1351,56 +1351,6 @@ private static string FormatJsonSerializerDefaults(JsonSerializerDefaults defaul
13511351
private static string CreateTypeInfoMethodName(TypeGenerationSpec typeSpec)
13521352
=> $"Create_{typeSpec.TypeInfoPropertyName}";
13531353

1354-
private static string FormatDefaultConstructorParameter(object? value, TypeRef type)
1355-
{
1356-
if (value == null)
1357-
{
1358-
return $"default({type.FullyQualifiedName})";
1359-
}
1360-
1361-
if (type.TypeKind is TypeKind.Enum)
1362-
{
1363-
// Return the numeric value.
1364-
return FormatNumber();
1365-
}
1366-
1367-
switch (value)
1368-
{
1369-
case string @string:
1370-
return SymbolDisplay.FormatLiteral(@string, quote: true); ;
1371-
case char @char:
1372-
return SymbolDisplay.FormatLiteral(@char, quote: true);
1373-
case double.NegativeInfinity:
1374-
return "double.NegativeInfinity";
1375-
case double.PositiveInfinity:
1376-
return "double.PositiveInfinity";
1377-
case double.NaN:
1378-
return "double.NaN";
1379-
case double @double:
1380-
return $"({type.FullyQualifiedName})({@double.ToString(JsonConstants.DoubleFormatString, CultureInfo.InvariantCulture)})";
1381-
case float.NegativeInfinity:
1382-
return "float.NegativeInfinity";
1383-
case float.PositiveInfinity:
1384-
return "float.PositiveInfinity";
1385-
case float.NaN:
1386-
return "float.NaN";
1387-
case float @float:
1388-
return $"({type.FullyQualifiedName})({@float.ToString(JsonConstants.SingleFormatString, CultureInfo.InvariantCulture)})";
1389-
case decimal.MaxValue:
1390-
return "decimal.MaxValue";
1391-
case decimal.MinValue:
1392-
return "decimal.MinValue";
1393-
case decimal @decimal:
1394-
return @decimal.ToString(CultureInfo.InvariantCulture);
1395-
case bool @bool:
1396-
return FormatBool(@bool);
1397-
default:
1398-
// Assume this is a number.
1399-
return FormatNumber();
1400-
}
1401-
1402-
string FormatNumber() => $"({type.FullyQualifiedName})({Convert.ToString(value, CultureInfo.InvariantCulture)})";
1403-
}
14041354

14051355
private static string FormatDefaultConstructorExpr(TypeGenerationSpec typeSpec)
14061356
{

src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.targets

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
<Compile Include="$(CoreLibSharedDir)System\Runtime\CompilerServices\RequiredMemberAttribute.cs" Link="Common\System\Runtime\CompilerServices\RequiredMemberAttribute.cs" />
3131
<Compile Include="$(CommonPath)\Roslyn\DiagnosticDescriptorHelper.cs" Link="Common\Roslyn\DiagnosticDescriptorHelper.cs" />
3232
<Compile Include="$(CommonPath)\Roslyn\GetBestTypeByMetadataName.cs" Link="Common\Roslyn\GetBestTypeByMetadataName.cs" />
33+
<Compile Include="$(CommonPath)\SourceGenerators\CSharpSyntaxUtilities.cs" Link="Common\SourceGenerators\CSharpSyntaxUtilities.cs" />
3334
<Compile Include="$(CommonPath)\SourceGenerators\DiagnosticInfo.cs" Link="Common\SourceGenerators\DiagnosticInfo.cs" />
3435
<Compile Include="$(CommonPath)\SourceGenerators\ImmutableEquatableArray.cs" Link="Common\SourceGenerators\ImmutableEquatableArray.cs" />
3536
<Compile Include="$(CommonPath)\SourceGenerators\SourceWriter.cs" Link="Common\SourceGenerators\SourceWriter.cs" />

0 commit comments

Comments
 (0)