File: JsonSourceGenerator.Parser.cs
Web Access
Project: src\src\libraries\System.Text.Json\gen\System.Text.Json.SourceGeneration.Roslyn4.4.csproj (System.Text.Json.SourceGeneration)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using SourceGenerators;
 
namespace System.Text.Json.SourceGeneration
{
    public sealed partial class JsonSourceGenerator
    {
        // The source generator requires NRT and init-only property support.
        private const LanguageVersion MinimumSupportedLanguageVersion = LanguageVersion.CSharp9;
 
        private sealed class Parser
        {
            private const string SystemTextJsonNamespace = "System.Text.Json";
            private const string JsonExtensionDataAttributeFullName = "System.Text.Json.Serialization.JsonExtensionDataAttribute";
            private const string JsonIgnoreAttributeFullName = "System.Text.Json.Serialization.JsonIgnoreAttribute";
            private const string JsonIgnoreConditionFullName = "System.Text.Json.Serialization.JsonIgnoreCondition";
            private const string JsonIncludeAttributeFullName = "System.Text.Json.Serialization.JsonIncludeAttribute";
            private const string JsonNumberHandlingAttributeFullName = "System.Text.Json.Serialization.JsonNumberHandlingAttribute";
            private const string JsonObjectCreationHandlingAttributeFullName = "System.Text.Json.Serialization.JsonObjectCreationHandlingAttribute";
            private const string JsonPropertyNameAttributeFullName = "System.Text.Json.Serialization.JsonPropertyNameAttribute";
            private const string JsonPropertyOrderAttributeFullName = "System.Text.Json.Serialization.JsonPropertyOrderAttribute";
            private const string JsonRequiredAttributeFullName = "System.Text.Json.Serialization.JsonRequiredAttribute";
 
            internal const string JsonSerializableAttributeFullName = "System.Text.Json.Serialization.JsonSerializableAttribute";
 
            private readonly KnownTypeSymbols _knownSymbols;
            private readonly bool _compilationContainsCoreJsonTypes;
 
            // Keeps track of generated context type names
            private readonly HashSet<(string ContextName, string TypeName)> _generatedContextAndTypeNames = new();
 
            private readonly HashSet<ITypeSymbol> _builtInSupportTypes;
            private readonly Queue<TypeToGenerate> _typesToGenerate = new();
#pragma warning disable RS1024 // Compare symbols correctly https://github.com/dotnet/roslyn-analyzers/issues/5804
            private readonly Dictionary<ITypeSymbol, TypeGenerationSpec> _generatedTypes = new(SymbolEqualityComparer.Default);
#pragma warning restore
 
            public List<Diagnostic> Diagnostics { get; } = new();
            private Location? _contextClassLocation;
 
            public void ReportDiagnostic(DiagnosticDescriptor descriptor, Location? location, params object?[]? messageArgs)
            {
                Debug.Assert(_contextClassLocation != null);
 
                if (location is null || !_knownSymbols.Compilation.ContainsLocation(location))
                {
                    // If location is null or is a location outside of the current compilation, fall back to the location of the context class.
                    location = _contextClassLocation;
                }
 
                Diagnostics.Add(Diagnostic.Create(descriptor, location, messageArgs));
            }
 
            public Parser(KnownTypeSymbols knownSymbols)
            {
                _knownSymbols = knownSymbols;
                _compilationContainsCoreJsonTypes =
                    knownSymbols.JsonSerializerContextType != null &&
                    knownSymbols.JsonSerializableAttributeType != null &&
                    knownSymbols.JsonSourceGenerationOptionsAttributeType != null &&
                    knownSymbols.JsonConverterType != null;
 
                _builtInSupportTypes = (knownSymbols.BuiltInSupportTypes ??= CreateBuiltInSupportTypeSet(knownSymbols));
            }
 
            public ContextGenerationSpec? ParseContextGenerationSpec(ClassDeclarationSyntax contextClassDeclaration, SemanticModel semanticModel, CancellationToken cancellationToken)
            {
                if (!_compilationContainsCoreJsonTypes)
                {
                    return null;
                }
 
                Debug.Assert(_knownSymbols.JsonSerializerContextType != null);
 
                // Ensure context-scoped metadata caches are empty.
                Debug.Assert(_typesToGenerate.Count == 0);
                Debug.Assert(_generatedTypes.Count == 0);
                Debug.Assert(_contextClassLocation is null);
 
                INamedTypeSymbol? contextTypeSymbol = semanticModel.GetDeclaredSymbol(contextClassDeclaration, cancellationToken);
                Debug.Assert(contextTypeSymbol != null);
 
                _contextClassLocation = contextTypeSymbol.GetLocation();
                Debug.Assert(_contextClassLocation is not null);
 
                if (!_knownSymbols.JsonSerializerContextType.IsAssignableFrom(contextTypeSymbol))
                {
                    ReportDiagnostic(DiagnosticDescriptors.JsonSerializableAttributeOnNonContextType, _contextClassLocation, contextTypeSymbol.ToDisplayString());
                    return null;
                }
 
                // When a context class is split across multiple partial declarations with
                // [JsonSerializable] attributes on different partials, we only want to
                // generate code once (from the canonical partial) to avoid duplicate hintNames.
                if (!IsCanonicalPartialDeclaration(contextTypeSymbol, contextClassDeclaration))
                {
                    _contextClassLocation = null;
                    return null;
                }
 
                ParseJsonSerializerContextAttributes(contextTypeSymbol,
                    out List<TypeToGenerate>? rootSerializableTypes,
                    out SourceGenerationOptionsSpec? options);
 
                if (rootSerializableTypes is null)
                {
                    // No types were annotated with JsonSerializableAttribute.
                    // Can only be reached if a [JsonSerializable(null)] declaration has been made.
                    // Do not emit a diagnostic since a NRT warning will also be emitted.
                    return null;
                }
 
                Debug.Assert(rootSerializableTypes.Count > 0);
 
                LanguageVersion? langVersion = _knownSymbols.Compilation.GetLanguageVersion();
                if (langVersion is null or < MinimumSupportedLanguageVersion)
                {
                    // Unsupported lang version should be the first (and only) diagnostic emitted by the generator.
                    ReportDiagnostic(DiagnosticDescriptors.JsonUnsupportedLanguageVersion, _contextClassLocation, langVersion?.ToDisplayString(), MinimumSupportedLanguageVersion.ToDisplayString());
                    return null;
                }
 
                if (!TryGetNestedTypeDeclarations(contextClassDeclaration, semanticModel, cancellationToken, out List<string>? classDeclarationList))
                {
                    // Class or one of its containing types is not partial so we can't add to it.
                    ReportDiagnostic(DiagnosticDescriptors.ContextClassesMustBePartial, _contextClassLocation, contextTypeSymbol.Name);
                    return null;
                }
 
                // Enqueue attribute data for spec generation
                foreach (TypeToGenerate rootSerializableType in rootSerializableTypes)
                {
                    _typesToGenerate.Enqueue(rootSerializableType);
                }
 
                // Walk the transitive type graph generating specs for every encountered type.
                while (_typesToGenerate.Count > 0)
                {
                    cancellationToken.ThrowIfCancellationRequested();
                    TypeToGenerate typeToGenerate = _typesToGenerate.Dequeue();
                    if (!_generatedTypes.ContainsKey(typeToGenerate.Type))
                    {
                        TypeGenerationSpec spec = ParseTypeGenerationSpec(typeToGenerate, contextTypeSymbol, options);
                        _generatedTypes.Add(typeToGenerate.Type, spec);
                    }
                }
 
                Debug.Assert(_generatedTypes.Count > 0);
 
                ContextGenerationSpec contextGenSpec = new()
                {
                    ContextType = new(contextTypeSymbol),
                    GeneratedTypes = _generatedTypes.Values.OrderBy(t => t.TypeRef.FullyQualifiedName).ToImmutableEquatableArray(),
                    Namespace = contextTypeSymbol.ContainingNamespace is { IsGlobalNamespace: false } ns ? ns.ToDisplayString() : null,
                    ContextClassDeclarations = classDeclarationList.ToImmutableEquatableArray(),
                    GeneratedOptionsSpec = options,
                };
 
                // Clear the caches of generated metadata between the processing of context classes.
                _generatedTypes.Clear();
                _typesToGenerate.Clear();
                _contextClassLocation = null;
                return contextGenSpec;
            }
 
            private static bool TryGetNestedTypeDeclarations(ClassDeclarationSyntax contextClassSyntax, SemanticModel semanticModel, CancellationToken cancellationToken, [NotNullWhen(true)] out List<string>? typeDeclarations)
            {
                typeDeclarations = null;
 
                for (TypeDeclarationSyntax? currentType = contextClassSyntax; currentType != null; currentType = currentType.Parent as TypeDeclarationSyntax)
                {
                    StringBuilder stringBuilder = new();
                    bool isPartialType = false;
 
                    foreach (SyntaxToken modifier in currentType.Modifiers)
                    {
                        stringBuilder.Append(modifier.Text);
                        stringBuilder.Append(' ');
                        isPartialType |= modifier.IsKind(SyntaxKind.PartialKeyword);
                    }
 
                    if (!isPartialType)
                    {
                        typeDeclarations = null;
                        return false;
                    }
 
                    stringBuilder.Append(currentType.GetTypeKindKeyword());
                    stringBuilder.Append(' ');
 
                    INamedTypeSymbol? typeSymbol = semanticModel.GetDeclaredSymbol(currentType, cancellationToken);
                    Debug.Assert(typeSymbol != null);
 
                    string typeName = typeSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
                    stringBuilder.Append(typeName);
 
                    (typeDeclarations ??= new()).Add(stringBuilder.ToString());
                }
 
                Debug.Assert(typeDeclarations?.Count > 0);
                return true;
            }
 
            private TypeRef EnqueueType(ITypeSymbol type, JsonSourceGenerationMode? generationMode)
            {
                // Trim compile-time erased metadata such as tuple labels and NRT annotations.
                type = _knownSymbols.Compilation.EraseCompileTimeMetadata(type);
 
                if (_generatedTypes.TryGetValue(type, out TypeGenerationSpec? spec))
                {
                    return spec.TypeRef;
                }
 
                _typesToGenerate.Enqueue(new TypeToGenerate
                {
                    Type = type,
                    Mode = generationMode,
                    TypeInfoPropertyName = null,
                    Location = type.GetLocation(),
                    AttributeLocation = null,
                });
 
                return new TypeRef(type);
            }
 
            private void ParseJsonSerializerContextAttributes(
                INamedTypeSymbol contextClassSymbol,
                out List<TypeToGenerate>? rootSerializableTypes,
                out SourceGenerationOptionsSpec? options)
            {
                Debug.Assert(_knownSymbols.JsonSerializableAttributeType != null);
                Debug.Assert(_knownSymbols.JsonSourceGenerationOptionsAttributeType != null);
 
                rootSerializableTypes = null;
                options = null;
 
                foreach (AttributeData attributeData in contextClassSymbol.GetAttributes())
                {
                    INamedTypeSymbol? attributeClass = attributeData.AttributeClass;
 
                    if (SymbolEqualityComparer.Default.Equals(attributeClass, _knownSymbols.JsonSerializableAttributeType))
                    {
                        TypeToGenerate? typeToGenerate = ParseJsonSerializableAttribute(attributeData);
                        if (typeToGenerate is null)
                        {
                            continue;
                        }
 
                        (rootSerializableTypes ??= new()).Add(typeToGenerate.Value);
                    }
                    else if (SymbolEqualityComparer.Default.Equals(attributeClass, _knownSymbols.JsonSourceGenerationOptionsAttributeType))
                    {
                        options = ParseJsonSourceGenerationOptionsAttribute(contextClassSymbol, attributeData);
                    }
                }
            }
 
