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 attributedMembers) { var members = new List(); 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(); 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().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 ValidateAndCreateMemberModels(SourceProductionContext context, INamedTypeSymbol type, List members) { var models = new List(); var invalidDeclarationKeys = new HashSet(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().Select(static variable => variable.Parent?.Parent).OfType().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(); var profileNames = new Dictionary(StringComparer.Ordinal); var generatedNames = new Dictionary(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() .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 members) { var builder = new StringBuilder(); var typeName = EscapeIdentifier(type.Name); var autoLoad = GetAutoLoad(type); var configuredPath = GetProfilePath(type); builder.AppendLine("// "); 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 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 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 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 GetAttributeStrings(ISymbol member, string metadataName) { var builder = ImmutableArray.CreateBuilder(); 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().Any(method => method.DeclaringSyntaxReferences.Select(static reference => reference.GetSyntax()).OfType().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 Members { get; } = new(); } private sealed class ProfileMemberModel { public ProfileMemberModel(ISymbol member, ITypeSymbol type, string propertyName, string storageMemberName, bool isPartialProperty, string? initializerExpression, ImmutableArray getStatements, ImmutableArray 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 GetStatements { get; } public ImmutableArray SetStatements { get; } } }