XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEExtension.NetCore.AutoConfig

【DLL】自动实现配置文件的存储

公开
关注 0 Fork 0 Star 0
UTF-8
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;

namespace XFEExtension.NetCore.AutoConfig.Analyzer.Generator;

[Generator]
public sealed class ProfilePropertyAutoGenerator : IIncrementalGenerator
{
    private const string ProfileBaseTypeName = "XFEExtension.NetCore.AutoConfig.XFEProfile";
    private const string ProfilePropertyAttributeName = "XFEExtension.NetCore.AutoConfig.ProfilePropertyAttribute";
    private const string ProfilePropertyAddGetAttributeName = "XFEExtension.NetCore.AutoConfig.ProfilePropertyAddGetAttribute";
    private const string ProfilePropertyAddSetAttributeName = "XFEExtension.NetCore.AutoConfig.ProfilePropertyAddSetAttribute";
    private const string AutoLoadProfileAttributeName = "XFEExtension.NetCore.AutoConfig.AutoLoadProfileAttribute";
    private const string ProfilePathAttributeName = "XFEExtension.NetCore.AutoConfig.ProfilePathAttribute";
    private const string ProfileOwnedCollectionTypeName = "XFEExtension.NetCore.AutoConfig.IProfileOwnedCollection";

    private static readonly SymbolDisplayFormat DeclarationTypeDisplayFormat = SymbolDisplayFormat.FullyQualifiedFormat.WithMiscellaneousOptions(SymbolDisplayFormat.FullyQualifiedFormat.MiscellaneousOptions | SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier);
    private static readonly SymbolDisplayFormat RuntimeTypeDisplayFormat = SymbolDisplayFormat.FullyQualifiedFormat.WithMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers);

    private static readonly DiagnosticDescriptor ProfileMustBePartial = new(
        "XFE1001", "配置类型必须声明为 partial", "配置类型“{0}”必须声明为 partial 才能生成配置成员", "XFEExtension.NetCore.AutoConfig.Generator",
        DiagnosticSeverity.Error, true);

    private static readonly DiagnosticDescriptor GeneratedPropertyConflict = new(
        "XFE1002", "生成属性名称冲突", "成员“{0}”生成的成员名称“{1}”与另一个成员冲突", "XFEExtension.NetCore.AutoConfig.Generator",
        DiagnosticSeverity.Error, true);

    private static readonly DiagnosticDescriptor MultipleVariableFieldUnsupported = new(
        "XFE1003", "不支持多变量字段声明", "带有 ProfileProperty 的字段必须单独声明,不能在同一声明中包含多个变量", "XFEExtension.NetCore.AutoConfig.Generator",
        DiagnosticSeverity.Error, true);

    private static readonly DiagnosticDescriptor UnsupportedProfileShape = new(
        "XFE1004", "不支持的配置类型形状", "配置类型“{0}”不能是泛型、嵌套类型或非 class 类型", "XFEExtension.NetCore.AutoConfig.Generator",
        DiagnosticSeverity.Error, true);

    private static readonly DiagnosticDescriptor MissingParameterlessConstructor = new(
        "XFE1005", "缺少可用的无参构造函数", "配置类型“{0}”必须是非抽象类型并提供无参构造函数", "XFEExtension.NetCore.AutoConfig.Generator",
        DiagnosticSeverity.Error, true);

    private static readonly DiagnosticDescriptor GeneratedMemberConflict = new(
        "XFE1006", "配置类型包含生成器保留成员", "配置类型“{0}”已声明生成器保留成员“{1}”", "XFEExtension.NetCore.AutoConfig.Generator",
        DiagnosticSeverity.Error, true);

    private static readonly DiagnosticDescriptor UnsupportedField = new(
        "XFE1007", "不支持的配置字段", "字段“{0}”不能是 static、const 或 readonly", "XFEExtension.NetCore.AutoConfig.Generator",
        DiagnosticSeverity.Error, true);

    private static readonly DiagnosticDescriptor ProfileBaseTypeRequired = new(
        "XFE1008", "配置类型必须继承 XFEProfile", "成员“{0}”使用了 ProfileProperty,但其包含类型未继承 XFEProfile", "XFEExtension.NetCore.AutoConfig.Generator",
        DiagnosticSeverity.Error, true);

    private static readonly DiagnosticDescriptor UnsupportedPartialProperty = new(
        "XFE1009", "不支持的部分配置属性", "属性“{0}”必须使用 C# 14 的 public static partial Xxx {{ get; set; }} 声明,并且不能是 required、virtual 或显式接口实现", "XFEExtension.NetCore.AutoConfig.Generator",
        DiagnosticSeverity.Error, true);

    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        var attributedMembers = context.SyntaxProvider.ForAttributeWithMetadataName(
            ProfilePropertyAttributeName,
            static (_, _) => true,
            static (attributeContext, _) => attributeContext.TargetSymbol);

        context.RegisterSourceOutput(attributedMembers.Collect(), EmitSources);
    }

    private static void EmitSources(SourceProductionContext context, ImmutableArray<ISymbol> attributedMembers)
    {
        var members = new List<ISymbol>();
        foreach (var member in attributedMembers)
        {
            if (member is not IFieldSymbol && member is not IPropertySymbol)
            {
                context.ReportDiagnostic(Diagnostic.Create(UnsupportedField, GetLocation(member), member.Name));
                continue;
            }
            if (!members.Any(existing => SymbolEqualityComparer.Default.Equals(existing, member)))
                members.Add(member);
        }

        var profileGroups = new List<ProfileGroup>();
        foreach (var member in members)
        {
            var containingType = member.ContainingType;
            var group = profileGroups.FirstOrDefault(candidate => SymbolEqualityComparer.Default.Equals(candidate.Type, containingType));
            if (group is null)
            {
                group = new ProfileGroup(containingType);
                profileGroups.Add(group);
            }
            group.Members.Add(member);
        }

        foreach (var group in profileGroups.OrderBy(static group => group.Type.ToDisplayString(), StringComparer.Ordinal))
        {
            if (!ValidateProfileType(context, group.Type))
                continue;

            var validMembers = ValidateAndCreateMemberModels(context, group.Type, group.Members);
            if (validMembers.Count == 0)
                continue;

            var source = GenerateProfileSource(group.Type, validMembers);
            context.AddSource(CreateHintName(group.Type), SourceText.From(source, Encoding.UTF8));
        }
    }

    private static bool ValidateProfileType(SourceProductionContext context, INamedTypeSymbol type)
    {
        if (!DerivesFromXfeProfile(type))
        {
            context.ReportDiagnostic(Diagnostic.Create(ProfileBaseTypeRequired, GetLocation(type), type.Name));
            return false;
        }

        var typeDeclarations = type.DeclaringSyntaxReferences.Select(static reference => reference.GetSyntax()).OfType<TypeDeclarationSyntax>().ToArray();
        if (type.Arity != 0 || type.ContainingType is not null || typeDeclarations.Any(static declaration => declaration is not ClassDeclarationSyntax))
        {
            context.ReportDiagnostic(Diagnostic.Create(UnsupportedProfileShape, GetLocation(type), type.Name));
            return false;
        }

        var isValid = true;
        foreach (var declaration in typeDeclarations.Where(static declaration => !declaration.Modifiers.Any(SyntaxKind.PartialKeyword)))
        {
            context.ReportDiagnostic(Diagnostic.Create(ProfileMustBePartial, declaration.Identifier.GetLocation(), type.Name));
            isValid = false;
        }

        if (type.IsAbstract || !type.InstanceConstructors.Any(static constructor => constructor.Parameters.Length == 0))
        {
            context.ReportDiagnostic(Diagnostic.Create(MissingParameterlessConstructor, GetLocation(type), type.Name));
            isValid = false;
        }

        var reservedMembers = new[]
        {
            "__profileInstanceSyncRoot", "__current", "__profilePath", "__profileExtension", "__UpdateCurrentProfilePath", "__BindProfileOwnedCollections",
            "Current", "ProfilePath", "ProfileExtension", "Initialize", "LoadProfile", "SaveProfile", "SaveProfileAsync",
            "FlushAsync", "DeleteProfile", "ExportProfile", "ExportProfileBytes", "ImportProfile", "ImportProfileBytes"
        };
        foreach (var reservedMember in reservedMembers)
        {
            if (type.GetMembers(reservedMember).Length == 0)
                continue;
            context.ReportDiagnostic(Diagnostic.Create(GeneratedMemberConflict, GetLocation(type.GetMembers(reservedMember)[0]), type.Name, reservedMember));
            isValid = false;
        }

        var explicitStaticConstructor = type.StaticConstructors.FirstOrDefault(static constructor => !constructor.IsImplicitlyDeclared);
        if (explicitStaticConstructor is not null)
        {
            context.ReportDiagnostic(Diagnostic.Create(GeneratedMemberConflict, GetLocation(explicitStaticConstructor), type.Name, type.Name + "()"));
            isValid = false;
        }

        return isValid;
    }

    private static List<ProfileMemberModel> ValidateAndCreateMemberModels(SourceProductionContext context, INamedTypeSymbol type, List<ISymbol> members)
    {
        var models = new List<ProfileMemberModel>();
        var invalidDeclarationKeys = new HashSet<string>(StringComparer.Ordinal);
        foreach (var member in members.OrderBy(static member => GetLocation(member).SourceTree?.FilePath, StringComparer.Ordinal).ThenBy(static member => GetLocation(member).SourceSpan.Start))
        {
            if (member is IPropertySymbol property)
            {
                var propertyModel = ValidateAndCreatePartialPropertyModel(context, property);
                if (propertyModel is not null)
                    models.Add(propertyModel);
                continue;
            }

            var field = (IFieldSymbol)member;
            var fieldDeclaration = field.DeclaringSyntaxReferences.Select(static reference => reference.GetSyntax()).OfType<VariableDeclaratorSyntax>().Select(static variable => variable.Parent?.Parent).OfType<FieldDeclarationSyntax>().FirstOrDefault();
            if (fieldDeclaration is not null && fieldDeclaration.Declaration.Variables.Count != 1)
            {
                var key = $"{fieldDeclaration.SyntaxTree.FilePath}:{fieldDeclaration.SpanStart}";
                if (invalidDeclarationKeys.Add(key))
                    context.ReportDiagnostic(Diagnostic.Create(MultipleVariableFieldUnsupported, fieldDeclaration.GetLocation()));
                continue;
            }
            if (field.IsStatic || field.IsConst || field.IsReadOnly)
            {
                context.ReportDiagnostic(Diagnostic.Create(UnsupportedField, GetLocation(field), field.Name));
                continue;
            }

            var propertyName = GetGeneratedPropertyName(field);
            if (!IsValidGeneratedIdentifier(propertyName))
            {
                context.ReportDiagnostic(Diagnostic.Create(GeneratedPropertyConflict, GetLocation(field), field.Name, propertyName));
                continue;
            }
            models.Add(new ProfileMemberModel(field, field.Type, propertyName, field.Name, false, null, GetAttributeStrings(field, ProfilePropertyAddGetAttributeName), GetAttributeStrings(field, ProfilePropertyAddSetAttributeName)));
        }

        var invalidModels = new HashSet<ProfileMemberModel>();
        var profileNames = new Dictionary<string, ProfileMemberModel>(StringComparer.Ordinal);
        var generatedNames = new Dictionary<string, ProfileMemberModel>(StringComparer.Ordinal);
        foreach (var model in models)
        {
            if (profileNames.TryGetValue(model.PropertyName, out var existingProfileModel))
            {
                context.ReportDiagnostic(Diagnostic.Create(GeneratedPropertyConflict, GetLocation(model.Member), model.Member.Name, model.PropertyName));
                invalidModels.Add(model);
                invalidModels.Add(existingProfileModel);
            }
            else
            {
                profileNames.Add(model.PropertyName, model);
            }

            var names = model.IsPartialProperty
                ? new[] { model.StorageMemberName, "Instance" + model.PropertyName, "Get" + model.PropertyName + "Property", "Set" + model.PropertyName + "Property" }
                : new[] { model.PropertyName, "Instance" + model.PropertyName, "Get" + model.PropertyName + "Property", "Set" + model.PropertyName + "Property" };
            foreach (var name in names)
            {
                if (generatedNames.TryGetValue(name, out var existingModel))
                {
                    context.ReportDiagnostic(Diagnostic.Create(GeneratedPropertyConflict, GetLocation(model.Member), model.Member.Name, name));
                    invalidModels.Add(model);
                    invalidModels.Add(existingModel);
                }
                else
                {
                    generatedNames.Add(name, model);
                }

                if ((name == "Get" + model.PropertyName + "Property" || name == "Set" + model.PropertyName + "Property") && IsPartialHook(type, name))
                    continue;
                var conflictingMember = type.GetMembers(name).FirstOrDefault();
                if (conflictingMember is null)
                    continue;
                context.ReportDiagnostic(Diagnostic.Create(GeneratedPropertyConflict, GetLocation(model.Member), model.Member.Name, name));
                invalidModels.Add(model);
            }
        }

        return models.Where(model => !invalidModels.Contains(model)).ToList();
    }

    private static ProfileMemberModel? ValidateAndCreatePartialPropertyModel(SourceProductionContext context, IPropertySymbol property)
    {
        var declaration = property.DeclaringSyntaxReferences
            .Select(static reference => reference.GetSyntax())
            .OfType<PropertyDeclarationSyntax>()
            .FirstOrDefault(static candidate => candidate.AttributeLists.Count > 0);
        var parseOptions = declaration?.SyntaxTree.Options as CSharpParseOptions;
        var accessors = declaration?.AccessorList?.Accessors;
        var hasSupportedAccessors = accessors is { Count: 2 }
            && accessors.Value.Any(static accessor => accessor.IsKind(SyntaxKind.GetAccessorDeclaration) && accessor.Body is null && accessor.ExpressionBody is null && !accessor.SemicolonToken.IsMissing)
            && accessors.Value.Any(static accessor => accessor.IsKind(SyntaxKind.SetAccessorDeclaration) && accessor.Body is null && accessor.ExpressionBody is null && !accessor.SemicolonToken.IsMissing)
            && accessors.Value.All(static accessor => accessor.Modifiers.Count == 0);
        var propertyName = GetGeneratedPropertyName(property);
        if (!IsValidGeneratedIdentifier(propertyName))
        {
            context.ReportDiagnostic(Diagnostic.Create(GeneratedPropertyConflict, GetLocation(property), property.Name, propertyName));
            return null;
        }
        var isSupported = declaration is not null
            && parseOptions is not null
            && parseOptions.LanguageVersion >= LanguageVersion.CSharp14
            && declaration.Modifiers.Any(SyntaxKind.PartialKeyword)
            && declaration.Modifiers.All(static modifier => modifier.IsKind(SyntaxKind.PublicKeyword) || modifier.IsKind(SyntaxKind.StaticKeyword) || modifier.IsKind(SyntaxKind.PartialKeyword))
            && property.DeclaredAccessibility == Accessibility.Public
            && property.IsStatic
            && !property.IsAbstract
            && !property.IsVirtual
            && !property.IsOverride
            && !property.IsSealed
            && !property.IsRequired
            && property.ExplicitInterfaceImplementations.Length == 0
            && property.Parameters.Length == 0
            && property.RefKind == RefKind.None
            && property.GetMethod is not null
            && property.SetMethod is not null
            && !property.SetMethod.IsInitOnly
            && hasSupportedAccessors;

        if (!isSupported)
        {
            context.ReportDiagnostic(Diagnostic.Create(UnsupportedPartialProperty, GetLocation(property), property.Name));
            return null;
        }

        return new ProfileMemberModel(
            property,
            property.Type,
            propertyName,
            "__profileStorage_" + property.Name,
            true,
            declaration?.Initializer?.Value.ToFullString().Trim(),
            GetAttributeStrings(property, ProfilePropertyAddGetAttributeName),
            GetAttributeStrings(property, ProfilePropertyAddSetAttributeName));
    }

    private static string GenerateProfileSource(INamedTypeSymbol type, List<ProfileMemberModel> members)
    {
        var builder = new StringBuilder();
        var typeName = EscapeIdentifier(type.Name);
        var autoLoad = GetAutoLoad(type);
        var configuredPath = GetProfilePath(type);

        builder.AppendLine("// <auto-generated/>");
        builder.AppendLine("#nullable enable");
        if (!type.ContainingNamespace.IsGlobalNamespace)
        {
            builder.Append("namespace ").Append(type.ContainingNamespace.ToDisplayString()).AppendLine(";");
            builder.AppendLine();
        }
        builder.Append("partial class ").Append(typeName).AppendLine();
        builder.AppendLine("{");
        builder.AppendLine("    private static readonly object __profileInstanceSyncRoot = new();");
        builder.Append("    private static string __profilePath = ").Append(configuredPath is null
            ? $"global::System.IO.Path.Combine(global::XFEExtension.NetCore.AutoConfig.XFEProfile.ProfilesDefaultPath, nameof({typeName}))"
            : SymbolDisplay.FormatLiteral(configuredPath, true)).AppendLine(";");
        builder.AppendLine("    private static string __profileExtension = string.Empty;");
        builder.Append("    private static ").Append(typeName).AppendLine(" __current = null!;");
        foreach (var member in members.Where(static member => member.IsPartialProperty))
        {
            var typeDisplay = member.Type.ToDisplayString(DeclarationTypeDisplayFormat);
            builder.Append("    private ").Append(typeDisplay).Append(' ').Append(EscapeIdentifier(member.StorageMemberName))
                .Append(" = __current is null ? ").Append(EscapeIdentifier(member.Member.Name)).Append(" : ")
                .Append(member.InitializerExpression ?? "default!").AppendLine(";");
        }
        builder.AppendLine();
        builder.Append("    static ").Append(typeName).AppendLine("()");
        builder.AppendLine("    {");
        builder.Append("        __current = new ").Append(typeName).AppendLine("();");
        builder.AppendLine("        __current.Initialize();");
        if (autoLoad)
        {
            builder.AppendLine("        if (__current.InstanceLoadProfile(static () => new " + typeName + "()) is " + typeName + " loadedProfile)");
            builder.AppendLine("            __current = loadedProfile;");
        }
        builder.AppendLine("    }");
        builder.AppendLine();
        builder.Append("    public static ").Append(typeName).AppendLine(" Current");
        builder.AppendLine("    {");
        builder.AppendLine("        get { lock (__profileInstanceSyncRoot) return __current; }");
        builder.AppendLine("        set");
        builder.AppendLine("        {");
        builder.AppendLine("            if (value is null) throw new global::System.ArgumentNullException(nameof(value));");
        builder.AppendLine("            lock (__profileInstanceSyncRoot)");
        builder.AppendLine("            {");
        builder.AppendLine("                value.Initialize();");
        builder.AppendLine("                __current = value;");
        builder.AppendLine("                __UpdateCurrentProfilePath();");
        builder.AppendLine("            }");
        builder.AppendLine("        }");
        builder.AppendLine("    }");
        builder.AppendLine();
        AppendPathProperty(builder, "ProfilePath", "__profilePath");
        AppendPathProperty(builder, "ProfileExtension", "__profileExtension");
        builder.AppendLine("    private static void __UpdateCurrentProfilePath()");
        builder.AppendLine("    {");
        builder.AppendLine("        if (__current is not null)");
        builder.AppendLine("            __current.CurrentProfilePath = __profilePath + (string.IsNullOrEmpty(__profileExtension) ? __current.CurrentProfileExtension : __profileExtension);");
        builder.AppendLine("    }");
        builder.AppendLine();
        builder.AppendLine("    public override void Initialize()");
        builder.AppendLine("    {");
        builder.AppendLine("        this.SetProfileOperation();");
        builder.AppendLine("        this.CurrentProfilePath = __profilePath + (string.IsNullOrEmpty(__profileExtension) ? this.CurrentProfileExtension : __profileExtension);");
        builder.AppendLine("        this.PropertyInfoDictionary.Clear();");
        builder.AppendLine("        this.PropertySetFuncDictionary.Clear();");
        builder.AppendLine("        this.PropertyGetFuncDictionary.Clear();");
        foreach (var member in members)
        {
            var storageMemberName = EscapeIdentifier(member.StorageMemberName);
            var propertyName = EscapeIdentifier(member.PropertyName);
            var propertyKeyExpression = member.IsPartialProperty
                ? SyntaxFactory.Literal(member.PropertyName).ToFullString()
                : "nameof(" + propertyName + ")";
            var declarationTypeDisplay = member.Type.ToDisplayString(DeclarationTypeDisplayFormat);
            var runtimeTypeDisplay = member.Type.ToDisplayString(RuntimeTypeDisplayFormat);
            builder.Append("        this.PropertyInfoDictionary[").Append(propertyKeyExpression).Append("] = typeof(").Append(runtimeTypeDisplay).AppendLine(");");
            builder.Append("        this.PropertySetFuncDictionary[").Append(propertyKeyExpression).Append("] = value => this.").Append(storageMemberName).Append(" = (").Append(declarationTypeDisplay).AppendLine(")value!;");
            builder.Append("        this.PropertyGetFuncDictionary[").Append(propertyKeyExpression).Append("] = () => this.").Append(storageMemberName).AppendLine(";");
        }
        builder.AppendLine("        this.__BindProfileOwnedCollections();");
        builder.AppendLine("    }");
        builder.AppendLine();
        builder.AppendLine("    private void __BindProfileOwnedCollections()");
        builder.AppendLine("    {");
        foreach (var member in members)
        {
            if (!IsProfileOwnedCollection(member.Type))
                continue;
            var storageMemberName = EscapeIdentifier(member.StorageMemberName);
            var safeLocalSuffix = new string(member.PropertyName.Select(static character => char.IsLetterOrDigit(character) ? character : '_').ToArray());
            builder.Append("        if ((object?)this.").Append(storageMemberName).Append(" is global::XFEExtension.NetCore.AutoConfig.IProfileOwnedCollection owned_").Append(safeLocalSuffix).AppendLine(")");
            builder.Append("            owned_").Append(safeLocalSuffix).AppendLine(".CurrentProfile = this;");
        }
        builder.AppendLine("    }");
        builder.AppendLine();
        AppendProfileOperations(builder, typeName);
        foreach (var member in members)
            AppendProfileMember(builder, member);
        builder.AppendLine("}");
        return builder.ToString();
    }

    private static void AppendPathProperty(StringBuilder builder, string propertyName, string fieldName)
    {
        builder.Append("    public static string ").Append(propertyName).AppendLine();
        builder.AppendLine("    {");
        builder.Append("        get { lock (__profileInstanceSyncRoot) return ").Append(fieldName).AppendLine("; }");
        builder.AppendLine("        set");
        builder.AppendLine("        {");
        builder.AppendLine("            if (value is null) throw new global::System.ArgumentNullException(nameof(value));");
        builder.AppendLine("            lock (__profileInstanceSyncRoot)");
        builder.AppendLine("            {");
        builder.Append("                ").Append(fieldName).AppendLine(" = value;");
        builder.AppendLine("                __UpdateCurrentProfilePath();");
        builder.AppendLine("            }");
        builder.AppendLine("        }");
        builder.AppendLine("    }");
        builder.AppendLine();
    }

    private static void AppendProfileOperations(StringBuilder builder, string typeName)
    {
        builder.AppendLine("    public static void LoadProfile()");
        builder.AppendLine("    {");
        builder.AppendLine("        lock (__profileInstanceSyncRoot)");
        builder.AppendLine("            if (__current.InstanceLoadProfile(static () => new " + typeName + "()) is " + typeName + " loadedProfile) __current = loadedProfile;");
        builder.AppendLine("    }");
        builder.AppendLine();
        builder.AppendLine("    public static void SaveProfile()");
        builder.AppendLine("    {");
        builder.AppendLine("        lock (__profileInstanceSyncRoot) __current.InstanceSaveProfile();");
        builder.AppendLine("    }");
        builder.AppendLine();
        builder.AppendLine("    public static async global::System.Threading.Tasks.Task SaveProfileAsync(global::System.Threading.CancellationToken cancellationToken = default)");
        builder.AppendLine("    {");
        builder.Append("        ").Append(typeName).AppendLine(" current;");
        builder.AppendLine("        lock (__profileInstanceSyncRoot) current = __current;");
        builder.AppendLine("        await current.InstanceSaveProfileAsync(cancellationToken).ConfigureAwait(false);");
        builder.AppendLine("    }");
        builder.AppendLine();
        builder.AppendLine("    public static async global::System.Threading.Tasks.Task FlushAsync(global::System.Threading.CancellationToken cancellationToken = default)");
        builder.AppendLine("    {");
        builder.Append("        ").Append(typeName).AppendLine(" current;");
        builder.AppendLine("        lock (__profileInstanceSyncRoot) current = __current;");
        builder.AppendLine("        await current.InstanceFlushProfileAsync(cancellationToken).ConfigureAwait(false);");
        builder.AppendLine("    }");
        builder.AppendLine();
        builder.AppendLine("    public static void DeleteProfile()");
        builder.AppendLine("    {");
        builder.AppendLine("        lock (__profileInstanceSyncRoot) __current.InstanceDeleteProfile();");
        builder.AppendLine("    }");
        builder.AppendLine();
        builder.AppendLine("    public static string ExportProfile()");
        builder.AppendLine("    {");
        builder.AppendLine("        lock (__profileInstanceSyncRoot) return __current.InstanceExportProfile();");
        builder.AppendLine("    }");
        builder.AppendLine();
        builder.AppendLine("    public static byte[] ExportProfileBytes()");
        builder.AppendLine("    {");
        builder.AppendLine("        lock (__profileInstanceSyncRoot) return __current.InstanceExportProfileBytes();");
        builder.AppendLine("    }");
        builder.AppendLine();
        builder.AppendLine("    public static void ImportProfile(string profileString)");
        builder.AppendLine("    {");
        builder.AppendLine("        if (profileString is null) throw new global::System.ArgumentNullException(nameof(profileString));");
        builder.AppendLine("        lock (__profileInstanceSyncRoot)");
        builder.AppendLine("            if (__current.InstanceImportProfile(profileString, static () => new " + typeName + "()) is " + typeName + " importedProfile) __current = importedProfile;");
        builder.AppendLine("    }");
        builder.AppendLine();
        builder.AppendLine("    public static void ImportProfileBytes(global::System.ReadOnlyMemory<byte> profileContent)");
        builder.AppendLine("    {");
        builder.AppendLine("        lock (__profileInstanceSyncRoot)");
        builder.AppendLine("            if (__current.InstanceImportProfileBytes(profileContent, static () => new " + typeName + "()) is " + typeName + " importedProfile) __current = importedProfile;");
        builder.AppendLine("    }");
        builder.AppendLine();
    }

    private static void AppendProfileMember(StringBuilder builder, ProfileMemberModel member)
    {
        var storageMemberName = EscapeIdentifier(member.StorageMemberName);
        var propertyName = EscapeIdentifier(member.PropertyName);
        var instancePropertyName = "Instance" + propertyName;
        var getMethodName = "Get" + propertyName + "Property";
        var setMethodName = "Set" + propertyName + "Property";
        var typeDisplay = member.Type.ToDisplayString(DeclarationTypeDisplayFormat);
        var xmlElementNameLiteral = SyntaxFactory.Literal(member.PropertyName).ToFullString();

        builder.Append("    static partial void ").Append(getMethodName).AppendLine("();");
        builder.Append("    static partial void ").Append(setMethodName).Append("(ref ").Append(typeDisplay).AppendLine(" value);");
        builder.AppendLine();

        if (member.IsPartialProperty)
        {
            AppendPartialProfileProperty(builder, member, storageMemberName, instancePropertyName, getMethodName, setMethodName, typeDisplay, xmlElementNameLiteral);
            return;
        }

        builder.AppendLine("    [global::XFEExtension.NetCore.AutoConfig.ProfileFieldAutoGenerateAttribute]");
        builder.Append("    public static ").Append(typeDisplay).Append(' ').Append(propertyName).AppendLine();
        builder.AppendLine("    {");
        builder.AppendLine("        get");
        builder.AppendLine("        {");
        builder.AppendLine("            lock (__profileInstanceSyncRoot)");
        builder.AppendLine("            lock (__current.ProfileSyncRoot)");
        builder.AppendLine("            {");
        builder.Append("                ").Append(getMethodName).AppendLine("();");
        if (member.GetStatements.Length == 0)
            builder.Append("                return __current.").Append(storageMemberName).AppendLine(";");
        else
            AppendStatements(builder, member.GetStatements, 16);
        builder.AppendLine("            }");
        builder.AppendLine("        }");
        builder.AppendLine("        set");
        builder.AppendLine("        {");
        builder.AppendLine("            lock (__profileInstanceSyncRoot)");
        builder.AppendLine("            lock (__current.ProfileSyncRoot)");
        builder.AppendLine("            {");
        builder.Append("                ").Append(setMethodName).AppendLine("(ref value);");
        if (member.SetStatements.Length == 0)
            builder.Append("                __current.").Append(storageMemberName).AppendLine(" = value;");
        else
            AppendStatements(builder, member.SetStatements, 16);
        if (IsProfileOwnedCollection(member.Type))
            builder.AppendLine("                __current.__BindProfileOwnedCollections();");
        builder.AppendLine("                __current.InstanceRequestSaveProfile();");
        builder.AppendLine("            }");
        builder.AppendLine("        }");
        builder.AppendLine("    }");
        builder.AppendLine();
        builder.AppendLine("    [global::XFEExtension.NetCore.AutoConfig.ProfileFieldAutoGenerateAttribute]");
        builder.Append("    [global::System.Xml.Serialization.XmlElementAttribute(").Append(xmlElementNameLiteral).AppendLine(")]");
        builder.Append("    public ").Append(typeDisplay).Append(' ').Append(instancePropertyName).AppendLine();
        builder.AppendLine("    {");
        builder.Append("        get { lock (ProfileSyncRoot) return this.").Append(storageMemberName).AppendLine("; }");
        if (IsProfileOwnedCollection(member.Type))
        {
            builder.Append("        set { lock (ProfileSyncRoot) { this.").Append(storageMemberName).AppendLine(" = value; this.__BindProfileOwnedCollections(); } }");
        }
        else
        {
            builder.Append("        set { lock (ProfileSyncRoot) this.").Append(storageMemberName).AppendLine(" = value; }");
        }
        builder.AppendLine("    }");
        builder.AppendLine();
    }

    private static void AppendPartialProfileProperty(
        StringBuilder builder,
        ProfileMemberModel member,
        string storageMemberName,
        string instancePropertyName,
        string getMethodName,
        string setMethodName,
        string typeDisplay,
        string xmlElementNameLiteral)
    {
        var declaredPropertyName = EscapeIdentifier(member.Member.Name);
        builder.AppendLine("    [global::XFEExtension.NetCore.AutoConfig.ProfileFieldAutoGenerateAttribute]");
        builder.Append("    public static partial ").Append(typeDisplay).Append(' ').Append(declaredPropertyName).AppendLine();
        builder.AppendLine("    {");
        builder.AppendLine("        get");
        builder.AppendLine("        {");
        builder.AppendLine("            if (__current is null) return field;");
        builder.AppendLine("            lock (__profileInstanceSyncRoot)");
        builder.AppendLine("            lock (__current.ProfileSyncRoot)");
        builder.AppendLine("            {");
        builder.Append("                ").Append(getMethodName).AppendLine("();");
        if (member.GetStatements.Length == 0)
            builder.Append("                return __current.").Append(storageMemberName).AppendLine(";");
        else
            AppendPartialStatements(builder, member.GetStatements, storageMemberName, 16);
        builder.AppendLine("            }");
        builder.AppendLine("        }");
        builder.AppendLine("        set");
        builder.AppendLine("        {");
        builder.AppendLine("            if (__current is null) { field = value; return; }");
        builder.AppendLine("            lock (__profileInstanceSyncRoot)");
        builder.AppendLine("            lock (__current.ProfileSyncRoot)");
        builder.AppendLine("            {");
        builder.Append("                ").Append(setMethodName).AppendLine("(ref value);");
        if (member.SetStatements.Length == 0)
            builder.Append("                __current.").Append(storageMemberName).AppendLine(" = value;");
        else
            AppendPartialStatements(builder, member.SetStatements, storageMemberName, 16);
        if (IsProfileOwnedCollection(member.Type))
            builder.AppendLine("                __current.__BindProfileOwnedCollections();");
        builder.AppendLine("                __current.InstanceRequestSaveProfile();");
        builder.AppendLine("            }");
        builder.AppendLine("        }");
        builder.AppendLine("    }");
        builder.AppendLine();
        builder.AppendLine("    [global::XFEExtension.NetCore.AutoConfig.ProfileFieldAutoGenerateAttribute]");
        builder.Append("    [global::System.Xml.Serialization.XmlElementAttribute(").Append(xmlElementNameLiteral).AppendLine(")]");
        builder.Append("    [global::System.Text.Json.Serialization.JsonPropertyNameAttribute(").Append(xmlElementNameLiteral).AppendLine(")]");
        builder.Append("    public ").Append(typeDisplay).Append(' ').Append(instancePropertyName).AppendLine();
        builder.AppendLine("    {");
        builder.Append("        get { lock (ProfileSyncRoot) return this.").Append(storageMemberName).AppendLine("; }");
        if (IsProfileOwnedCollection(member.Type))
        {
            builder.Append("        set { lock (ProfileSyncRoot) { this.").Append(storageMemberName).AppendLine(" = value; this.__BindProfileOwnedCollections(); } }");
        }
        else
        {
            builder.Append("        set { lock (ProfileSyncRoot) this.").Append(storageMemberName).AppendLine(" = value; }");
        }
        builder.AppendLine("    }");
        builder.AppendLine();
    }

    private static void AppendPartialStatements(StringBuilder builder, ImmutableArray<string> statements, string storageMemberName, int indentation)
    {
        var storageExpression = "__current." + storageMemberName;
        AppendStatements(builder, [.. statements.Select(statement => Regex.Replace(statement, @"\bfield\b", storageExpression))], indentation);
    }

    private static void AppendStatements(StringBuilder builder, ImmutableArray<string> statements, int indentation)
    {
        var prefix = new string(' ', indentation);
        foreach (var statement in statements)
        {
            var normalized = statement.Trim();
            if (!normalized.EndsWith(";", StringComparison.Ordinal) && !normalized.EndsWith("}", StringComparison.Ordinal))
                normalized += ";";
            foreach (var line in normalized.Replace("\r\n", "\n").Split('\n'))
                builder.Append(prefix).AppendLine(line);
        }
    }

    private static string GetGeneratedPropertyName(ISymbol member)
    {
        var attribute = GetAttribute(member, ProfilePropertyAttributeName);
        var explicitName = GetStringArgument(attribute);
        if (string.IsNullOrWhiteSpace(explicitName) && attribute is not null)
        {
            foreach (var namedArgument in attribute.NamedArguments)
                if (namedArgument.Key == "PropertyName" && namedArgument.Value.Value is string namedValue)
                    explicitName = namedValue;
        }
        if (!string.IsNullOrWhiteSpace(explicitName))
            return explicitName!;

        if (member is IPropertySymbol property)
            return property.Name;

        var fieldName = member.Name.StartsWith("_", StringComparison.Ordinal) ? member.Name.Substring(1) : member.Name;
        if (fieldName.Length == 0)
            return fieldName;
        return char.ToUpperInvariant(fieldName[0]) + fieldName.Substring(1);
    }

    private static bool GetAutoLoad(INamedTypeSymbol type)
    {
        var attribute = GetAttribute(type, AutoLoadProfileAttributeName);
        if (attribute is null || attribute.ConstructorArguments.Length == 0)
            return true;
        return attribute.ConstructorArguments[0].Value is not bool value || value;
    }

    private static string? GetProfilePath(INamedTypeSymbol type) => GetStringArgument(GetAttribute(type, ProfilePathAttributeName));

    private static ImmutableArray<string> GetAttributeStrings(ISymbol member, string metadataName)
    {
        var builder = ImmutableArray.CreateBuilder<string>();
        foreach (var attribute in member.GetAttributes().Where(attribute => IsAttribute(attribute, metadataName)))
        {
            var value = GetStringArgument(attribute);
            if (!string.IsNullOrWhiteSpace(value))
                builder.Add(value!);
        }
        return builder.ToImmutable();
    }

    private static AttributeData? GetAttribute(ISymbol symbol, string metadataName) => symbol.GetAttributes().FirstOrDefault(attribute => IsAttribute(attribute, metadataName));

    private static bool IsAttribute(AttributeData attribute, string metadataName) => attribute.AttributeClass?.ToDisplayString() == metadataName;

    private static string? GetStringArgument(AttributeData? attribute) => attribute is not null && attribute.ConstructorArguments.Length > 0 ? attribute.ConstructorArguments[0].Value as string : null;

    private static bool DerivesFromXfeProfile(INamedTypeSymbol type)
    {
        for (var current = type.BaseType; current is not null; current = current.BaseType)
            if (current.ToDisplayString() == ProfileBaseTypeName)
                return true;
        return false;
    }

    private static bool IsProfileOwnedCollection(ITypeSymbol type) => type.ToDisplayString() == ProfileOwnedCollectionTypeName
        || type is INamedTypeSymbol namedType && namedType.AllInterfaces.Any(interfaceType => interfaceType.ToDisplayString() == ProfileOwnedCollectionTypeName);

    private static bool IsPartialHook(INamedTypeSymbol type, string name) => type.GetMembers(name).OfType<IMethodSymbol>().Any(method => method.DeclaringSyntaxReferences.Select(static reference => reference.GetSyntax()).OfType<MethodDeclarationSyntax>().Any(static method => method.Modifiers.Any(SyntaxKind.PartialKeyword)));

    private static bool IsValidGeneratedIdentifier(string identifier) => !string.IsNullOrWhiteSpace(identifier)
        && SyntaxFacts.IsValidIdentifier(identifier)
        && SyntaxFacts.GetKeywordKind(identifier) == SyntaxKind.None
        && SyntaxFacts.GetContextualKeywordKind(identifier) == SyntaxKind.None;

    private static string EscapeIdentifier(string identifier) => SyntaxFacts.GetKeywordKind(identifier) != SyntaxKind.None || SyntaxFacts.GetContextualKeywordKind(identifier) != SyntaxKind.None ? "@" + identifier : identifier;

    private static Location GetLocation(ISymbol symbol) => symbol.Locations.FirstOrDefault(static location => location.IsInSource) ?? Location.None;

    private static string CreateHintName(INamedTypeSymbol type)
    {
        var fullName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
        string hash;
        using (var algorithm = SHA256.Create())
            hash = BitConverter.ToString(algorithm.ComputeHash(Encoding.UTF8.GetBytes(fullName))).Replace("-", string.Empty).Substring(0, 12);
        var safeName = new string(fullName.Select(static character => char.IsLetterOrDigit(character) ? character : '_').ToArray());
        return $"{safeName}.{hash}.AutoConfig.g.cs";
    }

    private sealed class ProfileGroup
    {
        public ProfileGroup(INamedTypeSymbol type) => Type = type;
        public INamedTypeSymbol Type { get; }
        public List<ISymbol> Members { get; } = new();
    }

    private sealed class ProfileMemberModel
    {
        public ProfileMemberModel(ISymbol member, ITypeSymbol type, string propertyName, string storageMemberName, bool isPartialProperty, string? initializerExpression, ImmutableArray<string> getStatements, ImmutableArray<string> setStatements)
        {
            Member = member;
            Type = type;
            PropertyName = propertyName;
            StorageMemberName = storageMemberName;
            IsPartialProperty = isPartialProperty;
            InitializerExpression = initializerExpression;
            GetStatements = getStatements;
            SetStatements = setStatements;
        }

        public ISymbol Member { get; }
        public ITypeSymbol Type { get; }
        public string PropertyName { get; }
        public string StorageMemberName { get; }
        public bool IsPartialProperty { get; }
        public string? InitializerExpression { get; }
        public ImmutableArray<string> GetStatements { get; }
        public ImmutableArray<string> SetStatements { get; }
    }
}