Skip to content
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})";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: wouldn't it be preferable to just use null for the case of nullable types?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am seeing we used null before.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

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 @@ -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
};

Expand Down Expand Up @@ -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,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,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,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;
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
Original file line number Diff line number Diff line change
Expand Up @@ -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)}}
};

""");
Expand Down Expand Up @@ -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)
{
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 @@ -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<Class_With_Parameters_Default_Values>(json);
result.Verify();
}

[Fact]
public async Task TestClassWithCustomConverterOnCtorParameter_ShouldPassCorrectTypeToConvertParameter()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))]
Expand Down Expand Up @@ -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))]
Expand Down