Skip to content
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

Add sourcegen support for required & init-only properties. #79828

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
6 changes: 3 additions & 3 deletions src/libraries/System.Text.Json/System.Text.Json.sln
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,15 @@ Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "System.Text.Json.FSharp.Tes
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.TestLibrary.Roslyn3.11", "tests\System.Text.Json.SourceGeneration.TestLibrary\System.Text.Json.TestLibrary.Roslyn3.11.csproj", "{5C0CE30B-DD4A-4F7A-87C0-5243F0C86885}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.TestLibrary.Roslyn4.0", "tests\System.Text.Json.SourceGeneration.TestLibrary\System.Text.Json.TestLibrary.Roslyn4.0.csproj", "{FCA21178-0411-45D6-B597-B7BE145CEE33}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.TestLibrary.Roslyn4.4", "tests\System.Text.Json.SourceGeneration.TestLibrary\System.Text.Json.TestLibrary.Roslyn4.4.csproj", "{FCA21178-0411-45D6-B597-B7BE145CEE33}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.SourceGeneration.Roslyn3.11.Tests", "tests\System.Text.Json.SourceGeneration.Tests\System.Text.Json.SourceGeneration.Roslyn3.11.Tests.csproj", "{66AD4B7E-CF15-4A8F-8BF8-7E1BC6176D07}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.SourceGeneration.Roslyn4.0.Tests", "tests\System.Text.Json.SourceGeneration.Tests\System.Text.Json.SourceGeneration.Roslyn4.0.Tests.csproj", "{33599A6C-F340-4E1B-9B4D-CB8946C22140}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.SourceGeneration.Roslyn4.4.Tests", "tests\System.Text.Json.SourceGeneration.Tests\System.Text.Json.SourceGeneration.Roslyn4.4.Tests.csproj", "{33599A6C-F340-4E1B-9B4D-CB8946C22140}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.SourceGeneration.Roslyn3.11.Unit.Tests", "tests\System.Text.Json.SourceGeneration.Unit.Tests\System.Text.Json.SourceGeneration.Roslyn3.11.Unit.Tests.csproj", "{256A4653-4287-44B3-BDEF-67FC1522ED2F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.SourceGeneration.Roslyn4.0.Unit.Tests", "tests\System.Text.Json.SourceGeneration.Unit.Tests\System.Text.Json.SourceGeneration.Roslyn4.0.Unit.Tests.csproj", "{F6A18EB5-A8CC-4A39-9E85-5FA226019C3D}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.SourceGeneration.Roslyn4.4.Unit.Tests", "tests\System.Text.Json.SourceGeneration.Unit.Tests\System.Text.Json.SourceGeneration.Roslyn4.4.Unit.Tests.csproj", "{F6A18EB5-A8CC-4A39-9E85-5FA226019C3D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.Tests", "tests\System.Text.Json.Tests\System.Text.Json.Tests.csproj", "{A0178BAA-A1AF-4C69-8E4A-A700A2723DDC}"
EndProject
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ private static class ExceptionMessages
public const string IncompatibleConverterType =
"The converter '{0}' is not compatible with the type '{1}'.";

public const string InitOnlyPropertyDeserializationNotSupported =
"Deserialization of init-only properties is currently not supported in source generation mode.";
public const string InitOnlyPropertySetterNotSupported =
"Setting init-only properties is not supported in source generation mode.";

public const string InvalidJsonConverterFactoryOutput =
"The converter '{0}' cannot return null or a JsonConverterFactory instance.";
Expand Down
73 changes: 57 additions & 16 deletions src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text.Json.Reflection;
using System.Text.Json.Serialization;
Expand Down Expand Up @@ -699,7 +700,7 @@ private static string GeneratePropMetadataInitFunc(TypeGenerationSpec typeGenera
{
{ DefaultIgnoreCondition: JsonIgnoreCondition.Always } => "null",
{ CanUseSetter: true, IsInitOnlySetter: true }
=> @$"static (obj, value) => throw new {InvalidOperationExceptionTypeRef}(""{ExceptionMessages.InitOnlyPropertyDeserializationNotSupported}"")",
=> @$"static (obj, value) => throw new {InvalidOperationExceptionTypeRef}(""{ExceptionMessages.InitOnlyPropertySetterNotSupported}"")",
{ CanUseSetter: true } when typeGenerationSpec.IsValueType
=> $@"static (obj, value) => {UnsafeTypeRef}.Unbox<{declaringTypeCompilableName}>(obj).{nameSpecifiedInSourceCode} = value!",
{ CanUseSetter: true }
Expand Down Expand Up @@ -743,7 +744,8 @@ private static string GeneratePropMetadataInitFunc(TypeGenerationSpec typeGenera

{JsonPropertyInfoTypeRef} {propertyInfoVarName} = {JsonMetadataServicesTypeRef}.CreatePropertyInfo<{memberTypeCompilableName}>({OptionsLocalVariableName}, {infoVarName});");

if (memberMetadata.IsRequired)
if (memberMetadata.HasJsonRequiredAttribute ||
(memberMetadata.IsRequired && !typeGenerationSpec.ConstructorSetsRequiredParameters))
{
sb.Append($@"
{propertyInfoVarName}.IsRequired = true;");
Expand Down Expand Up @@ -772,7 +774,8 @@ string GenerateCtorParamMetadataInitFunc(TypeGenerationSpec typeGenerationSpec)
Debug.Assert(typeGenerationSpec.CtorParamGenSpecArray != null);

ParameterGenerationSpec[] parameters = typeGenerationSpec.CtorParamGenSpecArray;
int paramCount = parameters.Length;
List<PropertyInitializerGenerationSpec>? propertyInitializers = typeGenerationSpec.PropertyInitializerSpecList;
int paramCount = parameters.Length + (propertyInitializers?.Count(propInit => !propInit.MatchesConstructorParameter) ?? 0);
Debug.Assert(paramCount > 0);

StringBuilder sb = new($@"
Expand All @@ -782,10 +785,9 @@ string GenerateCtorParamMetadataInitFunc(TypeGenerationSpec typeGenerationSpec)
{JsonParameterInfoValuesTypeRef}[] {parametersVarName} = new {JsonParameterInfoValuesTypeRef}[{paramCount}];
{JsonParameterInfoValuesTypeRef} info;
");

for (int i = 0; i < paramCount; i++)
foreach (ParameterGenerationSpec spec in parameters)
{
ParameterInfo reflectionInfo = parameters[i].ParameterInfo;
ParameterInfo reflectionInfo = spec.ParameterInfo;
Type parameterType = reflectionInfo.ParameterType;
string parameterTypeRef = parameterType.GetCompilableName();

Expand All @@ -801,8 +803,31 @@ string GenerateCtorParamMetadataInitFunc(TypeGenerationSpec typeGenerationSpec)
HasDefaultValue = {FormatBool(reflectionInfo.HasDefaultValue)},
DefaultValue = {defaultValueAsStr}
}};
{parametersVarName}[{i}] = {InfoVarName};
{parametersVarName}[{spec.ParameterIndex}] = {InfoVarName};
");
}

if (propertyInitializers != null)
{
Debug.Assert(propertyInitializers.Count > 0);

foreach (PropertyInitializerGenerationSpec spec in propertyInitializers)
{
if (spec.MatchesConstructorParameter)
continue;

sb.Append(@$"
{InfoVarName} = new()
{{
Name = ""{spec.Property.JsonPropertyName ?? spec.Property.ClrName}"",
ParameterType = typeof({spec.Property.TypeGenerationSpec.TypeRef}),
Position = {spec.ParameterIndex},
HasDefaultValue = false,
DefaultValue = default({spec.Property.TypeGenerationSpec.TypeRef}),
}};
{parametersVarName}[{spec.ParameterIndex}] = {InfoVarName};
");
}
}

sb.Append(@$"
Expand Down Expand Up @@ -959,27 +984,43 @@ private static bool ShouldIncludePropertyForFastPath(PropertyGenerationSpec prop
private static string GetParameterizedCtorInvocationFunc(TypeGenerationSpec typeGenerationSpec)
{
Debug.Assert(typeGenerationSpec.CtorParamGenSpecArray != null);

ParameterGenerationSpec[] parameters = typeGenerationSpec.CtorParamGenSpecArray;
int paramCount = parameters.Length;
Debug.Assert(paramCount != 0);
List<PropertyInitializerGenerationSpec>? propertyInitializers = typeGenerationSpec.PropertyInitializerSpecList;

const string ArgsVarName = "args";
int lastIndex = paramCount - 1;

StringBuilder sb = new($"static ({ArgsVarName}) => new {typeGenerationSpec.TypeRef}(");

for (int i = 0; i < lastIndex; i++)
if (parameters.Length > 0)
{
sb.Append($"{GetParamUnboxing(parameters[i], i)}, ");
foreach (ParameterGenerationSpec param in parameters)
{
int index = param.ParameterIndex;
sb.Append($"{GetParamUnboxing(param.ParameterInfo.ParameterType, index)}, ");
}

sb.Length -= 2; // delete the last ", " token
}

sb.Append($"{GetParamUnboxing(parameters[lastIndex], lastIndex)})");
sb.Append(')');

if (propertyInitializers != null)
{
Debug.Assert(propertyInitializers.Count > 0);
sb.Append("{ ");
foreach (PropertyInitializerGenerationSpec property in propertyInitializers)
{
sb.Append($"{property.Property.ClrName} = {GetParamUnboxing(property.Property.TypeGenerationSpec.Type, property.ParameterIndex)}, ");
}

sb.Length -= 2; // delete the last ", " token
layomia marked this conversation as resolved.
Show resolved Hide resolved
sb.Append(" }");
}

return sb.ToString();

static string GetParamUnboxing(ParameterGenerationSpec spec, int index)
=> $"({spec.ParameterInfo.ParameterType.GetCompilableName()}){ArgsVarName}[{index}]";
static string GetParamUnboxing(Type type, int index)
=> $"({type.GetCompilableName()}){ArgsVarName}[{index}]";
}

private string? GetWriterMethod(Type type)
Expand Down
Loading