diff --git a/Alchemy.SourceGenerator/AlchemySerializeGenerator.cs b/Alchemy.SourceGenerator/AlchemySerializeGenerator.cs index 98d9e7f..11a5cc4 100644 --- a/Alchemy.SourceGenerator/AlchemySerializeGenerator.cs +++ b/Alchemy.SourceGenerator/AlchemySerializeGenerator.cs @@ -31,20 +31,21 @@ namespace Alchemy.SourceGenerator if (!IsPartial(typeSyntax)) { - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.MustBePartial, typeSyntax.Identifier.GetLocation(), typeSymbol.Name)); + context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.MustBePartial, + typeSyntax.Identifier.GetLocation(), typeSymbol.Name)); continue; } if (IsNested(typeSyntax)) { - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.NestedNotAllow, typeSyntax.Identifier.GetLocation(), typeSymbol.Name)); + context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.NestedNotAllow, + typeSyntax.Identifier.GetLocation(), typeSymbol.Name)); continue; } var fieldSymbols = new List(); var fields = typeSyntax.Members - .Where(x => x is FieldDeclarationSyntax) - .Select(x => (FieldDeclarationSyntax)x); + .OfType(); foreach (var field in fields) { var model = context.Compilation.GetSemanticModel(field.SyntaxTree); @@ -54,22 +55,24 @@ namespace Alchemy.SourceGenerator 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) { if (nonSerializedAttribute == null) { - context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.ShouldBeNonSerialized, variable.Identifier.GetLocation(), fieldSymbol.Name)); + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.ShouldBeNonSerialized, variable.Identifier.GetLocation(), + fieldSymbol.Name)); } fieldSymbols.Add(fieldSymbol); @@ -88,16 +91,76 @@ namespace Alchemy.SourceGenerator } catch (Exception ex) { - var diagnosticDescriptor = new DiagnosticDescriptor("AlchemySerializeGeneratorError", "AlchemySerializeGeneratorError", $"Generation failed with:\n {ex}", "AlchemySerializeGeneratorError", DiagnosticSeverity.Error, true); - context.ReportDiagnostic(Diagnostic.Create(diagnosticDescriptor, Location.None, DiagnosticSeverity.Error)); + var diagnosticDescriptor = new DiagnosticDescriptor("AlchemySerializeGeneratorError", + "AlchemySerializeGeneratorError", $"Generation failed with:\n {ex}", + "AlchemySerializeGeneratorError", DiagnosticSeverity.Error, true); + 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 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 alchemySerializationDataName = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + .Replace("global::", ""); + alchemySerializationDataName = ReplaceGenericsToCount(alchemySerializationDataName,genericsCount) + "_alchemySerializationData"; + + var inheritedSerializationCallback = hasInheritedImplementation + ? "base.SerializationCallback_AlchemyImpl(isBeforeSerialize);" + : string.Empty; var hasShowSerializationData = typeSymbol.GetAttributes().Any(x => x.AttributeClass.Name is "ShowAlchemySerializationData" @@ -105,18 +168,22 @@ namespace Alchemy.SourceGenerator 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.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) {{ @@ -124,11 +191,11 @@ catch (global::System.Exception 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) @@ -143,23 +210,37 @@ catch (global::System.Exception ex) } return -@$" + @$" // {ns} - partial class {typeSymbol.Name} : global::UnityEngine.ISerializationCallbackReceiver + partial class {typeSymbol.Name}{typeGenerics} : global::UnityEngine.ISerializationCallbackReceiver {{ void global::UnityEngine.ISerializationCallbackReceiver.OnAfterDeserialize() {{ - {onAfterDeserializeCodeBuilder} + SerializationCallback_AlchemyImpl(false); 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} + SerializationCallback_AlchemyImpl(true); + }} + + protected {(hasInheritedImplementation ? "new" : "")} void SerializationCallback_AlchemyImpl(bool isBeforeSerialize = false) + {{ + {inheritedSerializationCallback} + if (isBeforeSerialize) + {{ + {alchemySerializationDataName}.UnityObjectReferences.Clear(); + {onBeforeSerializeCodeBuilder} + + }} + else + {{ + {onAfterDeserializeCodeBuilder} + }} }} [global::System.Serializable] @@ -178,7 +259,7 @@ catch (global::System.Exception ex) }} }} - {serializationDataAttibutesCode} private AlchemySerializationData alchemySerializationData = new(); + {serializationDataAttributesCode} private AlchemySerializationData {alchemySerializationDataName} = new(); }} {(string.IsNullOrEmpty(ns) ? "" : "}")} @@ -209,12 +290,12 @@ catch (global::System.Exception ex) 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); diff --git a/Alchemy/Assets/Alchemy/Editor/Internal/InspectorHelper.cs b/Alchemy/Assets/Alchemy/Editor/Internal/InspectorHelper.cs index 372f926..600a684 100644 --- a/Alchemy/Assets/Alchemy/Editor/Internal/InspectorHelper.cs +++ b/Alchemy/Assets/Alchemy/Editor/Internal/InspectorHelper.cs @@ -235,13 +235,21 @@ namespace Alchemy.Editor #if ALCHEMY_SUPPORT_SERIALIZATION if (serializedObject.targetObject != null && - serializedObject.targetObject.GetType().HasCustomAttribute() && + memberInfo.DeclaringType.HasCustomAttribute() && memberInfo.HasCustomAttribute()) { 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 = declaredType.FullName.Replace("`","").Replace(".", "_") + "_alchemySerializationData"; + + SerializedProperty GetProperty() => findPropertyFunc?.Invoke(dataName) + .FindPropertyRelative(memberInfo.Name); var p = GetProperty(); if (p != null) diff --git a/Alchemy/Assets/Alchemy/Generator/Alchemy.SourceGenerator.dll b/Alchemy/Assets/Alchemy/Generator/Alchemy.SourceGenerator.dll index e7145a8..8436b83 100644 Binary files a/Alchemy/Assets/Alchemy/Generator/Alchemy.SourceGenerator.dll and b/Alchemy/Assets/Alchemy/Generator/Alchemy.SourceGenerator.dll differ diff --git a/Alchemy/Assets/Tests/InheritedSerializeTest.cs b/Alchemy/Assets/Tests/InheritedSerializeTest.cs new file mode 100644 index 0000000..864437b --- /dev/null +++ b/Alchemy/Assets/Tests/InheritedSerializeTest.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using Alchemy.Serialization; +using UnityEngine; + + +[AlchemySerialize] +public partial class InheritedSerializeTest : InheritedSerializeTestBase +{ + [AlchemySerializeField, NonSerialized] int? nullableInt; +} \ No newline at end of file diff --git a/Alchemy/Assets/Tests/InheritedSerializeTest.cs.meta b/Alchemy/Assets/Tests/InheritedSerializeTest.cs.meta new file mode 100644 index 0000000..e2f03de --- /dev/null +++ b/Alchemy/Assets/Tests/InheritedSerializeTest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 54b828d414a20a74b92a3ec43f78f187 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Alchemy/Assets/Tests/InheritedSerializeTestBase.cs b/Alchemy/Assets/Tests/InheritedSerializeTestBase.cs new file mode 100644 index 0000000..4459c8e --- /dev/null +++ b/Alchemy/Assets/Tests/InheritedSerializeTestBase.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using Alchemy.Serialization; +using UnityEngine; + + +[AlchemySerialize] +public partial class InheritedSerializeTestBase : MonoBehaviour +{ + [AlchemySerializeField, NonSerialized] HashSet set; + +} + diff --git a/Alchemy/Assets/Tests/InheritedSerializeTestBase.cs.meta b/Alchemy/Assets/Tests/InheritedSerializeTestBase.cs.meta new file mode 100644 index 0000000..7427156 --- /dev/null +++ b/Alchemy/Assets/Tests/InheritedSerializeTestBase.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 74b376952c9a4c42a10525c553c6e044 +timeCreated: 1709000968 \ No newline at end of file