-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
Apply the SourceWriter pattern to the STJ source generator. #86526
Apply the SourceWriter pattern to the STJ source generator. #86526
Conversation
Tagging subscribers to this area: @dotnet/area-system-text-json, @gregsdennis Issue DetailsMakes the following changes:
Generated code now looks as follows: Main context file// <auto-generated/>
#nullable enable annotations
#nullable disable warnings
// Suppress warnings about [Obsolete] member usage in generated code.
#pragma warning disable CS0612, CS0618
namespace System.Text.Json.SourceGeneration.Tests
{
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Text.Json.SourceGeneration", "42.42.42.42")]
internal partial class MixedModeContext
{
private readonly static global::System.Text.Json.JsonSerializerOptions s_defaultOptions = new()
{
DefaultIgnoreCondition = global::System.Text.Json.Serialization.JsonIgnoreCondition.Never,
IgnoreReadOnlyFields = false,
IgnoreReadOnlyProperties = false,
IncludeFields = true,
WriteIndented = false,
PropertyNamingPolicy = null
};
private static global::System.Text.Json.SourceGeneration.Tests.MixedModeContext? s_defaultContext;
/// <summary>
/// The default <see cref="global::System.Text.Json.Serialization.JsonSerializerContext"/> associated with a default <see cref="global::System.Text.Json.JsonSerializerOptions"/> instance.
/// </summary>
public static global::System.Text.Json.SourceGeneration.Tests.MixedModeContext Default => s_defaultContext ??= new global::System.Text.Json.SourceGeneration.Tests.MixedModeContext(new global::System.Text.Json.JsonSerializerOptions(s_defaultOptions));
/// <summary>
/// The source-generated options associated with this context.
/// </summary>
protected override global::System.Text.Json.JsonSerializerOptions? GeneratedSerializerOptions { get; } = s_defaultOptions;
/// <inheritdoc/>
public MixedModeContext() : base(null)
{
}
/// <inheritdoc/>
public MixedModeContext(global::System.Text.Json.JsonSerializerOptions options) : base(options)
{
}
private bool TryGetTypeInfoForRuntimeCustomConverter<TJsonMetadataType>(global::System.Text.Json.JsonSerializerOptions options, out global::System.Text.Json.Serialization.Metadata.JsonTypeInfo<TJsonMetadataType> jsonTypeInfo)
{
foreach (global::System.Text.Json.Serialization.JsonConverter? converter in options.Converters)
{
if (converter?.CanConvert(typeof(TJsonMetadataType)) == true)
{
jsonTypeInfo = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreateValueInfo<TJsonMetadataType>(options, ExpandConverter(typeof(TJsonMetadataType), converter, options));
return true;
}
}
jsonTypeInfo = null;
return false;
}
private static global::System.Text.Json.Serialization.JsonConverter ExpandConverter(global::System.Type type, global::System.Text.Json.Serialization.JsonConverter converter, global::System.Text.Json.JsonSerializerOptions options)
{
if (converter is global::System.Text.Json.Serialization.JsonConverterFactory factory)
{
converter = factory.CreateConverter(type, options);
if (converter is null or global::System.Text.Json.Serialization.JsonConverterFactory)
{
throw new global::System.InvalidOperationException(string.Format("The converter '{0}' cannot return null or a JsonConverterFactory instance.", factory.GetType()));
}
}
if (!converter.CanConvert(type))
{
throw new global::System.InvalidOperationException(string.Format("The converter '{0}' is not compatible with the type '{1}'.", converter.GetType(), type));
}
return converter;
}
}
} JsonTypeInfo declaration file// <auto-generated/>
#nullable enable annotations
#nullable disable warnings
// Suppress warnings about [Obsolete] member usage in generated code.
#pragma warning disable CS0612, CS0618
namespace System.Text.Json.SourceGeneration.Tests
{
internal partial class MixedModeContext
{
private global::System.Text.Json.Serialization.Metadata.JsonTypeInfo<global::System.Text.Json.SourceGeneration.Tests.HighLowTempsRecord>? _HighLowTempsRecord;
/// <summary>
/// Defines the source generated JSON serialization contract metadata for a given type.
/// </summary>
public global::System.Text.Json.Serialization.Metadata.JsonTypeInfo<global::System.Text.Json.SourceGeneration.Tests.HighLowTempsRecord> HighLowTempsRecord
{
get => _HighLowTempsRecord ??= (global::System.Text.Json.Serialization.Metadata.JsonTypeInfo<global::System.Text.Json.SourceGeneration.Tests.HighLowTempsRecord>)Options.GetTypeInfo(typeof(global::System.Text.Json.SourceGeneration.Tests.HighLowTempsRecord));
}
private global::System.Text.Json.Serialization.Metadata.JsonTypeInfo<global::System.Text.Json.SourceGeneration.Tests.HighLowTempsRecord> Create_HighLowTempsRecord(global::System.Text.Json.JsonSerializerOptions options)
{
if (!TryGetTypeInfoForRuntimeCustomConverter<global::System.Text.Json.SourceGeneration.Tests.HighLowTempsRecord>(options, out global::System.Text.Json.Serialization.Metadata.JsonTypeInfo<global::System.Text.Json.SourceGeneration.Tests.HighLowTempsRecord> jsonTypeInfo))
{
var objectInfo = new global::System.Text.Json.Serialization.Metadata.JsonObjectInfoValues<global::System.Text.Json.SourceGeneration.Tests.HighLowTempsRecord>()
{
ObjectCreator = null,
ObjectWithParameterizedConstructorCreator = static (args) => new global::System.Text.Json.SourceGeneration.Tests.HighLowTempsRecord((int)args[0], (int)args[1]),
PropertyMetadataInitializer = _ => HighLowTempsRecordPropInit(options),
ConstructorParameterMetadataInitializer = HighLowTempsRecordCtorParamInit,
NumberHandling = default,
SerializeHandler = null
};
jsonTypeInfo = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreateObjectInfo<global::System.Text.Json.SourceGeneration.Tests.HighLowTempsRecord>(options, objectInfo);
}
jsonTypeInfo.OriginatingResolver = this;
return jsonTypeInfo;
}
private static global::System.Text.Json.Serialization.Metadata.JsonPropertyInfo[] HighLowTempsRecordPropInit(global::System.Text.Json.JsonSerializerOptions options)
{
var properties = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfo[3];
var info0 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<global::System.Type>()
{
IsProperty = true,
IsPublic = false,
IsVirtual = true,
DeclaringType = typeof(global::System.Text.Json.SourceGeneration.Tests.HighLowTempsRecord),
Converter = null,
Getter = null,
Setter = null,
IgnoreCondition = null,
HasJsonInclude = false,
IsExtensionData = false,
NumberHandling = default,
PropertyName = "EqualityContract",
JsonPropertyName = null
};
properties[0] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo<global::System.Type>(options, info0);
var info1 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<int>()
{
IsProperty = true,
IsPublic = true,
IsVirtual = false,
DeclaringType = typeof(global::System.Text.Json.SourceGeneration.Tests.HighLowTempsRecord),
Converter = null,
Getter = static (obj) => ((global::System.Text.Json.SourceGeneration.Tests.HighLowTempsRecord)obj).High,
Setter = static (obj, value) => throw new global::System.InvalidOperationException("Setting init-only properties is not supported in source generation mode."),
IgnoreCondition = null,
HasJsonInclude = false,
IsExtensionData = false,
NumberHandling = default,
PropertyName = "High",
JsonPropertyName = null
};
properties[1] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo<int>(options, info1);
var info2 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<int>()
{
IsProperty = true,
IsPublic = true,
IsVirtual = false,
DeclaringType = typeof(global::System.Text.Json.SourceGeneration.Tests.HighLowTempsRecord),
Converter = null,
Getter = static (obj) => ((global::System.Text.Json.SourceGeneration.Tests.HighLowTempsRecord)obj).Low,
Setter = static (obj, value) => throw new global::System.InvalidOperationException("Setting init-only properties is not supported in source generation mode."),
IgnoreCondition = null,
HasJsonInclude = false,
IsExtensionData = false,
NumberHandling = default,
PropertyName = "Low",
JsonPropertyName = null
};
properties[2] = global::System.Text.Json.Serialization.Metadata.JsonMetadataServices.CreatePropertyInfo<int>(options, info2);
return properties;
}
private static global::System.Text.Json.Serialization.Metadata.JsonParameterInfoValues[] HighLowTempsRecordCtorParamInit()
{
var parameters = new global::System.Text.Json.Serialization.Metadata.JsonParameterInfoValues[2];
parameters[0] = new()
{
Name = "High",
ParameterType = typeof(int),
Position = 0,
HasDefaultValue = false,
DefaultValue = default(int)
};
parameters[1] = new()
{
Name = "Low",
ParameterType = typeof(int),
Position = 1,
HasDefaultValue = false,
DefaultValue = default(int)
};
return parameters;
}
}
}
|
cc @Sergio0694 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM - just a couple questions about the writer.
ImmutableEquatableArray<string> declarationList = _currentContext.ContextClassDeclarations; | ||
int declarationCount = declarationList.Count; | ||
Debug.Assert(declarationCount >= 1); | ||
writer.WriteLine(""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this method be renamed given that this isn't one line?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any suggestions? I figured it should follow StringBuilder
convention in which "Line" suggests that a newline is inserted at the end of the string, which is happening here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm using WriteBlock
in the config generator. My comments about this apply if we can consider this a C# writer. If it's a general source writer, I'm not sure why IndentedStringBuilder
isn't sufficient for writing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you mean IndentedSourceWriter
, the type doesn't support multi-line string literals. It's intended as a direct replacement of that type.
$"{contextName}.g.cs", | ||
GetRootJsonContextImplementation(), | ||
isRootContextDef: true); | ||
_sourceGenerationContext.AddSource($"{contextName}.g.cs", GetRootJsonContextImplementation(contextGenerationSpec)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: first assign to sourceText
declared above?
{ | ||
valueToWrite = $"{valueToWrite}.ToString()"; | ||
} | ||
writer.WriteLine('{'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not let the writer handle brackets and indentation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are a few reasons, one I'd prefer if the writer class remained language agnostic, secondly you might want to indent without inserting brackets (e.g. loop containing a single statement), thirdly in some cases it makes sense to make non-trivial changes to indentation, e.g. here:
return writer; | ||
} | ||
|
||
private static SourceText CompleteSourceFileAndReturnText(SourceWriter writer) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this be a helper on the writer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It could, although I wanted to make the writer language agnostic (and only concern itself with the responsibility of track indentation).
string metadataInitSource = $$""" | ||
var {{InfoVarName}} = new {{JsonCollectionInfoValuesTypeRef}}<{{typeFQN}}>() | ||
{ | ||
{{ObjectCreatorPropName}} = {{FormatDefaultConstructorExpr(typeGenerationSpec)}}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reckon it's worth it, wrt. saving LOC, to skip props that would be set to default values in the init logic?
e.g. many of these props
var info0 = new global::System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<global::System.Type>()
{
IsProperty = true,
IsPublic = false,
IsVirtual = true,
DeclaringType = typeof(global::System.Text.Json.SourceGeneration.Tests.HighLowTempsRecord),
Converter = null,
Getter = null,
Setter = null,
IgnoreCondition = null,
HasJsonInclude = false,
IsExtensionData = false,
NumberHandling = default,
PropertyName = "EqualityContract",
JsonPropertyName = null
};
(btw should we fix #77675 soon?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've been thinking about it, one concern is that conditionally emitting each of the assignments would result in harder-to-read emitter code. I've been thinking about approaches such as using Regex to trim Foo = null
assignments but it's probably too hacky to consider. Happy to entertain other approaches that make this simple to achieve.
(btw should we fix #77675 soon?)
Absolutely. I've been trying to avoid any functional changes for the scope of these refactoring PRs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very cool! 🙌
Will do another before-after comparison in the Store as well to check the binary size with the changes from this PR as well, it's possible that multiplied by all the types we have this could result in a visible difference (maybe not huge, but still nice) 😄
Makes the following changes:
SourceWriter
pattern already used in the ConfigurationBinder source generator. This should minimize the amount indentation-related problems and minimize the amount of intermediate strings being created by the emitter.Generated code now looks as follows:
Main context file
JsonTypeInfo declaration file
JsonTypeInfo with design-time custom converter
Contributes to #68353.