            /// <summary>
            /// Determines if the given class declaration is the canonical partial declaration
            /// for the context type. When a context class is split across multiple partial
            /// declarations with [JsonSerializable] attributes on different partials, we only
            /// want to generate code once (from the canonical partial) to avoid duplicate hintNames.
            /// The canonical partial is determined by picking the first syntax tree alphabetically
            /// by file path among all trees that have at least one [JsonSerializable] attribute.
            /// If file paths are empty or identical, comparison falls back to ordinal string order
            /// which provides deterministic behavior. If no attributes are found (edge case that
            /// shouldn't occur since this method is called from a context triggered by the attribute),
            /// the current partial is treated as canonical.
            /// </summary>
            private bool IsCanonicalPartialDeclaration(INamedTypeSymbol contextTypeSymbol, ClassDeclarationSyntax contextClassDeclaration)
            {
                Debug.Assert(_knownSymbols.JsonSerializableAttributeType != null);
 
                // Collect all distinct syntax trees that have [JsonSerializable] attributes for this type
                SyntaxTree? canonicalTree = null;
 
                foreach (AttributeData attributeData in contextTypeSymbol.GetAttributes())
                {
                    if (!SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, _knownSymbols.JsonSerializableAttributeType))
                    {
                        continue;
                    }
 
                    SyntaxTree? attributeTree = attributeData.ApplicationSyntaxReference?.SyntaxTree;
                    if (attributeTree is null)
                    {
                        continue;
                    }
 
                    // Pick the first tree alphabetically by file path.
                    // Empty file paths compare as less than non-empty paths with ordinal comparison.
                    if (canonicalTree is null ||
                        string.Compare(attributeTree.FilePath, canonicalTree.FilePath, StringComparison.Ordinal) < 0)
                    {
                        canonicalTree = attributeTree;
                    }
                }
 
                // This partial is canonical if its syntax tree is the canonical tree.
                // If canonicalTree is null (no attributes found), treat current partial as canonical.
                // This is a fallback that shouldn't normally occur since this method is called
                // from a context triggered by ForAttributeWithMetadataName for JsonSerializableAttribute.
                return canonicalTree is null || canonicalTree == contextClassDeclaration.SyntaxTree;
            }
 
            private SourceGenerationOptionsSpec ParseJsonSourceGenerationOptionsAttribute(INamedTypeSymbol contextType, AttributeData attributeData)
            {
                JsonSourceGenerationMode? generationMode = null;
                List<TypeRef>? converters = null;
                JsonSerializerDefaults? defaults = null;
                bool? allowOutOfOrderMetadataProperties = null;
                bool? allowTrailingCommas = null;
                int? defaultBufferSize = null;
                JsonIgnoreCondition? defaultIgnoreCondition = null;
                JsonKnownNamingPolicy? dictionaryKeyPolicy = null;
                bool? respectNullableAnnotations = null;
                bool? ignoreReadOnlyFields = null;
                bool? respectRequiredConstructorParameters = null;
                bool? ignoreReadOnlyProperties = null;
                bool? includeFields = null;
                int? maxDepth = null;
                string? newLine = null;
                JsonNumberHandling? numberHandling = null;
                JsonObjectCreationHandling? preferredObjectCreationHandling = null;
                bool? propertyNameCaseInsensitive = null;
                JsonKnownNamingPolicy? propertyNamingPolicy = null;
                JsonCommentHandling? readCommentHandling = null;
                JsonKnownReferenceHandler? referenceHandler = null;
                JsonUnknownTypeHandling? unknownTypeHandling = null;
                JsonUnmappedMemberHandling? unmappedMemberHandling = null;
                bool? useStringEnumConverter = null;
                bool? writeIndented = null;
                char? indentCharacter = null;
                int? indentSize = null;
                bool? allowDuplicateProperties = null;
 
                if (attributeData.ConstructorArguments.Length > 0)
                {
                    Debug.Assert(attributeData.ConstructorArguments.Length == 1 & attributeData.ConstructorArguments[0].Type?.Name is nameof(JsonSerializerDefaults));
                    defaults = (JsonSerializerDefaults)attributeData.ConstructorArguments[0].Value!;
                }
 
                foreach (KeyValuePair<string, TypedConstant> namedArg in attributeData.NamedArguments)
                {
                    switch (namedArg.Key)
                    {
                        case nameof(JsonSourceGenerationOptionsAttribute.AllowOutOfOrderMetadataProperties):
                            allowOutOfOrderMetadataProperties = (bool)namedArg.Value.Value!;
                            break;
 
                        case nameof(JsonSourceGenerationOptionsAttribute.AllowTrailingCommas):
                            allowTrailingCommas = (bool)namedArg.Value.Value!;
                            break;
 
                        case nameof(JsonSourceGenerationOptionsAttribute.Converters):
                            converters = new List<TypeRef>();
                            foreach (TypedConstant element in namedArg.Value.Values)
                            {
                                var converterType = (ITypeSymbol?)element.Value;
                                TypeRef? typeRef = GetConverterTypeFromAttribute(contextType, converterType, contextType, attributeData);
                                if (typeRef != null)
                                {
                                    converters.Add(typeRef);
                                }
                            }
 
                            break;
 
                        case nameof(JsonSourceGenerationOptionsAttribute.DefaultBufferSize):
                            defaultBufferSize = (int)namedArg.Value.Value!;
                            break;
 
                        case nameof(JsonSourceGenerationOptionsAttribute.DefaultIgnoreCondition):
                            defaultIgnoreCondition = (JsonIgnoreCondition)namedArg.Value.Value!;
                            break;
 
                        case nameof(JsonSourceGenerationOptionsAttribute.DictionaryKeyPolicy):
                            dictionaryKeyPolicy = (JsonKnownNamingPolicy)namedArg.Value.Value!;
                            break;
 
                        case nameof(JsonSourceGenerationOptionsAttribute.RespectNullableAnnotations):
                            respectNullableAnnotations = (bool)namedArg.Value.Value!;
                            break;
 
                        case nameof(JsonSourceGenerationOptionsAttribute.RespectRequiredConstructorParameters):
                            respectRequiredConstructorParameters = (bool)namedArg.Value.Value!;
                            break;
 
                        case nameof(JsonSourceGenerationOptionsAttribute.IgnoreReadOnlyFields):
                            ignoreReadOnlyFields = (bool)namedArg.Value.Value!;
                            break;
 
                        case nameof(JsonSourceGenerationOptionsAttribute.IgnoreReadOnlyProperties):
                            ignoreReadOnlyProperties = (bool)namedArg.Value.Value!;
                            break;
 
                        case nameof(JsonSourceGenerationOptionsAttribute.IncludeFields):
                            includeFields = (bool)namedArg.Value.Value!;
                            break;
 
                        case nameof(JsonSourceGenerationOptionsAttribute.MaxDepth):
                            maxDepth = (int)namedArg.Value.Value!;
                            break;
 
                        case nameof(JsonSourceGenerationOptionsAttribute.NewLine):
                            newLine = (string)namedArg.Value.Value!;
                            break;
 
                        case nameof(JsonSourceGenerationOptionsAttribute.NumberHandling):
                            numberHandling = (JsonNumberHandling)namedArg.Value.Value!;
                            break;
 
                        case nameof(JsonSourceGenerationOptionsAttribute.PreferredObjectCreationHandling):
                            preferredObjectCreationHandling = (JsonObjectCreationHandling)namedArg.Value.Value!;
                            break;
 
                        case nameof(JsonSourceGenerationOptionsAttribute.PropertyNameCaseInsensitive):
                            propertyNameCaseInsensitive = (bool)namedArg.Value.Value!;
                            break;
 
                        case nameof(JsonSourceGenerationOptionsAttribute.PropertyNamingPolicy):
                            propertyNamingPolicy = (JsonKnownNamingPolicy)namedArg.Value.Value!;
                            break;
 
                        case nameof(JsonSourceGenerationOptionsAttribute.ReadCommentHandling):
                            readCommentHandling = (JsonCommentHandling)namedArg.Value.Value!;
                            break;
 
                        case nameof(JsonSourceGenerationOptionsAttribute.ReferenceHandler):
                            referenceHandler = (JsonKnownReferenceHandler)namedArg.Value.Value!;
                            break;
 
                        case nameof(JsonSourceGenerationOptionsAttribute.UnknownTypeHandling):
                            unknownTypeHandling = (JsonUnknownTypeHandling)namedArg.Value.Value!;
                            break;
 
                        case nameof(JsonSourceGenerationOptionsAttribute.UnmappedMemberHandling):
                            unmappedMemberHandling = (JsonUnmappedMemberHandling)namedArg.Value.Value!;
                            break;
 
                        case nameof(JsonSourceGenerationOptionsAttribute.UseStringEnumConverter):
                            useStringEnumConverter = (bool)namedArg.Value.Value!;
                            break;
 
                        case nameof(JsonSourceGenerationOptionsAttribute.WriteIndented):
                            writeIndented = (bool)namedArg.Value.Value!;
                            break;
 
                        case nameof(JsonSourceGenerationOptionsAttribute.IndentCharacter):
                            indentCharacter = (char)namedArg.Value.Value!;
                            break;
 
                        case nameof(JsonSourceGenerationOptionsAttribute.IndentSize):
                            indentSize = (int)namedArg.Value.Value!;
                            break;
 
                        case nameof(JsonSourceGenerationOptionsAttribute.GenerationMode):
                            generationMode = (JsonSourceGenerationMode)namedArg.Value.Value!;
                            break;
 
                        case nameof(JsonSourceGenerationOptionsAttribute.AllowDuplicateProperties):
                            allowDuplicateProperties = (bool)namedArg.Value.Value!;
                            break;
 
                        default:
                            throw new InvalidOperationException();
                    }
                }
 
                return new SourceGenerationOptionsSpec
                {
                    GenerationMode = generationMode,
                    Defaults = defaults,
                    AllowOutOfOrderMetadataProperties = allowOutOfOrderMetadataProperties,
                    AllowTrailingCommas = allowTrailingCommas,
                    DefaultBufferSize = defaultBufferSize,
                    Converters = converters?.ToImmutableEquatableArray(),
                    DefaultIgnoreCondition = defaultIgnoreCondition,
                    DictionaryKeyPolicy = dictionaryKeyPolicy,
                    RespectNullableAnnotations = respectNullableAnnotations,
                    RespectRequiredConstructorParameters = respectRequiredConstructorParameters,
                    IgnoreReadOnlyFields = ignoreReadOnlyFields,
                    IgnoreReadOnlyProperties = ignoreReadOnlyProperties,
                    IncludeFields = includeFields,
                    MaxDepth = maxDepth,
                    NewLine = newLine,
                    NumberHandling = numberHandling,
                    PreferredObjectCreationHandling = preferredObjectCreationHandling,
                    PropertyNameCaseInsensitive = propertyNameCaseInsensitive,
                    PropertyNamingPolicy = propertyNamingPolicy,
                    ReadCommentHandling = readCommentHandling,
                    ReferenceHandler = referenceHandler,
                    UnknownTypeHandling = unknownTypeHandling,
                    UnmappedMemberHandling = unmappedMemberHandling,
                    UseStringEnumConverter = useStringEnumConverter,
                    WriteIndented = writeIndented,
                    IndentCharacter = indentCharacter,
                    IndentSize = indentSize,
                    AllowDuplicateProperties = allowDuplicateProperties,
                };
            }
 
