From c5cfd7b97f904e38c2d8daf28e8f2c99c8528603 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Mon, 22 May 2023 18:30:34 +0100 Subject: [PATCH] Apply the SourceWriter pattern to the STJ source generator. (#86526) * Use the SourceWriter pattern in the STJ SG emitter. * Make helper method static. * Minimize the size of the generic helper method. --- .../gen/Helpers/KnownTypeSymbols.cs | 4 +- .../System.Text.Json/gen/Helpers/SourceWriter.cs | 136 ++ .../gen/JsonSourceGenerator.Emitter.cs | 1399 +++++++++----------- .../gen/JsonSourceGenerator.Parser.cs | 127 +- .../gen/Model/PropertyGenerationSpec.cs | 12 +- .../gen/Model/PropertyInitializerGenerationSpec.cs | 4 +- .../gen/Model/TypeGenerationSpec.cs | 8 +- .../gen/System.Text.Json.SourceGeneration.targets | 2 + 8 files changed, 855 insertions(+), 837 deletions(-) create mode 100644 src/libraries/System.Text.Json/gen/Helpers/SourceWriter.cs diff --git a/src/libraries/System.Text.Json/gen/Helpers/KnownTypeSymbols.cs b/src/libraries/System.Text.Json/gen/Helpers/KnownTypeSymbols.cs index c57f596..412a33b 100644 --- a/src/libraries/System.Text.Json/gen/Helpers/KnownTypeSymbols.cs +++ b/src/libraries/System.Text.Json/gen/Helpers/KnownTypeSymbols.cs @@ -64,9 +64,7 @@ namespace System.Text.Json.SourceGeneration public readonly INamedTypeSymbol? VersionType = compilation!.GetBestTypeByMetadataName(typeof(Version)); // System.Text.Json types - public readonly INamedTypeSymbol? JsonConverterOfTType = compilation!.GetBestTypeByMetadataName("System.Text.Json.Serialization.JsonConverter`1"); - public readonly INamedTypeSymbol? JsonConverterFactoryType = compilation!.GetBestTypeByMetadataName("System.Text.Json.Serialization.JsonConverterFactory"); - + public readonly INamedTypeSymbol? JsonConverterType = compilation!.GetBestTypeByMetadataName("System.Text.Json.Serialization.JsonConverter"); public readonly INamedTypeSymbol? JsonSerializerContextType = compilation.GetBestTypeByMetadataName("System.Text.Json.Serialization.JsonSerializerContext"); public readonly INamedTypeSymbol? JsonSerializableAttributeType = compilation.GetBestTypeByMetadataName("System.Text.Json.Serialization.JsonSerializableAttribute"); diff --git a/src/libraries/System.Text.Json/gen/Helpers/SourceWriter.cs b/src/libraries/System.Text.Json/gen/Helpers/SourceWriter.cs new file mode 100644 index 0000000..8bada81 --- /dev/null +++ b/src/libraries/System.Text.Json/gen/Helpers/SourceWriter.cs @@ -0,0 +1,136 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis.Text; +using System.Diagnostics; + +namespace System.Text.Json.SourceGeneration +{ + internal sealed class SourceWriter + { + private readonly StringBuilder _sb = new(); + private int _indentation; + + public SourceWriter() + { + IndentationChar = ' '; + CharsPerIndentation = 4; + } + + public SourceWriter(char indentationChar, int charsPerIndentation) + { + if (!char.IsWhiteSpace(indentationChar)) + { + throw new ArgumentOutOfRangeException(nameof(indentationChar)); + } + + if (charsPerIndentation < 1) + { + throw new ArgumentOutOfRangeException(nameof(charsPerIndentation)); + } + + IndentationChar = indentationChar; + CharsPerIndentation = charsPerIndentation; + } + + public char IndentationChar { get; } + public int CharsPerIndentation { get; } + + public int Length => _sb.Length; + public int Indentation + { + get => _indentation; + set + { + if (value < 0) + { + Throw(); + static void Throw() => throw new ArgumentOutOfRangeException(nameof(value)); + } + + _indentation = value; + } + } + + public void WriteLine(char value) + { + AddIndentation(); + _sb.Append(value); + _sb.AppendLine(); + } + + public void WriteLine(string text) + { + if (_indentation == 0) + { + _sb.AppendLine(text); + return; + } + + bool isFinalLine; + ReadOnlySpan remainingText = text.AsSpan(); + do + { + ReadOnlySpan nextLine = GetNextLine(ref remainingText, out isFinalLine); + + AddIndentation(); + AppendSpan(_sb, nextLine); + _sb.AppendLine(); + } + while (!isFinalLine); + } + + public void WriteLine() => _sb.AppendLine(); + + public SourceText ToSourceText() + { + Debug.Assert(_indentation == 0 && _sb.Length > 0); + return SourceText.From(_sb.ToString(), Encoding.UTF8); + } + + private void AddIndentation() + => _sb.Append(IndentationChar, CharsPerIndentation * _indentation); + + private static ReadOnlySpan GetNextLine(ref ReadOnlySpan remainingText, out bool isFinalLine) + { + if (remainingText.IsEmpty) + { + isFinalLine = true; + return default; + } + + ReadOnlySpan next; + ReadOnlySpan rest; + + int lineLength = remainingText.IndexOf('\n'); + if (lineLength == -1) + { + lineLength = remainingText.Length; + isFinalLine = true; + rest = default; + } + else + { + rest = remainingText.Slice(lineLength + 1); + isFinalLine = false; + } + + if ((uint)lineLength > 0 && remainingText[lineLength - 1] == '\r') + { + lineLength--; + } + + next = remainingText.Slice(0, lineLength); + remainingText = rest; + return next; + } + + private static unsafe void AppendSpan(StringBuilder builder, ReadOnlySpan span) + { + fixed (char* ptr = span) + { + builder.Append(ptr, span.Length); + } + } + } +} diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index eba96de..5f78d34 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -16,8 +16,6 @@ namespace System.Text.Json.SourceGeneration { public sealed partial class JsonSourceGenerator { - private const string OptionsLocalVariableName = "options"; - private sealed partial class Emitter { // Literals in generated source @@ -25,11 +23,8 @@ namespace System.Text.Json.SourceGeneration private const string CtorParamInitMethodNameSuffix = "CtorParamInit"; private const string DefaultOptionsStaticVarName = "s_defaultOptions"; private const string DefaultContextBackingStaticVarName = "s_defaultContext"; - internal const string GetConverterFromFactoryMethodName = "GetConverterFromFactory"; private const string OriginatingResolverPropertyName = "OriginatingResolver"; private const string InfoVarName = "info"; - private const string PropertyInfoVarName = "propertyInfo"; - internal const string JsonContextVarName = "jsonContext"; private const string NumberHandlingPropName = "NumberHandling"; private const string UnmappedMemberHandlingPropName = "UnmappedMemberHandling"; private const string PreferredPropertyObjectCreationHandlingPropName = "PreferredPropertyObjectCreationHandling"; @@ -37,23 +32,20 @@ namespace System.Text.Json.SourceGeneration private const string OptionsInstanceVariableName = "Options"; private const string JsonTypeInfoReturnValueLocalVariableName = "jsonTypeInfo"; private const string PropInitMethodNameSuffix = "PropInit"; - private const string RuntimeCustomConverterFetchingMethodName = "GetRuntimeProvidedCustomConverter"; + private const string TryGetTypeInfoForRuntimeCustomConverterMethodName = "TryGetTypeInfoForRuntimeCustomConverter"; + private const string ExpandConverterMethodName = "ExpandConverter"; private const string SerializeHandlerPropName = "SerializeHandler"; + private const string OptionsLocalVariableName = "options"; private const string ValueVarName = "value"; private const string WriterVarName = "writer"; private static readonly AssemblyName s_assemblyName = typeof(Emitter).Assembly.GetName(); - private static readonly string s_generatedCodeAttributeSource = $@" -[global::System.CodeDom.Compiler.GeneratedCodeAttribute(""{s_assemblyName.Name}"", ""{s_assemblyName.Version}"")] -"; // global::fully.qualified.name for referenced types - private const string ArrayTypeRef = "global::System.Array"; private const string InvalidOperationExceptionTypeRef = "global::System.InvalidOperationException"; private const string TypeTypeRef = "global::System.Type"; private const string UnsafeTypeRef = "global::System.Runtime.CompilerServices.Unsafe"; private const string EqualityComparerTypeRef = "global::System.Collections.Generic.EqualityComparer"; - private const string IListTypeRef = "global::System.Collections.Generic.IList"; private const string KeyValuePairTypeRef = "global::System.Collections.Generic.KeyValuePair"; private const string JsonEncodedTextTypeRef = "global::System.Text.Json.JsonEncodedText"; private const string JsonNamingPolicyTypeRef = "global::System.Text.Json.JsonNamingPolicy"; @@ -77,9 +69,6 @@ namespace System.Text.Json.SourceGeneration private const string JsonTypeInfoResolverTypeRef = "global::System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver"; private readonly JsonSourceGenerationContext _sourceGenerationContext; - - private ContextGenerationSpec _currentContext = null!; - private readonly SourceGenerationSpec _generationSpec; /// @@ -88,14 +77,11 @@ namespace System.Text.Json.SourceGeneration private readonly Dictionary _typeIndex = new(); /// - /// Cache of runtime property names (statically determined) found across the type graph of the JsonSerializerContext. + /// Cache of property names (statically determined) found across the type graph of the JsonSerializerContext. /// The dictionary Key is the JSON property name, and the Value is the variable name which is the same as the property /// name except for cases where special characters are used with [JsonPropertyName]. /// - private readonly Dictionary _runtimePropertyNames = new(); - - private bool _generateGetConverterMethodForTypes; - private bool _generateGetConverterMethodForProperties; + private readonly Dictionary _propertyNames = new(); public Emitter(in JsonSourceGenerationContext sourceGenerationContext, SourceGenerationSpec generationSpec) { @@ -107,408 +93,346 @@ namespace System.Text.Json.SourceGeneration { foreach (Diagnostic diagnostic in _generationSpec.Diagnostics) { - // Emit any diagnostics produced by the parser ahead of formatting source code. + // Report any diagnostics produced by the parser ahead of formatting source code. _sourceGenerationContext.ReportDiagnostic(diagnostic); } foreach (ContextGenerationSpec contextGenerationSpec in _generationSpec.ContextGenerationSpecs) { - _currentContext = contextGenerationSpec; - _generateGetConverterMethodForTypes = false; - _generateGetConverterMethodForProperties = false; - Debug.Assert(_typeIndex.Count == 0); + Debug.Assert(_propertyNames.Count == 0); - foreach (TypeGenerationSpec spec in _currentContext.GeneratedTypes) + foreach (TypeGenerationSpec spec in contextGenerationSpec.GeneratedTypes) { _typeIndex.Add(spec.TypeRef, spec); } - foreach (TypeGenerationSpec typeGenerationSpec in _currentContext.GeneratedTypes) + foreach (TypeGenerationSpec typeGenerationSpec in contextGenerationSpec.GeneratedTypes) { - GenerateTypeInfo(typeGenerationSpec); + SourceText? sourceText = GenerateTypeInfo(contextGenerationSpec, typeGenerationSpec); + if (sourceText != null) + { + _sourceGenerationContext.AddSource($"{contextGenerationSpec.ContextType.Name}.{typeGenerationSpec.TypeInfoPropertyName}.g.cs", sourceText); + } } - string contextName = _currentContext.ContextType.Name; + string contextName = contextGenerationSpec.ContextType.Name; // Add root context implementation. - AddSource( - $"{contextName}.g.cs", - GetRootJsonContextImplementation(), - isRootContextDef: true); + _sourceGenerationContext.AddSource($"{contextName}.g.cs", GetRootJsonContextImplementation(contextGenerationSpec)); // Add GetJsonTypeInfo override implementation. - AddSource($"{contextName}.GetJsonTypeInfo.g.cs", GetGetTypeInfoImplementation(contextGenerationSpec), interfaceImplementation: JsonTypeInfoResolverTypeRef); + _sourceGenerationContext.AddSource($"{contextName}.GetJsonTypeInfo.g.cs", GetGetTypeInfoImplementation(contextGenerationSpec)); // Add property name initialization. - AddSource($"{contextName}.PropertyNames.g.cs", GetPropertyNameInitialization()); + _sourceGenerationContext.AddSource($"{contextName}.PropertyNames.g.cs", GetPropertyNameInitialization(contextGenerationSpec)); + _propertyNames.Clear(); _typeIndex.Clear(); } } - private void AddSource(string fileName, string source, bool isRootContextDef = false, string? interfaceImplementation = null) + private static SourceWriter CreateSourceWriterWithContextHeader(ContextGenerationSpec contextSpec, bool isPrimaryContextSourceFile = false, string? interfaceImplementation = null) { - string? generatedCodeAttributeSource = isRootContextDef ? s_generatedCodeAttributeSource : null; + var writer = new SourceWriter(); - ImmutableEquatableArray declarationList = _currentContext.ContextClassDeclarations; - int declarationCount = declarationList.Count; - Debug.Assert(declarationCount >= 1); + writer.WriteLine(""" + // - string? @namespace = _currentContext.Namespace; - bool isInGlobalNamespace = @namespace == JsonConstants.GlobalNamespaceValue; + #nullable enable annotations + #nullable disable warnings - StringBuilder sb = new(""" -// + // Suppress warnings about [Obsolete] member usage in generated code. + #pragma warning disable CS0612, CS0618 -#nullable enable annotations -#nullable disable warnings - -// Suppress warnings about [Obsolete] member usage in generated code. -#pragma warning disable CS0612, CS0618 + """); + if (contextSpec.Namespace != JsonConstants.GlobalNamespaceValue) + { + writer.WriteLine($"namespace {contextSpec.Namespace}"); + writer.WriteLine('{'); + writer.Indentation++; + } -"""); - int indentation = 0; + ImmutableEquatableArray contextClasses = contextSpec.ContextClassDeclarations; + Debug.Assert(contextClasses.Count > 0); - if (!isInGlobalNamespace) + // Emit any containing classes first. + for (int i = contextClasses.Count - 1; i > 0; i--) { - sb.AppendLine($$""" -namespace {{@namespace}} -{ -"""); - indentation++; + writer.WriteLine(contextClasses[i]); + writer.WriteLine('{'); + writer.Indentation++; } - for (int i = 0; i < declarationCount - 1; i++) + if (isPrimaryContextSourceFile) { - string declarationSource = $$""" -{{declarationList[declarationCount - 1 - i]}} -{ -"""; - sb.AppendLine(IndentSource(declarationSource, numIndentations: indentation++)); + // Annotate context class with the GeneratedCodeAttribute + writer.WriteLine($"""[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{s_assemblyName.Name}", "{s_assemblyName.Version}")]"""); } - // Add the core implementation for the derived context class. - string partialContextImplementation = $$""" -{{generatedCodeAttributeSource}}{{declarationList[0]}}{{(interfaceImplementation is null ? "" : ": " + interfaceImplementation)}} -{ -{{IndentSource(source, 1)}} -} -"""; - sb.AppendLine(IndentSource(partialContextImplementation, numIndentations: indentation--)); + // Emit the JsonSerializerContext class declaration + writer.WriteLine($"{contextClasses[0]}{(interfaceImplementation is null ? "" : " : " + interfaceImplementation)}"); + writer.WriteLine('{'); + writer.Indentation++; - // Match curly braces for each containing type/namespace. - for (; indentation >= 0; indentation--) + return writer; + } + + private static SourceText CompleteSourceFileAndReturnText(SourceWriter writer) + { + while (writer.Indentation > 0) { - sb.AppendLine(IndentSource("}", numIndentations: indentation)); + writer.Indentation--; + writer.WriteLine('}'); } - _sourceGenerationContext.AddSource(fileName, SourceText.From(sb.ToString(), Encoding.UTF8)); + return writer.ToSourceText(); } - private void GenerateTypeInfo(TypeGenerationSpec typeGenerationSpec) + private SourceText? GenerateTypeInfo(ContextGenerationSpec contextSpec, TypeGenerationSpec typeGenerationSpec) { - Debug.Assert(typeGenerationSpec != null); - - string source; - switch (typeGenerationSpec.ClassType) { case ClassType.BuiltInSupportType: - { - source = GenerateForTypeWithKnownConverter(typeGenerationSpec); - } - break; + return GenerateForTypeWithBuiltInConverter(contextSpec, typeGenerationSpec); + case ClassType.TypeWithDesignTimeProvidedCustomConverter: - { - source = GenerateForTypeWithUnknownConverter(typeGenerationSpec); - } - break; + return GenerateForTypeWithCustomConverter(contextSpec, typeGenerationSpec); + case ClassType.Nullable: - { - Debug.Assert(typeGenerationSpec.NullableUnderlyingType != null); + return GenerateForNullable(contextSpec, typeGenerationSpec); - source = GenerateForNullable(typeGenerationSpec); - } - break; case ClassType.Enum: - { - source = GenerateForEnum(typeGenerationSpec); - } - break; - case ClassType.Enumerable: - { - Debug.Assert(typeGenerationSpec.CollectionValueType != null); + return GenerateForEnum(contextSpec, typeGenerationSpec); - source = GenerateForCollection(typeGenerationSpec); - } - break; + case ClassType.Enumerable: case ClassType.Dictionary: - { - source = GenerateForCollection(typeGenerationSpec); - } - break; + return GenerateForCollection(contextSpec, typeGenerationSpec); + case ClassType.Object: - { - source = GenerateForObject(typeGenerationSpec); - } - break; + return GenerateForObject(contextSpec, typeGenerationSpec); + case ClassType.UnsupportedType: - { - source = GenerateForUnsupportedType(typeGenerationSpec); - } - break; + return GenerateForUnsupportedType(contextSpec, typeGenerationSpec); + case ClassType.TypeUnsupportedBySourceGen: - return; // Do not emit a file for the type. + return null; // Do not emit a source file for the type. + default: - { - throw new InvalidOperationException(); - } + Debug.Fail($"Unexpected class type {typeGenerationSpec.ClassType}"); + return null; } - - string propertyFileName = $"{_currentContext.ContextType.Name}.{typeGenerationSpec.TypeInfoPropertyName}.g.cs"; - AddSource(propertyFileName, source); - - _generateGetConverterMethodForTypes |= typeGenerationSpec.HasTypeFactoryConverter; - _generateGetConverterMethodForProperties |= typeGenerationSpec.HasPropertyFactoryConverters; } - private static string GenerateForTypeWithKnownConverter(TypeGenerationSpec typeMetadata) + private static SourceText GenerateForTypeWithBuiltInConverter(ContextGenerationSpec contextSpec, TypeGenerationSpec typeMetadata) { - string typeCompilableName = typeMetadata.TypeRef.FullyQualifiedName; - string typeFriendlyName = typeMetadata.TypeInfoPropertyName; + SourceWriter writer = CreateSourceWriterWithContextHeader(contextSpec); - string metadataInitSource = $@"{JsonTypeInfoReturnValueLocalVariableName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeCompilableName)}({OptionsLocalVariableName}, {JsonMetadataServicesTypeRef}.{typeFriendlyName}Converter);"; + string typeFQN = typeMetadata.TypeRef.FullyQualifiedName; + string typeInfoPropertyName = typeMetadata.TypeInfoPropertyName; + string metadataInitSource = $"{JsonTypeInfoReturnValueLocalVariableName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeFQN)}({OptionsLocalVariableName}, {JsonMetadataServicesTypeRef}.{typeInfoPropertyName}Converter);"; + GenerateTypeInfoProperty(writer, typeMetadata, metadataInitSource); - return GenerateForType(typeMetadata, metadataInitSource); + return CompleteSourceFileAndReturnText(writer); } - private static string GenerateForTypeWithUnknownConverter(TypeGenerationSpec typeMetadata) + private static SourceText GenerateForTypeWithCustomConverter(ContextGenerationSpec contextSpec, TypeGenerationSpec typeMetadata) { - string typeCompilableName = typeMetadata.TypeRef.FullyQualifiedName; + Debug.Assert(typeMetadata.ConverterType != null); - // TODO (https://github.com/dotnet/runtime/issues/52218): consider moving this verification source to common helper. - StringBuilder metadataInitSource = new( - $@"{JsonConverterTypeRef} converter = {typeMetadata.ConverterInstantiationLogic}; - {TypeTypeRef} typeToConvert = typeof({typeCompilableName});"); + SourceWriter writer = CreateSourceWriterWithContextHeader(contextSpec); - metadataInitSource.Append($@" - if (!converter.CanConvert(typeToConvert)) - {{ - throw new {InvalidOperationExceptionTypeRef}(string.Format(""{ExceptionMessages.IncompatibleConverterType}"", converter.GetType(), typeToConvert)); - }}"); + string typeFQN = typeMetadata.TypeRef.FullyQualifiedName; + string metadataInitSource = $$""" + {{JsonConverterTypeRef}} converter = {{ExpandConverterMethodName}}(typeof({{typeFQN}}), new {{typeMetadata.ConverterType.FullyQualifiedName}}(), {{OptionsLocalVariableName}}); + {{JsonTypeInfoReturnValueLocalVariableName}} = {{JsonMetadataServicesTypeRef}}.{{GetCreateValueInfoMethodRef(typeFQN)}} ({{OptionsLocalVariableName}}, converter); + """; - metadataInitSource.Append($@" - {JsonTypeInfoReturnValueLocalVariableName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeCompilableName)} ({OptionsLocalVariableName}, converter); "); + GenerateTypeInfoProperty(writer, typeMetadata, metadataInitSource); - return GenerateForType(typeMetadata, metadataInitSource.ToString()); + return CompleteSourceFileAndReturnText(writer); } - private static string GenerateForNullable(TypeGenerationSpec typeMetadata) + private static SourceText GenerateForNullable(ContextGenerationSpec contextSpec, TypeGenerationSpec typeMetadata) { - string typeCompilableName = typeMetadata.TypeRef.FullyQualifiedName; + Debug.Assert(typeMetadata.NullableUnderlyingType != null); + + SourceWriter writer = CreateSourceWriterWithContextHeader(contextSpec); - TypeRef? underlyingTypeMetadata = typeMetadata.NullableUnderlyingType; - Debug.Assert(underlyingTypeMetadata != null); + string typeFQN = typeMetadata.TypeRef.FullyQualifiedName; + string underlyingTypeFQN = typeMetadata.NullableUnderlyingType.FullyQualifiedName; - string underlyingTypeCompilableName = underlyingTypeMetadata.FullyQualifiedName; + string metadataInitSource = $$""" + {{JsonTypeInfoReturnValueLocalVariableName}} = {{JsonMetadataServicesTypeRef}}.{{GetCreateValueInfoMethodRef(typeFQN)}}( + {{OptionsLocalVariableName}}, + {{JsonMetadataServicesTypeRef}}.GetNullableConverter<{{underlyingTypeFQN}}>({{OptionsLocalVariableName}})); + """; - string metadataInitSource = @$"{JsonTypeInfoReturnValueLocalVariableName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeCompilableName)}( - {OptionsLocalVariableName}, - {JsonMetadataServicesTypeRef}.GetNullableConverter<{underlyingTypeCompilableName}>({OptionsLocalVariableName})); -"; + GenerateTypeInfoProperty(writer, typeMetadata, metadataInitSource); - return GenerateForType(typeMetadata, metadataInitSource); + return CompleteSourceFileAndReturnText(writer); } - private static string GenerateForUnsupportedType(TypeGenerationSpec typeMetadata) + private static SourceText GenerateForUnsupportedType(ContextGenerationSpec contextSpec, TypeGenerationSpec typeMetadata) { - string typeCompilableName = typeMetadata.TypeRef.FullyQualifiedName; + SourceWriter writer = CreateSourceWriterWithContextHeader(contextSpec); - string metadataInitSource = $"{JsonTypeInfoReturnValueLocalVariableName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeCompilableName)}({OptionsLocalVariableName}, {JsonMetadataServicesTypeRef}.GetUnsupportedTypeConverter<{typeCompilableName}>());"; + string typeFQN = typeMetadata.TypeRef.FullyQualifiedName; + string metadataInitSource = $"{JsonTypeInfoReturnValueLocalVariableName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeFQN)}({OptionsLocalVariableName}, {JsonMetadataServicesTypeRef}.GetUnsupportedTypeConverter<{typeFQN}>());"; - return GenerateForType(typeMetadata, metadataInitSource); + GenerateTypeInfoProperty(writer, typeMetadata, metadataInitSource); + + return CompleteSourceFileAndReturnText(writer); } - private static string GenerateForEnum(TypeGenerationSpec typeMetadata) + private static SourceText GenerateForEnum(ContextGenerationSpec contextSpec, TypeGenerationSpec typeMetadata) { - string typeCompilableName = typeMetadata.TypeRef.FullyQualifiedName; + SourceWriter writer = CreateSourceWriterWithContextHeader(contextSpec); - string metadataInitSource = $"{JsonTypeInfoReturnValueLocalVariableName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeCompilableName)}({OptionsLocalVariableName}, {JsonMetadataServicesTypeRef}.GetEnumConverter<{typeCompilableName}>({OptionsLocalVariableName}));"; + string typeFQN = typeMetadata.TypeRef.FullyQualifiedName; + string metadataInitSource = $"{JsonTypeInfoReturnValueLocalVariableName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeFQN)}({OptionsLocalVariableName}, {JsonMetadataServicesTypeRef}.GetEnumConverter<{typeFQN}>({OptionsLocalVariableName}));"; + GenerateTypeInfoProperty(writer, typeMetadata, metadataInitSource); - return GenerateForType(typeMetadata, metadataInitSource); + return CompleteSourceFileAndReturnText(writer); } - private string GenerateForCollection(TypeGenerationSpec typeGenerationSpec) + private SourceText GenerateForCollection(ContextGenerationSpec contextSpec, TypeGenerationSpec typeGenerationSpec) { + SourceWriter writer = CreateSourceWriterWithContextHeader(contextSpec); + // Key metadata - TypeRef? collectionKeyTypeMetadata = typeGenerationSpec.CollectionKeyType; - Debug.Assert(!(typeGenerationSpec.ClassType == ClassType.Dictionary && collectionKeyTypeMetadata == null)); - string? keyTypeCompilableName = collectionKeyTypeMetadata?.FullyQualifiedName; + TypeRef? collectionKeyType = typeGenerationSpec.CollectionKeyType; + Debug.Assert(!(typeGenerationSpec.ClassType == ClassType.Dictionary && collectionKeyType == null)); + string? keyTypeFQN = collectionKeyType?.FullyQualifiedName; // Value metadata - TypeRef? collectionValueTypeMetadata = typeGenerationSpec.CollectionValueType; - Debug.Assert(collectionValueTypeMetadata != null); - string valueTypeCompilableName = collectionValueTypeMetadata.FullyQualifiedName; - - string numberHandlingArg = $"{GetNumberHandlingAsStr(typeGenerationSpec.NumberHandling)}"; - - string serializeHandlerValue; - - string? serializeHandlerSource; - if (!ShouldGenerateSerializationLogic(typeGenerationSpec)) - { - serializeHandlerSource = null; - serializeHandlerValue = "null"; - } - else - { - serializeHandlerSource = typeGenerationSpec.ClassType == ClassType.Enumerable - ? GenerateFastPathFuncForEnumerable(typeGenerationSpec) - : GenerateFastPathFuncForDictionary(typeGenerationSpec); - - serializeHandlerValue = $"{typeGenerationSpec.TypeInfoPropertyName}{SerializeHandlerPropName}"; - } + TypeRef? collectionValueType = typeGenerationSpec.CollectionValueType; + Debug.Assert(collectionValueType != null); + string valueTypeFQN = collectionValueType.FullyQualifiedName; CollectionType collectionType = typeGenerationSpec.CollectionType; - string typeRef = typeGenerationSpec.TypeRef.FullyQualifiedName; - - string objectCreatorValue; - if (typeGenerationSpec.RuntimeTypeRef != null) - { - objectCreatorValue = $"() => new {typeGenerationSpec.RuntimeTypeRef}()"; - } - else if (typeGenerationSpec.IsValueTuple) - { - objectCreatorValue = $"() => default({typeRef})"; - } - else - { - objectCreatorValue = typeGenerationSpec.ConstructionStrategy == ObjectConstructionStrategy.ParameterlessConstructor - ? $"() => new {typeRef}()" - : "null"; - } - - string collectionInfoCreationPrefix = collectionType switch - { - CollectionType.IListOfT => $"{JsonMetadataServicesTypeRef}.CreateIListInfo<", - CollectionType.ICollectionOfT => $"{JsonMetadataServicesTypeRef}.CreateICollectionInfo<", - CollectionType.StackOfT => $"{JsonMetadataServicesTypeRef}.CreateStackInfo<", - CollectionType.QueueOfT => $"{JsonMetadataServicesTypeRef}.CreateQueueInfo<", - CollectionType.Stack => $"{JsonMetadataServicesTypeRef}.CreateStackInfo<", - CollectionType.Queue => $"{JsonMetadataServicesTypeRef}.CreateQueueInfo<", - CollectionType.IEnumerableOfT => $"{JsonMetadataServicesTypeRef}.CreateIEnumerableInfo<", - CollectionType.IAsyncEnumerableOfT => $"{JsonMetadataServicesTypeRef}.CreateIAsyncEnumerableInfo<", - CollectionType.IDictionaryOfTKeyTValue => $"{JsonMetadataServicesTypeRef}.CreateIDictionaryInfo<", - _ => $"{JsonMetadataServicesTypeRef}.Create{collectionType}Info<" - }; - - string dictInfoCreationPrefix = $"{collectionInfoCreationPrefix}{typeRef}, {keyTypeCompilableName!}, {valueTypeCompilableName}>({OptionsLocalVariableName}, {InfoVarName}"; - string enumerableInfoCreationPrefix = $"{collectionInfoCreationPrefix}{typeRef}, {valueTypeCompilableName}>({OptionsLocalVariableName}, {InfoVarName}"; - string immutableCollectionCreationSuffix = $"createRangeFunc: {typeGenerationSpec.ImmutableCollectionFactoryMethod}"; + string? serializeMethodName = ShouldGenerateSerializationLogic(typeGenerationSpec) + ? $"{typeGenerationSpec.TypeInfoPropertyName}{SerializeHandlerPropName}" + : null; - string collectionTypeInfoValue; + string typeFQN = typeGenerationSpec.TypeRef.FullyQualifiedName; + string createCollectionInfoMethodName = GetCollectionInfoMethodName(collectionType); + string createCollectionMethodExpr; switch (collectionType) { case CollectionType.Array: - collectionTypeInfoValue = $"{collectionInfoCreationPrefix}{valueTypeCompilableName}>({OptionsLocalVariableName}, {InfoVarName})"; + createCollectionMethodExpr = $"{createCollectionInfoMethodName}<{valueTypeFQN}>({OptionsLocalVariableName}, {InfoVarName})"; break; case CollectionType.IEnumerable: + case CollectionType.IDictionary: case CollectionType.IList: - collectionTypeInfoValue = $"{collectionInfoCreationPrefix}{typeRef}>({OptionsLocalVariableName}, {InfoVarName})"; + createCollectionMethodExpr = $"{createCollectionInfoMethodName}<{typeFQN}>({OptionsLocalVariableName}, {InfoVarName})"; break; case CollectionType.Stack: case CollectionType.Queue: string addMethod = collectionType == CollectionType.Stack ? "Push" : "Enqueue"; - string addFuncNamedArg = $"addFunc: (collection, {ValueVarName}) => collection.{addMethod}({ValueVarName})"; - collectionTypeInfoValue = $"{collectionInfoCreationPrefix}{typeRef}>({OptionsLocalVariableName}, {InfoVarName}, {addFuncNamedArg})"; + string addFuncNamedArg = $"(collection, {ValueVarName}) => collection.{addMethod}({ValueVarName})"; + createCollectionMethodExpr = $"{createCollectionInfoMethodName}<{typeFQN}>({OptionsLocalVariableName}, {InfoVarName}, addFunc: {addFuncNamedArg})"; break; case CollectionType.ImmutableEnumerable: - collectionTypeInfoValue = $"{enumerableInfoCreationPrefix}, {immutableCollectionCreationSuffix})"; - break; - case CollectionType.IDictionary: - collectionTypeInfoValue = $"{collectionInfoCreationPrefix}{typeRef}>({OptionsLocalVariableName}, {InfoVarName})"; + createCollectionMethodExpr = $"{createCollectionInfoMethodName}<{typeFQN}, {valueTypeFQN}>({OptionsLocalVariableName}, {InfoVarName}, createRangeFunc: {typeGenerationSpec.ImmutableCollectionFactoryMethod})"; break; case CollectionType.Dictionary: case CollectionType.IDictionaryOfTKeyTValue: case CollectionType.IReadOnlyDictionary: - collectionTypeInfoValue = $"{dictInfoCreationPrefix})"; + Debug.Assert(keyTypeFQN != null); + createCollectionMethodExpr = $"{createCollectionInfoMethodName}<{typeFQN}, {keyTypeFQN!}, {valueTypeFQN}>({OptionsLocalVariableName}, {InfoVarName})"; break; case CollectionType.ImmutableDictionary: - collectionTypeInfoValue = $"{dictInfoCreationPrefix}, {immutableCollectionCreationSuffix})"; + Debug.Assert(keyTypeFQN != null); + createCollectionMethodExpr = $"{createCollectionInfoMethodName}<{typeFQN}, {keyTypeFQN!}, {valueTypeFQN}>({OptionsLocalVariableName}, {InfoVarName}, createRangeFunc: {typeGenerationSpec.ImmutableCollectionFactoryMethod})"; break; default: - collectionTypeInfoValue = $"{enumerableInfoCreationPrefix})"; + createCollectionMethodExpr = $"{createCollectionInfoMethodName}<{typeFQN}, {valueTypeFQN}>({OptionsLocalVariableName}, {InfoVarName})"; break; } - string metadataInitSource = @$"{JsonCollectionInfoValuesTypeRef}<{typeRef}> {InfoVarName} = new {JsonCollectionInfoValuesTypeRef}<{typeRef}>() - {{ - {ObjectCreatorPropName} = {objectCreatorValue}, - {NumberHandlingPropName} = {numberHandlingArg}, - {SerializeHandlerPropName} = {serializeHandlerValue} - }}; + string metadataInitSource = $$""" + var {{InfoVarName}} = new {{JsonCollectionInfoValuesTypeRef}}<{{typeFQN}}>() + { + {{ObjectCreatorPropName}} = {{FormatDefaultConstructorExpr(typeGenerationSpec)}}, + {{NumberHandlingPropName}} = {{GetNumberHandlingAsStr(typeGenerationSpec.NumberHandling)}}, + {{SerializeHandlerPropName}} = {{serializeMethodName ?? "null"}} + }; + + {{JsonTypeInfoReturnValueLocalVariableName}} = {{JsonMetadataServicesTypeRef}}.{{createCollectionMethodExpr}}; + """; + + GenerateTypeInfoProperty(writer, typeGenerationSpec, metadataInitSource); - {JsonTypeInfoReturnValueLocalVariableName} = {collectionTypeInfoValue}; -"; + if (serializeMethodName != null) + { + writer.WriteLine(); - return GenerateForType(typeGenerationSpec, metadataInitSource, serializeHandlerSource); + if (typeGenerationSpec.ClassType == ClassType.Enumerable) + { + GenerateFastPathFuncForEnumerable(writer, serializeMethodName, typeGenerationSpec); + } + else + { + GenerateFastPathFuncForDictionary(writer, serializeMethodName, typeGenerationSpec); + } + } + + return CompleteSourceFileAndReturnText(writer); } - private string GenerateFastPathFuncForEnumerable(TypeGenerationSpec typeGenerationSpec) + private void GenerateFastPathFuncForEnumerable(SourceWriter writer, string serializeMethodName, TypeGenerationSpec typeGenerationSpec) { Debug.Assert(typeGenerationSpec.CollectionValueType != null); - TypeGenerationSpec valueTypeGenerationSpec = _typeIndex[typeGenerationSpec.CollectionValueType]; - string? writerMethodToCall = GetWriterMethod(valueTypeGenerationSpec); - string iterationLogic; - string valueToWrite; + GenerateFastPathFuncHeader(writer, typeGenerationSpec, serializeMethodName); + writer.WriteLine($"{WriterVarName}.WriteStartArray();"); + writer.WriteLine(); + + string getCurrentElementExpr; switch (typeGenerationSpec.CollectionType) { case CollectionType.Array: - iterationLogic = $"for (int i = 0; i < {ValueVarName}.Length; i++)"; - valueToWrite = $"{ValueVarName}[i]"; + writer.WriteLine($"for (int i = 0; i < {ValueVarName}.Length; i++)"); + getCurrentElementExpr = $"{ValueVarName}[i]"; break; + case CollectionType.IListOfT: case CollectionType.List: case CollectionType.IList: - iterationLogic = $"for (int i = 0; i < {ValueVarName}.Count; i++)"; - valueToWrite = $"{ValueVarName}[i]"; + writer.WriteLine($"for (int i = 0; i < {ValueVarName}.Count; i++)"); + getCurrentElementExpr = $"{ValueVarName}[i]"; break; + default: const string elementVarName = "element"; - iterationLogic = $"foreach ({valueTypeGenerationSpec.TypeRef.FullyQualifiedName} {elementVarName} in {ValueVarName})"; - valueToWrite = elementVarName; + writer.WriteLine($"foreach ({valueTypeGenerationSpec.TypeRef.FullyQualifiedName} {elementVarName} in {ValueVarName})"); + getCurrentElementExpr = elementVarName; break; }; - if (valueTypeGenerationSpec.PrimitiveTypeKind is JsonPrimitiveTypeKind.Char) - { - valueToWrite = $"{valueToWrite}.ToString()"; - } + writer.WriteLine('{'); + writer.Indentation++; - string elementSerializationLogic = writerMethodToCall == null - ? GetSerializeLogicForNonPrimitiveType(valueTypeGenerationSpec, valueToWrite) - : $"{writerMethodToCall}Value({valueToWrite});"; + GenerateSerializeValueStatement(writer, valueTypeGenerationSpec, getCurrentElementExpr); - string serializationLogic = $@"{WriterVarName}.WriteStartArray(); + writer.Indentation--; + writer.WriteLine('}'); - {iterationLogic} - {{ - {elementSerializationLogic} - }} + writer.WriteLine(); + writer.WriteLine($"{WriterVarName}.WriteEndArray();"); - {WriterVarName}.WriteEndArray();"; - - return GenerateFastPathFuncForType(typeGenerationSpec, serializationLogic, emitNullCheck: typeGenerationSpec.TypeRef.CanBeNull); + writer.Indentation--; + writer.WriteLine('}'); } - private string GenerateFastPathFuncForDictionary(TypeGenerationSpec typeGenerationSpec) + private void GenerateFastPathFuncForDictionary(SourceWriter writer, string serializeMethodName, TypeGenerationSpec typeGenerationSpec) { Debug.Assert(typeGenerationSpec.CollectionKeyType != null); Debug.Assert(typeGenerationSpec.CollectionValueType != null); @@ -516,235 +440,210 @@ namespace {{@namespace}} TypeRef keyType = typeGenerationSpec.CollectionKeyType; TypeGenerationSpec valueTypeGenerationSpec = _typeIndex[typeGenerationSpec.CollectionValueType]; - string? writerMethodToCall = GetWriterMethod(valueTypeGenerationSpec); - string elementSerializationLogic; + GenerateFastPathFuncHeader(writer, typeGenerationSpec, serializeMethodName); - const string pairVarName = "pair"; - string keyToWrite = $"{pairVarName}.Key"; - string valueToWrite = $"{pairVarName}.Value"; + writer.WriteLine($"{WriterVarName}.WriteStartObject();"); + writer.WriteLine(); - if (valueTypeGenerationSpec.PrimitiveTypeKind is JsonPrimitiveTypeKind.Char) - { - valueToWrite = $"{valueToWrite}.ToString()"; - } + writer.WriteLine($"foreach ({KeyValuePairTypeRef}<{keyType.FullyQualifiedName}, {valueTypeGenerationSpec.TypeRef.FullyQualifiedName}> entry in {ValueVarName})"); + writer.WriteLine('{'); + writer.Indentation++; - if (writerMethodToCall != null) - { - elementSerializationLogic = $"{writerMethodToCall}({keyToWrite}, {valueToWrite});"; - } - else - { - elementSerializationLogic = $@"{WriterVarName}.WritePropertyName({keyToWrite}); - {GetSerializeLogicForNonPrimitiveType(valueTypeGenerationSpec, valueToWrite)}"; - } - - string serializationLogic = $@"{WriterVarName}.WriteStartObject(); + GenerateSerializePropertyStatement(writer, valueTypeGenerationSpec, propertyNameExpr: "entry.Key", valueExpr: "entry.Value"); - foreach ({KeyValuePairTypeRef}<{keyType.FullyQualifiedName}, {valueTypeGenerationSpec.TypeRef.FullyQualifiedName}> {pairVarName} in {ValueVarName}) - {{ - {elementSerializationLogic} - }} + writer.Indentation--; + writer.WriteLine('}'); - {WriterVarName}.WriteEndObject();"; + writer.WriteLine(); + writer.WriteLine($"{WriterVarName}.WriteEndObject();"); - return GenerateFastPathFuncForType(typeGenerationSpec, serializationLogic, emitNullCheck: typeGenerationSpec.TypeRef.CanBeNull); + writer.Indentation--; + writer.WriteLine('}'); } - private string GenerateForObject(TypeGenerationSpec typeMetadata) + private SourceText GenerateForObject(ContextGenerationSpec contextSpec, TypeGenerationSpec typeMetadata) { + SourceWriter writer = CreateSourceWriterWithContextHeader(contextSpec); + string typeFriendlyName = typeMetadata.TypeInfoPropertyName; ObjectConstructionStrategy constructionStrategy = typeMetadata.ConstructionStrategy; - string creatorInvocation = (typeMetadata.IsValueTuple, constructionStrategy) switch - { - (true, _) => $"static () => default({typeMetadata.TypeRef.FullyQualifiedName})", - (false, ObjectConstructionStrategy.ParameterlessConstructor) => $"static () => new {typeMetadata.TypeRef.FullyQualifiedName}()", - _ => "null", - }; - - + string creatorInvocation = FormatDefaultConstructorExpr(typeMetadata); string parameterizedCreatorInvocation = constructionStrategy == ObjectConstructionStrategy.ParameterizedConstructor ? GetParameterizedCtorInvocationFunc(typeMetadata) : "null"; - string? propMetadataInitFuncSource = null; - string? ctorParamMetadataInitFuncSource = null; - string? serializeFuncSource = null; - - string propInitMethod = "null"; - string ctorParamMetadataInitMethodName = "null"; - string serializeMethodName = "null"; + string? propInitMethodName = null; + string? propInitAdapterFunc = null; + string? ctorParamMetadataInitMethodName = null; + string? serializeMethodName = null; if (ShouldGenerateMetadata(typeMetadata)) { - propMetadataInitFuncSource = GeneratePropMetadataInitFunc(typeMetadata); - propInitMethod = $"_ => {typeFriendlyName}{PropInitMethodNameSuffix}({OptionsLocalVariableName})"; + propInitMethodName = $"{typeFriendlyName}{PropInitMethodNameSuffix}"; + propInitAdapterFunc = $"_ => {propInitMethodName}({OptionsLocalVariableName})"; if (constructionStrategy == ObjectConstructionStrategy.ParameterizedConstructor) { - ctorParamMetadataInitFuncSource = GenerateCtorParamMetadataInitFunc(typeMetadata); ctorParamMetadataInitMethodName = $"{typeFriendlyName}{CtorParamInitMethodNameSuffix}"; } } if (ShouldGenerateSerializationLogic(typeMetadata)) { - serializeFuncSource = GenerateFastPathFuncForObject(typeMetadata); serializeMethodName = $"{typeFriendlyName}{SerializeHandlerPropName}"; } const string ObjectInfoVarName = "objectInfo"; string genericArg = typeMetadata.TypeRef.FullyQualifiedName; - string objectInfoInitSource = $@"{JsonObjectInfoValuesTypeRef}<{genericArg}> {ObjectInfoVarName} = new {JsonObjectInfoValuesTypeRef}<{genericArg}>() - {{ - {ObjectCreatorPropName} = {creatorInvocation}, - ObjectWithParameterizedConstructorCreator = {parameterizedCreatorInvocation}, - PropertyMetadataInitializer = {propInitMethod}, - ConstructorParameterMetadataInitializer = {ctorParamMetadataInitMethodName}, - {NumberHandlingPropName} = {GetNumberHandlingAsStr(typeMetadata.NumberHandling)}, - {SerializeHandlerPropName} = {serializeMethodName} - }}; + string metadataInitSource = $$""" + var {{ObjectInfoVarName}} = new {{JsonObjectInfoValuesTypeRef}}<{{genericArg}}>() + { + {{ObjectCreatorPropName}} = {{creatorInvocation}}, + ObjectWithParameterizedConstructorCreator = {{parameterizedCreatorInvocation}}, + PropertyMetadataInitializer = {{propInitAdapterFunc ?? "null"}}, + ConstructorParameterMetadataInitializer = {{ctorParamMetadataInitMethodName ?? "null"}}, + {{NumberHandlingPropName}} = {{GetNumberHandlingAsStr(typeMetadata.NumberHandling)}}, + {{SerializeHandlerPropName}} = {{serializeMethodName ?? "null"}} + }; - {JsonTypeInfoReturnValueLocalVariableName} = {JsonMetadataServicesTypeRef}.CreateObjectInfo<{typeMetadata.TypeRef.FullyQualifiedName}>({OptionsLocalVariableName}, {ObjectInfoVarName});"; + {{JsonTypeInfoReturnValueLocalVariableName}} = {{JsonMetadataServicesTypeRef}}.CreateObjectInfo<{{typeMetadata.TypeRef.FullyQualifiedName}}>({{OptionsLocalVariableName}}, {{ObjectInfoVarName}}); + """; if (typeMetadata.UnmappedMemberHandling != null) { - objectInfoInitSource += $""" + metadataInitSource += $""" - {JsonTypeInfoReturnValueLocalVariableName}.{UnmappedMemberHandlingPropName} = {GetUnmappedMemberHandlingAsStr(typeMetadata.UnmappedMemberHandling.Value)}; -"""; + {JsonTypeInfoReturnValueLocalVariableName}.{UnmappedMemberHandlingPropName} = {GetUnmappedMemberHandlingAsStr(typeMetadata.UnmappedMemberHandling.Value)}; + """; } if (typeMetadata.PreferredPropertyObjectCreationHandling != null) { - objectInfoInitSource += $""" + metadataInitSource += $""" - {JsonTypeInfoReturnValueLocalVariableName}.{PreferredPropertyObjectCreationHandlingPropName} = {GetObjectCreationHandlingAsStr(typeMetadata.PreferredPropertyObjectCreationHandling.Value)}; -"""; + {JsonTypeInfoReturnValueLocalVariableName}.{PreferredPropertyObjectCreationHandlingPropName} = {GetObjectCreationHandlingAsStr(typeMetadata.PreferredPropertyObjectCreationHandling.Value)}; + """; } - string additionalSource = @$"{propMetadataInitFuncSource}{serializeFuncSource}{ctorParamMetadataInitFuncSource}"; + GenerateTypeInfoProperty(writer, typeMetadata, metadataInitSource); - return GenerateForType(typeMetadata, objectInfoInitSource, additionalSource); - } + if (propInitMethodName != null) + { + writer.WriteLine(); + GeneratePropMetadataInitFunc(writer, propInitMethodName, typeMetadata); + } - private static string GeneratePropMetadataInitFunc(TypeGenerationSpec typeGenerationSpec) - { - const string PropVarName = "properties"; + if (serializeMethodName != null) + { + writer.WriteLine(); + GenerateFastPathFuncForObject(writer, contextSpec, serializeMethodName, typeMetadata); + } - ImmutableEquatableArray properties = typeGenerationSpec.PropertyGenSpecs!; + if (ctorParamMetadataInitMethodName != null) + { + writer.WriteLine(); + GenerateCtorParamMetadataInitFunc(writer, ctorParamMetadataInitMethodName, typeMetadata); + } - int propCount = properties.Count; + writer.Indentation--; + writer.WriteLine('}'); - string propertyArrayInstantiationValue = propCount == 0 - ? $"{ArrayTypeRef}.Empty<{JsonPropertyInfoTypeRef}>()" - : $"new {JsonPropertyInfoTypeRef}[{propCount}]"; + return CompleteSourceFileAndReturnText(writer); + } - string propInitMethodName = $"{typeGenerationSpec.TypeInfoPropertyName}{PropInitMethodNameSuffix}"; + private static void GeneratePropMetadataInitFunc(SourceWriter writer, string propInitMethodName, TypeGenerationSpec typeGenerationSpec) + { + Debug.Assert(typeGenerationSpec.PropertyGenSpecs != null); + ImmutableEquatableArray properties = typeGenerationSpec.PropertyGenSpecs; - StringBuilder sb = new(); + writer.WriteLine($"private static {JsonPropertyInfoTypeRef}[] {propInitMethodName}({JsonSerializerOptionsTypeRef} {OptionsLocalVariableName})"); + writer.WriteLine('{'); + writer.Indentation++; - sb.Append($@" -private static {JsonPropertyInfoTypeRef}[] {propInitMethodName}({JsonSerializerOptionsTypeRef} {OptionsLocalVariableName}) -{{ - {JsonPropertyInfoTypeRef}[] {PropVarName} = {propertyArrayInstantiationValue}; -"); + writer.WriteLine($"var properties = new {JsonPropertyInfoTypeRef}[{properties.Count}];"); + writer.WriteLine(); - for (int i = 0; i < propCount; i++) + for (int i = 0; i < properties.Count; i++) { - PropertyGenerationSpec memberMetadata = properties[i]; - string nameSpecifiedInSourceCode = memberMetadata.NameSpecifiedInSourceCode; - - string declaringTypeCompilableName = memberMetadata.DeclaringTypeRef; - - string jsonPropertyNameValue = memberMetadata.JsonPropertyName != null - ? @$"""{memberMetadata.JsonPropertyName}""" - : "null"; + PropertyGenerationSpec property = properties[i]; + string propertyName = property.NameSpecifiedInSourceCode; + string declaringTypeFQN = property.DeclaringType.FullyQualifiedName; + string propertyTypeFQN = property.PropertyType.FullyQualifiedName; - string getterValue = memberMetadata switch + string getterValue = property switch { { DefaultIgnoreCondition: JsonIgnoreCondition.Always } => "null", - { CanUseGetter: true } => $"static (obj) => (({declaringTypeCompilableName})obj).{nameSpecifiedInSourceCode}", + { CanUseGetter: true } => $"static (obj) => (({declaringTypeFQN})obj).{propertyName}", { CanUseGetter: false, HasJsonInclude: true } - => @$"static (obj) => throw new {InvalidOperationExceptionTypeRef}(""{string.Format(ExceptionMessages.InaccessibleJsonIncludePropertiesNotSupported, typeGenerationSpec.TypeRef.Name, nameSpecifiedInSourceCode)}"")", + => $"""static (obj) => throw new {InvalidOperationExceptionTypeRef}("{string.Format(ExceptionMessages.InaccessibleJsonIncludePropertiesNotSupported, typeGenerationSpec.TypeRef.Name, propertyName)}")""", _ => "null" }; - string setterValue = memberMetadata switch + string setterValue = property switch { { DefaultIgnoreCondition: JsonIgnoreCondition.Always } => "null", { CanUseSetter: true, IsInitOnlySetter: true } - => @$"static (obj, value) => throw new {InvalidOperationExceptionTypeRef}(""{ExceptionMessages.InitOnlyPropertySetterNotSupported}"")", + => $"""static (obj, value) => throw new {InvalidOperationExceptionTypeRef}("{ExceptionMessages.InitOnlyPropertySetterNotSupported}")""", { CanUseSetter: true } when typeGenerationSpec.TypeRef.IsValueType - => $@"static (obj, value) => {UnsafeTypeRef}.Unbox<{declaringTypeCompilableName}>(obj).{nameSpecifiedInSourceCode} = value!", + => $"""static (obj, value) => {UnsafeTypeRef}.Unbox<{declaringTypeFQN}>(obj).{propertyName} = value!""", { CanUseSetter: true } - => @$"static (obj, value) => (({declaringTypeCompilableName})obj).{nameSpecifiedInSourceCode} = value!", + => $"""static (obj, value) => (({declaringTypeFQN})obj).{propertyName} = value!""", { CanUseSetter: false, HasJsonInclude: true } - => @$"static (obj, value) => throw new {InvalidOperationExceptionTypeRef}(""{string.Format(ExceptionMessages.InaccessibleJsonIncludePropertiesNotSupported, typeGenerationSpec.TypeRef.Name, memberMetadata.MemberName)}"")", + => $"""static (obj, value) => throw new {InvalidOperationExceptionTypeRef}("{string.Format(ExceptionMessages.InaccessibleJsonIncludePropertiesNotSupported, typeGenerationSpec.TypeRef.Name, property.MemberName)}")""", _ => "null", }; - JsonIgnoreCondition? ignoreCondition = memberMetadata.DefaultIgnoreCondition; - string ignoreConditionNamedArg = ignoreCondition.HasValue - ? $"{JsonIgnoreConditionTypeRef}.{ignoreCondition.Value}" + string ignoreConditionNamedArg = property.DefaultIgnoreCondition.HasValue + ? $"{JsonIgnoreConditionTypeRef}.{property.DefaultIgnoreCondition.Value}" : "null"; - string converterValue = memberMetadata.ConverterInstantiationLogic == null - ? "null" - : $"{memberMetadata.ConverterInstantiationLogic}"; - - string memberTypeCompilableName = memberMetadata.PropertyType.FullyQualifiedName; - - string infoVarName = $"{InfoVarName}{i}"; - string propertyInfoVarName = $"{PropertyInfoVarName}{i}"; - - sb.Append($@" - {JsonPropertyInfoValuesTypeRef}<{memberTypeCompilableName}> {infoVarName} = new {JsonPropertyInfoValuesTypeRef}<{memberTypeCompilableName}>() - {{ - IsProperty = {FormatBool(memberMetadata.IsProperty)}, - IsPublic = {FormatBool(memberMetadata.IsPublic)}, - IsVirtual = {FormatBool(memberMetadata.IsVirtual)}, - DeclaringType = typeof({memberMetadata.DeclaringTypeRef}), - Converter = {converterValue}, - Getter = {getterValue}, - Setter = {setterValue}, - IgnoreCondition = {ignoreConditionNamedArg}, - HasJsonInclude = {FormatBool(memberMetadata.HasJsonInclude)}, - IsExtensionData = {FormatBool(memberMetadata.IsExtensionData)}, - NumberHandling = {GetNumberHandlingAsStr(memberMetadata.NumberHandling)}, - PropertyName = ""{memberMetadata.MemberName}"", - JsonPropertyName = {jsonPropertyNameValue} - }}; - - {JsonPropertyInfoTypeRef} {propertyInfoVarName} = {JsonMetadataServicesTypeRef}.CreatePropertyInfo<{memberTypeCompilableName}>({OptionsLocalVariableName}, {infoVarName});"); - - if (memberMetadata.HasJsonRequiredAttribute || - (memberMetadata.IsRequired && !typeGenerationSpec.ConstructorSetsRequiredParameters)) + string converterInstantiationExpr = property.ConverterType != null + ? $"({JsonConverterTypeRef}<{propertyTypeFQN}>){ExpandConverterMethodName}(typeof({propertyTypeFQN}), new {property.ConverterType.FullyQualifiedName}(), {OptionsLocalVariableName})" + : "null"; + + writer.WriteLine($$""" + var {{InfoVarName}}{{i}} = new {{JsonPropertyInfoValuesTypeRef}}<{{propertyTypeFQN}}>() + { + IsProperty = {{FormatBool(property.IsProperty)}}, + IsPublic = {{FormatBool(property.IsPublic)}}, + IsVirtual = {{FormatBool(property.IsVirtual)}}, + DeclaringType = typeof({{property.DeclaringType.FullyQualifiedName}}), + Converter = {{converterInstantiationExpr}}, + Getter = {{getterValue}}, + Setter = {{setterValue}}, + IgnoreCondition = {{ignoreConditionNamedArg}}, + HasJsonInclude = {{FormatBool(property.HasJsonInclude)}}, + IsExtensionData = {{FormatBool(property.IsExtensionData)}}, + NumberHandling = {{GetNumberHandlingAsStr(property.NumberHandling)}}, + PropertyName = {{FormatStringLiteral(property.MemberName)}}, + JsonPropertyName = {{FormatStringLiteral(property.JsonPropertyName)}} + }; + + properties[{{i}}] = {{JsonMetadataServicesTypeRef}}.CreatePropertyInfo<{{propertyTypeFQN}}>({{OptionsLocalVariableName}}, {{InfoVarName}}{{i}}); + """); + + if (property.HasJsonRequiredAttribute || + (property.IsRequired && !typeGenerationSpec.ConstructorSetsRequiredParameters)) { - sb.Append($@" - {propertyInfoVarName}.IsRequired = true;"); + writer.WriteLine($"properties[{i}].IsRequired = true;"); } - if (memberMetadata.ObjectCreationHandling != null) + if (property.ObjectCreationHandling != null) { - sb.Append($@" - {propertyInfoVarName}.ObjectCreationHandling = {GetObjectCreationHandlingAsStr(memberMetadata.ObjectCreationHandling.Value)};"); + writer.WriteLine($"properties[{i}].ObjectCreationHandling = {GetObjectCreationHandlingAsStr(property.ObjectCreationHandling.Value)};"); } - sb.Append($@" - {PropVarName}[{i}] = {propertyInfoVarName}; -"); + writer.WriteLine(); } - sb.Append(@$" - return {PropVarName}; -}}"); - - return sb.ToString(); + writer.WriteLine($"return properties;"); + writer.Indentation--; + writer.WriteLine('}'); } - private static string GenerateCtorParamMetadataInitFunc(TypeGenerationSpec typeGenerationSpec) + private static void GenerateCtorParamMetadataInitFunc(SourceWriter writer, string ctorParamMetadataInitMethodName, TypeGenerationSpec typeGenerationSpec) { const string parametersVarName = "parameters"; @@ -755,31 +654,26 @@ private static {JsonPropertyInfoTypeRef}[] {propInitMethodName}({JsonSerializerO int paramCount = parameters.Count + (propertyInitializers?.Count(propInit => !propInit.MatchesConstructorParameter) ?? 0); Debug.Assert(paramCount > 0); - StringBuilder sb = new($@" + writer.WriteLine($"private static {JsonParameterInfoValuesTypeRef}[] {ctorParamMetadataInitMethodName}()"); + writer.WriteLine('{'); + writer.Indentation++; + + writer.WriteLine($"var {parametersVarName} = new {JsonParameterInfoValuesTypeRef}[{paramCount}];"); + writer.WriteLine(); -private static {JsonParameterInfoValuesTypeRef}[] {typeGenerationSpec.TypeInfoPropertyName}{CtorParamInitMethodNameSuffix}() -{{ - {JsonParameterInfoValuesTypeRef}[] {parametersVarName} = new {JsonParameterInfoValuesTypeRef}[{paramCount}]; - {JsonParameterInfoValuesTypeRef} info; -"); foreach (ParameterGenerationSpec spec in parameters) { - string parameterTypeRef = spec.ParameterType.FullyQualifiedName; - - object? defaultValue = spec.DefaultValue; - string defaultValueAsStr = GetParamDefaultValueAsString(defaultValue, spec.ParameterType); - - sb.Append(@$" - {InfoVarName} = new() - {{ - Name = ""{spec.Name}"", - ParameterType = typeof({parameterTypeRef}), - Position = {spec.ParameterIndex}, - HasDefaultValue = {FormatBool(spec.HasDefaultValue)}, - DefaultValue = {defaultValueAsStr} - }}; - {parametersVarName}[{spec.ParameterIndex}] = {InfoVarName}; -"); + writer.WriteLine($$""" + {{parametersVarName}}[{{spec.ParameterIndex}}] = new() + { + Name = "{{spec.Name}}", + ParameterType = typeof({{spec.ParameterType.FullyQualifiedName}}), + Position = {{spec.ParameterIndex}}, + HasDefaultValue = {{FormatBool(spec.HasDefaultValue)}}, + DefaultValue = {{FormatDefaultConstructorParameter(spec.DefaultValue, spec.ParameterType)}} + }; + + """); } if (propertyInitializers != null) @@ -789,33 +683,31 @@ private static {JsonParameterInfoValuesTypeRef}[] {typeGenerationSpec.TypeInfoPr foreach (PropertyInitializerGenerationSpec spec in propertyInitializers) { if (spec.MatchesConstructorParameter) + { continue; + } + + writer.WriteLine($$""" + {{parametersVarName}}[{{spec.ParameterIndex}}] = new() + { + Name = "{{spec.Name}}", + ParameterType = typeof({{spec.ParameterType.FullyQualifiedName}}), + Position = {{spec.ParameterIndex}}, + }; - sb.Append(@$" - {InfoVarName} = new() - {{ - Name = ""{spec.Property.MemberName}"", - ParameterType = typeof({spec.Property.PropertyType.FullyQualifiedName}), - Position = {spec.ParameterIndex}, - HasDefaultValue = false, - DefaultValue = default({spec.Property.PropertyType.FullyQualifiedName}), - }}; - {parametersVarName}[{spec.ParameterIndex}] = {InfoVarName}; -"); + """); } } - sb.Append(@$" - return {parametersVarName}; -}}"); + writer.WriteLine($"return {parametersVarName};"); - return sb.ToString(); + writer.Indentation--; + writer.WriteLine('}'); } - private string GenerateFastPathFuncForObject(TypeGenerationSpec typeGenSpec) + private void GenerateFastPathFuncForObject(SourceWriter writer, ContextGenerationSpec contextSpec, string serializeMethodName, TypeGenerationSpec typeGenSpec) { string typeRef = typeGenSpec.TypeRef.FullyQualifiedName; - ContextGenerationSpec contextSpec = _currentContext; if (!TryFilterSerializableProps( typeGenSpec, @@ -823,23 +715,26 @@ private static {JsonParameterInfoValuesTypeRef}[] {typeGenerationSpec.TypeInfoPr out Dictionary? serializableProperties, out bool castingRequiredForProps)) { - string exceptionMessage = string.Format(ExceptionMessages.InvalidSerializablePropertyConfiguration, typeRef); + // Type uses configuration that doesn't support fast-path: emit a stub that just throws. + GenerateFastPathFuncHeader(writer, typeGenSpec, serializeMethodName, skipNullCheck: true); - return GenerateFastPathFuncForType(typeGenSpec, - $@"throw new {InvalidOperationExceptionTypeRef}(""{exceptionMessage}"");", - emitNullCheck: false); // Skip null check since we want to throw an exception straightaway. + string exceptionMessage = string.Format(ExceptionMessages.InvalidSerializablePropertyConfiguration, typeRef); + writer.WriteLine($"""throw new {InvalidOperationExceptionTypeRef}("{exceptionMessage}");"""); + writer.Indentation--; + writer.WriteLine('}'); + return; } - StringBuilder sb = new(); + GenerateFastPathFuncHeader(writer, typeGenSpec, serializeMethodName); - // Begin method logic. if (typeGenSpec.ImplementsIJsonOnSerializing) { - sb.Append($@"((global::{JsonConstants.IJsonOnSerializingFullName}){ValueVarName}).OnSerializing();"); - sb.Append($@"{Environment.NewLine} "); + writer.WriteLine($"((global::{JsonConstants.IJsonOnSerializingFullName}){ValueVarName}).OnSerializing();"); + writer.WriteLine(); } - sb.Append($@"{WriterVarName}.WriteStartObject();"); + writer.WriteLine($"{WriterVarName}.WriteStartObject();"); + writer.WriteLine(); // Provide generation logic for each prop. foreach (PropertyGenerationSpec propertyGenSpec in serializableProperties.Values) @@ -851,71 +746,53 @@ private static {JsonParameterInfoValuesTypeRef}[] {typeGenerationSpec.TypeInfoPr continue; } - string runtimePropName = propertyGenSpec.RuntimePropertyName; - string propVarName = propertyGenSpec.PropertyNameVarName; + string propNameVarName = propertyGenSpec.PropertyNameVarName; // Add the property names to the context-wide cache; we'll generate the source to initialize them at the end of generation. - Debug.Assert(!_runtimePropertyNames.TryGetValue(runtimePropName, out string? existingName) || existingName == propVarName); - _runtimePropertyNames.TryAdd(runtimePropName, propVarName); + Debug.Assert(!_propertyNames.TryGetValue(runtimePropName, out string? existingName) || existingName == propNameVarName); + _propertyNames.TryAdd(runtimePropName, propNameVarName); - string? objectRef = castingRequiredForProps ? $"(({propertyGenSpec.DeclaringTypeRef}){ValueVarName})" : ValueVarName; - string propValue = $"{objectRef}.{propertyGenSpec.NameSpecifiedInSourceCode}"; - string methodArgs = $"{propVarName}, {propValue}"; + DefaultCheckType defaultCheckType = GetDefaultCheckType(contextSpec, propertyGenSpec); + string? objectExpr = castingRequiredForProps ? $"(({propertyGenSpec.DeclaringType.FullyQualifiedName}){ValueVarName})" : ValueVarName; + string propValueExpr = $"{objectExpr}.{propertyGenSpec.NameSpecifiedInSourceCode}"; - string? methodToCall = GetWriterMethod(propertyTypeSpec); - - if (propertyTypeSpec.PrimitiveTypeKind is JsonPrimitiveTypeKind.Char) + switch (defaultCheckType) { - methodArgs = $"{methodArgs}.ToString()"; - } - - string serializationLogic; + case DefaultCheckType.Null: + writer.WriteLine($"if ({propValueExpr} != null)"); + writer.WriteLine('{'); + writer.Indentation++; + break; - if (methodToCall != null) - { - serializationLogic = $@" - {methodToCall}({methodArgs});"; - } - else - { - serializationLogic = $@" - {WriterVarName}.WritePropertyName({propVarName}); - {GetSerializeLogicForNonPrimitiveType(propertyTypeSpec, propValue)}"; + case DefaultCheckType.Default: + writer.WriteLine($"if (!{EqualityComparerTypeRef}<{propertyGenSpec.PropertyType.FullyQualifiedName}>.Default.Equals(default, {propValueExpr}))"); + writer.WriteLine('{'); + writer.Indentation++; + break; } - JsonIgnoreCondition ignoreCondition = propertyGenSpec.DefaultIgnoreCondition ?? contextSpec.DefaultIgnoreCondition; - DefaultCheckType defaultCheckType; - bool typeCanBeNull = propertyTypeSpec.TypeRef.CanBeNull; + GenerateSerializePropertyStatement(writer, propertyTypeSpec, propNameVarName, propValueExpr); - switch (ignoreCondition) + if (defaultCheckType != DefaultCheckType.None) { - case JsonIgnoreCondition.WhenWritingNull: - defaultCheckType = typeCanBeNull ? DefaultCheckType.Null : DefaultCheckType.None; - break; - case JsonIgnoreCondition.WhenWritingDefault: - defaultCheckType = typeCanBeNull ? DefaultCheckType.Null : DefaultCheckType.Default; - break; - default: - defaultCheckType = DefaultCheckType.None; - break; + writer.Indentation--; + writer.WriteLine('}'); } - - sb.Append(WrapSerializationLogicInDefaultCheckIfRequired(serializationLogic, propValue, propertyGenSpec.PropertyType.FullyQualifiedName, defaultCheckType)); } // End method logic. - sb.Append($@" - - {WriterVarName}.WriteEndObject();"); + writer.WriteLine(); + writer.WriteLine($"{WriterVarName}.WriteEndObject();"); if (typeGenSpec.ImplementsIJsonOnSerialized) { - sb.Append($@"{Environment.NewLine} "); - sb.Append($@"((global::{JsonConstants.IJsonOnSerializedFullName}){ValueVarName}).OnSerialized();"); - }; + writer.WriteLine(); + writer.WriteLine($"((global::{JsonConstants.IJsonOnSerializedFullName}){ValueVarName}).OnSerialized();"); + } - return GenerateFastPathFuncForType(typeGenSpec, sb.ToString(), emitNullCheck: typeGenSpec.TypeRef.CanBeNull); + writer.Indentation--; + writer.WriteLine('}'); } private static bool ShouldIncludePropertyForFastPath(PropertyGenerationSpec propertyGenSpec, TypeGenerationSpec propertyTypeSpec, ContextGenerationSpec contextSpec) @@ -982,7 +859,7 @@ private static {JsonParameterInfoValuesTypeRef}[] {typeGenerationSpec.TypeInfoPr sb.Append("{ "); foreach (PropertyInitializerGenerationSpec property in propertyInitializers) { - sb.Append($"{property.Property.MemberName} = {GetParamUnboxing(property.Property.PropertyType, property.ParameterIndex)}, "); + sb.Append($"{property.Name} = {GetParamUnboxing(property.ParameterType, property.ParameterIndex)}, "); } sb.Length -= 2; // delete the last ", " token @@ -995,58 +872,99 @@ private static {JsonParameterInfoValuesTypeRef}[] {typeGenerationSpec.TypeInfoPr => $"({type.FullyQualifiedName}){ArgsVarName}[{index}]"; } - private static string? GetWriterMethod(TypeGenerationSpec type) + private static string? GetPrimitiveWriterMethod(TypeGenerationSpec type) { return type.PrimitiveTypeKind switch { - JsonPrimitiveTypeKind.Number => $"{WriterVarName}.WriteNumber", - JsonPrimitiveTypeKind.String or JsonPrimitiveTypeKind.Char => $"{WriterVarName}.WriteString", - JsonPrimitiveTypeKind.Boolean => $"{WriterVarName}.WriteBoolean", - JsonPrimitiveTypeKind.ByteArray => $"{WriterVarName}.WriteBase64String", + JsonPrimitiveTypeKind.Number => "WriteNumber", + JsonPrimitiveTypeKind.String or JsonPrimitiveTypeKind.Char => "WriteString", + JsonPrimitiveTypeKind.Boolean => "WriteBoolean", + JsonPrimitiveTypeKind.ByteArray => "WriteBase64String", _ => null }; } - private static string GenerateFastPathFuncForType(TypeGenerationSpec typeGenSpec, string serializeMethodBody, bool emitNullCheck) + private static void GenerateFastPathFuncHeader(SourceWriter writer, TypeGenerationSpec typeGenSpec, string methodName, bool skipNullCheck = false) { - Debug.Assert(!emitNullCheck || typeGenSpec.TypeRef.CanBeNull); - - string serializeMethodName = $"{typeGenSpec.TypeInfoPropertyName}{SerializeHandlerPropName}"; // fast path serializers for reference types always support null inputs. - string valueTypeRef = $"{typeGenSpec.TypeRef.FullyQualifiedName}{(typeGenSpec.TypeRef.IsValueType ? "" : "?")}"; + string valueTypeRef = typeGenSpec.TypeRef.IsValueType + ? typeGenSpec.TypeRef.FullyQualifiedName + : typeGenSpec.TypeRef.FullyQualifiedName + "?"; + + writer.WriteLine($$""" + // Intentionally not a static method because we create a delegate to it. Invoking delegates to instance + // methods is almost as fast as virtual calls. Static methods need to go through a shuffle thunk. + private void {{methodName}}({{Utf8JsonWriterTypeRef}} {{WriterVarName}}, {{valueTypeRef}} {{ValueVarName}}) + { + """); - return $@" + writer.Indentation++; + + if (!skipNullCheck && typeGenSpec.TypeRef.CanBeNull) + { + writer.WriteLine($$""" + if ({{ValueVarName}} == null) + { + {{WriterVarName}}.WriteNullValue(); + return; + } -// Intentionally not a static method because we create a delegate to it. Invoking delegates to instance -// methods is almost as fast as virtual calls. Static methods need to go through a shuffle thunk. -private void {serializeMethodName}({Utf8JsonWriterTypeRef} {WriterVarName}, {valueTypeRef} {ValueVarName}) -{{ - {GetEarlyNullCheckSource(emitNullCheck)} - {serializeMethodBody} -}}"; + """); + } } - private static string? GetEarlyNullCheckSource(bool canBeNull) + private static void GenerateSerializeValueStatement(SourceWriter writer, TypeGenerationSpec typeSpec, string valueExpr) { - return canBeNull - ? $@"if ({ValueVarName} == null) - {{ - {WriterVarName}.WriteNullValue(); - return; - }} -" - : null; + if (GetPrimitiveWriterMethod(typeSpec) is string primitiveWriterMethod) + { + if (typeSpec.PrimitiveTypeKind is JsonPrimitiveTypeKind.Char) + { + writer.WriteLine($"{WriterVarName}.{primitiveWriterMethod}Value({valueExpr}.ToString());"); + } + else + { + writer.WriteLine($"{WriterVarName}.{primitiveWriterMethod}Value({valueExpr});"); + } + } + else + { + if (ShouldGenerateSerializationLogic(typeSpec)) + { + writer.WriteLine($"{typeSpec.TypeInfoPropertyName}{SerializeHandlerPropName}({WriterVarName}, {valueExpr});"); + } + else + { + writer.WriteLine($"{JsonSerializerTypeRef}.Serialize({WriterVarName}, {valueExpr}, {typeSpec.TypeInfoPropertyName});"); + } + } } - private string GetSerializeLogicForNonPrimitiveType(TypeGenerationSpec typeGenerationSpec, string valueExpr) + private static void GenerateSerializePropertyStatement(SourceWriter writer, TypeGenerationSpec typeSpec, string propertyNameExpr, string valueExpr) { - if (ShouldGenerateSerializationLogic(typeGenerationSpec)) + if (GetPrimitiveWriterMethod(typeSpec) is string primitiveWriterMethod) { - return $"{typeGenerationSpec.TypeInfoPropertyName}{SerializeHandlerPropName}({WriterVarName}, {valueExpr});"; + if (typeSpec.PrimitiveTypeKind is JsonPrimitiveTypeKind.Char) + { + writer.WriteLine($"{WriterVarName}.{primitiveWriterMethod}({propertyNameExpr}, {valueExpr}.ToString());"); + } + else + { + writer.WriteLine($"{WriterVarName}.{primitiveWriterMethod}({propertyNameExpr}, {valueExpr});"); + } } + else + { + writer.WriteLine($"{WriterVarName}.WritePropertyName({propertyNameExpr});"); - string typeInfoRef = $"{_currentContext.ContextType.FullyQualifiedName}.Default.{typeGenerationSpec.TypeInfoPropertyName}!"; - return $"{JsonSerializerTypeRef}.Serialize({WriterVarName}, {valueExpr}, {typeInfoRef});"; + if (ShouldGenerateSerializationLogic(typeSpec)) + { + writer.WriteLine($"{typeSpec.TypeInfoPropertyName}{SerializeHandlerPropName}({WriterVarName}, {valueExpr});"); + } + else + { + writer.WriteLine($"{JsonSerializerTypeRef}.Serialize({WriterVarName}, {valueExpr}, {typeSpec.TypeInfoPropertyName});"); + } + } } private enum DefaultCheckType @@ -1056,74 +974,57 @@ private void {serializeMethodName}({Utf8JsonWriterTypeRef} {WriterVarName}, {val Default, } - private static string WrapSerializationLogicInDefaultCheckIfRequired(string serializationLogic, string propValue, string propTypeRef, DefaultCheckType defaultCheckType) + private static DefaultCheckType GetDefaultCheckType(ContextGenerationSpec contextSpec, PropertyGenerationSpec propertySpec) { - string comparisonLogic; - - switch (defaultCheckType) + return (propertySpec.DefaultIgnoreCondition ?? contextSpec.DefaultIgnoreCondition) switch { - case DefaultCheckType.None: - return serializationLogic; - case DefaultCheckType.Null: - comparisonLogic = $"{propValue} != null"; - break; - case DefaultCheckType.Default: - comparisonLogic = $"!{EqualityComparerTypeRef}<{propTypeRef}>.Default.Equals(default, {propValue})"; - break; - default: - throw new InvalidOperationException(); - } - - return $@" - if ({comparisonLogic}) - {{{IndentSource(serializationLogic, numIndentations: 1)} - }}"; + JsonIgnoreCondition.WhenWritingNull => propertySpec.PropertyType.CanBeNull ? DefaultCheckType.Null : DefaultCheckType.None, + JsonIgnoreCondition.WhenWritingDefault => propertySpec.PropertyType.CanBeNull ? DefaultCheckType.Null : DefaultCheckType.Default, + _ => DefaultCheckType.None, + }; } - private static string GenerateForType(TypeGenerationSpec typeMetadata, string metadataInitSource, string? additionalSource = null) + private static void GenerateTypeInfoProperty(SourceWriter writer, TypeGenerationSpec typeMetadata, string metadataInitSource) { - string typeCompilableName = typeMetadata.TypeRef.FullyQualifiedName; - string typeFriendlyName = typeMetadata.TypeInfoPropertyName; - string typeInfoPropertyTypeRef = $"{JsonTypeInfoTypeRef}<{typeCompilableName}>"; + string typeFQN = typeMetadata.TypeRef.FullyQualifiedName; + string typeInfoPropertyName = typeMetadata.TypeInfoPropertyName; + string typeInfoFQN = $"{JsonTypeInfoTypeRef}<{typeFQN}>"; - return @$"private {typeInfoPropertyTypeRef}? _{typeFriendlyName}; + writer.WriteLine($$""" + private {{typeInfoFQN}}? _{{typeInfoPropertyName}}; -/// -/// Defines the source generated JSON serialization contract metadata for a given type. -/// -public {typeInfoPropertyTypeRef} {typeFriendlyName} -{{ - get => _{typeFriendlyName} ??= ({typeInfoPropertyTypeRef}){OptionsInstanceVariableName}.GetTypeInfo(typeof({typeCompilableName})); -}} + /// + /// Defines the source generated JSON serialization contract metadata for a given type. + /// + public {{typeInfoFQN}} {{typeInfoPropertyName}} + { + get => _{{typeInfoPropertyName}} ??= ({{typeInfoFQN}}){{OptionsInstanceVariableName}}.GetTypeInfo(typeof({{typeFQN}})); + } + + private {{typeInfoFQN}} {{CreateTypeInfoMethodName(typeMetadata)}}({{JsonSerializerOptionsTypeRef}} {{OptionsLocalVariableName}}) + { + if (!{{TryGetTypeInfoForRuntimeCustomConverterMethodName}}<{{typeFQN}}>({{OptionsLocalVariableName}}, out {{typeInfoFQN}} {{JsonTypeInfoReturnValueLocalVariableName}})) + { + """); -private {typeInfoPropertyTypeRef} {CreateTypeInfoMethodName(typeMetadata)}({JsonSerializerOptionsTypeRef} {OptionsLocalVariableName}) -{{ - {typeInfoPropertyTypeRef}? {JsonTypeInfoReturnValueLocalVariableName} = null; - {WrapWithCheckForCustomConverter(metadataInitSource, typeCompilableName)} + writer.Indentation += 2; + writer.WriteLine(metadataInitSource); + writer.Indentation -= 2; - { /* NB OriginatingResolver should be the last property set by the source generator. */ ""} - {JsonTypeInfoReturnValueLocalVariableName}.{OriginatingResolverPropertyName} = this; + // NB OriginatingResolver should be the last property set by the source generator. + writer.WriteLine($$""" + } - return {JsonTypeInfoReturnValueLocalVariableName}; -}} -{additionalSource}"; + {{JsonTypeInfoReturnValueLocalVariableName}}.{{OriginatingResolverPropertyName}} = this; + return jsonTypeInfo; + } + """); } - private static string WrapWithCheckForCustomConverter(string source, string typeCompilableName) - => @$"{JsonConverterTypeRef}? customConverter; - if ({OptionsLocalVariableName}.Converters.Count > 0 && (customConverter = {RuntimeCustomConverterFetchingMethodName}({OptionsLocalVariableName}, typeof({typeCompilableName}))) != null) - {{ - {JsonTypeInfoReturnValueLocalVariableName} = {JsonMetadataServicesTypeRef}.{GetCreateValueInfoMethodRef(typeCompilableName)}({OptionsLocalVariableName}, customConverter); - }} - else - {{ - {source} - }}"; - - private string GetRootJsonContextImplementation() + private static SourceText GetRootJsonContextImplementation(ContextGenerationSpec contextSpec) { - string contextTypeRef = _currentContext.ContextType.FullyQualifiedName; - string contextTypeName = _currentContext.ContextType.Name; + string contextTypeRef = contextSpec.ContextType.FullyQualifiedName; + string contextTypeName = contextSpec.ContextType.Name; int backTickIndex = contextTypeName.IndexOf('`'); if (backTickIndex != -1) @@ -1131,51 +1032,45 @@ private {typeInfoPropertyTypeRef} {CreateTypeInfoMethodName(typeMetadata)}({Json contextTypeName = contextTypeName.Substring(0, backTickIndex); } - StringBuilder sb = new(); + SourceWriter writer = CreateSourceWriterWithContextHeader(contextSpec, isPrimaryContextSourceFile: true); - sb.Append(@$"{GetLogicForDefaultSerializerOptionsInit()} + GetLogicForDefaultSerializerOptionsInit(contextSpec, writer); -private static {contextTypeRef}? {DefaultContextBackingStaticVarName}; + writer.WriteLine(); -/// -/// The default associated with a default instance. -/// -public static {contextTypeRef} Default => {DefaultContextBackingStaticVarName} ??= new {contextTypeRef}(new {JsonSerializerOptionsTypeRef}({DefaultOptionsStaticVarName})); + writer.WriteLine($$""" + private static {{contextTypeRef}}? {{DefaultContextBackingStaticVarName}}; -/// -/// The source-generated options associated with this context. -/// -protected override {JsonSerializerOptionsTypeRef}? GeneratedSerializerOptions {{ get; }} = {DefaultOptionsStaticVarName}; + /// + /// The default associated with a default instance. + /// + public static {{contextTypeRef}} Default => {{DefaultContextBackingStaticVarName}} ??= new {{contextTypeRef}}(new {{JsonSerializerOptionsTypeRef}}({{DefaultOptionsStaticVarName}})); -/// -public {contextTypeName}() : base(null) -{{ -}} + /// + /// The source-generated options associated with this context. + /// + protected override {{JsonSerializerOptionsTypeRef}}? GeneratedSerializerOptions { get; } = {{DefaultOptionsStaticVarName}}; -/// -public {contextTypeName}({JsonSerializerOptionsTypeRef} {OptionsLocalVariableName}) : base({OptionsLocalVariableName}) -{{ -}} + /// + public {{contextTypeName}}() : base(null) + { + } -{GetFetchLogicForRuntimeSpecifiedCustomConverter()}"); + /// + public {{contextTypeName}}({{JsonSerializerOptionsTypeRef}} {{OptionsLocalVariableName}}) : base({{OptionsLocalVariableName}}) + { + } + """); - if (_generateGetConverterMethodForProperties) - { - sb.Append(GetFetchLogicForGetCustomConverter_PropertiesWithFactories()); - } + writer.WriteLine(); - if (_generateGetConverterMethodForProperties || _generateGetConverterMethodForTypes) - { - sb.Append(GetFetchLogicForGetCustomConverter_TypesWithFactories()); - } + GenerateConverterHelpers(writer); - return sb.ToString(); + return CompleteSourceFileAndReturnText(writer); } - private string GetLogicForDefaultSerializerOptionsInit() + private static void GetLogicForDefaultSerializerOptionsInit(ContextGenerationSpec contextSpec, SourceWriter writer) { - ContextGenerationSpec contextSpec = _currentContext; - string? namingPolicyName = contextSpec.PropertyNamingPolicy switch { JsonKnownNamingPolicy.CamelCase => nameof(JsonNamingPolicy.CamelCase), @@ -1186,140 +1081,130 @@ public {contextTypeName}({JsonSerializerOptionsTypeRef} {OptionsLocalVariableNam _ => null, }; - string? namingPolicyInit = namingPolicyName != null - ? $@" - PropertyNamingPolicy = {JsonNamingPolicyTypeRef}.{namingPolicyName}" - : null; + string namingPolicy = namingPolicyName != null + ? $"{JsonNamingPolicyTypeRef}.{namingPolicyName}" + : "null"; - return $@" -private static {JsonSerializerOptionsTypeRef} {DefaultOptionsStaticVarName} {{ get; }} = new {JsonSerializerOptionsTypeRef}() -{{ - DefaultIgnoreCondition = {JsonIgnoreConditionTypeRef}.{contextSpec.DefaultIgnoreCondition}, - IgnoreReadOnlyFields = {FormatBool(contextSpec.IgnoreReadOnlyFields)}, - IgnoreReadOnlyProperties = {FormatBool(contextSpec.IgnoreReadOnlyProperties)}, - IncludeFields = {FormatBool(contextSpec.IncludeFields)}, - WriteIndented = {FormatBool(contextSpec.WriteIndented)},{namingPolicyInit} -}};"; + writer.WriteLine($$""" + private readonly static {{JsonSerializerOptionsTypeRef}} {{DefaultOptionsStaticVarName}} = new() + { + DefaultIgnoreCondition = {{JsonIgnoreConditionTypeRef}}.{{contextSpec.DefaultIgnoreCondition}}, + IgnoreReadOnlyFields = {{FormatBool(contextSpec.IgnoreReadOnlyFields)}}, + IgnoreReadOnlyProperties = {{FormatBool(contextSpec.IgnoreReadOnlyProperties)}}, + IncludeFields = {{FormatBool(contextSpec.IncludeFields)}}, + WriteIndented = {{FormatBool(contextSpec.WriteIndented)}}, + PropertyNamingPolicy = {{namingPolicy}} + }; + """); } - private static string GetFetchLogicForRuntimeSpecifiedCustomConverter() + private static void GenerateConverterHelpers(SourceWriter writer) { - // TODO (https://github.com/dotnet/runtime/issues/52218): use a dictionary if count > ~15. - return @$"private static {JsonConverterTypeRef}? {RuntimeCustomConverterFetchingMethodName}({JsonSerializerOptionsTypeRef} {OptionsLocalVariableName}, {TypeTypeRef} type) -{{ - {IListTypeRef}<{JsonConverterTypeRef}> converters = {OptionsLocalVariableName}.Converters; - - for (int i = 0; i < converters.Count; i++) - {{ - {JsonConverterTypeRef}? converter = converters[i]; - - if (converter.CanConvert(type)) - {{ - if (converter is {JsonConverterFactoryTypeRef} factory) - {{ - converter = factory.CreateConverter(type, {OptionsLocalVariableName}); - if (converter == null || converter is {JsonConverterFactoryTypeRef}) - {{ - throw new {InvalidOperationExceptionTypeRef}(string.Format(""{ExceptionMessages.InvalidJsonConverterFactoryOutput}"", factory.GetType())); - }} - }} - - return converter; - }} - }} - - return null; -}}"; - } + // The generic type parameter could capture type parameters from containing types, + // so use a name that is unlikely to be used. + const string TypeParameter = "TJsonMetadataType"; - private static string GetFetchLogicForGetCustomConverter_PropertiesWithFactories() - { - return @$" + writer.WriteLine($$""" + private static bool {{TryGetTypeInfoForRuntimeCustomConverterMethodName}}<{{TypeParameter}}>({{JsonSerializerOptionsTypeRef}} options, out {{JsonTypeInfoTypeRef}}<{{TypeParameter}}> jsonTypeInfo) + { + {{JsonConverterTypeRef}}? converter = GetRuntimeConverterForType(typeof({{TypeParameter}}), options); + if (converter != null) + { + jsonTypeInfo = {{JsonMetadataServicesTypeRef}}.{{CreateValueInfoMethodName}}<{{TypeParameter}}>(options, converter); + return true; + } -private static {JsonConverterTypeRef} {GetConverterFromFactoryMethodName}({JsonSerializerOptionsTypeRef} {OptionsLocalVariableName}, {JsonConverterFactoryTypeRef} factory) -{{ - return ({JsonConverterTypeRef}) {GetConverterFromFactoryMethodName}({OptionsLocalVariableName}, typeof(T), factory); -}}"; - } + jsonTypeInfo = null; + return false; + } - private static string GetFetchLogicForGetCustomConverter_TypesWithFactories() - { - return @$" - -private static {JsonConverterTypeRef} {GetConverterFromFactoryMethodName}({JsonSerializerOptionsTypeRef} {OptionsLocalVariableName}, {TypeTypeRef} type, {JsonConverterFactoryTypeRef} factory) -{{ - {JsonConverterTypeRef}? converter = factory.CreateConverter(type, {OptionsLocalVariableName}); - if (converter == null || converter is {JsonConverterFactoryTypeRef}) - {{ - throw new {InvalidOperationExceptionTypeRef}(string.Format(""{ExceptionMessages.InvalidJsonConverterFactoryOutput}"", factory.GetType())); - }} - - return converter; -}}"; + private static {{JsonConverterTypeRef}}? GetRuntimeConverterForType({{TypeTypeRef}} type, {{JsonSerializerOptionsTypeRef}} options) + { + for (int i = 0; i < options.Converters.Count; i++) + { + {{JsonConverterTypeRef}}? converter = options.Converters[i]; + if (converter?.CanConvert(type) == true) + { + return {{ExpandConverterMethodName}}(type, converter, options); + } + } + + return null; + } + + private static {{JsonConverterTypeRef}} {{ExpandConverterMethodName}}({{TypeTypeRef}} type, {{JsonConverterTypeRef}} converter, {{JsonSerializerOptionsTypeRef}} options) + { + if (converter is {{JsonConverterFactoryTypeRef}} factory) + { + converter = factory.CreateConverter(type, options); + if (converter is null || converter is {{JsonConverterFactoryTypeRef}}) + { + throw new {{InvalidOperationExceptionTypeRef}}(string.Format("{{ExceptionMessages.InvalidJsonConverterFactoryOutput}}", factory.GetType())); + } + } + + if (!converter.CanConvert(type)) + { + throw new {{InvalidOperationExceptionTypeRef}}(string.Format("{{ExceptionMessages.IncompatibleConverterType}}", converter.GetType(), type)); + } + + return converter; + } + """); } - private static string GetGetTypeInfoImplementation(ContextGenerationSpec contextGenerationSpec) + private static SourceText GetGetTypeInfoImplementation(ContextGenerationSpec contextSpec) { - StringBuilder sb = new(); + SourceWriter writer = CreateSourceWriterWithContextHeader(contextSpec, interfaceImplementation: JsonTypeInfoResolverTypeRef); // JsonSerializerContext.GetTypeInfo override -- returns cached metadata via JsonSerializerOptions - sb.Append( -@$"/// -public override {JsonTypeInfoTypeRef}? GetTypeInfo({TypeTypeRef} type) -{{ - {OptionsInstanceVariableName}.TryGetTypeInfo(type, out {JsonTypeInfoTypeRef}? typeInfo); - return typeInfo; -}} -"); + writer.WriteLine($$""" + /// + public override {{JsonTypeInfoTypeRef}}? GetTypeInfo({{TypeTypeRef}} type) + { + {{OptionsInstanceVariableName}}.TryGetTypeInfo(type, out {{JsonTypeInfoTypeRef}}? typeInfo); + return typeInfo; + } + """); + + writer.WriteLine(); + // Explicit IJsonTypeInfoResolver implementation -- the source of truth for metadata resolution - sb.AppendLine(); - sb.Append(@$"{JsonTypeInfoTypeRef}? {JsonTypeInfoResolverTypeRef}.GetTypeInfo({TypeTypeRef} type, {JsonSerializerOptionsTypeRef} {OptionsLocalVariableName}) -{{"); - foreach (TypeGenerationSpec metadata in contextGenerationSpec.GeneratedTypes) + writer.WriteLine($"{JsonTypeInfoTypeRef}? {JsonTypeInfoResolverTypeRef}.GetTypeInfo({TypeTypeRef} type, {JsonSerializerOptionsTypeRef} {OptionsLocalVariableName})"); + writer.WriteLine('{'); + writer.Indentation++; + + foreach (TypeGenerationSpec metadata in contextSpec.GeneratedTypes) { if (metadata.ClassType != ClassType.TypeUnsupportedBySourceGen) { - sb.Append($@" - if (type == typeof({metadata.TypeRef.FullyQualifiedName})) - {{ - return {CreateTypeInfoMethodName(metadata)}({OptionsLocalVariableName}); - }} -"); + writer.WriteLine($$""" + if (type == typeof({{metadata.TypeRef.FullyQualifiedName}})) + { + return {{CreateTypeInfoMethodName(metadata)}}({{OptionsLocalVariableName}}); + } + """); } } - sb.Append($@" - return null; -}} -"); + writer.WriteLine("return null;"); - return sb.ToString(); - } - - private string GetPropertyNameInitialization() - { + writer.Indentation--; + writer.WriteLine('}'); - StringBuilder sb = new(); - - foreach (KeyValuePair name_varName_pair in _runtimePropertyNames) - { - sb.Append($@" -private static readonly {JsonEncodedTextTypeRef} {name_varName_pair.Value} = {JsonEncodedTextTypeRef}.Encode(""{name_varName_pair.Key}"");"); - } - - _runtimePropertyNames.Clear(); // Clear the cache for the next context. - return sb.ToString(); + return CompleteSourceFileAndReturnText(writer); } - private static string IndentSource(string source, int numIndentations) + private SourceText GetPropertyNameInitialization(ContextGenerationSpec contextSpec) { - if (numIndentations == 0) + SourceWriter writer = CreateSourceWriterWithContextHeader(contextSpec); + + foreach (KeyValuePair name_varName_pair in _propertyNames) { - return source; + writer.WriteLine($$"""private static readonly {{JsonEncodedTextTypeRef}} {{name_varName_pair.Value}} = {{JsonEncodedTextTypeRef}}.Encode("{{name_varName_pair.Key}}");"""); } - string indentation = new string(' ', 4 * numIndentations); // 4 spaces per indentation. - return indentation + source.Replace(Environment.NewLine, Environment.NewLine + indentation); + return CompleteSourceFileAndReturnText(writer); } private static string GetNumberHandlingAsStr(JsonNumberHandling? numberHandling) => @@ -1343,6 +1228,7 @@ private static readonly {JsonEncodedTextTypeRef} {name_varName_pair.Value} = {Js private static string GetCreateValueInfoMethodRef(string typeCompilableName) => $"{CreateValueInfoMethodName}<{typeCompilableName}>"; private static string FormatBool(bool value) => value ? "true" : "false"; + private static string FormatStringLiteral(string? value) => value is null ? "null" : $"\"{value}\""; /// /// Method used to generate JsonTypeInfo given options instance @@ -1350,7 +1236,7 @@ private static readonly {JsonEncodedTextTypeRef} {name_varName_pair.Value} = {Js private static string CreateTypeInfoMethodName(TypeGenerationSpec typeSpec) => $"Create_{typeSpec.TypeInfoPropertyName}"; - private static string GetParamDefaultValueAsString(object? value, TypeRef type) + private static string FormatDefaultConstructorParameter(object? value, TypeRef type) { if (value == null) { @@ -1401,6 +1287,43 @@ private static readonly {JsonEncodedTextTypeRef} {name_varName_pair.Value} = {Js string FormatNumber() => $"({type.FullyQualifiedName})({Convert.ToString(value, CultureInfo.InvariantCulture)})"; } + private static string FormatDefaultConstructorExpr(TypeGenerationSpec typeSpec) + { + return typeSpec switch + { + { RuntimeTypeRef: TypeRef runtimeType } => $"() => new {runtimeType.FullyQualifiedName}()", + { IsValueTuple: true } => $"() => default({typeSpec.TypeRef.FullyQualifiedName})", + { ConstructionStrategy: ObjectConstructionStrategy.ParameterlessConstructor } => $"() => new {typeSpec.TypeRef.FullyQualifiedName}()", + _ => "null", + }; + } + + private static string GetCollectionInfoMethodName(CollectionType collectionType) + { + return collectionType switch + { + CollectionType.Array => "CreateArrayInfo", + CollectionType.List => "CreateListInfo", + CollectionType.IListOfT or CollectionType.IList => "CreateIListInfo", + CollectionType.ICollectionOfT => "CreateICollectionInfo", + CollectionType.IEnumerableOfT or CollectionType.IEnumerable => "CreateIEnumerableInfo", + CollectionType.StackOfT or CollectionType.Stack => "CreateStackInfo", + CollectionType.QueueOfT or CollectionType.Queue => "CreateQueueInfo", + CollectionType.ConcurrentStack => "CreateConcurrentStackInfo", + CollectionType.ConcurrentQueue => "CreateConcurrentQueueInfo", + CollectionType.ImmutableEnumerable => "CreateImmutableEnumerableInfo", + CollectionType.IAsyncEnumerableOfT => "CreateIAsyncEnumerableInfo", + CollectionType.ISet => "CreateISetInfo", + + CollectionType.Dictionary => "CreateDictionaryInfo", + CollectionType.IDictionaryOfTKeyTValue or CollectionType.IDictionary => "CreateIDictionaryInfo", + CollectionType.IReadOnlyDictionary => "CreateIReadOnlyDictionaryInfo", + CollectionType.ImmutableDictionary => "CreateImmutableDictionaryInfo", + + _ => throw new Exception(), + }; + } + private static bool ShouldGenerateMetadata(TypeGenerationSpec typeSpec) => IsGenerationModeSpecified(typeSpec, JsonSourceGenerationMode.Metadata); @@ -1542,7 +1465,7 @@ private static readonly {JsonEncodedTextTypeRef} {name_varName_pair.Value} = {Js if (property.PropertyType.SpecialType is SpecialType.System_Object || property.NumberHandling == JsonNumberHandling.AllowNamedFloatingPointLiterals || property.NumberHandling == JsonNumberHandling.WriteAsString || - property.ConverterInstantiationLogic is not null) + property.ConverterType is not null) { return false; } diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs index 88f633c..d616033 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs @@ -34,8 +34,6 @@ namespace System.Text.Json.SourceGeneration internal const string JsonSerializableAttributeFullName = "System.Text.Json.Serialization.JsonSerializableAttribute"; - private const string DictionaryTypeRef = "global::System.Collections.Generic.Dictionary"; - private readonly Compilation _compilation; private readonly KnownTypeSymbols _knownSymbols; @@ -139,12 +137,12 @@ namespace System.Text.Json.SourceGeneration INamedTypeSymbol? jsonSerializerContextSymbol = _knownSymbols.JsonSerializerContextType; INamedTypeSymbol? jsonSerializableAttributeSymbol = _knownSymbols.JsonSerializableAttributeType; INamedTypeSymbol? jsonSourceGenerationOptionsAttributeSymbol = _knownSymbols.JsonSourceGenerationOptionsAttributeType; - INamedTypeSymbol? jsonConverterOfTSymbol = _knownSymbols.JsonConverterOfTType; + INamedTypeSymbol? jsonConverterSymbol = _knownSymbols.JsonConverterType; if (jsonSerializerContextSymbol == null || jsonSerializableAttributeSymbol == null || jsonSourceGenerationOptionsAttributeSymbol == null || - jsonConverterOfTSymbol == null) + jsonConverterSymbol == null) { return null; } @@ -567,24 +565,22 @@ namespace System.Text.Json.SourceGeneration TypeRef? collectionValueType = null; TypeRef? nullableUnderlyingType = null; TypeRef? extensionDataPropertyType = null; - string? runtimeTypeRef = null; + TypeRef? runtimeTypeRef = null; List? propGenSpecList = null; ObjectConstructionStrategy constructionStrategy = default; bool constructorSetsRequiredMembers = false; - ParameterGenerationSpec[]? paramGenSpecArray = null; - List? propertyInitializerSpecList = null; + ParameterGenerationSpec[]? paramGenSpecs = null; + List? propertyInitializers = null; CollectionType collectionType = CollectionType.NotApplicable; JsonNumberHandling? numberHandling = null; JsonUnmappedMemberHandling? unmappedMemberHandling = null; JsonObjectCreationHandling? preferredPropertyObjectCreationHandling = null; string? immutableCollectionFactoryTypeFullName = null; bool foundDesignTimeCustomConverter = false; - string? converterInstantiationLogic = null; + TypeRef? converterType = null; bool implementsIJsonOnSerialized = false; bool implementsIJsonOnSerializing = false; bool isPolymorphic = false; - bool hasTypeFactoryConverter = false; - bool hasPropertyFactoryConverters = false; IList attributeDataList = type.GetAttributes(); foreach (AttributeData attributeData in attributeDataList) @@ -611,12 +607,8 @@ namespace System.Text.Json.SourceGeneration } else if (!foundDesignTimeCustomConverter && _knownSymbols.JsonConverterAttributeType.IsAssignableFrom(attributeType)) { + converterType = GetConverterTypeFromAttribute(attributeData); foundDesignTimeCustomConverter = true; - converterInstantiationLogic = GetConverterInstantiationLogic( - type, - attributeData, - forType: true, - ref hasTypeFactoryConverter); } if (SymbolEqualityComparer.Default.Equals(attributeType, _knownSymbols.JsonDerivedTypeAttributeType)) @@ -636,7 +628,7 @@ namespace System.Text.Json.SourceGeneration if (foundDesignTimeCustomConverter) { - classType = converterInstantiationLogic != null + classType = converterType != null ? ClassType.TypeWithDesignTimeProvidedCustomConverter : ClassType.TypeUnsupportedBySourceGen; } @@ -829,7 +821,7 @@ namespace System.Text.Json.SourceGeneration if (needsRuntimeType) { - runtimeTypeRef = GetDictionaryTypeRef(collectionKeyType, collectionValueType); + runtimeTypeRef = GetDictionaryTypeRef(keyType, valueType); } } } @@ -859,14 +851,14 @@ namespace System.Text.Json.SourceGeneration else { constructionStrategy = ObjectConstructionStrategy.ParameterizedConstructor; - paramGenSpecArray = new ParameterGenerationSpec[paramCount]; + paramGenSpecs = new ParameterGenerationSpec[paramCount]; for (int i = 0; i < paramCount; i++) { IParameterSymbol parameterInfo = parameters![i]; TypeRef parameterTypeRef = EnqueueType(parameterInfo.Type, generationMode); - paramGenSpecArray[i] = new ParameterGenerationSpec + paramGenSpecs[i] = new ParameterGenerationSpec { ParameterType = parameterTypeRef, Name = parameterInfo.Name!, @@ -886,12 +878,14 @@ namespace System.Text.Json.SourceGeneration Dictionary? ignoredMembers = null; bool propertyOrderSpecified = false; - paramGenSpecArray ??= Array.Empty(); - int nextParameterIndex = paramGenSpecArray.Length; + paramGenSpecs ??= Array.Empty(); + int nextParameterIndex = paramGenSpecs.Length; // Walk the type hierarchy starting from the current type up to the base type(s) foreach (INamedTypeSymbol currentType in type.GetSortedTypeHierarchy()) { + var declaringTypeRef = new TypeRef(currentType); + foreach (IPropertySymbol propertyInfo in currentType.GetMembers().OfType()) { bool isVirtual = propertyInfo.IsVirtual(); @@ -906,7 +900,7 @@ namespace System.Text.Json.SourceGeneration continue; } - PropertyGenerationSpec? spec = GetPropertyGenerationSpec(currentType, propertyInfo.Type, propertyInfo, isVirtual, generationMode); + PropertyGenerationSpec? spec = GetPropertyGenerationSpec(declaringTypeRef, propertyInfo.Type, propertyInfo, isVirtual, generationMode); if (spec is null) { continue; @@ -931,7 +925,7 @@ namespace System.Text.Json.SourceGeneration continue; } - PropertyGenerationSpec? spec = GetPropertyGenerationSpec(currentType, fieldInfo.Type, fieldInfo, isVirtual: false, generationMode); + PropertyGenerationSpec? spec = GetPropertyGenerationSpec(declaringTypeRef, fieldInfo.Type, fieldInfo, isVirtual: false, generationMode); if (spec is null) { continue; @@ -945,7 +939,6 @@ namespace System.Text.Json.SourceGeneration CacheMember(memberInfo, spec, ref propGenSpecList, ref ignoredMembers); propertyOrderSpecified |= spec.Order != 0; - hasPropertyFactoryConverters |= spec.HasFactoryConverter; if (spec.IsExtensionData) { @@ -965,7 +958,7 @@ namespace System.Text.Json.SourceGeneration if (constructionStrategy is not ObjectConstructionStrategy.NotApplicable && spec.CanUseSetter && ((spec.IsRequired && !constructorSetsRequiredMembers) || spec.IsInitOnlySetter)) { - ParameterGenerationSpec? matchingConstructorParameter = GetMatchingConstructorParameter(spec, paramGenSpecArray); + ParameterGenerationSpec? matchingConstructorParameter = GetMatchingConstructorParameter(spec, paramGenSpecs); if (spec.IsRequired || matchingConstructorParameter is null) { @@ -973,12 +966,13 @@ namespace System.Text.Json.SourceGeneration var propInitializerSpec = new PropertyInitializerGenerationSpec { - Property = spec, + Name = spec.MemberName, + ParameterType = spec.PropertyType, MatchesConstructorParameter = matchingConstructorParameter is not null, ParameterIndex = matchingConstructorParameter?.ParameterIndex ?? nextParameterIndex++, }; - (propertyInitializerSpecList ??= new()).Add(propInitializerSpec); + (propertyInitializers ??= new()).Add(propInitializerSpec); } } @@ -1024,8 +1018,8 @@ namespace System.Text.Json.SourceGeneration UnmappedMemberHandling = unmappedMemberHandling, PreferredPropertyObjectCreationHandling = preferredPropertyObjectCreationHandling, PropertyGenSpecs = propGenSpecList?.ToImmutableEquatableArray(), - PropertyInitializerSpecs = propertyInitializerSpecList?.ToImmutableEquatableArray(), - CtorParamGenSpecs = paramGenSpecArray?.ToImmutableEquatableArray(), + PropertyInitializerSpecs = propertyInitializers?.ToImmutableEquatableArray(), + CtorParamGenSpecs = paramGenSpecs?.ToImmutableEquatableArray(), CollectionType = collectionType, CollectionKeyType = collectionKeyType, CollectionValueType = collectionValueType, @@ -1035,17 +1029,18 @@ namespace System.Text.Json.SourceGeneration RuntimeTypeRef = runtimeTypeRef, IsValueTuple = type.IsTupleType, ExtensionDataPropertyType = extensionDataPropertyType, - ConverterInstantiationLogic = converterInstantiationLogic, + ConverterType = converterType, ImplementsIJsonOnSerialized = implementsIJsonOnSerialized, ImplementsIJsonOnSerializing = implementsIJsonOnSerializing, ImmutableCollectionFactoryMethod = DetermineImmutableCollectionFactoryMethod(immutableCollectionFactoryTypeFullName), - HasTypeFactoryConverter = hasTypeFactoryConverter, - HasPropertyFactoryConverters = hasPropertyFactoryConverters, }; } - private static string GetDictionaryTypeRef(TypeRef keyType, TypeRef valueType) - => $"{DictionaryTypeRef}<{keyType.FullyQualifiedName}, {valueType.FullyQualifiedName}>"; + private TypeRef? GetDictionaryTypeRef(ITypeSymbol keyType, ITypeSymbol valueType) + { + INamedTypeSymbol? dictionary = _knownSymbols.DictionaryOfTKeyTValueType?.Construct(keyType, valueType); + return dictionary is null ? null : new TypeRef(dictionary); + } private bool IsValidDataExtensionPropertyType(ITypeSymbol type) { @@ -1080,9 +1075,9 @@ namespace System.Text.Json.SourceGeneration } } - private static ParameterGenerationSpec? GetMatchingConstructorParameter(PropertyGenerationSpec propSpec, ParameterGenerationSpec[]? paramGenSpecArray) + private static ParameterGenerationSpec? GetMatchingConstructorParameter(PropertyGenerationSpec propSpec, ParameterGenerationSpec[]? paramGenSpecs) { - return paramGenSpecArray?.FirstOrDefault(MatchesConstructorParameter); + return paramGenSpecs?.FirstOrDefault(MatchesConstructorParameter); bool MatchesConstructorParameter(ParameterGenerationSpec paramSpec) => propSpec.MemberName.Equals(paramSpec.Name, StringComparison.OrdinalIgnoreCase); @@ -1105,7 +1100,7 @@ namespace System.Text.Json.SourceGeneration } private PropertyGenerationSpec? GetPropertyGenerationSpec( - INamedTypeSymbol declaringType, + TypeRef declaringType, ITypeSymbol memberType, ISymbol memberInfo, bool isVirtual, @@ -1114,16 +1109,14 @@ namespace System.Text.Json.SourceGeneration Debug.Assert(memberInfo is IFieldSymbol or IPropertySymbol); ProcessMemberCustomAttributes( - memberType, memberInfo, out bool hasJsonInclude, out string? jsonPropertyName, out JsonIgnoreCondition? ignoreCondition, out JsonNumberHandling? numberHandling, out JsonObjectCreationHandling? objectCreationHandling, - out string? converterInstantiationLogic, + out TypeRef? converterType, out int order, - out bool hasFactoryConverter, out bool isExtensionData, out bool hasJsonRequiredAttribute); @@ -1171,23 +1164,20 @@ namespace System.Text.Json.SourceGeneration HasJsonInclude = hasJsonInclude, IsExtensionData = isExtensionData, PropertyType = EnqueueType(memberType, generationMode), - DeclaringTypeRef = declaringType.GetFullyQualifiedName(), - ConverterInstantiationLogic = converterInstantiationLogic, - HasFactoryConverter = hasFactoryConverter + DeclaringType = declaringType, + ConverterType = converterType, }; } private void ProcessMemberCustomAttributes( - ITypeSymbol memberCLRType, ISymbol memberInfo, out bool hasJsonInclude, out string? jsonPropertyName, out JsonIgnoreCondition? ignoreCondition, out JsonNumberHandling? numberHandling, out JsonObjectCreationHandling? objectCreationHandling, - out string? converterInstantiationLogic, + out TypeRef? converterType, out int order, - out bool hasFactoryConverter, out bool isExtensionData, out bool hasJsonRequiredAttribute) { @@ -1198,14 +1188,11 @@ namespace System.Text.Json.SourceGeneration ignoreCondition = default; numberHandling = default; objectCreationHandling = default; - converterInstantiationLogic = null; + converterType = null; order = 0; isExtensionData = false; hasJsonRequiredAttribute = false; - bool foundDesignTimeCustomConverter = false; - hasFactoryConverter = false; - foreach (AttributeData attributeData in memberInfo.GetAttributes()) { INamedTypeSymbol? attributeType = attributeData.AttributeClass; @@ -1215,14 +1202,9 @@ namespace System.Text.Json.SourceGeneration continue; } - if (!foundDesignTimeCustomConverter && _knownSymbols.JsonConverterAttributeType.IsAssignableFrom(attributeType)) + if (converterType is null && _knownSymbols.JsonConverterAttributeType.IsAssignableFrom(attributeType)) { - foundDesignTimeCustomConverter = true; - converterInstantiationLogic = GetConverterInstantiationLogic( - memberCLRType, - attributeData, - forType: false, - ref hasFactoryConverter); + converterType = GetConverterTypeFromAttribute(attributeData); } else if (attributeType.ContainingAssembly.Name == SystemTextJsonNamespace) { @@ -1372,39 +1354,20 @@ namespace System.Text.Json.SourceGeneration private static bool PropertyAccessorCanBeReferenced(MethodInfo? accessor) => accessor != null && (accessor.IsPublic || accessor.IsAssembly); - private string? GetConverterInstantiationLogic( - ITypeSymbol type, AttributeData attributeData, - bool forType, // whether for a type or a property - ref bool hasFactoryConverter) + private TypeRef? GetConverterTypeFromAttribute(AttributeData attributeData) { Debug.Assert(_knownSymbols.JsonConverterAttributeType.IsAssignableFrom(attributeData.AttributeClass)); - var converterType = (INamedTypeSymbol?)attributeData.ConstructorArguments[0].Value; - if (converterType == null || !converterType.Constructors.Any(c => c.Parameters.Length == 0) || converterType.IsNestedPrivate()) + if (converterType == null || + !_knownSymbols.JsonConverterType.IsAssignableFrom(converterType) || + !converterType.Constructors.Any(c => c.Parameters.Length == 0) || + converterType.IsNestedPrivate()) { return null; } - if (converterType.GetCompatibleGenericBaseType(_knownSymbols.JsonConverterOfTType) != null) - { - return $"new {converterType.GetFullyQualifiedName()}()"; - } - else if (_knownSymbols.JsonConverterFactoryType.IsAssignableFrom(converterType)) - { - hasFactoryConverter = true; - - if (forType) - { - return $"{Emitter.GetConverterFromFactoryMethodName}({OptionsLocalVariableName}, typeof({type.GetFullyQualifiedName()}), new {converterType.GetFullyQualifiedName()}())"; - } - else - { - return $"{Emitter.GetConverterFromFactoryMethodName}<{type.GetFullyQualifiedName()}>({OptionsLocalVariableName}, new {converterType.GetFullyQualifiedName()}())"; - } - } - - return null; + return new TypeRef(converterType); } private static string DetermineRuntimePropName(string clrPropName, string? jsonPropName, JsonKnownNamingPolicy namingPolicy) diff --git a/src/libraries/System.Text.Json/gen/Model/PropertyGenerationSpec.cs b/src/libraries/System.Text.Json/gen/Model/PropertyGenerationSpec.cs index 18dc18c..7ce484d 100644 --- a/src/libraries/System.Text.Json/gen/Model/PropertyGenerationSpec.cs +++ b/src/libraries/System.Text.Json/gen/Model/PropertyGenerationSpec.cs @@ -124,20 +124,18 @@ namespace System.Text.Json.SourceGeneration public required bool IsExtensionData { get; init; } /// - /// Generation specification for the property's type. + /// Gets a reference to the property type. /// public required TypeRef PropertyType { get; init; } /// - /// Compilable name of the property's declaring type. + /// Gets a reference to the declaring type of the property. /// - public required string DeclaringTypeRef { get; init; } + public required TypeRef DeclaringType { get; init; } /// - /// Source code to instantiate design-time specified custom converter. + /// Design-time specified custom converter type. /// - public required string? ConverterInstantiationLogic { get; init; } - - public required bool HasFactoryConverter { get; init; } + public required TypeRef? ConverterType { get; init; } } } diff --git a/src/libraries/System.Text.Json/gen/Model/PropertyInitializerGenerationSpec.cs b/src/libraries/System.Text.Json/gen/Model/PropertyInitializerGenerationSpec.cs index bc78505..9fc68a1 100644 --- a/src/libraries/System.Text.Json/gen/Model/PropertyInitializerGenerationSpec.cs +++ b/src/libraries/System.Text.Json/gen/Model/PropertyInitializerGenerationSpec.cs @@ -22,7 +22,9 @@ namespace System.Text.Json.SourceGeneration /// public sealed record PropertyInitializerGenerationSpec { - public required PropertyGenerationSpec Property { get; init; } + public required string Name { get; init; } + + public required TypeRef ParameterType { get; init; } public required int ParameterIndex { get; init; } diff --git a/src/libraries/System.Text.Json/gen/Model/TypeGenerationSpec.cs b/src/libraries/System.Text.Json/gen/Model/TypeGenerationSpec.cs index db63b3d..f001446 100644 --- a/src/libraries/System.Text.Json/gen/Model/TypeGenerationSpec.cs +++ b/src/libraries/System.Text.Json/gen/Model/TypeGenerationSpec.cs @@ -77,15 +77,11 @@ namespace System.Text.Json.SourceGeneration /// Supports deserialization of extension data dictionaries typed as I[ReadOnly]Dictionary<string, object/JsonElement>. /// Specifies a concrete type to instantiate, which would be Dictionary<string, object/JsonElement>. /// - public required string? RuntimeTypeRef { get; init; } + public required TypeRef? RuntimeTypeRef { get; init; } public required TypeRef? ExtensionDataPropertyType { get; init; } - public required string? ConverterInstantiationLogic { get; init; } - - // Only generate certain helper methods if necessary. - public required bool HasPropertyFactoryConverters { get; init; } - public required bool HasTypeFactoryConverter { get; init; } + public required TypeRef? ConverterType { get; init; } public required string? ImmutableCollectionFactoryMethod { get; init; } } diff --git a/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.targets b/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.targets index 10731dc..9874f29 100644 --- a/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.targets +++ b/src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.targets @@ -10,6 +10,7 @@ false true cs + true @@ -50,6 +51,7 @@ + -- 2.7.4