Skip to content

Commit 213f1a2

Browse files
[release/8.0-staging] Fix literal formatting in source generators (#94070)
* 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 * Fix binding types with optional string parameters (#93563) * Add a baseline test for constructor parameters * Fix binding types with optional string parameters Ensure the source generator emits the declaration with default value for all cases when we emit the bind logic. * Add a test case that uses a Primary Constructor with default values * Split baseline data for added test. * Fix .NETFramework test baseline * Update baselines after global namespace change # Conflicts: * Update added baseline after global:: change --------- Co-authored-by: Eric StJohn <[email protected]>
1 parent ef625e4 commit 213f1a2

File tree

16 files changed

+523
-82
lines changed

16 files changed

+523
-82
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
@@ -582,9 +582,8 @@ private ObjectSpec CreateObjectSpec(TypeParseInfo typeParseInfo)
582582
AttributeData? attributeData = property.GetAttributes().FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, _typeSymbols.ConfigurationKeyNameAttribute));
583583
string configKeyName = attributeData?.ConstructorArguments.FirstOrDefault().Value as string ?? propertyName;
584584

585-
PropertySpec spec = new(property)
585+
PropertySpec spec = new(property, propertyTypeRef)
586586
{
587-
TypeRef = propertyTypeRef,
588587
ConfigurationKeyName = configKeyName
589588
};
590589

@@ -616,9 +615,8 @@ private ObjectSpec CreateObjectSpec(TypeParseInfo typeParseInfo)
616615
}
617616
else
618617
{
619-
ParameterSpec paramSpec = new ParameterSpec(parameter)
618+
ParameterSpec paramSpec = new ParameterSpec(parameter, propertySpec.TypeRef)
620619
{
621-
TypeRef = propertySpec.TypeRef,
622620
ConfigurationKeyName = propertySpec.ConfigurationKeyName,
623621
};
624622

src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/CoreBindingHelpers.cs

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,6 @@ void EmitBindImplForMember(MemberSpec member)
379379
TypeSpec memberType = _typeIndex.GetTypeSpec(member.TypeRef);
380380
string parsedMemberDeclarationLhs = $"{memberType.TypeRef.FullyQualifiedName} {member.Name}";
381381
string configKeyName = member.ConfigurationKeyName;
382-
string parsedMemberAssignmentLhsExpr;
383382

384383
switch (memberType)
385384
{
@@ -392,31 +391,22 @@ void EmitBindImplForMember(MemberSpec member)
392391
_writer.WriteLine();
393392
return;
394393
}
395-
396-
parsedMemberAssignmentLhsExpr = parsedMemberDeclarationLhs;
397394
}
398395
break;
399396
case ConfigurationSectionSpec:
400397
{
401398
_writer.WriteLine($"{parsedMemberDeclarationLhs} = {GetSectionFromConfigurationExpression(configKeyName)};");
402399
return;
403400
}
404-
default:
405-
{
406-
string bangExpr = memberType.IsValueType ? string.Empty : "!";
407-
string parsedMemberIdentifierDeclaration = $"{parsedMemberDeclarationLhs} = {member.DefaultValueExpr}{bangExpr};";
408-
409-
_writer.WriteLine(parsedMemberIdentifierDeclaration);
410-
_emitBlankLineBeforeNextStatement = false;
411-
412-
parsedMemberAssignmentLhsExpr = member.Name;
413-
}
414-
break;
415401
}
416402

403+
string bangExpr = memberType.IsValueType ? string.Empty : "!";
404+
_writer.WriteLine($"{parsedMemberDeclarationLhs} = {member.DefaultValueExpr}{bangExpr};");
405+
_emitBlankLineBeforeNextStatement = false;
406+
417407
bool canBindToMember = this.EmitBindImplForMember(
418408
member,
419-
parsedMemberAssignmentLhsExpr,
409+
member.Name,
420410
sectionPathExpr: GetSectionPathFromConfigurationExpression(configKeyName),
421411
canSet: true,
422412
InitializationKind.None);

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
@@ -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" />

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: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,17 +112,41 @@ 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 = "John Doe", string address = "1 Microsoft Way",
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

@@ -132,6 +156,13 @@ public class ClassWithPrimaryCtor(string color, int length)
132156
public int Length { get; } = length;
133157
}
134158

159+
public class ClassWithPrimaryCtorDefaultValues(string color = "blue", int length = 15, decimal height = 5.946238490567943927384M, EditorBrowsableState eb = EditorBrowsableState.Never)
160+
{
161+
public string Color { get; } = color;
162+
public int Length { get; } = length;
163+
public decimal Height { get; } = height;
164+
public EditorBrowsableState EB { get;} = eb;
165+
}
135166
public record RecordTypeOptions(string Color, int Length);
136167

137168
public record Line(string Color, int Length, int Thickness);

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

Lines changed: 29 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]
@@ -1404,6 +1415,24 @@ public void CanBindClassWithPrimaryCtor()
14041415
Assert.Equal("Green", options.Color);
14051416
}
14061417

1418+
[Fact]
1419+
public void CanBindClassWithPrimaryCtorWithDefaultValues()
1420+
{
1421+
var dic = new Dictionary<string, string>
1422+
{
1423+
{"Length", "-1"}
1424+
};
1425+
var configurationBuilder = new ConfigurationBuilder();
1426+
configurationBuilder.AddInMemoryCollection(dic);
1427+
var config = configurationBuilder.Build();
1428+
1429+
var options = config.Get<ClassWithPrimaryCtorDefaultValues>();
1430+
Assert.Equal(-1, options.Length);
1431+
Assert.Equal("blue", options.Color);
1432+
Assert.Equal(5.946238490567943927384M, options.Height);
1433+
Assert.Equal(EditorBrowsableState.Never, options.EB);
1434+
}
1435+
14071436
[Fact]
14081437
public void CanBindRecordStructOptions()
14091438
{

0 commit comments

Comments
 (0)