            private TypeToGenerate? ParseJsonSerializableAttribute(AttributeData attributeData)
            {
                Debug.Assert(attributeData.ConstructorArguments.Length == 1);
                var typeSymbol = (ITypeSymbol?)attributeData.ConstructorArguments[0].Value;
                if (typeSymbol is null)
                {
                    return null;
                }
 
                JsonSourceGenerationMode? generationMode = null;
                string? typeInfoPropertyName = null;
 
                foreach (KeyValuePair<string, TypedConstant> namedArg in attributeData.NamedArguments)
                {
                    switch (namedArg.Key)
                    {
                        case nameof(JsonSerializableAttribute.TypeInfoPropertyName):
                            typeInfoPropertyName = (string)namedArg.Value.Value!;
                            break;
                        case nameof(JsonSerializableAttribute.GenerationMode):
                            generationMode = (JsonSourceGenerationMode)namedArg.Value.Value!;
                            break;
                        default:
                            throw new InvalidOperationException();
                    }
                }
 
                Location? location = typeSymbol.GetLocation();
                Location? attributeLocation = attributeData.GetLocation();
                Debug.Assert(attributeLocation != null);
 
                if (location is null || !_knownSymbols.Compilation.ContainsLocation(location))
                {
                    // For symbols located outside the compilation, fall back to attribute location instead.
                    location = attributeLocation;
                }
 
                return new TypeToGenerate
                {
                    Type = _knownSymbols.Compilation.EraseCompileTimeMetadata(typeSymbol),
                    Mode = generationMode,
                    TypeInfoPropertyName = typeInfoPropertyName,
                    Location = location,
                    AttributeLocation = attributeLocation,
                };
            }
 
            private TypeGenerationSpec ParseTypeGenerationSpec(in TypeToGenerate typeToGenerate, INamedTypeSymbol contextType, SourceGenerationOptionsSpec? options)
            {
                Debug.Assert(IsSymbolAccessibleWithin(typeToGenerate.Type, within: contextType), "should not generate metadata for inaccessible types.");
 
                ITypeSymbol type = typeToGenerate.Type;
 
                ClassType classType;
                JsonPrimitiveTypeKind? primitiveTypeKind = GetPrimitiveTypeKind(type);
                TypeRef? collectionKeyType = null;
                TypeRef? collectionValueType = null;
                TypeRef? nullableUnderlyingType = null;
                bool hasExtensionDataProperty = false;
                TypeRef? runtimeTypeRef = null;
                List<PropertyGenerationSpec>? propertySpecs = null;
                List<int>? fastPathPropertyIndices = null;
                ObjectConstructionStrategy constructionStrategy = default;
                bool constructorSetsRequiredMembers = false;
                ParameterGenerationSpec[]? ctorParamSpecs = null;
                List<PropertyInitializerGenerationSpec>? propertyInitializerSpecs = null;
                CollectionType collectionType = CollectionType.NotApplicable;
                string? immutableCollectionFactoryTypeFullName = null;
                bool implementsIJsonOnSerialized = false;
                bool implementsIJsonOnSerializing = false;
 
                ProcessTypeCustomAttributes(typeToGenerate, contextType,
                    out JsonNumberHandling? numberHandling,
                    out JsonUnmappedMemberHandling? unmappedMemberHandling,
                    out JsonObjectCreationHandling? preferredPropertyObjectCreationHandling,
                    out JsonKnownNamingPolicy? typeNamingPolicy,
                    out JsonIgnoreCondition? typeIgnoreCondition,
                    out bool foundJsonConverterAttribute,
                    out TypeRef? customConverterType,
                    out bool isPolymorphic);
 
                if (type is { IsRefLikeType: true } or INamedTypeSymbol { IsUnboundGenericType: true } or IErrorTypeSymbol)
                {
                    classType = ClassType.TypeUnsupportedBySourceGen;
                }
                else if (foundJsonConverterAttribute)
                {
                    classType = customConverterType != null
                        ? ClassType.TypeWithDesignTimeProvidedCustomConverter
                        : ClassType.TypeUnsupportedBySourceGen;
                }
                else if (IsBuiltInSupportType(type))
                {
                    classType = ClassType.BuiltInSupportType;
                }
                else if (IsUnsupportedType(type))
                {
                    classType = ClassType.UnsupportedType;
                }
                else if (type.IsNullableValueType(out ITypeSymbol? underlyingType))
                {
                    classType = ClassType.Nullable;
                    nullableUnderlyingType = EnqueueType(underlyingType, typeToGenerate.Mode);
                }
                else if (type.TypeKind is TypeKind.Enum)
                {
                    if (options?.UseStringEnumConverter == true)
                    {
                        Debug.Assert(_knownSymbols.JsonStringEnumConverterOfTType != null);
                        INamedTypeSymbol converterSymbol = _knownSymbols.JsonStringEnumConverterOfTType.Construct(type);
 
                        customConverterType = new TypeRef(converterSymbol);
                        classType = ClassType.TypeWithDesignTimeProvidedCustomConverter;
                    }
                    else
                    {
                        classType = ClassType.Enum;
                    }
                }
                else if (TryResolveCollectionType(type,
                    out ITypeSymbol? valueType,
                    out ITypeSymbol? keyType,
                    out collectionType,
                    out immutableCollectionFactoryTypeFullName,
                    out bool needsRuntimeType))
                {
                    if (!IsSymbolAccessibleWithin(valueType, within: contextType) ||
                        (keyType != null && !IsSymbolAccessibleWithin(keyType, within: contextType)))
                    {
                        classType = ClassType.UnsupportedType;
                        immutableCollectionFactoryTypeFullName = null;
                        collectionType = default;
                    }
                    else if (valueType.IsRefLikeType || keyType?.IsRefLikeType is true)
                    {
                        classType = ClassType.TypeUnsupportedBySourceGen;
                        immutableCollectionFactoryTypeFullName = null;
                        collectionType = default;
                    }
                    else
                    {
                        if (type.CanUseDefaultConstructorForDeserialization(out IMethodSymbol? defaultCtor))
                        {
                            constructionStrategy = ObjectConstructionStrategy.ParameterlessConstructor;
                            constructorSetsRequiredMembers = defaultCtor?.ContainsAttribute(_knownSymbols.SetsRequiredMembersAttributeType) == true;
                        }
 
                        classType = keyType != null ? ClassType.Dictionary : ClassType.Enumerable;
                        collectionValueType = EnqueueType(valueType, typeToGenerate.Mode);
 
                        if (keyType != null)
                        {
                            collectionKeyType = EnqueueType(keyType, typeToGenerate.Mode);
 
                            if (needsRuntimeType)
                            {
                                runtimeTypeRef = GetDictionaryTypeRef(keyType, valueType);
                            }
                        }
                    }
                }
                else
                {
                    bool useDefaultCtorInAnnotatedStructs = type.GetCompatibleGenericBaseType(_knownSymbols.KeyValuePair) is null;
 
                    if (!TryGetDeserializationConstructor(type, useDefaultCtorInAnnotatedStructs, out IMethodSymbol? constructor))
                    {
                        ReportDiagnostic(DiagnosticDescriptors.MultipleJsonConstructorAttribute, typeToGenerate.Location, type.ToDisplayString());
                    }
                    else if (constructor != null && !IsSymbolAccessibleWithin(constructor, within: contextType))
                    {
                        ReportDiagnostic(DiagnosticDescriptors.JsonConstructorInaccessible, typeToGenerate.Location, type.ToDisplayString());
                        constructor = null;
                    }
 
                    classType = ClassType.Object;
 
                    implementsIJsonOnSerializing = _knownSymbols.IJsonOnSerializingType.IsAssignableFrom(type);
                    implementsIJsonOnSerialized = _knownSymbols.IJsonOnSerializedType.IsAssignableFrom(type);
 
                    ctorParamSpecs = ParseConstructorParameters(typeToGenerate, constructor, out constructionStrategy, out constructorSetsRequiredMembers);
                    propertySpecs = ParsePropertyGenerationSpecs(contextType, typeToGenerate, typeIgnoreCondition, options, typeNamingPolicy, out hasExtensionDataProperty, out fastPathPropertyIndices);
                    propertyInitializerSpecs = ParsePropertyInitializers(ctorParamSpecs, propertySpecs, constructorSetsRequiredMembers, ref constructionStrategy);
                }
 
                var typeRef = new TypeRef(type);
                string typeInfoPropertyName = typeToGenerate.TypeInfoPropertyName ?? GetTypeInfoPropertyName(type);
 
                if (classType is ClassType.TypeUnsupportedBySourceGen)
                {
                    ReportDiagnostic(DiagnosticDescriptors.TypeNotSupported, typeToGenerate.AttributeLocation ?? typeToGenerate.Location, type.ToDisplayString());
                }
 
                if (!_generatedContextAndTypeNames.Add((contextType.Name, typeInfoPropertyName)))
                {
                    // The context name/property name combination will result in a conflict in generated types.
                    // Workaround for https://github.com/dotnet/roslyn/issues/54185 by keeping track of the file names we've used.
                    ReportDiagnostic(DiagnosticDescriptors.DuplicateTypeName, typeToGenerate.AttributeLocation ?? _contextClassLocation, typeInfoPropertyName);
                    classType = ClassType.TypeUnsupportedBySourceGen;
                }
 
                return new TypeGenerationSpec
                {
                    TypeRef = typeRef,
                    TypeInfoPropertyName = typeInfoPropertyName,
                    GenerationMode = typeToGenerate.Mode ?? options?.GenerationMode ?? JsonSourceGenerationMode.Default,
                    ClassType = classType,
                    PrimitiveTypeKind = primitiveTypeKind,
                    IsPolymorphic = isPolymorphic,
                    NumberHandling = numberHandling,
                    UnmappedMemberHandling = unmappedMemberHandling,
                    PreferredPropertyObjectCreationHandling = preferredPropertyObjectCreationHandling,
                    PropertyGenSpecs = propertySpecs?.ToImmutableEquatableArray() ?? ImmutableEquatableArray<PropertyGenerationSpec>.Empty,
                    FastPathPropertyIndices = fastPathPropertyIndices?.ToImmutableEquatableArray(),
                    PropertyInitializerSpecs = propertyInitializerSpecs?.ToImmutableEquatableArray() ?? ImmutableEquatableArray<PropertyInitializerGenerationSpec>.Empty,
                    CtorParamGenSpecs = ctorParamSpecs?.ToImmutableEquatableArray() ?? ImmutableEquatableArray<ParameterGenerationSpec>.Empty,
                    CollectionType = collectionType,
                    CollectionKeyType = collectionKeyType,
                    CollectionValueType = collectionValueType,
                    ConstructionStrategy = constructionStrategy,
                    ConstructorSetsRequiredParameters = constructorSetsRequiredMembers,
                    NullableUnderlyingType = nullableUnderlyingType,
                    RuntimeTypeRef = runtimeTypeRef,
                    IsValueTuple = type.IsTupleType,
                    HasExtensionDataPropertyType = hasExtensionDataProperty,
                    ConverterType = customConverterType,
                    ImplementsIJsonOnSerialized = implementsIJsonOnSerialized,
                    ImplementsIJsonOnSerializing = implementsIJsonOnSerializing,
                    ImmutableCollectionFactoryMethod = DetermineImmutableCollectionFactoryMethod(immutableCollectionFactoryTypeFullName),
                };
            }
 
