Skip to content

Commit

Permalink
Merge pull request #61 from Akeit0/alchemy-serialize-with-inheritance
Browse files Browse the repository at this point in the history
Add : Support AlchemySerialization with generics and inheritance
  • Loading branch information
AnnulusGames authored Mar 21, 2024
2 parents db0d05a + c91f1d6 commit 93a5675
Show file tree
Hide file tree
Showing 8 changed files with 152 additions and 50 deletions.
132 changes: 103 additions & 29 deletions Alchemy.SourceGenerator/AlchemySerializeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@ public void Execute(GeneratorExecutionContext context)

var fieldSymbols = new List<IFieldSymbol>();
var fields = typeSyntax.Members
.Where(x => x is FieldDeclarationSyntax)
.Select(x => (FieldDeclarationSyntax)x);
.OfType<FieldDeclarationSyntax>();
foreach (var field in fields)
{
var model = context.Compilation.GetSemanticModel(field.SyntaxTree);
Expand All @@ -54,16 +53,16 @@ public void Execute(GeneratorExecutionContext context)
var alchemySerializeAttribute = fieldSymbol.GetAttributes()
.FirstOrDefault(x =>
x.AttributeClass.Name is "AlchemySerializeField"
or "AlchemySerializeFieldAttribute"
or "Alchemy.Serialization.AlchemySerializeField"
or "Alchemy.Serialization.AlchemySerializeFieldAttribute");
or "AlchemySerializeFieldAttribute"
or "Alchemy.Serialization.AlchemySerializeField"
or "Alchemy.Serialization.AlchemySerializeFieldAttribute");

var nonSerializedAttribute = fieldSymbol.GetAttributes()
.FirstOrDefault(x =>
x.AttributeClass.Name is "NonSerialized"
or "NonSerializedAttribute"
or "System.NonSerialized"
or "System.NonSerializedAttribute");
or "NonSerializedAttribute"
or "System.NonSerialized"
or "System.NonSerializedAttribute");

if (alchemySerializeAttribute != null)
{
Expand Down Expand Up @@ -92,43 +91,105 @@ x.AttributeClass.Name is "NonSerialized"
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)
{
var onAfterDeserializeCodeBuilder = new StringBuilder();
var onBeforeSerializeCodeBuilder = 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
is "ShowAlchemySerializationData"
or "ShowAlchemySerializationDataAttribute"
or "Alchemy.Serialization.ShowAlchemySerializationData"
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
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)
{
var serializeCode =
@$"try
@$"try
{{
alchemySerializationData.{field.Name}.data = global::Alchemy.Serialization.Internal.SerializationHelper.ToJson(this.{field.Name} , alchemySerializationData.UnityObjectReferences);
alchemySerializationData.{field.Name}.isCreated = true;
{alchemySerializationDataName}.{field.Name}.data = global::Alchemy.Serialization.Internal.SerializationHelper.ToJson(this.{field.Name} , {alchemySerializationDataName}.UnityObjectReferences);
{alchemySerializationDataName}.{field.Name}.isCreated = true;
}}
catch (global::System.Exception ex)
{{
global::UnityEngine.Debug.LogException(ex);
}}";

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)
Expand All @@ -143,25 +204,38 @@ static string ProcessClass(INamedTypeSymbol typeSymbol, List<IFieldSymbol> field
}

return
@$"
@$"
// <auto-generated/>
{ns}
partial class {typeSymbol.Name} : global::UnityEngine.ISerializationCallbackReceiver
partial class {typeSymbol.Name}{typeGenerics} : global::UnityEngine.ISerializationCallbackReceiver
{{
void global::UnityEngine.ISerializationCallbackReceiver.OnAfterDeserialize()
{{
{onAfterDeserializeCodeBuilder}
__AlchemyOnAfterDeserialize();
if (this is global::Alchemy.Serialization.IAlchemySerializationCallbackReceiver receiver) receiver.OnAfterDeserialize();
}}
void global::UnityEngine.ISerializationCallbackReceiver.OnBeforeSerialize()
{{
if (this is global::Alchemy.Serialization.IAlchemySerializationCallbackReceiver receiver) receiver.OnBeforeSerialize();
alchemySerializationData.UnityObjectReferences.Clear();
{onBeforeSerializeCodeBuilder}
__AlchemyOnBeforeSerialize();
}}
protected {(hasInheritedImplementation ? "new" : "")} void __AlchemyOnAfterDeserialize()
{{
{inheritedOnAfterDeserialize}
{onAfterDeserializeCodeBuilder}
}}
protected {(hasInheritedImplementation ? "new" : "")} void __AlchemyOnBeforeSerialize()
{{
{inheritedOnBeforeSerialize}
{alchemySerializationDataName}.UnityObjectReferences.Clear();
{onBeforeSerializeCodeBuilder}
}}
[global::System.Serializable]
sealed class AlchemySerializationData
{{
Expand All @@ -178,7 +252,7 @@ public sealed class Item
}}
}}
{serializationDataAttibutesCode} private AlchemySerializationData alchemySerializationData = new();
{serializationDataAttributesCode} private AlchemySerializationData {alchemySerializationDataName} = new();
}}
{(string.IsNullOrEmpty(ns) ? "" : "}")}
Expand Down Expand Up @@ -209,12 +283,12 @@ public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
if (syntaxNode is TypeDeclarationSyntax typeDeclarationSyntax)
{
var hasAttribute = typeDeclarationSyntax.AttributeLists
.SelectMany(x => x.Attributes)
.Any(x => x.Name.ToString()
is "AlchemySerialize"
or "AlchemySerializeAttribute"
or "Alchemy.Serialization.AlchemySerialize"
or "Alchemy.Serialization.AlchemySerializeAttribute");
.SelectMany(x => x.Attributes)
.Any(x => x.Name.ToString()
is "AlchemySerialize"
or "AlchemySerializeAttribute"
or "Alchemy.Serialization.AlchemySerialize"
or "Alchemy.Serialization.AlchemySerializeAttribute");
if (hasAttribute)
{
TargetTypes.Add(typeDeclarationSyntax);
Expand Down
12 changes: 10 additions & 2 deletions Alchemy/Assets/Alchemy/Editor/Internal/InspectorHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -230,13 +230,21 @@ public static VisualElement CreateMemberElement(SerializedObject serializedObjec

#if ALCHEMY_SUPPORT_SERIALIZATION
if (serializedObject.targetObject != null &&
serializedObject.targetObject.GetType().HasCustomAttribute<AlchemySerializeAttribute>() &&
memberInfo.DeclaringType.HasCustomAttribute<AlchemySerializeAttribute>() &&
memberInfo.HasCustomAttribute<AlchemySerializeFieldAttribute>())
{
var element = default(VisualElement);
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();
if (p != null)
Expand Down
Binary file modified Alchemy/Assets/Alchemy/Generator/Alchemy.SourceGenerator.dll
Binary file not shown.
19 changes: 0 additions & 19 deletions Alchemy/Assets/Alchemy/Generator/Alchemy.SourceGenerator.dll.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions Alchemy/Assets/Tests/InheritedSerializeTest.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
11 changes: 11 additions & 0 deletions Alchemy/Assets/Tests/InheritedSerializeTest.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions Alchemy/Assets/Tests/InheritedSerializeTestBase.cs
Original file line number Diff line number Diff line change
@@ -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;
}

3 changes: 3 additions & 0 deletions Alchemy/Assets/Tests/InheritedSerializeTestBase.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 93a5675

Please sign in to comment.