From a4d9a3bdc2373277efdaa951347e72081195ea55 Mon Sep 17 00:00:00 2001
From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
Date: Sun, 7 Nov 2021 13:20:11 -0800
Subject: [PATCH] Adds dictionary to codegen (#838)
* Adds Dictionary serialization rule for codegen
* Adds Tidy for Dictionary. By default will remove key/value pairs where the key or value is either null or deleted. Only works for ISerializable keys or values (or both).
---
.../Rules/DictionaryMigrationRule.cs | 235 ++++++++++++++++++
.../SerializableMigrationRulesEngine.cs | 1 +
.../SymbolMetadata/SymbolMetadata.Builtin.cs | 11 +-
Projects/Server/Utilities/Utility.cs | 40 +++
4 files changed, 285 insertions(+), 2 deletions(-)
create mode 100644 Projects/SerializationGenerator/SerializableMigration/Rules/DictionaryMigrationRule.cs
diff --git a/Projects/SerializationGenerator/SerializableMigration/Rules/DictionaryMigrationRule.cs b/Projects/SerializationGenerator/SerializableMigration/Rules/DictionaryMigrationRule.cs
new file mode 100644
index 000000000..632fa61fd
--- /dev/null
+++ b/Projects/SerializationGenerator/SerializableMigration/Rules/DictionaryMigrationRule.cs
@@ -0,0 +1,235 @@
+/*************************************************************************
+ * ModernUO *
+ * Copyright 2019-2021 - ModernUO Development Team *
+ * Email: hi@modernuo.com *
+ * File: DictionaryMigrationRule.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 *
+ * the Free Software Foundation, either version 3 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see . *
+ *************************************************************************/
+
+using System;
+using System.Collections.Immutable;
+using System.IO;
+using System.Linq;
+using System.Text;
+using Microsoft.CodeAnalysis;
+using SerializationGenerator;
+
+namespace SerializableMigration
+{
+ public class DictionaryMigrationRule : ISerializableMigrationRule
+ {
+ private const string KEY_VALUE_PAIR_DELIMITER = "----";
+ public string RuleName => nameof(DictionaryMigrationRule);
+
+ public bool GenerateRuleState(
+ Compilation compilation,
+ ISymbol symbol,
+ ImmutableArray attributes,
+ ImmutableArray serializableTypes,
+ ImmutableArray embeddedSerializableTypes,
+ ISymbol? parentSymbol,
+ out string[] ruleArguments
+ )
+ {
+ if (symbol is not INamedTypeSymbol namedTypeSymbol || !symbol.IsDictionary(compilation))
+ {
+ ruleArguments = null;
+ return false;
+ }
+
+ var keySymbolType = namedTypeSymbol.TypeArguments[0];
+
+ var serializableKeyProperty = SerializableMigrationRulesEngine.GenerateSerializableProperty(
+ compilation,
+ "KeyEntry",
+ keySymbolType,
+ 0,
+ attributes,
+ serializableTypes,
+ embeddedSerializableTypes,
+ parentSymbol,
+ null
+ );
+
+ var valueSymbolType = namedTypeSymbol.TypeArguments[1];
+
+ var serializableValueProperty = SerializableMigrationRulesEngine.GenerateSerializableProperty(
+ compilation,
+ "ValueEntry",
+ valueSymbolType,
+ 0,
+ attributes,
+ serializableTypes,
+ embeddedSerializableTypes,
+ parentSymbol,
+ null
+ );
+
+ var extraOptions = "";
+ if (attributes.Any(a => a.IsTidy(compilation)))
+ {
+ extraOptions += "@Tidy";
+ }
+
+ var keyPropertyLength = serializableKeyProperty.RuleArguments?.Length ?? 0;
+ var valuePropertyLength = serializableValueProperty.RuleArguments?.Length ?? 0;
+ ruleArguments = new string[keyPropertyLength + valuePropertyLength + 6];
+ ruleArguments[0] = extraOptions;
+ ruleArguments[1] = keySymbolType.ToDisplayString();
+ ruleArguments[2] = serializableKeyProperty.Rule;
+
+ if (keyPropertyLength > 0)
+ {
+ Array.Copy(serializableKeyProperty.RuleArguments!, 0, ruleArguments, 3, keyPropertyLength);
+ }
+
+ ruleArguments[3 + keyPropertyLength] = KEY_VALUE_PAIR_DELIMITER;
+ ruleArguments[4 + keyPropertyLength] = valueSymbolType.ToDisplayString();
+ ruleArguments[5 + keyPropertyLength] = serializableValueProperty.Rule;
+
+ if (valuePropertyLength > 0)
+ {
+ Array.Copy(serializableValueProperty.RuleArguments!, 0, ruleArguments, 6 + keyPropertyLength, valuePropertyLength);
+ }
+
+ return true;
+ }
+
+ public void GenerateDeserializationMethod(StringBuilder source, string indent, SerializableProperty property, string? parentReference)
+ {
+ var expectedRule = RuleName;
+ var ruleName = property.Rule;
+ if (expectedRule != ruleName)
+ {
+ throw new ArgumentException($"Invalid rule applied to property {ruleName}. Expecting {expectedRule}, but received {ruleName}.");
+ }
+
+ var ruleArguments = property.RuleArguments;
+
+ var keyElementRule = SerializableMigrationRulesEngine.Rules[ruleArguments![2]];
+ var valueRuleIndex = Array.IndexOf(ruleArguments, KEY_VALUE_PAIR_DELIMITER, 4);
+ if (valueRuleIndex == -1)
+ {
+ throw new InvalidDataException($"Cannot find key-value delimiter in arguments for {property.Name}");
+ }
+
+ var keyRuleArguments = new string[valueRuleIndex - 3];
+ Array.Copy(ruleArguments, 3, keyRuleArguments, 0, keyRuleArguments.Length);
+
+ var valueElementRule = SerializableMigrationRulesEngine.Rules[ruleArguments[valueRuleIndex + 2]];
+ var valueRuleArguments = new string[ruleArguments.Length - valueRuleIndex - 2];
+ Array.Copy(ruleArguments, 2 + valueRuleIndex, valueRuleArguments, 0, valueRuleArguments.Length);
+
+ var propertyName = property.Name;
+ var propertyVarPrefix = $"{char.ToLower(propertyName[0])}{propertyName.Substring(1, propertyName.Length - 1)}";
+ var propertyIndex = $"{propertyVarPrefix}Index";
+ var propertyKeyEntry = $"{propertyVarPrefix}Key";
+ var propertyValueEntry = $"{propertyVarPrefix}Value";
+ var propertyCount = $"{propertyVarPrefix}Count";
+
+ source.AppendLine($"{indent}{ruleArguments[1]} {propertyKeyEntry};");
+ source.AppendLine($"{indent}{ruleArguments[valueRuleIndex + 1]} {propertyValueEntry};");
+ source.AppendLine($"{indent}var {propertyCount} = reader.ReadEncodedInt();");
+ source.AppendLine($"{indent}{propertyName} = new System.Collections.Generic.Dictionary<{ruleArguments[1]}, {ruleArguments[valueRuleIndex + 1]}>({propertyCount});");
+ source.AppendLine($"{indent}for (var {propertyIndex} = 0; {propertyIndex} < {propertyCount}; {propertyIndex}++)");
+ source.AppendLine($"{indent}{{");
+
+ var serializableKeyElement = new SerializableProperty
+ {
+ Name = propertyKeyEntry,
+ Type = ruleArguments[1],
+ Rule = keyElementRule.RuleName,
+ RuleArguments = keyRuleArguments
+ };
+
+ keyElementRule.GenerateDeserializationMethod(source, $"{indent} ", serializableKeyElement, parentReference);
+
+ var serializableValueElement = new SerializableProperty
+ {
+ Name = propertyValueEntry,
+ Type = ruleArguments[valueRuleIndex + 1],
+ Rule = valueElementRule.RuleName,
+ RuleArguments = valueRuleArguments
+ };
+
+ valueElementRule.GenerateDeserializationMethod(source, $"{indent} ", serializableValueElement, parentReference);
+ source.AppendLine($"{indent} {propertyName}.Add({propertyKeyEntry}, {propertyValueEntry});");
+
+ source.AppendLine($"{indent}}}");
+ }
+
+ public void GenerateSerializationMethod(StringBuilder source, string indent, SerializableProperty property)
+ {
+ var expectedRule = RuleName;
+ var ruleName = property.Rule;
+ if (expectedRule != ruleName)
+ {
+ throw new ArgumentException($"Invalid rule applied to property {ruleName}. Expecting {expectedRule}, but received {ruleName}.");
+ }
+
+ var ruleArguments = property.RuleArguments;
+ var shouldTidy = ruleArguments![0].Contains("@Tidy");
+
+ var keyElementRule = SerializableMigrationRulesEngine.Rules[ruleArguments[2]];
+ var valueRuleIndex = Array.IndexOf(ruleArguments, KEY_VALUE_PAIR_DELIMITER, 3);
+ if (valueRuleIndex == -1)
+ {
+ throw new InvalidDataException($"Cannot find key-value delimiter in arguments for {property.Name}");
+ }
+
+ var keyRuleArguments = new string[valueRuleIndex - 3];
+ Array.Copy(ruleArguments, 3, keyRuleArguments, 0, keyRuleArguments.Length);
+
+ var valueElementRule = SerializableMigrationRulesEngine.Rules[ruleArguments[valueRuleIndex + 2]];
+ var valueRuleArguments = new string[ruleArguments.Length - valueRuleIndex - 2];
+ Array.Copy(ruleArguments, 2 + valueRuleIndex, valueRuleArguments, 0, valueRuleArguments.Length);
+
+ var propertyName = property.Name;
+ var propertyVarPrefix = $"{char.ToLower(propertyName[0])}{propertyName.Substring(1, propertyName.Length - 1)}";
+ var propertyKeyEntry = $"{propertyVarPrefix}Key";
+ var propertyValueEntry = $"{propertyVarPrefix}Value";
+ var propertyCount = $"{propertyVarPrefix}Count";
+
+ if (shouldTidy)
+ {
+ source.AppendLine($"{indent}{property.Name}?.Tidy();");
+ }
+ source.AppendLine($"{indent}var {propertyCount} = {property.Name}?.Count ?? 0;");
+ source.AppendLine($"{indent}writer.WriteEncodedInt({propertyCount});");
+ source.AppendLine($"{indent}if ({propertyCount} > 0)");
+ source.AppendLine($"{indent}{{");
+ source.AppendLine($"{indent} foreach (var ({propertyKeyEntry}, {propertyValueEntry}) in {property.Name}!)");
+ source.AppendLine($"{indent} {{");
+
+ var serializableKeyElement = new SerializableProperty
+ {
+ Name = propertyKeyEntry,
+ Type = ruleArguments[1],
+ Rule = keyElementRule.RuleName,
+ RuleArguments = keyRuleArguments
+ };
+
+ keyElementRule.GenerateSerializationMethod(source, $"{indent} ", serializableKeyElement);
+
+ var serializableValueElement = new SerializableProperty
+ {
+ Name = propertyValueEntry,
+ Type = ruleArguments[valueRuleIndex + 1],
+ Rule = valueElementRule.RuleName,
+ RuleArguments = valueRuleArguments
+ };
+
+ keyElementRule.GenerateSerializationMethod(source, $"{indent} ", serializableValueElement);
+
+ source.AppendLine($"{indent} }}");
+ source.AppendLine($"{indent}}}");
+ }
+ }
+}
diff --git a/Projects/SerializationGenerator/SerializableMigration/SerializableMigrationRulesEngine.cs b/Projects/SerializationGenerator/SerializableMigration/SerializableMigrationRulesEngine.cs
index 15abef66a..1fe3e32f5 100644
--- a/Projects/SerializationGenerator/SerializableMigration/SerializableMigrationRulesEngine.cs
+++ b/Projects/SerializationGenerator/SerializableMigration/SerializableMigrationRulesEngine.cs
@@ -33,6 +33,7 @@ namespace SerializableMigration
new ListMigrationRule(),
new ArrayMigrationRule(),
new HashSetMigrationRule(),
+ new DictionaryMigrationRule(),
new KeyValuePairMigrationRule(),
new PrimitiveTypeMigrationRule(),
new PrimitiveUOTypeMigrationRule(),
diff --git a/Projects/SerializationGenerator/SourceGeneration/SymbolMetadata/SymbolMetadata.Builtin.cs b/Projects/SerializationGenerator/SourceGeneration/SymbolMetadata/SymbolMetadata.Builtin.cs
index af2bc3241..28241d463 100644
--- a/Projects/SerializationGenerator/SourceGeneration/SymbolMetadata/SymbolMetadata.Builtin.cs
+++ b/Projects/SerializationGenerator/SourceGeneration/SymbolMetadata/SymbolMetadata.Builtin.cs
@@ -19,6 +19,7 @@ namespace SerializationGenerator
{
public static partial class SymbolMetadata
{
+ public const string DICTIONARY_CLASS = "System.Collections.Generic.Dictionary`2";
public const string LIST_CLASS = "System.Collections.Generic.List`1";
public const string HASHSET_CLASS = "System.Collections.Generic.HashSet`1";
public const string IPADDRESS_CLASS = "System.Net.IPAddress";
@@ -43,9 +44,9 @@ namespace SerializationGenerator
SymbolEqualityComparer.Default
) == true;
- public static bool IsList(this ISymbol symbol, Compilation compilation) =>
+ public static bool IsDictionary(this ISymbol symbol, Compilation compilation) =>
(symbol as INamedTypeSymbol)?.ConstructedFrom.Equals(
- compilation.GetTypeByMetadataName(LIST_CLASS),
+ compilation.GetTypeByMetadataName(DICTIONARY_CLASS),
SymbolEqualityComparer.Default
) == true;
@@ -55,6 +56,12 @@ namespace SerializationGenerator
SymbolEqualityComparer.Default
) == true;
+ public static bool IsList(this ISymbol symbol, Compilation compilation) =>
+ (symbol as INamedTypeSymbol)?.ConstructedFrom.Equals(
+ compilation.GetTypeByMetadataName(LIST_CLASS),
+ SymbolEqualityComparer.Default
+ ) == true;
+
public static bool IsPrimitiveFromTypeDisplayString(string type) =>
type is "bool" or "sbyte" or "short" or "int" or "long" or "byte" or "ushort"
or "uint" or "ulong" or "float" or "double" or "string" or "decimal";
diff --git a/Projects/Server/Utilities/Utility.cs b/Projects/Server/Utilities/Utility.cs
index e7cd004fb..a184d4b23 100644
--- a/Projects/Server/Utilities/Utility.cs
+++ b/Projects/Server/Utilities/Utility.cs
@@ -10,6 +10,7 @@ using System.Text;
using System.Xml;
using Microsoft.Toolkit.HighPerformance;
using Server.Buffers;
+using Server.Collections;
using Server.Random;
using Server.Text;
@@ -1188,6 +1189,45 @@ namespace Server
set.RemoveWhere(entry => entry?.Deleted != false);
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Tidy(this Dictionary dictionary)
+ {
+ var serializable = typeof(ISerializable);
+ var serializableKey = typeof(K).IsAssignableTo(serializable);
+ var serializableValue = typeof(V).IsAssignableTo(serializable);
+
+ if (!serializableKey && !serializableValue)
+ {
+ return;
+ }
+
+ using var queue = PooledRefQueue.Create();
+ foreach (var (key, value) in dictionary)
+ {
+ if (serializableKey)
+ {
+ if (key == null || ((ISerializable)key).Deleted)
+ {
+ queue.Enqueue(key);
+ }
+ }
+ else
+ {
+ if (value == null || ((ISerializable)value).Deleted)
+ {
+ queue.Enqueue(key);
+ }
+ }
+ }
+
+ while (queue.Count > 0)
+ {
+ dictionary.Remove(queue.Dequeue());
+ }
+
+ dictionary.TrimExcess();
+ }
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int NumberOfSetBits(this ulong i)
{