            private void ProcessTypeCustomAttributes(
                in TypeToGenerate typeToGenerate,
                INamedTypeSymbol contextType,
                out JsonNumberHandling? numberHandling,
                out JsonUnmappedMemberHandling? unmappedMemberHandling,
                out JsonObjectCreationHandling? objectCreationHandling,
                out JsonKnownNamingPolicy? namingPolicy,
                out JsonIgnoreCondition? typeIgnoreCondition,
                out bool foundJsonConverterAttribute,
                out TypeRef? customConverterType,
                out bool isPolymorphic)
            {
                numberHandling = null;
                unmappedMemberHandling = null;
                objectCreationHandling = null;
                namingPolicy = null;
                typeIgnoreCondition = null;
                customConverterType = null;
                foundJsonConverterAttribute = false;
                isPolymorphic = false;
 
                foreach (AttributeData attributeData in typeToGenerate.Type.GetAttributes())
                {
                    INamedTypeSymbol? attributeType = attributeData.AttributeClass;
 
                    if (SymbolEqualityComparer.Default.Equals(attributeType, _knownSymbols.JsonNumberHandlingAttributeType))
                    {
                        numberHandling = (JsonNumberHandling)attributeData.ConstructorArguments[0].Value!;
                        continue;
                    }
                    else if (SymbolEqualityComparer.Default.Equals(attributeType, _knownSymbols.JsonUnmappedMemberHandlingAttributeType))
                    {
                        unmappedMemberHandling = (JsonUnmappedMemberHandling)attributeData.ConstructorArguments[0].Value!;
                        continue;
                    }
                    else if (SymbolEqualityComparer.Default.Equals(attributeType, _knownSymbols.JsonObjectCreationHandlingAttributeType))
                    {
                        objectCreationHandling = (JsonObjectCreationHandling)attributeData.ConstructorArguments[0].Value!;
                        continue;
                    }
                    else if (_knownSymbols.JsonNamingPolicyAttributeType?.IsAssignableFrom(attributeType) == true)
                    {
                        if (attributeData.ConstructorArguments.Length == 1 &&
                            attributeData.ConstructorArguments[0].Value is int knownPolicyValue)
                        {
                            namingPolicy = (JsonKnownNamingPolicy)knownPolicyValue;
                        }
                        else
                        {
                            // The attribute uses a custom naming policy that can't be resolved at compile time.
                            // Use Unspecified to prevent the global naming policy from incorrectly applying.
                            namingPolicy = JsonKnownNamingPolicy.Unspecified;
                        }
 
                        continue;
                    }
                    else if (!foundJsonConverterAttribute && _knownSymbols.JsonConverterAttributeType.IsAssignableFrom(attributeType))
                    {
                        customConverterType = GetConverterTypeFromJsonConverterAttribute(contextType, typeToGenerate.Type, attributeData);
                        foundJsonConverterAttribute = true;
                    }
 
                    if (SymbolEqualityComparer.Default.Equals(attributeType, _knownSymbols.JsonIgnoreAttributeType))
                    {
                        ImmutableArray<KeyValuePair<string, TypedConstant>> namedArgs = attributeData.NamedArguments;
 
                        if (namedArgs.Length == 0)
                        {
                            typeIgnoreCondition = JsonIgnoreCondition.Always;
                        }
                        else if (namedArgs.Length == 1 &&
                            namedArgs[0].Value.Type?.ToDisplayString() == JsonIgnoreConditionFullName)
                        {
                            typeIgnoreCondition = (JsonIgnoreCondition)namedArgs[0].Value.Value!;
                        }
 
                        if (typeIgnoreCondition == JsonIgnoreCondition.Always)
                        {
                            ReportDiagnostic(DiagnosticDescriptors.JsonIgnoreConditionAlwaysInvalidOnType, typeToGenerate.Location, typeToGenerate.Type.ToDisplayString());
                            typeIgnoreCondition = null; // Reset so it doesn't affect properties
                        }
                    }
 
                    if (SymbolEqualityComparer.Default.Equals(attributeType, _knownSymbols.JsonDerivedTypeAttributeType))
                    {
                        Debug.Assert(attributeData.ConstructorArguments.Length > 0);
                        var derivedType = (ITypeSymbol)attributeData.ConstructorArguments[0].Value!;
                        EnqueueType(derivedType, typeToGenerate.Mode);
 
                        if (!isPolymorphic && typeToGenerate.Mode == JsonSourceGenerationMode.Serialization)
                        {
                            ReportDiagnostic(DiagnosticDescriptors.PolymorphismNotSupported, typeToGenerate.Location, typeToGenerate.Type.ToDisplayString());
                        }
 
                        isPolymorphic = true;
                    }
                }
            }
 
            private bool TryResolveCollectionType(
                ITypeSymbol type,
                [NotNullWhen(true)] out ITypeSymbol? valueType,
                out ITypeSymbol? keyType,
                out CollectionType collectionType,
                out string? immutableCollectionFactoryTypeFullName,
                out bool needsRuntimeType)
            {
                INamedTypeSymbol? actualTypeToConvert;
                valueType = null;
                keyType = null;
                collectionType = default;
                immutableCollectionFactoryTypeFullName = null;
                needsRuntimeType = false;
 
                if (SymbolEqualityComparer.Default.Equals(type.OriginalDefinition, _knownSymbols.MemoryType))
                {
                    Debug.Assert(!SymbolEqualityComparer.Default.Equals(type, _knownSymbols.MemoryByteType));
                    valueType = ((INamedTypeSymbol)type).TypeArguments[0];
                    collectionType = CollectionType.MemoryOfT;
                    return true;
                }
 
                if (SymbolEqualityComparer.Default.Equals(type.OriginalDefinition, _knownSymbols.ReadOnlyMemoryType))
                {
                    Debug.Assert(!SymbolEqualityComparer.Default.Equals(type, _knownSymbols.ReadOnlyMemoryByteType));
                    valueType = ((INamedTypeSymbol)type).TypeArguments[0];
                    collectionType = CollectionType.ReadOnlyMemoryOfT;
                    return true;
                }
 
                // IAsyncEnumerable<T> takes precedence over IEnumerable.
                if (type.GetCompatibleGenericBaseType(_knownSymbols.IAsyncEnumerableOfTType) is INamedTypeSymbol iAsyncEnumerableType)
                {
                    valueType = iAsyncEnumerableType.TypeArguments[0];
                    collectionType = CollectionType.IAsyncEnumerableOfT;
                    return true;
                }
 
                if (!_knownSymbols.IEnumerableType.IsAssignableFrom(type))
                {
                    // Type is not IEnumerable and therefore not a collection type
                    return false;
                }
 
                if (type is IArrayTypeSymbol arraySymbol)
                {
                    Debug.Assert(arraySymbol.Rank == 1, "multi-dimensional arrays should have been handled earlier.");
                    collectionType = CollectionType.Array;
                    valueType = arraySymbol.ElementType;
                }
                else if ((actualTypeToConvert = type.GetCompatibleGenericBaseType(_knownSymbols.KeyedCollectionType)) != null)
                {
                    collectionType = CollectionType.ICollectionOfT;
                    valueType = actualTypeToConvert.TypeArguments[1];
                }
                else if ((actualTypeToConvert = type.GetCompatibleGenericBaseType(_knownSymbols.ListOfTType)) != null)
                {
                    collectionType = CollectionType.List;
                    valueType = actualTypeToConvert.TypeArguments[0];
                }
                else if ((actualTypeToConvert = type.GetCompatibleGenericBaseType(_knownSymbols.DictionaryOfTKeyTValueType)) != null)
                {
                    collectionType = CollectionType.Dictionary;
                    keyType = actualTypeToConvert.TypeArguments[0];
                    valueType = actualTypeToConvert.TypeArguments[1];
                }
                else if (_knownSymbols.IsImmutableDictionaryType(type, out immutableCollectionFactoryTypeFullName))
                {
                    collectionType = CollectionType.ImmutableDictionary;
                    ImmutableArray<ITypeSymbol> genericArgs = ((INamedTypeSymbol)type).TypeArguments;
                    keyType = genericArgs[0];
                    valueType = genericArgs[1];
                }
                else if ((actualTypeToConvert = type.GetCompatibleGenericBaseType(_knownSymbols.IDictionaryOfTKeyTValueType)) != null)
                {
                    collectionType = CollectionType.IDictionaryOfTKeyTValue;
                    keyType = actualTypeToConvert.TypeArguments[0];
                    valueType = actualTypeToConvert.TypeArguments[1];
                    needsRuntimeType = SymbolEqualityComparer.Default.Equals(type, actualTypeToConvert);
                }
                else if ((actualTypeToConvert = type.GetCompatibleGenericBaseType(_knownSymbols.IReadonlyDictionaryOfTKeyTValueType)) != null)
                {
                    collectionType = CollectionType.IReadOnlyDictionary;
                    keyType = actualTypeToConvert.TypeArguments[0];
                    valueType = actualTypeToConvert.TypeArguments[1];
                    needsRuntimeType = SymbolEqualityComparer.Default.Equals(type, actualTypeToConvert);
                }
                else if (_knownSymbols.IsImmutableEnumerableType(type, out immutableCollectionFactoryTypeFullName))
                {
                    collectionType = CollectionType.ImmutableEnumerable;
                    valueType = ((INamedTypeSymbol)type).TypeArguments[0];
                }
                else if ((actualTypeToConvert = type.GetCompatibleGenericBaseType(_knownSymbols.IListOfTType)) != null)
                {
                    collectionType = CollectionType.IListOfT;
                    valueType = actualTypeToConvert.TypeArguments[0];
                }
                else if ((actualTypeToConvert = type.GetCompatibleGenericBaseType(_knownSymbols.ISetOfTType)) != null)
                {
                    collectionType = CollectionType.ISet;
                    valueType = actualTypeToConvert.TypeArguments[0];
                }
                else if ((actualTypeToConvert = type.GetCompatibleGenericBaseType(_knownSymbols.IReadOnlySetOfTType)) != null)
                {
                    collectionType = CollectionType.IReadOnlySetOfT;
                    valueType = actualTypeToConvert.TypeArguments[0];
                }
                else if ((actualTypeToConvert = type.GetCompatibleGenericBaseType(_knownSymbols.ICollectionOfTType)) != null)
                {
                    collectionType = CollectionType.ICollectionOfT;
                    valueType = actualTypeToConvert.TypeArguments[0];
                }
                else if ((actualTypeToConvert = type.GetCompatibleGenericBaseType(_knownSymbols.StackOfTType)) != null)
                {
                    collectionType = CollectionType.StackOfT;
                    valueType = actualTypeToConvert.TypeArguments[0];
                }
                else if ((actualTypeToConvert = type.GetCompatibleGenericBaseType(_knownSymbols.QueueOfTType)) != null)
                {
                    collectionType = CollectionType.QueueOfT;
                    valueType = actualTypeToConvert.TypeArguments[0];
                }
                else if ((actualTypeToConvert = type.GetCompatibleGenericBaseType(_knownSymbols.ConcurrentStackType)) != null)
                {
                    collectionType = CollectionType.ConcurrentStack;
                    valueType = actualTypeToConvert.TypeArguments[0];
                }
                else if ((actualTypeToConvert = type.GetCompatibleGenericBaseType(_knownSymbols.ConcurrentQueueType)) != null)
                {
                    collectionType = CollectionType.ConcurrentQueue;
                    valueType = actualTypeToConvert.TypeArguments[0];
                }
                else if ((actualTypeToConvert = type.GetCompatibleGenericBaseType(_knownSymbols.IEnumerableOfTType)) != null)
                {
                    collectionType = CollectionType.IEnumerableOfT;
                    valueType = actualTypeToConvert.TypeArguments[0];
                }
                else if (_knownSymbols.IDictionaryType.IsAssignableFrom(type))
                {
                    collectionType = CollectionType.IDictionary;
                    keyType = _knownSymbols.StringType;
                    valueType = _knownSymbols.ObjectType;
                    needsRuntimeType = SymbolEqualityComparer.Default.Equals(type, actualTypeToConvert);
                }
                else if (_knownSymbols.IListType.IsAssignableFrom(type))
                {
                    collectionType = CollectionType.IList;
                    valueType = _knownSymbols.ObjectType;
                }
                else if (_knownSymbols.StackType.IsAssignableFrom(type))
                {
                    collectionType = CollectionType.Stack;
                    valueType = _knownSymbols.ObjectType;
                }
                else if (_knownSymbols.QueueType.IsAssignableFrom(type))
                {
                    collectionType = CollectionType.Queue;
                    valueType = _knownSymbols.ObjectType;
                }
                else
                {
                    collectionType = CollectionType.IEnumerable;
                    valueType = _knownSymbols.ObjectType;
                }
 
                return true;
            }
 
