Merge pull request #61 from Akeit0/alchemy-serialize-with-inheritance

Add : Support AlchemySerialization with generics and inheritance
This commit is contained in:
Annulus Games 2024-03-21 12:07:45 +09:00 committed by GitHub
commit 93a56757a8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 152 additions and 50 deletions

View File

@ -43,8 +43,7 @@ namespace Alchemy.SourceGenerator
var fieldSymbols = new List<IFieldSymbol>(); var fieldSymbols = new List<IFieldSymbol>();
var fields = typeSyntax.Members var fields = typeSyntax.Members
.Where(x => x is FieldDeclarationSyntax) .OfType<FieldDeclarationSyntax>();
.Select(x => (FieldDeclarationSyntax)x);
foreach (var field in fields) foreach (var field in fields)
{ {
var model = context.Compilation.GetSemanticModel(field.SyntaxTree); var model = context.Compilation.GetSemanticModel(field.SyntaxTree);
@ -54,16 +53,16 @@ namespace Alchemy.SourceGenerator
var alchemySerializeAttribute = fieldSymbol.GetAttributes() var alchemySerializeAttribute = fieldSymbol.GetAttributes()
.FirstOrDefault(x => .FirstOrDefault(x =>
x.AttributeClass.Name is "AlchemySerializeField" x.AttributeClass.Name is "AlchemySerializeField"
or "AlchemySerializeFieldAttribute" or "AlchemySerializeFieldAttribute"
or "Alchemy.Serialization.AlchemySerializeField" or "Alchemy.Serialization.AlchemySerializeField"
or "Alchemy.Serialization.AlchemySerializeFieldAttribute"); or "Alchemy.Serialization.AlchemySerializeFieldAttribute");
var nonSerializedAttribute = fieldSymbol.GetAttributes() var nonSerializedAttribute = fieldSymbol.GetAttributes()
.FirstOrDefault(x => .FirstOrDefault(x =>
x.AttributeClass.Name is "NonSerialized" x.AttributeClass.Name is "NonSerialized"
or "NonSerializedAttribute" or "NonSerializedAttribute"
or "System.NonSerialized" or "System.NonSerialized"
or "System.NonSerializedAttribute"); or "System.NonSerializedAttribute");
if (alchemySerializeAttribute != null) if (alchemySerializeAttribute != null)
{ {
@ -92,31 +91,93 @@ namespace Alchemy.SourceGenerator
context.ReportDiagnostic(Diagnostic.Create(diagnosticDescriptor, Location.None, DiagnosticSeverity.Error)); context.ReportDiagnostic(Diagnostic.Create(diagnosticDescriptor, Location.None, DiagnosticSeverity.Error));
} }
} }
static string ReplaceGenericsToCount( string typeName,int count)
{
if(count == 0) return typeName;
var builder = new StringBuilder();
bool skip = false;
foreach (var c in typeName)
{
if (c == '<')
{
skip = true;
builder.Append(count);
}
else if (c == '>')
{
skip = false;
}
else if (!skip)
{
builder.Append(c);
}
}
return builder.ToString();
}
static string ProcessClass(INamedTypeSymbol typeSymbol, List<IFieldSymbol> fieldSymbols) static string ProcessClass(INamedTypeSymbol typeSymbol, List<IFieldSymbol> fieldSymbols)
{ {
var onAfterDeserializeCodeBuilder = new StringBuilder(); var onAfterDeserializeCodeBuilder = new StringBuilder();
var onBeforeSerializeCodeBuilder = new StringBuilder(); var onBeforeSerializeCodeBuilder = new StringBuilder();
var serializationDataCodeBuilder = new StringBuilder(); var serializationDataCodeBuilder = new StringBuilder();
bool hasInheritedImplementation = false;
var baseType = typeSymbol.BaseType;
while (baseType != null)
{
if (baseType.GetAttributes().Any(x => x.AttributeClass!.Name
is "AlchemySerialize"
or "AlchemySerializeAttribute"
or "Alchemy.Serialization.AlchemySerialize"
or "Alchemy.Serialization.AlchemySerializeAttribute"))
{
hasInheritedImplementation = true;
break;
}
baseType = baseType.BaseType;
}
var genericsCount = 0;
if (typeSymbol.IsGenericType)
{
genericsCount = typeSymbol.TypeParameters.Length;
}
var typeGenerics = typeSymbol.IsGenericType
? "<" + string.Join(", ", typeSymbol.TypeParameters.Select(x => x.Name)) + ">"
: "";
var displayName = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", "");
var alchemySerializationDataName = displayName.Replace(".", "_");
alchemySerializationDataName ="__alchemySerializationData_"+ ReplaceGenericsToCount(alchemySerializationDataName,genericsCount) ;
var inheritedOnBeforeSerialize = hasInheritedImplementation
? "base.__AlchemyOnBeforeSerialize();"
: string.Empty;
var inheritedOnAfterDeserialize = hasInheritedImplementation
? "base.__AlchemyOnAfterDeserialize();"
: string.Empty;
var hasShowSerializationData = typeSymbol.GetAttributes().Any(x => x.AttributeClass.Name var hasShowSerializationData = typeSymbol.GetAttributes().Any(x => x.AttributeClass.Name
is "ShowAlchemySerializationData" is "ShowAlchemySerializationData"
or "ShowAlchemySerializationDataAttribute" or "ShowAlchemySerializationDataAttribute"
or "Alchemy.Serialization.ShowAlchemySerializationData" or "Alchemy.Serialization.ShowAlchemySerializationData"
or "Alchemy.Serialization.ShowAlchemySerializationDataAttribute"); or "Alchemy.Serialization.ShowAlchemySerializationDataAttribute");
var serializationDataAttibutesCode = hasShowSerializationData ? "[global::Alchemy.Inspector.ReadOnly, global::UnityEngine.TextArea(3, 999), global::UnityEngine.SerializeField]" : "[global::UnityEngine.HideInInspector, global::UnityEngine.SerializeField]"; var serializationDataAttributesCode = hasShowSerializationData
? $"[global::Alchemy.Inspector.LabelText(\"Alchemy Serialization Data ({displayName})\"),global::Alchemy.Inspector.ReadOnly, global::UnityEngine.TextArea(3, 999), global::UnityEngine.SerializeField]"
: "[global::UnityEngine.HideInInspector, global::UnityEngine.SerializeField]";
// target class namespace // target class namespace
var ns = typeSymbol.ContainingNamespace.IsGlobalNamespace ? string.Empty : $"namespace {typeSymbol.ContainingNamespace} {{"; var ns = typeSymbol.ContainingNamespace.IsGlobalNamespace
? string.Empty
: $"namespace {typeSymbol.ContainingNamespace} {{";
foreach (var field in fieldSymbols) foreach (var field in fieldSymbols)
{ {
var serializeCode = var serializeCode =
@$"try @$"try
{{ {{
alchemySerializationData.{field.Name}.data = global::Alchemy.Serialization.Internal.SerializationHelper.ToJson(this.{field.Name} , alchemySerializationData.UnityObjectReferences); {alchemySerializationDataName}.{field.Name}.data = global::Alchemy.Serialization.Internal.SerializationHelper.ToJson(this.{field.Name} , {alchemySerializationDataName}.UnityObjectReferences);
alchemySerializationData.{field.Name}.isCreated = true; {alchemySerializationDataName}.{field.Name}.isCreated = true;
}} }}
catch (global::System.Exception ex) catch (global::System.Exception ex)
{{ {{
@ -124,11 +185,11 @@ catch (global::System.Exception ex)
}}"; }}";
var deserializeCode = var deserializeCode =
@$"try @$"try
{{ {{
if (alchemySerializationData.{field.Name}.isCreated) if ({alchemySerializationDataName}.{field.Name}.isCreated)
{{ {{
this.{field.Name} = global::Alchemy.Serialization.Internal.SerializationHelper.FromJson<{field.Type.ToDisplayString()}>(alchemySerializationData.{field.Name}.data, alchemySerializationData.UnityObjectReferences); this.{field.Name} = global::Alchemy.Serialization.Internal.SerializationHelper.FromJson<{field.Type.ToDisplayString()}>({alchemySerializationDataName}.{field.Name}.data, {alchemySerializationDataName}.UnityObjectReferences);
}} }}
}} }}
catch (global::System.Exception ex) catch (global::System.Exception ex)
@ -143,25 +204,38 @@ catch (global::System.Exception ex)
} }
return return
@$" @$"
// <auto-generated/> // <auto-generated/>
{ns} {ns}
partial class {typeSymbol.Name} : global::UnityEngine.ISerializationCallbackReceiver partial class {typeSymbol.Name}{typeGenerics} : global::UnityEngine.ISerializationCallbackReceiver
{{ {{
void global::UnityEngine.ISerializationCallbackReceiver.OnAfterDeserialize() void global::UnityEngine.ISerializationCallbackReceiver.OnAfterDeserialize()
{{ {{
{onAfterDeserializeCodeBuilder} __AlchemyOnAfterDeserialize();
if (this is global::Alchemy.Serialization.IAlchemySerializationCallbackReceiver receiver) receiver.OnAfterDeserialize(); if (this is global::Alchemy.Serialization.IAlchemySerializationCallbackReceiver receiver) receiver.OnAfterDeserialize();
}} }}
void global::UnityEngine.ISerializationCallbackReceiver.OnBeforeSerialize() void global::UnityEngine.ISerializationCallbackReceiver.OnBeforeSerialize()
{{ {{
if (this is global::Alchemy.Serialization.IAlchemySerializationCallbackReceiver receiver) receiver.OnBeforeSerialize(); if (this is global::Alchemy.Serialization.IAlchemySerializationCallbackReceiver receiver) receiver.OnBeforeSerialize();
alchemySerializationData.UnityObjectReferences.Clear(); __AlchemyOnBeforeSerialize();
{onBeforeSerializeCodeBuilder} }}
protected {(hasInheritedImplementation ? "new" : "")} void __AlchemyOnAfterDeserialize()
{{
{inheritedOnAfterDeserialize}
{onAfterDeserializeCodeBuilder}
}} }}
protected {(hasInheritedImplementation ? "new" : "")} void __AlchemyOnBeforeSerialize()
{{
{inheritedOnBeforeSerialize}
{alchemySerializationDataName}.UnityObjectReferences.Clear();
{onBeforeSerializeCodeBuilder}
}}
[global::System.Serializable] [global::System.Serializable]
sealed class AlchemySerializationData sealed class AlchemySerializationData
{{ {{
@ -178,7 +252,7 @@ catch (global::System.Exception ex)
}} }}
}} }}
{serializationDataAttibutesCode} private AlchemySerializationData alchemySerializationData = new(); {serializationDataAttributesCode} private AlchemySerializationData {alchemySerializationDataName} = new();
}} }}
{(string.IsNullOrEmpty(ns) ? "" : "}")} {(string.IsNullOrEmpty(ns) ? "" : "}")}
@ -209,12 +283,12 @@ catch (global::System.Exception ex)
if (syntaxNode is TypeDeclarationSyntax typeDeclarationSyntax) if (syntaxNode is TypeDeclarationSyntax typeDeclarationSyntax)
{ {
var hasAttribute = typeDeclarationSyntax.AttributeLists var hasAttribute = typeDeclarationSyntax.AttributeLists
.SelectMany(x => x.Attributes) .SelectMany(x => x.Attributes)
.Any(x => x.Name.ToString() .Any(x => x.Name.ToString()
is "AlchemySerialize" is "AlchemySerialize"
or "AlchemySerializeAttribute" or "AlchemySerializeAttribute"
or "Alchemy.Serialization.AlchemySerialize" or "Alchemy.Serialization.AlchemySerialize"
or "Alchemy.Serialization.AlchemySerializeAttribute"); or "Alchemy.Serialization.AlchemySerializeAttribute");
if (hasAttribute) if (hasAttribute)
{ {
TargetTypes.Add(typeDeclarationSyntax); TargetTypes.Add(typeDeclarationSyntax);

View File

@ -230,13 +230,21 @@ namespace Alchemy.Editor
#if ALCHEMY_SUPPORT_SERIALIZATION #if ALCHEMY_SUPPORT_SERIALIZATION
if (serializedObject.targetObject != null && if (serializedObject.targetObject != null &&
serializedObject.targetObject.GetType().HasCustomAttribute<AlchemySerializeAttribute>() && memberInfo.DeclaringType.HasCustomAttribute<AlchemySerializeAttribute>() &&
memberInfo.HasCustomAttribute<AlchemySerializeFieldAttribute>()) memberInfo.HasCustomAttribute<AlchemySerializeFieldAttribute>())
{ {
var element = default(VisualElement); var element = default(VisualElement);
if (memberInfo is FieldInfo fieldInfo) if (memberInfo is FieldInfo fieldInfo)
{ {
SerializedProperty GetProperty() => findPropertyFunc?.Invoke("alchemySerializationData").FindPropertyRelative(memberInfo.Name); var declaredType = fieldInfo.DeclaringType;
if (declaredType.IsConstructedGenericType)
{
declaredType = declaredType.GetGenericTypeDefinition();
}
var dataName ="__alchemySerializationData_"+ declaredType.FullName.Replace("`","").Replace(".", "_") ;
SerializedProperty GetProperty() => findPropertyFunc?.Invoke(dataName)
.FindPropertyRelative(memberInfo.Name);
var p = GetProperty(); var p = GetProperty();
if (p != null) if (p != null)

View File

@ -18,21 +18,11 @@ PluginImporter:
second: second:
enabled: 0 enabled: 0
settings: settings:
Exclude Android: 1
Exclude Editor: 1 Exclude Editor: 1
Exclude Linux64: 1 Exclude Linux64: 1
Exclude OSXUniversal: 1 Exclude OSXUniversal: 1
Exclude WebGL: 1
Exclude Win: 1 Exclude Win: 1
Exclude Win64: 1 Exclude Win64: 1
Exclude iOS: 1
- first:
Android: Android
second:
enabled: 0
settings:
AndroidSharedLibraryType: Executable
CPU: ARMv7
- first: - first:
Any: Any:
second: second:
@ -76,15 +66,6 @@ PluginImporter:
enabled: 0 enabled: 0
settings: settings:
CPU: AnyCPU CPU: AnyCPU
- first:
iPhone: iOS
second:
enabled: 0
settings:
AddToEmbeddedBinaries: false
CPU: AnyCPU
CompileFlags:
FrameworkDependencies:
userData: userData:
assetBundleName: assetBundleName:
assetBundleVariant: assetBundleVariant:

View File

@ -0,0 +1,13 @@
using System;
using Alchemy.Serialization;
using UnityEngine;
namespace Test
{
[ShowAlchemySerializationData]
[AlchemySerialize]
public partial class InheritedSerializeTest : InheritedSerializeTestBase<string>
{
[AlchemySerializeField, NonSerialized] int? nullableInt;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 54b828d414a20a74b92a3ec43f78f187
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using Alchemy.Serialization;
using UnityEngine;
[ShowAlchemySerializationData]
[AlchemySerialize]
public partial class InheritedSerializeTestBase<T> : MonoBehaviour
{
[AlchemySerializeField, NonSerialized] HashSet<T> set;
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 74b376952c9a4c42a10525c553c6e044
timeCreated: 1709000968