From 3c6356e8fae5b306c197dc6a5be5722cde34e924 Mon Sep 17 00:00:00 2001
From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
Date: Wed, 26 May 2021 13:20:52 -0700
Subject: [PATCH] feat(codegen): Adds support for partial opt-in with already
existing properties (#617)
- [X] Fixes an issue with ordering of properties in serialization.
- [X] Adds opt-in with existing properties.
Example:
```cs
private int _myExistingField;
[SerializableField(1)]
public int MyExistingProperty
{
get => _myExistingField;
set
{
if (value == 0)
{
Parent = null;
}
if (value != _myExistingField)
{
((ISerializable)this).MarkDirty();
_myExistingField = value;
}
}
}
```
---
.../EntitySerializationGenerator.cs | 3 +-
.../SerializableEntityGeneration.Class.cs | 110 +++++++++++-------
...zableEntityGeneration.DeserializeMethod.cs | 11 +-
...lizableEntityGeneration.SerializeMethod.cs | 3 +-
.../Rules/ArrayMigrationRule.cs | 1 +
.../Rules/HashSetMigrationRule.cs | 1 +
.../Rules/KeyValuePairMigrationRule.cs | 2 +
.../Rules/ListMigrationRule.cs | 1 +
.../SerializableMetadata.cs | 10 +-
.../SerializableMigrationRulesEngine.cs | 1 +
.../SerializableProperty.cs | 5 +-
.../SerializablePropertyComparer.cs} | 34 ++++--
.../SerializerSyntaxReceiver.cs | 44 ++++---
.../SourceGeneration.Namespace.cs | 13 ---
.../SerializableFieldAttribute.cs | 7 +-
.../SerializableFieldAttributeAttribute.cs | 6 +
16 files changed, 152 insertions(+), 100 deletions(-)
rename Projects/{Server/Serialization/SerializablePropertyAttribute.cs => SerializationGenerator/SerializableMigration/SerializablePropertyComparer.cs} (58%)
diff --git a/Projects/SerializationGenerator/EntitySerializationGenerator.cs b/Projects/SerializationGenerator/EntitySerializationGenerator.cs
index 59b10c222..417ee1f2f 100755
--- a/Projects/SerializationGenerator/EntitySerializationGenerator.cs
+++ b/Projects/SerializationGenerator/EntitySerializationGenerator.cs
@@ -13,7 +13,6 @@
* along with this program. If not, see . *
*************************************************************************/
-using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
@@ -64,7 +63,7 @@ namespace SerializationGenerator
{
string classSource = SerializableEntityGeneration.GenerateSerializationPartialClass(
kvp.Key,
- kvp.Value,
+ kvp.Value.ToImmutableArray(),
context,
migrationPath,
jsonOptions,
diff --git a/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.Class.cs b/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.Class.cs
index 0365f4900..be4ebcb7c 100644
--- a/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.Class.cs
+++ b/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.Class.cs
@@ -13,6 +13,7 @@
* along with this program. If not, see . *
*************************************************************************/
+using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
@@ -44,7 +45,7 @@ namespace SerializationGenerator
var versionValue = classSymbol.GetAttributes()
.FirstOrDefault(
- attr => attr.AttributeClass?.Equals(serializableEntityAttribute, SymbolEqualityComparer.Default) ?? false
+ attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, serializableEntityAttribute)
)?.ConstructorArguments.FirstOrDefault().Value;
return versionValue != null;
@@ -52,7 +53,7 @@ namespace SerializationGenerator
public static string GenerateSerializationPartialClass(
INamedTypeSymbol classSymbol,
- IList fields,
+ ImmutableArray fieldsAndProperties,
GeneratorExecutionContext context,
string migrationPath,
JsonSerializerOptions jsonSerializerOptions,
@@ -85,11 +86,11 @@ namespace SerializationGenerator
var serializableAttribute = classSymbol.GetAttributes()
.FirstOrDefault(
- attr => attr.AttributeClass?.Equals(serializableEntityAttribute, SymbolEqualityComparer.Default) ?? false
+ attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, serializableEntityAttribute)
);
var version = (int)serializableAttribute?.ConstructorArguments[0].Value!;
- var encodedVersion = (bool)serializableAttribute?.ConstructorArguments[1].Value!;
+ var encodedVersion = (bool)serializableAttribute.ConstructorArguments[1].Value!;
var namespaceName = classSymbol.ContainingNamespace.ToDisplayString();
var className = classSymbol.Name;
@@ -115,61 +116,86 @@ namespace SerializationGenerator
);
source.AppendLine();
- var serializableProperties = new List();
+ var serializablePropertySet = new SortedSet();
- foreach (IFieldSymbol fieldSymbol in fields)
+ foreach (var fieldOrPropertySymbol in fieldsAndProperties)
{
- var allAttributes = fieldSymbol.GetAttributes();
+ var allAttributes = fieldOrPropertySymbol.GetAttributes();
- var hasAttribute = allAttributes
- .Any(
+ var serializableFieldAttr = allAttributes
+ .FirstOrDefault(
attr =>
SymbolEqualityComparer.Default.Equals(attr.AttributeClass, serializableFieldAttribute)
);
- if (hasAttribute)
+ if (serializableFieldAttr == null)
{
- foreach (var attr in allAttributes)
+ continue;
+ }
+
+ var order = (int)serializableFieldAttr.ConstructorArguments[0].Value!;
+
+ foreach (var attr in allAttributes)
+ {
+ if (!SymbolEqualityComparer.Default.Equals(attr.AttributeClass, serializableFieldAttrAttribute))
{
- if (!SymbolEqualityComparer.Default.Equals(attr.AttributeClass, serializableFieldAttrAttribute))
- {
- continue;
- }
-
- if (attr.AttributeClass == null)
- {
- continue;
- }
-
- var ctorArgs = attr.ConstructorArguments;
- var attrTypeArg = ctorArgs[0];
-
- if (attrTypeArg.Kind == TypedConstantKind.Primitive && attrTypeArg.Value is string attrStr)
- {
- source.AppendLine($" {attrStr}");
- }
- else
- {
- var attrType = (ITypeSymbol)attrTypeArg.Value;
- source.GenerateAttribute(attrType.Name, ctorArgs[1].Values);
- }
+ continue;
}
+ if (attr.AttributeClass == null)
+ {
+ continue;
+ }
+
+ var ctorArgs = attr.ConstructorArguments;
+ var attrTypeArg = ctorArgs[0];
+
+ if (attrTypeArg.Kind == TypedConstantKind.Primitive && attrTypeArg.Value is string attrStr)
+ {
+ source.AppendLine($" {attrStr}");
+ }
+ else
+ {
+ var attrType = (ITypeSymbol)attrTypeArg.Value;
+ source.GenerateAttribute(attrType.Name, ctorArgs[1].Values);
+ }
+ }
+
+ string propertyName;
+ ITypeSymbol propertyType;
+
+ if (fieldOrPropertySymbol is IFieldSymbol fieldSymbol)
+ {
source.GenerateSerializableProperty(fieldSymbol, compilation);
source.AppendLine();
- var serializableProperty = SerializableMigrationRulesEngine.GenerateSerializableProperty(
- compilation,
- fieldSymbol.GetPropertyName(),
- fieldSymbol.Type,
- allAttributes,
- serializableTypes
- );
-
- serializableProperties.Add(serializableProperty);
+ propertyName = fieldSymbol.GetPropertyName();
+ propertyType = fieldSymbol.Type;
}
+ else if (fieldOrPropertySymbol is IPropertySymbol propertySymbol)
+ {
+ propertyName = fieldOrPropertySymbol.Name;
+ propertyType = propertySymbol.Type;
+ }
+ else
+ {
+ throw new Exception($"Invalid node {fieldOrPropertySymbol.Name}. Expecting a field or property node.");
+ }
+
+ var serializableProperty = SerializableMigrationRulesEngine.GenerateSerializableProperty(
+ compilation,
+ propertyName,
+ propertyType,
+ order,
+ allAttributes,
+ serializableTypes
+ );
+
+ serializablePropertySet.Add(serializableProperty);
}
+ var serializableProperties = serializablePropertySet.ToImmutableArray();
+
// If we are not inheriting ISerializable, then we need to define some stuff
if (!isOverride)
{
diff --git a/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.DeserializeMethod.cs b/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.DeserializeMethod.cs
index 94bbe8d97..5a86c757b 100644
--- a/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.DeserializeMethod.cs
+++ b/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.DeserializeMethod.cs
@@ -31,7 +31,7 @@ namespace SerializationGenerator
int version,
bool encodedVersion,
List migrations,
- List properties
+ ImmutableArray properties
)
{
var genericReaderInterface = compilation.GetTypeByMetadataName(GENERIC_READER_INTERFACE);
@@ -106,12 +106,11 @@ namespace SerializationGenerator
m.ReturnsVoid &&
m.Parameters.Length == 0 &&
m.GetAttributes()
- .OfType()
.Any(
- attr => attr.AttributeClass?.Equals(
- compilation.GetTypeByMetadataName(AFTERDESERIALIZATION_ATTRIBUTE),
- SymbolEqualityComparer.Default
- ) ?? false
+ attr => SymbolEqualityComparer.Default.Equals(
+ attr.AttributeClass,
+ compilation.GetTypeByMetadataName(AFTERDESERIALIZATION_ATTRIBUTE)
+ )
)
);
diff --git a/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.SerializeMethod.cs b/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.SerializeMethod.cs
index 1bfe1a17e..b865834da 100644
--- a/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.SerializeMethod.cs
+++ b/Projects/SerializationGenerator/SerializableEntityGeneration/SerializableEntityGeneration.SerializeMethod.cs
@@ -13,7 +13,6 @@
* along with this program. If not, see . *
*************************************************************************/
-using System.Collections.Generic;
using System.Collections.Immutable;
using System.Text;
using Microsoft.CodeAnalysis;
@@ -27,7 +26,7 @@ namespace SerializationGenerator
Compilation compilation,
bool isOverride,
bool encodedVersion,
- List properties
+ ImmutableArray properties
)
{
var genericWriterInterface = compilation.GetTypeByMetadataName(GENERIC_WRITER_INTERFACE);
diff --git a/Projects/SerializationGenerator/SerializableMigration/Rules/ArrayMigrationRule.cs b/Projects/SerializationGenerator/SerializableMigration/Rules/ArrayMigrationRule.cs
index 1fffeb5cb..3576aff0a 100644
--- a/Projects/SerializationGenerator/SerializableMigration/Rules/ArrayMigrationRule.cs
+++ b/Projects/SerializationGenerator/SerializableMigration/Rules/ArrayMigrationRule.cs
@@ -42,6 +42,7 @@ namespace SerializationGenerator
compilation,
"ArrayEntry",
arrayTypeSymbol.ElementType,
+ 0,
attributes,
serializableTypes
);
diff --git a/Projects/SerializationGenerator/SerializableMigration/Rules/HashSetMigrationRule.cs b/Projects/SerializationGenerator/SerializableMigration/Rules/HashSetMigrationRule.cs
index c8416db7e..6f9c0836d 100644
--- a/Projects/SerializationGenerator/SerializableMigration/Rules/HashSetMigrationRule.cs
+++ b/Projects/SerializationGenerator/SerializableMigration/Rules/HashSetMigrationRule.cs
@@ -44,6 +44,7 @@ namespace SerializationGenerator
compilation,
"SetEntry",
setTypeSymbol,
+ 0,
attributes,
serializableTypes
);
diff --git a/Projects/SerializationGenerator/SerializableMigration/Rules/KeyValuePairMigrationRule.cs b/Projects/SerializationGenerator/SerializableMigration/Rules/KeyValuePairMigrationRule.cs
index d78a69d92..e91765eeb 100644
--- a/Projects/SerializationGenerator/SerializableMigration/Rules/KeyValuePairMigrationRule.cs
+++ b/Projects/SerializationGenerator/SerializableMigration/Rules/KeyValuePairMigrationRule.cs
@@ -44,6 +44,7 @@ namespace SerializationGenerator
compilation,
"key",
typeArguments[0],
+ 0,
attributes,
serializableTypes
);
@@ -52,6 +53,7 @@ namespace SerializationGenerator
compilation,
"value",
typeArguments[1],
+ 1,
attributes,
serializableTypes
);
diff --git a/Projects/SerializationGenerator/SerializableMigration/Rules/ListMigrationRule.cs b/Projects/SerializationGenerator/SerializableMigration/Rules/ListMigrationRule.cs
index 80bea3562..c613c58cb 100644
--- a/Projects/SerializationGenerator/SerializableMigration/Rules/ListMigrationRule.cs
+++ b/Projects/SerializationGenerator/SerializableMigration/Rules/ListMigrationRule.cs
@@ -44,6 +44,7 @@ namespace SerializationGenerator
compilation,
"ListEntry",
listTypeSymbol,
+ 0,
attributes,
serializableTypes
);
diff --git a/Projects/SerializationGenerator/SerializableMigration/SerializableMetadata.cs b/Projects/SerializationGenerator/SerializableMigration/SerializableMetadata.cs
index d827cec5a..289be342e 100644
--- a/Projects/SerializationGenerator/SerializableMigration/SerializableMetadata.cs
+++ b/Projects/SerializationGenerator/SerializableMigration/SerializableMetadata.cs
@@ -13,20 +13,20 @@
* along with this program. If not, see . *
*************************************************************************/
-using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Text.Json.Serialization;
namespace SerializationGenerator
{
- public class SerializableMetadata
+ public record SerializableMetadata
{
[JsonPropertyName("version")]
- public int Version { get; set; }
+ public int Version { get; init; }
[JsonPropertyName("type")]
- public string Type { get; set; }
+ public string Type { get; init; }
[JsonPropertyName("properties")]
- public List Properties { get; set; }
+ public ImmutableArray Properties { get; init; }
}
}
diff --git a/Projects/SerializationGenerator/SerializableMigration/SerializableMigrationRulesEngine.cs b/Projects/SerializationGenerator/SerializableMigration/SerializableMigrationRulesEngine.cs
index 8a053c843..26976d176 100644
--- a/Projects/SerializationGenerator/SerializableMigration/SerializableMigrationRulesEngine.cs
+++ b/Projects/SerializationGenerator/SerializableMigration/SerializableMigrationRulesEngine.cs
@@ -49,6 +49,7 @@ namespace SerializationGenerator
Compilation compilation,
string propertyName,
ISymbol propertyType,
+ int order,
ImmutableArray attributes,
ImmutableArray serializableTypes
)
diff --git a/Projects/SerializationGenerator/SerializableMigration/SerializableProperty.cs b/Projects/SerializationGenerator/SerializableMigration/SerializableProperty.cs
index 99b19cd48..72983713a 100644
--- a/Projects/SerializationGenerator/SerializableMigration/SerializableProperty.cs
+++ b/Projects/SerializationGenerator/SerializableMigration/SerializableProperty.cs
@@ -17,7 +17,7 @@ using System.Text.Json.Serialization;
namespace SerializationGenerator
{
- public class SerializableProperty
+ public record SerializableProperty
{
[JsonPropertyName("name")]
public string Name { get; init; }
@@ -30,5 +30,8 @@ namespace SerializationGenerator
[JsonPropertyName("ruleArguments")]
public string[] RuleArguments { get; init; }
+
+ [JsonIgnore]
+ public int Order { get; init; }
}
}
diff --git a/Projects/Server/Serialization/SerializablePropertyAttribute.cs b/Projects/SerializationGenerator/SerializableMigration/SerializablePropertyComparer.cs
similarity index 58%
rename from Projects/Server/Serialization/SerializablePropertyAttribute.cs
rename to Projects/SerializationGenerator/SerializableMigration/SerializablePropertyComparer.cs
index cbac334f6..4bad427f6 100644
--- a/Projects/Server/Serialization/SerializablePropertyAttribute.cs
+++ b/Projects/SerializationGenerator/SerializableMigration/SerializablePropertyComparer.cs
@@ -1,8 +1,8 @@
/*************************************************************************
* ModernUO *
- * Copyright (C) 2019-2021 - ModernUO Development Team *
+ * Copyright 2019-2021 - ModernUO Development Team *
* Email: hi@modernuo.com *
- * File: SerializablePropertyAttribute.cs *
+ * File: SerializablePropertyComparer.cs *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
@@ -13,18 +13,30 @@
* along with this program. If not, see . *
*************************************************************************/
-using System;
+using System.Collections.Generic;
-namespace Server
+namespace SerializationGenerator
{
- ///
- /// Marks a property as serializable. Requires a call to ISerializable.MarkDirty()
- ///
- [AttributeUsage(AttributeTargets.Property)]
- public sealed class SerializablePropertyAttribute : Attribute
+ public class SerializablePropertyComparer : IComparer
{
- public int Order { get; }
+ public int Compare(SerializableProperty x, SerializableProperty y)
+ {
+ if (Equals(x, y))
+ {
+ return 0;
+ }
- public SerializablePropertyAttribute(int order) => Order = order;
+ if (Equals(null, y))
+ {
+ return 1;
+ }
+
+ if (Equals(null, x))
+ {
+ return -1;
+ }
+
+ return x.Order.CompareTo(y.Order);
+ }
}
}
diff --git a/Projects/SerializationGenerator/SerializerSyntaxReceiver.cs b/Projects/SerializationGenerator/SerializerSyntaxReceiver.cs
index fa226b51c..f61357d28 100755
--- a/Projects/SerializationGenerator/SerializerSyntaxReceiver.cs
+++ b/Projects/SerializationGenerator/SerializerSyntaxReceiver.cs
@@ -23,7 +23,7 @@ namespace SerializationGenerator
public class SerializerSyntaxReceiver : ISyntaxContextReceiver
{
#pragma warning disable RS1024
- public Dictionary> ClassAndFields { get; } = new(SymbolEqualityComparer.Default);
+ public Dictionary> ClassAndFields { get; } = new(SymbolEqualityComparer.Default);
#pragma warning restore RS1024
public static HashSet AttributeTypes { get; } = new();
@@ -39,7 +39,7 @@ namespace SerializationGenerator
if (classSymbol.GetAttributes().Any(ad => AttributeTypes.Contains(ad.AttributeClass?.ToDisplayString()) && !ClassAndFields.ContainsKey(classSymbol)))
{
- ClassAndFields.Add(classSymbol, new List());
+ ClassAndFields.Add(classSymbol, new List());
}
return;
@@ -47,25 +47,35 @@ namespace SerializationGenerator
if (context.Node is FieldDeclarationSyntax { AttributeLists: { Count: > 0 } } fieldDeclarationSyntax)
{
- foreach (VariableDeclaratorSyntax variable in fieldDeclarationSyntax.Declaration.Variables)
+ foreach (var variable in fieldDeclarationSyntax.Declaration.Variables)
{
- if (context.SemanticModel.GetDeclaredSymbol(variable) is not IFieldSymbol fieldSymbol)
+ if (context.SemanticModel.GetDeclaredSymbol(variable) is IFieldSymbol fieldSymbol)
{
- return;
+ AddFieldOrProperty(fieldSymbol);
}
+ }
+ }
+ else if (context.Node is PropertyDeclarationSyntax { AttributeLists: { Count: > 0 } } propertyDeclarationSyntax)
+ {
+ if (context.SemanticModel.GetDeclaredSymbol(propertyDeclarationSyntax) is IPropertySymbol propertySymbol)
+ {
+ AddFieldOrProperty(propertySymbol);
+ }
+ }
+ }
- if (fieldSymbol.GetAttributes().Any(ad => AttributeTypes.Contains(ad.AttributeClass?.ToDisplayString())))
- {
- var classSymbol = fieldSymbol.ContainingType;
- if (ClassAndFields.TryGetValue(classSymbol, out var fieldsList))
- {
- fieldsList.Add(fieldSymbol);
- }
- else
- {
- ClassAndFields.Add(classSymbol, new List { fieldSymbol });
- }
- }
+ private void AddFieldOrProperty(ISymbol symbol)
+ {
+ if (symbol.GetAttributes().Any(ad => AttributeTypes.Contains(ad.AttributeClass?.ToDisplayString())))
+ {
+ var classSymbol = symbol.ContainingType;
+ if (ClassAndFields.TryGetValue(classSymbol, out var fieldsList))
+ {
+ fieldsList.Add(symbol);
+ }
+ else
+ {
+ ClassAndFields.Add(classSymbol, new List { symbol });
}
}
}
diff --git a/Projects/SerializationGenerator/SourceGeneration/SourceGeneration.Namespace.cs b/Projects/SerializationGenerator/SourceGeneration/SourceGeneration.Namespace.cs
index 5f25e614b..f2871efde 100644
--- a/Projects/SerializationGenerator/SourceGeneration/SourceGeneration.Namespace.cs
+++ b/Projects/SerializationGenerator/SourceGeneration/SourceGeneration.Namespace.cs
@@ -23,19 +23,6 @@ namespace SerializationGenerator
{
public static partial class SourceGeneration
{
- public static void GenerateUsings(this StringBuilder source, IImmutableList typesUsed)
- {
- var enumerable = typesUsed
- .Select(t => t.ContainingNamespace.Name)
- .Distinct()
- .OrderByDescending(t => t);
-
- foreach (var t in enumerable)
- {
- source.Insert(0, $"using {t}{Environment.NewLine}");
- }
- }
-
public static void GenerateNamespaceStart(this StringBuilder source, string namespaceName)
{
source.AppendLine($@"namespace {namespaceName}
diff --git a/Projects/Server/Serialization/SerializableFieldAttribute.cs b/Projects/Server/Serialization/SerializableFieldAttribute.cs
index aeb7861ae..5fa4f775a 100755
--- a/Projects/Server/Serialization/SerializableFieldAttribute.cs
+++ b/Projects/Server/Serialization/SerializableFieldAttribute.cs
@@ -17,7 +17,12 @@ using System;
namespace Server
{
- [AttributeUsage(AttributeTargets.Field)]
+ ///
+ /// Hints to the source generator that this field or property should be serialized.
+ /// When used on a field, the source generator will generate the property entirely.
+ /// When used on a property, the user must call ((ISerializable)this).MarkDirty().
+ ///
+ [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public sealed class SerializableFieldAttribute : Attribute
{
public int Order { get; }
diff --git a/Projects/Server/Serialization/SerializableFieldAttributeAttribute.cs b/Projects/Server/Serialization/SerializableFieldAttributeAttribute.cs
index 6e6142a82..791ccfe1b 100644
--- a/Projects/Server/Serialization/SerializableFieldAttributeAttribute.cs
+++ b/Projects/Server/Serialization/SerializableFieldAttributeAttribute.cs
@@ -17,6 +17,12 @@ using System;
namespace Server
{
+ ///
+ /// Hints to the source generator that this field will need this attribute on the generated property
+ /// [SerializableFieldAttr("[CommandProperty(AccessLevel.GameMaster)]")]
+ /// -or-
+ /// [SerializableFieldAttr(typeof(CommandPropertyAttribute), AccessLevel.GameMaster)]
+ ///
[AttributeUsage(AttributeTargets.Field)]
public sealed class SerializableFieldAttrAttribute : Attribute
{