            private TypeRef? GetDictionaryTypeRef(ITypeSymbol keyType, ITypeSymbol valueType)
            {
                INamedTypeSymbol? dictionary = _knownSymbols.DictionaryOfTKeyTValueType?.Construct(keyType, valueType);
                return dictionary is null ? null : new TypeRef(dictionary);
            }
 
            private List<PropertyGenerationSpec> ParsePropertyGenerationSpecs(
                INamedTypeSymbol contextType,
                in TypeToGenerate typeToGenerate,
                JsonIgnoreCondition? typeIgnoreCondition,
                SourceGenerationOptionsSpec? options,
                JsonKnownNamingPolicy? typeNamingPolicy,
                out bool hasExtensionDataProperty,
                out List<int>? fastPathPropertyIndices)
            {
                Location? typeLocation = typeToGenerate.Location;
                List<PropertyGenerationSpec> properties = new();
                PropertyHierarchyResolutionState state = new(options);
                hasExtensionDataProperty = false;
 
                // Walk the type hierarchy starting from the current type up to the base type(s)
                foreach (INamedTypeSymbol currentType in typeToGenerate.Type.GetSortedTypeHierarchy())
                {
                    var declaringTypeRef = new TypeRef(currentType);
                    ImmutableArray<ISymbol> members = currentType.GetMembers();
 
                    foreach (IPropertySymbol propertyInfo in members.OfType<IPropertySymbol>())
                    {
                        // Skip if:
                        if (
                            // property is static or an indexer
                            propertyInfo.IsStatic || propertyInfo.Parameters.Length > 0 ||
                            // It is overridden by a derived property
                            PropertyIsOverriddenAndIgnored(propertyInfo, state.IgnoredMembers))
                        {
                            continue;
                        }
 
                        AddMember(
                            declaringTypeRef,
                            memberType: propertyInfo.Type,
                            memberInfo: propertyInfo,
                            typeToGenerate.Mode,
                            ref state,
                            ref hasExtensionDataProperty);
                    }
 
                    foreach (IFieldSymbol fieldInfo in members.OfType<IFieldSymbol>())
                    {
                        // Skip if :
                        if (
                            // it is a static field, constant
                            fieldInfo.IsStatic || fieldInfo.IsConst ||
                            // it is a compiler-generated backing field
                            fieldInfo.AssociatedSymbol != null ||
                            // symbol represents an explicitly named tuple element
                            fieldInfo.IsExplicitlyNamedTupleElement)
                        {
                            continue;
                        }
 
                        AddMember(
                            declaringTypeRef,
                            memberType: fieldInfo.Type,
                            memberInfo: fieldInfo,
                            typeToGenerate.Mode,
                            ref state,
                            ref hasExtensionDataProperty);
                    }
                }
 
                if (state.IsPropertyOrderSpecified)
                {
                    state.Properties.StableSortByKey(i => properties[i].Order);
                }
 
                fastPathPropertyIndices = state.HasInvalidConfigurationForFastPath ? null : state.Properties;
                return properties;
 
                void AddMember(
                    TypeRef declaringTypeRef,
                    ITypeSymbol memberType,
                    ISymbol memberInfo,
                    JsonSourceGenerationMode? generationMode,
                    ref PropertyHierarchyResolutionState state,
                    ref bool hasExtensionDataProperty)
                {
                    PropertyGenerationSpec? propertySpec = ParsePropertyGenerationSpec(
                        contextType,
                        declaringTypeRef,
                        typeLocation,
                        memberType,
                        memberInfo,
                        typeIgnoreCondition,
                        ref hasExtensionDataProperty,
                        generationMode,
                        options,
                        typeNamingPolicy);
 
                    if (propertySpec is null)
                    {
                        // ignored invalid property
                        return;
                    }
 
                    AddPropertyWithConflictResolution(propertySpec, memberInfo, propertyIndex: properties.Count, ref state);
                    properties.Add(propertySpec);
 
                    // In case of JsonInclude fail if either:
                    // 1. the getter is not accessible by the source generator or
                    // 2. neither getter or setter methods are public.
                    if (propertySpec.HasJsonInclude && (!propertySpec.CanUseGetter || !propertySpec.IsPublic))
                    {
                        state.HasInvalidConfigurationForFastPath = true;
                    }
                }
 
                bool PropertyIsOverriddenAndIgnored(IPropertySymbol property, Dictionary<string, ISymbol>? ignoredMembers)
                {
                    return property.IsVirtual() &&
                        ignoredMembers?.TryGetValue(property.Name, out ISymbol? ignoredMember) == true &&
                        ignoredMember.IsVirtual() &&
                        SymbolEqualityComparer.Default.Equals(property.Type, ignoredMember.GetMemberType());
                }
            }
 
            private ref struct PropertyHierarchyResolutionState(SourceGenerationOptionsSpec? options)
            {
                public readonly List<int> Properties = new();
                public Dictionary<string, (PropertyGenerationSpec, ISymbol, int index)> AddedProperties = new(options?.PropertyNameCaseInsensitive == true ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal);
                public Dictionary<string, ISymbol>? IgnoredMembers;
                public bool IsPropertyOrderSpecified;
                public bool HasInvalidConfigurationForFastPath;
            }
 
            /// <summary>
            /// Runs the property name conflict resolution algorithm for the fast-path serializer
            /// using the compile-time specified naming policies. Note that the metadata serializer
            /// does not consult these results since its own run-time configuration can be different.
            /// Instead the same algorithm is executed at run-time within the JsonTypeInfo infrastructure.
            /// </summary>
            private static void AddPropertyWithConflictResolution(
                PropertyGenerationSpec propertySpec,
                ISymbol memberInfo,
                int propertyIndex,
                ref PropertyHierarchyResolutionState state)
            {
                // Algorithm should be kept in sync with the runtime equivalent in JsonTypeInfo.cs
                string memberName = propertySpec.MemberName;
 
                if (state.AddedProperties.TryAdd(propertySpec.EffectiveJsonPropertyName, (propertySpec, memberInfo, state.Properties.Count)))
                {
                    state.Properties.Add(propertyIndex);
                    state.IsPropertyOrderSpecified |= propertySpec.Order != 0;
                }
                else
                {
                    // The JsonPropertyNameAttribute or naming policy resulted in a collision.
                    (PropertyGenerationSpec other, ISymbol otherSymbol, int index) = state.AddedProperties[propertySpec.EffectiveJsonPropertyName];
 
                    if (other.DefaultIgnoreCondition == JsonIgnoreCondition.Always)
                    {
                        // Overwrite previously cached property since it has [JsonIgnore].
                        state.AddedProperties[propertySpec.EffectiveJsonPropertyName] = (propertySpec, memberInfo, index);
                        state.Properties[index] = propertyIndex;
                        state.IsPropertyOrderSpecified |= propertySpec.Order != 0;
                    }
                    else
                    {
                        bool ignoreCurrentProperty =
                            // Does the current property have `JsonIgnoreAttribute`?
                            propertySpec.DefaultIgnoreCondition == JsonIgnoreCondition.Always ||
                            // Is the current property hidden by the previously cached property
                            // (with `new` keyword, or by overriding)?
                            memberInfo.IsOverriddenOrShadowedBy(otherSymbol) ||
                            // Was a property with the same CLR name ignored? That property hid the current property,
                            // thus, if it was ignored, the current property should be ignored too.
                            (state.IgnoredMembers?.TryGetValue(memberName, out ISymbol? ignored) == true && memberInfo.IsOverriddenOrShadowedBy(ignored));
 
                        if (!ignoreCurrentProperty)
                        {
                            // Property name conflict cannot be reconciled
                            // Signal the fast-path generator to emit a throwing stub method.
                            state.HasInvalidConfigurationForFastPath = true;
                        }
                    }
                }
 
                if (propertySpec.DefaultIgnoreCondition == JsonIgnoreCondition.Always)
                {
                    (state.IgnoredMembers ??= new())[memberName] = memberInfo;
                }
            }
 
            private bool IsValidDataExtensionPropertyType(ITypeSymbol type)
            {
                if (SymbolEqualityComparer.Default.Equals(type, _knownSymbols.JsonObjectType))
                {
                    return true;
                }
 
                INamedTypeSymbol? actualDictionaryType = type.GetCompatibleGenericBaseType(_knownSymbols.IDictionaryOfTKeyTValueType);
                if (actualDictionaryType != null)
                {
                    if (SymbolEqualityComparer.Default.Equals(actualDictionaryType.TypeArguments[0], _knownSymbols.StringType) &&
                        (SymbolEqualityComparer.Default.Equals(actualDictionaryType.TypeArguments[1], _knownSymbols.ObjectType) ||
                         SymbolEqualityComparer.Default.Equals(actualDictionaryType.TypeArguments[1], _knownSymbols.JsonElementType)))
                    {
                        return true;
                    }
                }
 
                // Also check for IReadOnlyDictionary<string, object> or IReadOnlyDictionary<string, JsonElement>
                // but only if Dictionary can be assigned to it (to exclude ImmutableDictionary and similar types)
                INamedTypeSymbol? actualReadOnlyDictionaryType = type.GetCompatibleGenericBaseType(_knownSymbols.IReadonlyDictionaryOfTKeyTValueType);
                if (actualReadOnlyDictionaryType != null)
                {
                    if (SymbolEqualityComparer.Default.Equals(actualReadOnlyDictionaryType.TypeArguments[0], _knownSymbols.StringType) &&
                        (SymbolEqualityComparer.Default.Equals(actualReadOnlyDictionaryType.TypeArguments[1], _knownSymbols.ObjectType) ||
                         SymbolEqualityComparer.Default.Equals(actualReadOnlyDictionaryType.TypeArguments[1], _knownSymbols.JsonElementType)))
                    {
                        // Check if Dictionary can be assigned to this type
                        INamedTypeSymbol? dictionaryType = SymbolEqualityComparer.Default.Equals(actualReadOnlyDictionaryType.TypeArguments[1], _knownSymbols.ObjectType)
                            ? _knownSymbols.StringObjectDictionaryType
                            : _knownSymbols.StringJsonElementDictionaryType;
 
                        if (dictionaryType != null)
                        {
                            Conversion conversion = _knownSymbols.Compilation.ClassifyConversion(dictionaryType, type);
                            return conversion.IsImplicit || conversion.IsIdentity;
                        }
                    }
                }
 
                return false;
            }
 
