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 {