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; }
}
}
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; }
}
}