            private PropertyGenerationSpec? ParsePropertyGenerationSpec(
                INamedTypeSymbol contextType,
                TypeRef declaringType,
                Location? typeLocation,
                ITypeSymbol memberType,
                ISymbol memberInfo,
                JsonIgnoreCondition? typeIgnoreCondition,
                ref bool typeHasExtensionDataProperty,
                JsonSourceGenerationMode? generationMode,
                SourceGenerationOptionsSpec? options,
                JsonKnownNamingPolicy? typeNamingPolicy)
            {
                Debug.Assert(memberInfo is IFieldSymbol or IPropertySymbol);
 
                ProcessMemberCustomAttributes(
                    contextType,
                    memberInfo,
                    memberType,
                    out bool hasJsonInclude,
                    out string? jsonPropertyName,
                    out JsonIgnoreCondition? ignoreCondition,
                    out JsonNumberHandling? numberHandling,
                    out JsonObjectCreationHandling? objectCreationHandling,
                    out JsonKnownNamingPolicy? memberNamingPolicy,
                    out TypeRef? converterType,
                    out int order,
                    out bool isExtensionData,
                    out bool hasJsonRequiredAttribute);
 
                // Fall back to the type-level [JsonIgnore] if no member-level attribute is specified.
                // WhenWritingNull is invalid for non-nullable value types; treat as Never in that case
                // so that the type-level annotation still overrides the global JSO DefaultIgnoreCondition.
                if (ignoreCondition is null && typeIgnoreCondition is not null)
                {
                    ignoreCondition = typeIgnoreCondition == JsonIgnoreCondition.WhenWritingNull && !memberType.IsNullableType()
                        ? JsonIgnoreCondition.Never
                        : typeIgnoreCondition;
                }
 
                ProcessMember(
                    contextType,
                    memberInfo,
                    hasJsonInclude,
                    out bool isReadOnly,
                    out bool isAccessible,
                    out bool isRequired,
                    out bool canUseGetter,
                    out bool canUseSetter,
                    out bool hasJsonIncludeButIsInaccessible,
                    out bool setterIsInitOnly,
                    out bool isGetterNonNullable,
                    out bool isSetterNonNullable);
 
                if (hasJsonIncludeButIsInaccessible)
                {
                    ReportDiagnostic(DiagnosticDescriptors.InaccessibleJsonIncludePropertiesNotSupported, memberInfo.GetLocation(), declaringType.Name, memberInfo.Name);
                }
 
                if (isExtensionData)
                {
                    if (typeHasExtensionDataProperty)
                    {
                        ReportDiagnostic(DiagnosticDescriptors.MultipleJsonExtensionDataAttribute, typeLocation, declaringType.Name);
                    }
 
                    if (!IsValidDataExtensionPropertyType(memberType))
                    {
                        ReportDiagnostic(DiagnosticDescriptors.DataExtensionPropertyInvalid, memberInfo.GetLocation(), declaringType.Name, memberInfo.Name);
                    }
 
                    typeHasExtensionDataProperty = true;
                }
 
                if ((!canUseGetter && !canUseSetter && !hasJsonIncludeButIsInaccessible) ||
                    !IsSymbolAccessibleWithin(memberType, within: contextType))
                {
                    // Skip the member if either of the two conditions hold
                    // 1. Member has no accessible getters or setters (but is not marked with JsonIncludeAttribute since we need to throw a runtime exception) OR
                    // 2. The member type is not accessible within the generated context.
                    return null;
                }
 
                if (memberType.IsRefLikeType)
                {
                    // Skip all ref-like members and emit a diagnostic unless the property is being explicitly ignored.
                    if (ignoreCondition is not JsonIgnoreCondition.Always)
                    {
                        ReportDiagnostic(DiagnosticDescriptors.TypeContainsRefLikeMember, memberInfo.GetLocation(), declaringType.Name, memberInfo.Name);
                    }
 
                    return null;
                }
 
                string effectiveJsonPropertyName = DetermineEffectiveJsonPropertyName(memberInfo.Name, jsonPropertyName, memberNamingPolicy, typeNamingPolicy, options);
                string propertyNameFieldName = DeterminePropertyNameFieldName(effectiveJsonPropertyName);
 
                // For metadata-based serialization, embed the effective property name when a naming policy
                // attribute is present so that the runtime DeterminePropertyName uses it instead of
                // falling back to the global PropertyNamingPolicy. JsonPropertyNameAttribute values are
                // already captured in jsonPropertyName and take highest precedence.
                string? metadataJsonPropertyName = jsonPropertyName
                    ?? (memberNamingPolicy is not null || typeNamingPolicy is not null
                        ? effectiveJsonPropertyName
                        : null);
 
                // Enqueue the property type for generation, unless the member is ignored.
                TypeRef propertyTypeRef = ignoreCondition != JsonIgnoreCondition.Always
                    ? EnqueueType(memberType, generationMode)
                    : new TypeRef(memberType);
 
                return new PropertyGenerationSpec
                {
                    NameSpecifiedInSourceCode = memberInfo.MemberNameNeedsAtSign() ? "@" + memberInfo.Name : memberInfo.Name,
                    MemberName = memberInfo.Name,
                    IsProperty = memberInfo is IPropertySymbol,
                    IsPublic = isAccessible,
                    IsVirtual = memberInfo.IsVirtual(),
                    JsonPropertyName = metadataJsonPropertyName,
                    EffectiveJsonPropertyName = effectiveJsonPropertyName,
                    PropertyNameFieldName = propertyNameFieldName,
                    IsReadOnly = isReadOnly,
                    IsRequired = isRequired,
                    HasJsonRequiredAttribute = hasJsonRequiredAttribute,
                    IsInitOnlySetter = setterIsInitOnly,
                    CanUseGetter = canUseGetter,
                    CanUseSetter = canUseSetter,
                    DefaultIgnoreCondition = ignoreCondition,
                    NumberHandling = numberHandling,
                    ObjectCreationHandling = objectCreationHandling,
                    Order = order,
                    HasJsonInclude = hasJsonInclude,
                    IsExtensionData = isExtensionData,
                    PropertyType = propertyTypeRef,
                    DeclaringType = declaringType,
                    ConverterType = converterType,
                    IsGetterNonNullableAnnotation = isGetterNonNullable,
                    IsSetterNonNullableAnnotation = isSetterNonNullable,
                };
            }
 
            private void ProcessMemberCustomAttributes(
                INamedTypeSymbol contextType,
                ISymbol memberInfo,
                ITypeSymbol memberType,
                out bool hasJsonInclude,
                out string? jsonPropertyName,
                out JsonIgnoreCondition? ignoreCondition,
                out JsonNumberHandling? numberHandling,
                out JsonObjectCreationHandling? objectCreationHandling,
                out JsonKnownNamingPolicy? memberNamingPolicy,
                out TypeRef? converterType,
                out int order,
                out bool isExtensionData,
                out bool hasJsonRequiredAttribute)
            {
                Debug.Assert(memberInfo is IFieldSymbol or IPropertySymbol);
 
                hasJsonInclude = false;
                jsonPropertyName = null;
                ignoreCondition = default;
                numberHandling = default;
                objectCreationHandling = default;
                memberNamingPolicy = default;
                converterType = null;
                order = 0;
                isExtensionData = false;
                hasJsonRequiredAttribute = false;
 
                foreach (AttributeData attributeData in memberInfo.GetAttributes())
                {
                    INamedTypeSymbol? attributeType = attributeData.AttributeClass;
 
                    if (attributeType is null)
                    {
                        continue;
                    }
 
                    if (converterType is null && _knownSymbols.JsonConverterAttributeType.IsAssignableFrom(attributeType))
                    {
                        converterType = GetConverterTypeFromJsonConverterAttribute(contextType, memberInfo, attributeData, memberType);
                    }
                    else if (memberNamingPolicy is null && _knownSymbols.JsonNamingPolicyAttributeType?.IsAssignableFrom(attributeType) == true)
                    {
                        if (attributeData.ConstructorArguments.Length == 1 &&
                            attributeData.ConstructorArguments[0].Value is int knownPolicyValue)
                        {
                            memberNamingPolicy = (JsonKnownNamingPolicy)knownPolicyValue;
                        }
                        else
                        {
                            // The attribute uses a custom naming policy that can't be resolved at compile time.
                            // Use Unspecified to prevent the global naming policy from incorrectly applying.
                            memberNamingPolicy = JsonKnownNamingPolicy.Unspecified;
                        }
                    }
                    else if (attributeType.ContainingAssembly.Name == SystemTextJsonNamespace)
                    {
                        switch (attributeType.ToDisplayString())
                        {
                            case JsonIgnoreAttributeFullName:
                            {
                                ImmutableArray<KeyValuePair<string, TypedConstant>> namedArgs = attributeData.NamedArguments;
 
                                if (namedArgs.Length == 0)
                                {
                                    ignoreCondition = JsonIgnoreCondition.Always;
                                }
                                else if (namedArgs.Length == 1 &&
                                    namedArgs[0].Value.Type?.ToDisplayString() == JsonIgnoreConditionFullName)
                                {
                                    ignoreCondition = (JsonIgnoreCondition)namedArgs[0].Value.Value!;
                                }
                                break;
                            }
                            case JsonIncludeAttributeFullName:
                            {
                                hasJsonInclude = true;
                                break;
                            }
                            case JsonNumberHandlingAttributeFullName:
                            {
                                ImmutableArray<TypedConstant> ctorArgs = attributeData.ConstructorArguments;
                                numberHandling = (JsonNumberHandling)ctorArgs[0].Value!;
                                break;
                            }
                            case JsonObjectCreationHandlingAttributeFullName:
                            {
                                ImmutableArray<TypedConstant> ctorArgs = attributeData.ConstructorArguments;
                                objectCreationHandling = (JsonObjectCreationHandling)ctorArgs[0].Value!;
                                break;
                            }
                            case JsonPropertyNameAttributeFullName:
                            {
                                ImmutableArray<TypedConstant> ctorArgs = attributeData.ConstructorArguments;
                                jsonPropertyName = (string)ctorArgs[0].Value!;
                                // Null check here is done at runtime within JsonSerializer.
                                break;
                            }
                            case JsonPropertyOrderAttributeFullName:
                            {
                                ImmutableArray<TypedConstant> ctorArgs = attributeData.ConstructorArguments;
                                order = (int)ctorArgs[0].Value!;
                                break;
                            }
                            case JsonExtensionDataAttributeFullName:
                            {
                                isExtensionData = true;
                                break;
                            }
                            case JsonRequiredAttributeFullName:
                            {
                                hasJsonRequiredAttribute = true;
                                break;
                            }
                        }
                    }
                }
            }
 
            private void ProcessMember(
                INamedTypeSymbol contextType,
                ISymbol memberInfo,
                bool hasJsonInclude,
                out bool isReadOnly,
                out bool isAccessible,
                out bool isRequired,
                out bool canUseGetter,
                out bool canUseSetter,
                out bool hasJsonIncludeButIsInaccessible,
                out bool isSetterInitOnly,
                out bool isGetterNonNullable,
                out bool isSetterNonNullable)
            {
                isAccessible = false;
                isReadOnly = false;
                isRequired = false;
                canUseGetter = false;
                canUseSetter = false;
                hasJsonIncludeButIsInaccessible = false;
                isSetterInitOnly = false;
                isGetterNonNullable = false;
                isSetterNonNullable = false;
 
                switch (memberInfo)
                {
                    case IPropertySymbol propertyInfo:
#if ROSLYN4_4_OR_GREATER
                        isRequired = propertyInfo.IsRequired;
#endif
                        if (propertyInfo.GetMethod is { } getMethod)
                        {
                            if (getMethod.DeclaredAccessibility is Accessibility.Public)
                            {
                                isAccessible = true;
                                canUseGetter = true;
                            }
                            else if (IsSymbolAccessibleWithin(getMethod, within: contextType))
                            {
                                isAccessible = true;
                                canUseGetter = hasJsonInclude;
                            }
                            else
                            {
                                hasJsonIncludeButIsInaccessible = hasJsonInclude;
                            }
                        }
 
                        if (propertyInfo.SetMethod is { } setMethod)
                        {
                            isSetterInitOnly = setMethod.IsInitOnly;
 
                            if (setMethod.DeclaredAccessibility is Accessibility.Public)
                            {
                                isAccessible = true;
                                canUseSetter = true;
                            }
                            else if (IsSymbolAccessibleWithin(setMethod, within: contextType))
                            {
                                isAccessible = true;
                                canUseSetter = hasJsonInclude;
                            }
                            else
                            {
                                hasJsonIncludeButIsInaccessible = hasJsonInclude;
                            }
                        }
                        else
                        {
                            isReadOnly = true;
                        }
 
                        propertyInfo.ResolveNullabilityAnnotations(out isGetterNonNullable, out isSetterNonNullable);
                        break;
                    case IFieldSymbol fieldInfo:
                        isReadOnly = fieldInfo.IsReadOnly;
#if ROSLYN4_4_OR_GREATER
                        isRequired = fieldInfo.IsRequired;
#endif
                        if (fieldInfo.DeclaredAccessibility is Accessibility.Public)
                        {
                            isAccessible = true;
                            canUseGetter = true;
                            canUseSetter = !isReadOnly;
                        }
                        else if (IsSymbolAccessibleWithin(fieldInfo, within: contextType))
                        {
                            isAccessible = true;
                            canUseGetter = hasJsonInclude;
                            canUseSetter = hasJsonInclude && !isReadOnly;
                        }
                        else
                        {
                            hasJsonIncludeButIsInaccessible = hasJsonInclude;
                        }
 
                        fieldInfo.ResolveNullabilityAnnotations(out isGetterNonNullable, out isSetterNonNullable);
                        break;
                    default:
                        Debug.Fail("Method given an invalid symbol type.");
                        break;
                }
            }
 
