Skip to content

[release/8.0-staging] Fix literal formatting in source generators #94070

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions src/libraries/Common/src/SourceGenerators/CSharpSyntaxUtilities.cs
Original file line number Diff line number Diff line change
@@ -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)})";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -582,9 +582,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
};

Expand Down Expand Up @@ -616,9 +615,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,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,6 @@ void EmitBindImplForMember(MemberSpec member)
TypeSpec memberType = _typeIndex.GetTypeSpec(member.TypeRef);
string parsedMemberDeclarationLhs = $"{memberType.TypeRef.FullyQualifiedName} {member.Name}";
string configKeyName = member.ConfigurationKeyName;
string parsedMemberAssignmentLhsExpr;

switch (memberType)
{
Expand All @@ -392,31 +391,22 @@ void EmitBindImplForMember(MemberSpec member)
_writer.WriteLine();
return;
}

parsedMemberAssignmentLhsExpr = parsedMemberDeclarationLhs;
}
break;
case ConfigurationSectionSpec:
{
_writer.WriteLine($"{parsedMemberDeclarationLhs} = {GetSectionFromConfigurationExpression(configKeyName)};");
return;
}
default:
{
string bangExpr = memberType.IsValueType ? string.Empty : "!";
string parsedMemberIdentifierDeclaration = $"{parsedMemberDeclarationLhs} = {member.DefaultValueExpr}{bangExpr};";

_writer.WriteLine(parsedMemberIdentifierDeclaration);
_emitBlankLineBeforeNextStatement = false;

parsedMemberAssignmentLhsExpr = member.Name;
}
break;
}

string bangExpr = memberType.IsValueType ? string.Empty : "!";
_writer.WriteLine($"{parsedMemberDeclarationLhs} = {member.DefaultValueExpr}{bangExpr};");
_emitBlankLineBeforeNextStatement = false;

bool canBindToMember = this.EmitBindImplForMember(
member,
parsedMemberAssignmentLhsExpr,
member.Name,
sectionPathExpr: GetSectionPathFromConfigurationExpression(configKeyName),
canSet: true,
InitializationKind.None);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<Compile Include="$(CoreLibSharedDir)System\Runtime\CompilerServices\RequiredMemberAttribute.cs" Link="Common\System\Runtime\CompilerServices\RequiredMemberAttribute.cs" />
<Compile Include="$(CommonPath)\Roslyn\DiagnosticDescriptorHelper.cs" Link="Common\Roslyn\DiagnosticDescriptorHelper.cs" />
<Compile Include="$(CommonPath)\Roslyn\GetBestTypeByMetadataName.cs" Link="Common\Roslyn\GetBestTypeByMetadataName.cs" />
<Compile Include="$(CommonPath)\SourceGenerators\CSharpSyntaxUtilities.cs" Link="Common\SourceGenerators\CSharpSyntaxUtilities.cs" />
<Compile Include="$(CommonPath)\SourceGenerators\DiagnosticInfo.cs" Link="Common\SourceGenerators\DiagnosticInfo.cs" />
<Compile Include="$(CommonPath)\SourceGenerators\ImmutableEquatableArray.cs" Link="Common\SourceGenerators\ImmutableEquatableArray.cs" />
<Compile Include="$(CommonPath)\SourceGenerators\SourceWriter.cs" Link="Common\SourceGenerators\SourceWriter.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,17 +112,41 @@ 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 = "John Doe", string address = "1 Microsoft Way",
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;
}
}

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

public class ClassWithPrimaryCtorDefaultValues(string color = "blue", int length = 15, decimal height = 5.946238490567943927384M, EditorBrowsableState eb = EditorBrowsableState.Never)
{
public string Color { get; } = color;
public int Length { get; } = length;
public decimal Height { get; } = height;
public EditorBrowsableState EB { get;} = eb;
}
public record RecordTypeOptions(string Color, int Length);

public record Line(string Color, int Length, int Thickness);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -1404,6 +1415,24 @@ public void CanBindClassWithPrimaryCtor()
Assert.Equal("Green", options.Color);
}

[Fact]
public void CanBindClassWithPrimaryCtorWithDefaultValues()
{
var dic = new Dictionary<string, string>
{
{"Length", "-1"}
};
var configurationBuilder = new ConfigurationBuilder();
configurationBuilder.AddInMemoryCollection(dic);
var config = configurationBuilder.Build();

var options = config.Get<ClassWithPrimaryCtorDefaultValues>();
Assert.Equal(-1, options.Length);
Assert.Equal("blue", options.Color);
Assert.Equal(5.946238490567943927384M, options.Height);
Assert.Equal(EditorBrowsableState.Never, options.EB);
}

[Fact]
public void CanBindRecordStructOptions()
{
Expand Down
Loading