            private ParameterGenerationSpec[]? ParseConstructorParameters(
                in TypeToGenerate typeToGenerate,
                IMethodSymbol? constructor,
                out ObjectConstructionStrategy constructionStrategy,
                out bool constructorSetsRequiredMembers)
            {
                ITypeSymbol type = typeToGenerate.Type;
 
                if ((constructor is null && !type.IsValueType) || type.IsAbstract)
                {
                    constructionStrategy = ObjectConstructionStrategy.NotApplicable;
                    constructorSetsRequiredMembers = false;
                    return null;
                }
 
                ParameterGenerationSpec[] constructorParameters;
                int paramCount = constructor?.Parameters.Length ?? 0;
                constructorSetsRequiredMembers = constructor?.ContainsAttribute(_knownSymbols.SetsRequiredMembersAttributeType) == true;
 
                if (paramCount == 0)
                {
                    constructionStrategy = ObjectConstructionStrategy.ParameterlessConstructor;
                    constructorParameters = [];
                }
                else
                {
                    Debug.Assert(constructor != null);
 
                    constructionStrategy = ObjectConstructionStrategy.ParameterizedConstructor;
                    constructorParameters = new ParameterGenerationSpec[paramCount];
 
                    // Compute ArgsIndex for each parameter.
                    // out parameters don't have entries in the args array.
                    int argsIndex = 0;
                    for (int i = 0; i < paramCount; i++)
                    {
                        IParameterSymbol parameterInfo = constructor.Parameters[i];
 
                        if (parameterInfo.Type.IsRefLikeType)
                        {
                            ReportDiagnostic(DiagnosticDescriptors.TypeContainsRefLikeMember, parameterInfo.GetLocation(), type.Name, parameterInfo.Name);
                            constructionStrategy = ObjectConstructionStrategy.NotApplicable;
                            continue;
                        }
 
                        // Don't enqueue out parameter types for JSON contract generation — they
                        // aren't deserialized and may reference unsupported types (e.g. Task).
                        TypeRef parameterTypeRef = parameterInfo.RefKind == RefKind.Out
                            ? new TypeRef(parameterInfo.Type)
                            : EnqueueType(parameterInfo.Type, typeToGenerate.Mode);
 
                        // out parameters don't receive values from JSON, so they have ArgsIndex = -1.
                        int currentArgsIndex = parameterInfo.RefKind == RefKind.Out ? -1 : argsIndex++;
 
                        constructorParameters[i] = new ParameterGenerationSpec
                        {
                            ParameterType = parameterTypeRef,
                            Name = parameterInfo.Name,
                            HasDefaultValue = parameterInfo.HasExplicitDefaultValue,
                            DefaultValue = parameterInfo.HasExplicitDefaultValue ? parameterInfo.ExplicitDefaultValue : null,
                            ParameterIndex = i,
                            ArgsIndex = currentArgsIndex,
                            IsNullable = parameterInfo.IsNullable(),
                            RefKind = parameterInfo.RefKind,
                        };
                    }
                }
 
                return constructionStrategy is ObjectConstructionStrategy.NotApplicable ? null : constructorParameters;
            }
 
            private List<PropertyInitializerGenerationSpec>? ParsePropertyInitializers(
                ParameterGenerationSpec[]? constructorParameters,
                List<PropertyGenerationSpec>? properties,
                bool constructorSetsRequiredMembers,
                ref ObjectConstructionStrategy constructionStrategy)
            {
                if (constructionStrategy is ObjectConstructionStrategy.NotApplicable || properties is null)
                {
                    return null;
                }
 
                HashSet<string>? memberInitializerNames = null;
                List<PropertyInitializerGenerationSpec>? propertyInitializers = null;
 
                // Count non-out constructor parameters - out params don't have entries in the args array.
                int paramCount = constructorParameters?.Count(p => p.RefKind != RefKind.Out) ?? 0;
 
                // Determine potential init-only or required properties that need to be part of the constructor delegate signature.
                foreach (PropertyGenerationSpec property in properties)
                {
                    if (!property.CanUseSetter)
                    {
                        continue;
                    }
 
                    if (property.DefaultIgnoreCondition == JsonIgnoreCondition.Always && !property.IsRequired)
                    {
                        continue;
                    }
 
                    if ((property.IsRequired && !constructorSetsRequiredMembers) || property.IsInitOnlySetter)
                    {
                        if (!(memberInitializerNames ??= new()).Add(property.MemberName))
                        {
                            // We've already added another member initializer with the same name to our spec list.
                            // Duplicates can occur here because the provided list of properties includes shadowed members.
                            // This is because we generate metadata for *all* members, including shadowed or ignored ones,
                            // since we need to re-run the deduplication algorithm taking run-time configuration into account.
                            // This is a simple deduplication that keeps the first result for each member name --
                            // this should be fine since the properties are listed from most derived to least derived order,
                            // so the second instance of a member name is always shadowed by the first.
                            continue;
                        }
 
                        ParameterGenerationSpec? matchingConstructorParameter = GetMatchingConstructorParameter(property, constructorParameters);
 
                        if (property.IsRequired || matchingConstructorParameter is null)
                        {
                            constructionStrategy = ObjectConstructionStrategy.ParameterizedConstructor;
 
                            var propertyInitializer = new PropertyInitializerGenerationSpec
                            {
                                Name = property.NameSpecifiedInSourceCode,
                                ParameterType = property.PropertyType,
                                MatchesConstructorParameter = matchingConstructorParameter is not null,
                                // Use ArgsIndex for matching ctor params (excludes out params), or paramCount++ for new ones
                                ParameterIndex = matchingConstructorParameter?.ArgsIndex ?? paramCount++,
                                IsNullable = property.PropertyType.CanBeNull && !property.IsSetterNonNullableAnnotation,
                            };
 
                            (propertyInitializers ??= new()).Add(propertyInitializer);
                        }
 
                        static ParameterGenerationSpec? GetMatchingConstructorParameter(PropertyGenerationSpec propSpec, ParameterGenerationSpec[]? paramGenSpecs)
                        {
                            return paramGenSpecs?.FirstOrDefault(MatchesConstructorParameter);
 
                            bool MatchesConstructorParameter(ParameterGenerationSpec paramSpec)
                                // Don't match out parameters - they don't receive values from JSON.
                                => paramSpec.RefKind != RefKind.Out &&
                                   propSpec.MemberName.Equals(paramSpec.Name, StringComparison.OrdinalIgnoreCase);
                        }
                    }
                }
 
                return propertyInitializers;
            }
 
            private TypeRef? GetConverterTypeFromJsonConverterAttribute(INamedTypeSymbol contextType, ISymbol declaringSymbol, AttributeData attributeData, ITypeSymbol? typeToConvert = null)
            {
                Debug.Assert(_knownSymbols.JsonConverterAttributeType.IsAssignableFrom(attributeData.AttributeClass));
 
                if (!SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, _knownSymbols.JsonConverterAttributeType))
                {
                    ReportDiagnostic(DiagnosticDescriptors.DerivedJsonConverterAttributesNotSupported, attributeData.GetLocation(), attributeData.AttributeClass!.ToDisplayString());
                    return null;
                }
 
                Debug.Assert(attributeData.ConstructorArguments.Length == 1 && attributeData.ConstructorArguments[0].Value is null or ITypeSymbol);
                var converterType = (ITypeSymbol?)attributeData.ConstructorArguments[0].Value;
 
                // If typeToConvert is not provided, try to infer it from declaringSymbol
                typeToConvert ??= declaringSymbol as ITypeSymbol;
 
                return GetConverterTypeFromAttribute(contextType, converterType, declaringSymbol, attributeData, typeToConvert);
            }
 
            private TypeRef? GetConverterTypeFromAttribute(INamedTypeSymbol contextType, ITypeSymbol? converterType, ISymbol declaringSymbol, AttributeData attributeData, ITypeSymbol? typeToConvert = null)
            {
                INamedTypeSymbol? namedConverterType = converterType as INamedTypeSymbol;
 
                // Check if this is an unbound generic converter type that needs to be constructed.
                // For open generics, we construct the closed generic type first and then validate.
                if (namedConverterType is { IsUnboundGenericType: true } unboundConverterType &&
                    typeToConvert is INamedTypeSymbol { IsGenericType: true } genericTypeToConvert)
                {
                    // For nested generic types like Container<>.NestedConverter<>, we need to count
                    // all type parameters from the entire type hierarchy, not just the immediate type.
                    int totalTypeParameterCount = GetTotalTypeParameterCount(unboundConverterType);
 
                    if (totalTypeParameterCount == genericTypeToConvert.TypeArguments.Length)
                    {
                        namedConverterType = ConstructNestedGenericType(unboundConverterType, genericTypeToConvert.TypeArguments);
                    }
                }
 
                if (namedConverterType is null ||
                    !_knownSymbols.JsonConverterType.IsAssignableFrom(namedConverterType) ||
                    !namedConverterType.Constructors.Any(c => c.Parameters.Length == 0 && IsSymbolAccessibleWithin(c, within: contextType)))
                {
                    ReportDiagnostic(DiagnosticDescriptors.JsonConverterAttributeInvalidType, attributeData.GetLocation(), converterType?.ToDisplayString() ?? "null", declaringSymbol.ToDisplayString());
                    return null;
                }
 
                if (_knownSymbols.JsonStringEnumConverterType.IsAssignableFrom(namedConverterType))
                {
                    ReportDiagnostic(DiagnosticDescriptors.JsonStringEnumConverterNotSupportedInAot, attributeData.GetLocation(), declaringSymbol.ToDisplayString());
                }
 
                return new TypeRef(namedConverterType);
            }
 
            /// <summary>
            /// Gets the total number of type parameters from an unbound generic type,
            /// including type parameters from containing types for nested generics.
            /// For example, Container&lt;&gt;.NestedConverter&lt;&gt; has a total of 2 type parameters.
            /// </summary>
            private static int GetTotalTypeParameterCount(INamedTypeSymbol unboundType)
            {
                int count = 0;
                INamedTypeSymbol? current = unboundType;
                while (current != null)
                {
                    count += current.TypeParameters.Length;
                    current = current.ContainingType;
                }
                return count;
            }
 
            /// <summary>
            /// Constructs a closed generic type from an unbound generic type (potentially nested),
            /// using the provided type arguments in the order they should be applied.
            /// Returns null if the type cannot be constructed.
            /// </summary>
            private static INamedTypeSymbol? ConstructNestedGenericType(INamedTypeSymbol unboundType, ImmutableArray<ITypeSymbol> typeArguments)
            {
                // Build the chain of containing types from outermost to innermost
                var typeChain = new List<INamedTypeSymbol>();
                INamedTypeSymbol? current = unboundType;
                while (current != null)
                {
                    typeChain.Add(current);
                    current = current.ContainingType;
                }
 
                // Reverse to go from outermost to innermost
                typeChain.Reverse();
 
                // Track which type arguments have been used
                int typeArgIndex = 0;
                INamedTypeSymbol? constructedContainingType = null;
 
                foreach (var type in typeChain)
                {
                    int typeParamCount = type.TypeParameters.Length;
                    INamedTypeSymbol originalDef = type.OriginalDefinition;
 
                    if (typeParamCount > 0)
                    {
                        // Get the type arguments for this level
                        var args = typeArguments.Skip(typeArgIndex).Take(typeParamCount).ToArray();
                        typeArgIndex += typeParamCount;
 
                        // Construct this level
                        if (constructedContainingType == null)
                        {
                            constructedContainingType = originalDef.Construct(args);
                        }
                        else
                        {
                            // Get the nested type from the constructed containing type
                            var nestedTypeDef = constructedContainingType.GetTypeMembers(originalDef.Name, originalDef.Arity).FirstOrDefault();
                            if (nestedTypeDef != null)
                            {
                                constructedContainingType = nestedTypeDef.Construct(args);
                            }
                            else
                            {
                                return null;
                            }
                        }
                    }
                    else
                    {
                        // Non-generic type in the chain
                        if (constructedContainingType == null)
                        {
                            constructedContainingType = originalDef;
                        }
                        else
                        {
                            // Use arity 0 to avoid ambiguity with nested types of the same name but different arity
                            var nestedType = constructedContainingType.GetTypeMembers(originalDef.Name, 0).FirstOrDefault();
                            if (nestedType == null)
                            {
                                return null;
                            }
                            constructedContainingType = nestedType;
                        }
                    }
                }
 
                return constructedContainingType;
            }
 
            private static string DetermineEffectiveJsonPropertyName(
                string propertyName,
                string? jsonPropertyName,
                JsonKnownNamingPolicy? memberNamingPolicy,
                JsonKnownNamingPolicy? typeNamingPolicy,
                SourceGenerationOptionsSpec? options)
            {
                if (jsonPropertyName != null)
                {
                    return jsonPropertyName;
                }
 
                JsonKnownNamingPolicy? effectiveKnownPolicy = memberNamingPolicy
                    ?? typeNamingPolicy
                    ?? options?.GetEffectivePropertyNamingPolicy();
 
                JsonNamingPolicy? instance = effectiveKnownPolicy switch
                {
                    JsonKnownNamingPolicy.CamelCase => JsonNamingPolicy.CamelCase,
                    JsonKnownNamingPolicy.SnakeCaseLower => JsonNamingPolicy.SnakeCaseLower,
                    JsonKnownNamingPolicy.SnakeCaseUpper => JsonNamingPolicy.SnakeCaseUpper,
                    JsonKnownNamingPolicy.KebabCaseLower => JsonNamingPolicy.KebabCaseLower,
                    JsonKnownNamingPolicy.KebabCaseUpper => JsonNamingPolicy.KebabCaseUpper,
                    JsonKnownNamingPolicy.PascalCase => JsonNamingPolicy.PascalCase,
                    _ => null,
                };
 
                return instance?.ConvertName(propertyName) ?? propertyName;
            }
 
            private static string? DetermineImmutableCollectionFactoryMethod(string? immutableCollectionFactoryTypeFullName)
            {
                return immutableCollectionFactoryTypeFullName is not null ? $"global::{immutableCollectionFactoryTypeFullName}.CreateRange" : null;
            }
 
            private static string DeterminePropertyNameFieldName(string effectiveJsonPropertyName)
            {
                const string PropName = "PropName_";
 
                // Use a different prefix to avoid possible collisions with "PropName_" in
                // the rare case there is a C# property in a hex format.
                const string EncodedPropName = "EncodedPropName_";
 
                if (SyntaxFacts.IsValidIdentifier(effectiveJsonPropertyName))
                {
                    return PropName + effectiveJsonPropertyName;
                }
 
                // Encode the string to a byte[] and then convert to hexadecimal.
                // To make the generated code more readable, we could use a different strategy in the future
                // such as including the full class name + the CLR property name when there are duplicates,
                // but that will create unnecessary JsonEncodedText properties.
                byte[] utf8Json = Encoding.UTF8.GetBytes(effectiveJsonPropertyName);
 
                StringBuilder sb = new StringBuilder(
                    EncodedPropName,
                    capacity: EncodedPropName.Length + utf8Json.Length * 2);
 
                for (int i = 0; i < utf8Json.Length; i++)
                {
                    sb.Append(utf8Json[i].ToString("X2")); // X2 is hex format
                }
 
                return sb.ToString();
            }
 
            private JsonPrimitiveTypeKind? GetPrimitiveTypeKind(ITypeSymbol type)
            {
                if (type.IsNumberType())
                {
                    return JsonPrimitiveTypeKind.Number;
                }
 
                if (type.SpecialType is SpecialType.System_Boolean)
                {
                    return JsonPrimitiveTypeKind.Boolean;
                }
 
                if (type.SpecialType is SpecialType.System_Char)
                {
                    return JsonPrimitiveTypeKind.Char;
                }
 
                SymbolEqualityComparer cmp = SymbolEqualityComparer.Default;
 
                if (type.SpecialType is SpecialType.System_String or SpecialType.System_DateTime ||
                     cmp.Equals(type, _knownSymbols.DateTimeOffsetType) || cmp.Equals(type, _knownSymbols.GuidType))
                {
                    return JsonPrimitiveTypeKind.String;
                }
 
                if (cmp.Equals(type, _knownSymbols.ByteArrayType))
                {
                    return JsonPrimitiveTypeKind.ByteArray;
                }
 
                return null;
            }
 
            private static string GetTypeInfoPropertyName(ITypeSymbol type)
            {
                if (type is IArrayTypeSymbol arrayType)
                {
                    int rank = arrayType.Rank;
                    string suffix = rank == 1 ? "Array" : $"Array{rank}D"; // Array, Array2D, Array3D, ...
                    return GetTypeInfoPropertyName(arrayType.ElementType) + suffix;
                }
 
                if (type is not INamedTypeSymbol namedType || !namedType.IsGenericType)
                {
                    return type.Name;
                }
 
                StringBuilder sb = new();
 
                string name = namedType.Name;
 
                sb.Append(name);
 
                if (namedType.GetAllTypeArgumentsInScope() is List<ITypeSymbol> typeArgsInScope)
                {
                    foreach (ITypeSymbol genericArg in typeArgsInScope)
                    {
                        sb.Append(GetTypeInfoPropertyName(genericArg));
                    }
                }
 
                return sb.ToString();
            }
 
            private bool TryGetDeserializationConstructor(
                ITypeSymbol type,
                bool useDefaultCtorInAnnotatedStructs,
                out IMethodSymbol? deserializationCtor)
            {
                IMethodSymbol? ctorWithAttribute = null;
                IMethodSymbol? publicParameterlessCtor = null;
                IMethodSymbol? lonePublicCtor = null;
 
                if (type is not INamedTypeSymbol namedType)
                {
                    deserializationCtor = null;
                    return false;
                }
 
                IMethodSymbol[] publicCtors = namedType.GetExplicitlyDeclaredInstanceConstructors().Where(ctor => ctor.DeclaredAccessibility is Accessibility.Public).ToArray();
 
                if (publicCtors.Length == 1)
                {
                    lonePublicCtor = publicCtors[0];
                }
 
                foreach (IMethodSymbol constructor in publicCtors)
                {
                    if (constructor.ContainsAttribute(_knownSymbols.JsonConstructorAttributeType))
                    {
                        if (ctorWithAttribute != null)
                        {
                            deserializationCtor = null;
                            return false;
                        }
 
                        ctorWithAttribute = constructor;
                    }
                    else if (constructor.Parameters.Length == 0)
                    {
                        publicParameterlessCtor = constructor;
                    }
                }
 
                // Search for non-public ctors with [JsonConstructor].
                foreach (IMethodSymbol constructor in namedType.GetExplicitlyDeclaredInstanceConstructors().Where(ctor => ctor.DeclaredAccessibility is not Accessibility.Public))
                {
                    if (constructor.ContainsAttribute(_knownSymbols.JsonConstructorAttributeType))
                    {
                        if (ctorWithAttribute != null)
                        {
                            deserializationCtor = null;
                            return false;
                        }
 
                        ctorWithAttribute = constructor;
                    }
                }
 
                // Structs will use default constructor if attribute isn't used.
                if (useDefaultCtorInAnnotatedStructs && type.IsValueType && ctorWithAttribute == null)
                {
                    deserializationCtor = null;
                    return true;
                }
 
                deserializationCtor = ctorWithAttribute ?? publicParameterlessCtor ?? lonePublicCtor;
                return true;
            }
 
            private bool IsSymbolAccessibleWithin(ISymbol symbol, INamedTypeSymbol within)
                => _knownSymbols.Compilation.IsSymbolAccessibleWithin(symbol, within);
 
            private bool IsUnsupportedType(ITypeSymbol type)
            {
                return
                    SymbolEqualityComparer.Default.Equals(_knownSymbols.SerializationInfoType, type) ||
                    SymbolEqualityComparer.Default.Equals(_knownSymbols.IntPtrType, type) ||
                    SymbolEqualityComparer.Default.Equals(_knownSymbols.UIntPtrType, type) ||
                    _knownSymbols.MemberInfoType.IsAssignableFrom(type) ||
                    _knownSymbols.DelegateType.IsAssignableFrom(type) ||
                    type is IArrayTypeSymbol { Rank: > 1 };
            }
 
            private bool IsBuiltInSupportType(ITypeSymbol type)
            {
                return type.SpecialType is
                        SpecialType.System_Boolean or
                        SpecialType.System_Char or
                        SpecialType.System_DateTime or
                        SpecialType.System_String or
                        SpecialType.System_Object ||
 
                    type.IsNumberType() ||
                    _builtInSupportTypes.Contains(type);
            }
 
            private static HashSet<ITypeSymbol> CreateBuiltInSupportTypeSet(KnownTypeSymbols knownSymbols)
            {
#pragma warning disable RS1024 // Compare symbols correctly https://github.com/dotnet/roslyn-analyzers/issues/5804
                HashSet<ITypeSymbol> builtInSupportTypes = new(SymbolEqualityComparer.Default);
#pragma warning restore
 
                AddTypeIfNotNull(knownSymbols.ByteArrayType);
                AddTypeIfNotNull(knownSymbols.MemoryByteType);
                AddTypeIfNotNull(knownSymbols.ReadOnlyMemoryByteType);
                AddTypeIfNotNull(knownSymbols.TimeSpanType);
                AddTypeIfNotNull(knownSymbols.DateTimeOffsetType);
                AddTypeIfNotNull(knownSymbols.DateOnlyType);
                AddTypeIfNotNull(knownSymbols.TimeOnlyType);
                AddTypeIfNotNull(knownSymbols.Int128Type);
                AddTypeIfNotNull(knownSymbols.UInt128Type);
                AddTypeIfNotNull(knownSymbols.HalfType);
                AddTypeIfNotNull(knownSymbols.GuidType);
                AddTypeIfNotNull(knownSymbols.UriType);
                AddTypeIfNotNull(knownSymbols.VersionType);
 
                AddTypeIfNotNull(knownSymbols.JsonArrayType);
                AddTypeIfNotNull(knownSymbols.JsonElementType);
                AddTypeIfNotNull(knownSymbols.JsonNodeType);
                AddTypeIfNotNull(knownSymbols.JsonObjectType);
                AddTypeIfNotNull(knownSymbols.JsonValueType);
                AddTypeIfNotNull(knownSymbols.JsonDocumentType);
 
                return builtInSupportTypes;
 
                void AddTypeIfNotNull(ITypeSymbol? type)
                {
                    if (type != null)
                    {
                        builtInSupportTypes.Add(type);
                    }
                }
            }
 
            private readonly struct TypeToGenerate
            {
                public required ITypeSymbol Type { get; init; }
                public required JsonSourceGenerationMode? Mode { get; init; }
                public required string? TypeInfoPropertyName { get; init; }
                public required Location? Location { get; init; }
                public required Location? AttributeLocation { get; init; }
            }
        }
    }
}