diff --git a/ModernUO.Serialization.Generator.Tests/DiagnosticTests.cs b/ModernUO.Serialization.Generator.Tests/DiagnosticTests.cs index ed1d9f5..8f54323 100644 --- a/ModernUO.Serialization.Generator.Tests/DiagnosticTests.cs +++ b/ModernUO.Serialization.Generator.Tests/DiagnosticTests.cs @@ -172,7 +172,7 @@ public void MarkDirty() { } } [Fact] - public void SG3009_StructWithoutDeserialize_ReportsDiagnostic() + public void SG3009_StructWithDeserialize_ReportsDiagnostic() { const string source = """ using ModernUO.Serialization; @@ -181,10 +181,14 @@ public void SG3009_StructWithoutDeserialize_ReportsDiagnostic() namespace TestNamespace { [SerializationGenerator(0)] - public partial struct MissingDeserializeStruct + public partial struct ConflictingDeserializeStruct { [SerializableField(0)] private int _value; + + public void Deserialize(IGenericReader reader) + { + } } } """; diff --git a/ModernUO.Serialization.Generator.Tests/MigrationFileDiagnosticTests.cs b/ModernUO.Serialization.Generator.Tests/MigrationFileDiagnosticTests.cs new file mode 100644 index 0000000..d2d7f18 --- /dev/null +++ b/ModernUO.Serialization.Generator.Tests/MigrationFileDiagnosticTests.cs @@ -0,0 +1,92 @@ +using ModernUO.Serialization.Generator.Tests.Helpers; +using Xunit; + +namespace ModernUO.Serialization.Generator.Tests; + +public class MigrationFileDiagnosticTests +{ + private const string Source = """ + using System; + using ModernUO.Serialization; + using Server; + + namespace Server.TestContent + { + [SerializationGenerator(1)] + public partial class MigratingItem : ISerializable + { + [SerializableField(0)] + private string _name; + + public DateTime Created { get; set; } + public Serial Serial { get; } + public bool Deleted => false; + public void Delete() { } + + private void MigrateFrom(V0Content content) + { + _name = content.Name; + } + } + } + """; + + private const string MigrationJson = """ + { + "version": 0, + "type": "Server.TestContent.MigratingItem", + "properties": [ + { + "name": "Name", + "type": "string", + "rule": "PrimitiveTypeMigrationRule" + } + ] + } + """; + + // Two files with the same class and version (e.g. nested migration folders): only one can + // win, and silently ignoring the other loses data. SG3011 makes the conflict visible. + [Fact] + public void DuplicateMigrationFile_ReportsDiagnostic() + { + var (diagnostics, _) = SourceGeneratorTestHelper.RunGenerator( + Source, + additionalTexts: + [ + ("a/Server.TestContent.MigratingItem.v0.json", MigrationJson), + ("b/Server.TestContent.MigratingItem.v0.json", MigrationJson) + ] + ); + + Assert.True( + SourceGeneratorTestHelper.HasDiagnostic(diagnostics, "SG3011"), + "A duplicate migration file for the same version must be reported." + ); + } + + // A migration file above the current version is dead data - usually a version that was + // rolled back without deleting the file. The file at the current version is the schema + // record the migration tool maintains and must not be flagged. + [Fact] + public void StaleMigrationFile_ReportsDiagnostic() + { + var currentJson = MigrationJson.Replace("\"version\": 0", "\"version\": 1"); + var staleJson = MigrationJson.Replace("\"version\": 0", "\"version\": 2"); + + var (diagnostics, _) = SourceGeneratorTestHelper.RunGenerator( + Source, + additionalTexts: + [ + ("Server.TestContent.MigratingItem.v0.json", MigrationJson), + ("Server.TestContent.MigratingItem.v1.json", currentJson), + ("Server.TestContent.MigratingItem.v2.json", staleJson) + ] + ); + + Assert.Equal( + 1, + diagnostics.Count(d => d.Id == "SG3012") + ); + } +} diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/Structs/Expected/Server.TestContent.InstanceStruct.Serialization.g.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/CustomMarkDirty/Expected/Server.TestContent.CustomDirtyItem.Serialization.g.cs similarity index 69% rename from ModernUO.Serialization.Generator.Tests/Snapshots/Structs/Expected/Server.TestContent.InstanceStruct.Serialization.g.cs rename to ModernUO.Serialization.Generator.Tests/Snapshots/CustomMarkDirty/Expected/Server.TestContent.CustomDirtyItem.Serialization.g.cs index e177af1..4453d43 100644 --- a/ModernUO.Serialization.Generator.Tests/Snapshots/Structs/Expected/Server.TestContent.InstanceStruct.Serialization.g.cs +++ b/ModernUO.Serialization.Generator.Tests/Snapshots/CustomMarkDirty/Expected/Server.TestContent.CustomDirtyItem.Serialization.g.cs @@ -11,34 +11,40 @@ namespace Server.TestContent { [System.CodeDom.Compiler.GeneratedCode("ModernUO.Serialization.Generator", "{VERSION}")] - public partial struct InstanceStruct + public partial class CustomDirtyItem { private const int SerializationVersion = 0; - public int Value + public string Name { - get => _value; + get => _name; set { - if (value != _value) + if (value != _name) { - _value = value; + _name = value; + MarkDirty(); } } } + public CustomDirtyItem(Server.Serial serial) + { + Serial = serial; + } + public virtual void Serialize(Server.IGenericWriter writer) { writer.WriteEncodedInt(SerializationVersion); - writer.Write(_value); + writer.Write(_name); } public virtual void Deserialize(Server.IGenericReader reader) { var version = reader.ReadEncodedInt(); - _value = reader.ReadInt(); + _name = reader.ReadString(); } } } diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/CustomMarkDirty/Input.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/CustomMarkDirty/Input.cs new file mode 100644 index 0000000..0ac8e9e --- /dev/null +++ b/ModernUO.Serialization.Generator.Tests/Snapshots/CustomMarkDirty/Input.cs @@ -0,0 +1,20 @@ +using System; +using ModernUO.Serialization; +using Server; + +namespace Server.TestContent +{ + [SerializationGenerator(0)] + public partial class CustomDirtyItem : ISerializable + { + [SerializableField(0)] + private string _name; + + public DateTime Created { get; set; } + public Serial Serial { get; } + public bool Deleted => false; + public void Delete() { } + + public void MarkDirty() { } + } +} diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/KeyValuePairField/Expected/Server.TestContent.KeyValuePairItem.Serialization.g.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/KeyValuePairField/Expected/Server.TestContent.KeyValuePairItem.Serialization.g.cs new file mode 100644 index 0000000..a84cf97 --- /dev/null +++ b/ModernUO.Serialization.Generator.Tests/Snapshots/KeyValuePairField/Expected/Server.TestContent.KeyValuePairItem.Serialization.g.cs @@ -0,0 +1,55 @@ +// +// This code was generated by the ModernUO Serialization Generator tool. +// Version: {VERSION} +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// + +#pragma warning disable + +namespace Server.TestContent +{ + [System.CodeDom.Compiler.GeneratedCode("ModernUO.Serialization.Generator", "{VERSION}")] + public partial class KeyValuePairItem + { + private const int SerializationVersion = 0; + + public System.Collections.Generic.KeyValuePair Selected + { + get => _selected; + set + { + if (!System.Collections.Generic.EqualityComparer>.Default.Equals(value, _selected)) + { + _selected = value; + Server.ISerializableExtensions.MarkDirty(this); + } + } + } + + public KeyValuePairItem(Server.Serial serial) + { + Serial = serial; + } + + public virtual void Serialize(Server.IGenericWriter writer) + { + writer.WriteEncodedInt(SerializationVersion); + + writer.Write(_selected.Key); + writer.Write(_selected.Value); + } + + public virtual void Deserialize(Server.IGenericReader reader) + { + var version = reader.ReadEncodedInt(); + + int _selectedKey; + string _selectedValue; + _selectedKey = reader.ReadInt(); + _selectedValue = reader.ReadString(); + _selected = new System.Collections.Generic.KeyValuePair(_selectedKey, _selectedValue); + } + } +} diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/KeyValuePairField/Input.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/KeyValuePairField/Input.cs new file mode 100644 index 0000000..9efd34f --- /dev/null +++ b/ModernUO.Serialization.Generator.Tests/Snapshots/KeyValuePairField/Input.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using ModernUO.Serialization; +using Server; + +namespace Server.TestContent +{ + [SerializationGenerator(0)] + public partial class KeyValuePairItem : ISerializable + { + [SerializableField(0)] + private KeyValuePair _selected; + + public DateTime Created { get; set; } + public Serial Serial { get; } + public bool Deleted => false; + public void Delete() { } + } +} diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/NonServerNamespace/Expected/TestContent.ExternalItem.Serialization.g.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/NonServerNamespace/Expected/TestContent.ExternalItem.Serialization.g.cs new file mode 100644 index 0000000..edefd8c --- /dev/null +++ b/ModernUO.Serialization.Generator.Tests/Snapshots/NonServerNamespace/Expected/TestContent.ExternalItem.Serialization.g.cs @@ -0,0 +1,85 @@ +// +// This code was generated by the ModernUO Serialization Generator tool. +// Version: {VERSION} +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// + +#pragma warning disable + +namespace TestContent +{ + [System.CodeDom.Compiler.GeneratedCode("ModernUO.Serialization.Generator", "{VERSION}")] + public partial class ExternalItem + { + private const int SerializationVersion = 1; + + public string Name + { + get => _name; + set + { + if (value != _name) + { + _name = value; + Server.ISerializableExtensions.MarkDirty(this); + } + } + } + + public Server.Timer RefreshTimer + { + get => _refreshTimer; + set + { + if (value != _refreshTimer) + { + _refreshTimer = value; + Server.ISerializableExtensions.MarkDirty(this); + } + } + } + + public ExternalItem(Server.Serial serial) + { + Serial = serial; + } + + ref struct V0Content + { + internal readonly string Name; + internal V0Content(Server.IGenericReader reader, TestContent.ExternalItem entity) + { + Name = reader.ReadString(); + } + } + + public virtual void Serialize(Server.IGenericWriter writer) + { + writer.WriteEncodedInt(SerializationVersion); + + writer.Write(_name); + + writer.Write(RefreshTimer?.Next ?? System.DateTime.MinValue); + } + + public virtual void Deserialize(Server.IGenericReader reader) + { + var version = reader.ReadEncodedInt(); + + if (version == 0) + { + MigrateFrom(new V0Content(reader, this)); + Server.ISerializableExtensions.MarkDirty(this); + return; + } + + _name = reader.ReadString(); + + var RefreshTimerNext = reader.ReadDateTime(); + var RefreshTimerDelay = RefreshTimerNext == System.DateTime.MinValue ? System.TimeSpan.MinValue : RefreshTimerNext - Server.Core.Now; + DeserializeRefreshTimer(RefreshTimerDelay); + } + } +} diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/NonServerNamespace/Input.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/NonServerNamespace/Input.cs new file mode 100644 index 0000000..f300329 --- /dev/null +++ b/ModernUO.Serialization.Generator.Tests/Snapshots/NonServerNamespace/Input.cs @@ -0,0 +1,32 @@ +using System; +using ModernUO.Serialization; +using Server; + +namespace TestContent +{ + [SerializationGenerator(1)] + public partial class ExternalItem : ISerializable + { + [SerializableField(0)] + private string _name; + + [SerializableField(1)] + private Timer _refreshTimer; + + [DeserializeTimerField(1)] + private void DeserializeRefreshTimer(TimeSpan delay) + { + _refreshTimer = new Timer(); + } + + public DateTime Created { get; set; } + public Serial Serial { get; } + public bool Deleted => false; + public void Delete() { } + + private void MigrateFrom(V0Content content) + { + _name = content.Name; + } + } +} diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/NonServerNamespace/TestContent.ExternalItem.v0.json b/ModernUO.Serialization.Generator.Tests/Snapshots/NonServerNamespace/TestContent.ExternalItem.v0.json new file mode 100644 index 0000000..f03b9a7 --- /dev/null +++ b/ModernUO.Serialization.Generator.Tests/Snapshots/NonServerNamespace/TestContent.ExternalItem.v0.json @@ -0,0 +1,11 @@ +{ + "version": 0, + "type": "TestContent.ExternalItem", + "properties": [ + { + "name": "Name", + "type": "string", + "rule": "PrimitiveTypeMigrationRule" + } + ] +} diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/Structs/Expected/Server.TestContent.FactoryStruct.Serialization.g.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/Structs/Expected/Server.TestContent.PlainStruct.Serialization.g.cs similarity index 83% rename from ModernUO.Serialization.Generator.Tests/Snapshots/Structs/Expected/Server.TestContent.FactoryStruct.Serialization.g.cs rename to ModernUO.Serialization.Generator.Tests/Snapshots/Structs/Expected/Server.TestContent.PlainStruct.Serialization.g.cs index 7b5306c..19a23c5 100644 --- a/ModernUO.Serialization.Generator.Tests/Snapshots/Structs/Expected/Server.TestContent.FactoryStruct.Serialization.g.cs +++ b/ModernUO.Serialization.Generator.Tests/Snapshots/Structs/Expected/Server.TestContent.PlainStruct.Serialization.g.cs @@ -11,7 +11,7 @@ namespace Server.TestContent { [System.CodeDom.Compiler.GeneratedCode("ModernUO.Serialization.Generator", "{VERSION}")] - public partial struct FactoryStruct + public partial struct PlainStruct { private const int SerializationVersion = 0; @@ -27,14 +27,14 @@ public int Value } } - public virtual void Serialize(Server.IGenericWriter writer) + public void Serialize(Server.IGenericWriter writer) { writer.WriteEncodedInt(SerializationVersion); writer.Write(_value); } - public virtual void Deserialize(Server.IGenericReader reader) + public void Deserialize(Server.IGenericReader reader) { var version = reader.ReadEncodedInt(); diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/Structs/Expected/Server.TestContent.WideRecordStruct.Serialization.g.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/Structs/Expected/Server.TestContent.WideRecordStruct.Serialization.g.cs new file mode 100644 index 0000000..71c6134 --- /dev/null +++ b/ModernUO.Serialization.Generator.Tests/Snapshots/Structs/Expected/Server.TestContent.WideRecordStruct.Serialization.g.cs @@ -0,0 +1,60 @@ +// +// This code was generated by the ModernUO Serialization Generator tool. +// Version: {VERSION} +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// + +#pragma warning disable + +namespace Server.TestContent +{ + [System.CodeDom.Compiler.GeneratedCode("ModernUO.Serialization.Generator", "{VERSION}")] + public partial record struct WideRecordStruct + { + private const int SerializationVersion = 0; + + public int Amount + { + get => _amount; + set + { + if (value != _amount) + { + _amount = value; + } + } + } + + public string Label + { + get => _label; + set + { + if (value != _label) + { + _label = value; + } + } + } + + public void Serialize(Server.IGenericWriter writer) + { + writer.WriteEncodedInt(SerializationVersion); + + writer.Write(_amount); + + writer.Write(_label); + } + + public void Deserialize(Server.IGenericReader reader) + { + var version = reader.ReadEncodedInt(); + + _amount = reader.ReadInt(); + + _label = reader.ReadString(); + } + } +} diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/Structs/Input.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/Structs/Input.cs index 829f128..68d88fa 100644 --- a/ModernUO.Serialization.Generator.Tests/Snapshots/Structs/Input.cs +++ b/ModernUO.Serialization.Generator.Tests/Snapshots/Structs/Input.cs @@ -4,25 +4,19 @@ namespace Server.TestContent { [SerializationGenerator(0)] - public partial struct FactoryStruct + public partial struct PlainStruct { [SerializableField(0)] private int _value; - - public static FactoryStruct Deserialize(IGenericReader reader) - { - return new FactoryStruct(); - } } [SerializationGenerator(0)] - public partial struct InstanceStruct + public partial record struct WideRecordStruct { [SerializableField(0)] - private int _value; + private int _amount; - public void Deserialize(IGenericReader reader) - { - } + [SerializableField(1)] + private string _label; } } diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/Structs/KnownBroken.txt b/ModernUO.Serialization.Generator.Tests/Snapshots/Structs/KnownBroken.txt deleted file mode 100644 index 9b75a96..0000000 --- a/ModernUO.Serialization.Generator.Tests/Snapshots/Structs/KnownBroken.txt +++ /dev/null @@ -1,3 +0,0 @@ -Struct emission marks Serialize/Deserialize as `virtual`, which is illegal on structs -(CS0106). The pinned output preserves the current behavior; fixing the emission should -update this snapshot and delete this marker. diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/TimerAndDirtyTracking/Expected/Server.TestContent.TrackedChild.Serialization.g.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/TimerAndDirtyTracking/Expected/Server.TestContent.TrackedChild.Serialization.g.cs index 340705e..7c128c5 100644 --- a/ModernUO.Serialization.Generator.Tests/Snapshots/TimerAndDirtyTracking/Expected/Server.TestContent.TrackedChild.Serialization.g.cs +++ b/ModernUO.Serialization.Generator.Tests/Snapshots/TimerAndDirtyTracking/Expected/Server.TestContent.TrackedChild.Serialization.g.cs @@ -60,7 +60,7 @@ public virtual void Deserialize(Server.IGenericReader reader) var version = reader.ReadInt(); var RefreshTimerNext = reader.ReadDateTime(); - var RefreshTimerDelay = RefreshTimerNext == System.DateTime.MinValue ? System.TimeSpan.MinValue : RefreshTimerNext - Core.Now; + var RefreshTimerDelay = RefreshTimerNext == System.DateTime.MinValue ? System.TimeSpan.MinValue : RefreshTimerNext - Server.Core.Now; DeserializeRefreshTimer(RefreshTimerDelay); _progress = reader.ReadInt(); diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/VersionedMigrations/Expected/Server.TestContent.MigratingItem.Serialization.g.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/VersionedMigrations/Expected/Server.TestContent.MigratingItem.Serialization.g.cs index 29ce696..04a47aa 100644 --- a/ModernUO.Serialization.Generator.Tests/Snapshots/VersionedMigrations/Expected/Server.TestContent.MigratingItem.Serialization.g.cs +++ b/ModernUO.Serialization.Generator.Tests/Snapshots/VersionedMigrations/Expected/Server.TestContent.MigratingItem.Serialization.g.cs @@ -62,7 +62,7 @@ public MigratingItem(Server.Serial serial) ref struct V0Content { internal readonly string Name; - internal V0Content(IGenericReader reader, Server.TestContent.MigratingItem entity) + internal V0Content(Server.IGenericReader reader, Server.TestContent.MigratingItem entity) { Name = reader.ReadString(); } @@ -72,7 +72,7 @@ ref struct V1Content { internal readonly string Name; internal readonly int Charges; - internal V1Content(IGenericReader reader, Server.TestContent.MigratingItem entity) + internal V1Content(Server.IGenericReader reader, Server.TestContent.MigratingItem entity) { Name = reader.ReadString(); Charges = reader.ReadInt(); diff --git a/ModernUO.Serialization.Generator.Tests/StructRecordSerializationTests.cs b/ModernUO.Serialization.Generator.Tests/StructRecordSerializationTests.cs index a5264ac..1d77e35 100644 --- a/ModernUO.Serialization.Generator.Tests/StructRecordSerializationTests.cs +++ b/ModernUO.Serialization.Generator.Tests/StructRecordSerializationTests.cs @@ -6,7 +6,7 @@ namespace ModernUO.Serialization.Generator.Tests; public class StructRecordSerializationTests { [Fact] - public void Struct_WithStaticFactory_GeneratesCorrectly() + public void Struct_GeneratesSerializeAndDeserialize() { const string source = """ using ModernUO.Serialization; @@ -19,11 +19,6 @@ public partial struct SimpleStruct { [SerializableField(0)] private int _value; - - public static SimpleStruct Deserialize(IGenericReader reader) - { - return new SimpleStruct(); - } } } """; @@ -33,13 +28,17 @@ public static SimpleStruct Deserialize(IGenericReader reader) Assert.Empty(diagnostics.Where(d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error)); Assert.NotNull(generatedSource); Assert.Contains("partial struct SimpleStruct", generatedSource); - Assert.Contains("void Serialize(", generatedSource); + Assert.Contains("public void Serialize(", generatedSource); + Assert.Contains("public void Deserialize(", generatedSource); + // Structs cannot have virtual members + Assert.DoesNotContain("virtual", generatedSource); // Struct should NOT have a serial constructor Assert.DoesNotContain("SimpleStruct(Serial serial)", generatedSource); } + // The generator emits Deserialize for value types; a user-declared one collides with it. [Fact] - public void Struct_WithInstanceDeserialize_GeneratesCorrectly() + public void Struct_WithInstanceDeserialize_ReportsDiagnostic() { const string source = """ using ModernUO.Serialization; @@ -60,31 +59,6 @@ public void Deserialize(IGenericReader reader) } """; - var (diagnostics, generatedSource) = SourceGeneratorTestHelper.RunGenerator(source); - - Assert.Empty(diagnostics.Where(d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error)); - Assert.NotNull(generatedSource); - Assert.Contains("partial struct SimpleStruct", generatedSource); - } - - [Fact] - public void Struct_WithoutDeserializeMethod_ReportsDiagnostic() - { - const string source = """ - using ModernUO.Serialization; - using Server; - - namespace TestNamespace - { - [SerializationGenerator(0)] - public partial struct BadStruct - { - [SerializableField(0)] - private int _value; - } - } - """; - var (diagnostics, _) = SourceGeneratorTestHelper.RunGenerator(source); Assert.Contains(diagnostics, d => d.Id == "SG3009"); @@ -119,7 +93,7 @@ public void MarkDirty() { } } [Fact] - public void RecordStruct_WithStaticFactory_GeneratesCorrectly() + public void RecordStruct_GeneratesCorrectly() { const string source = """ using ModernUO.Serialization; @@ -132,11 +106,6 @@ public partial record struct SimpleRecordStruct { [SerializableField(0)] private int _value; - - public static SimpleRecordStruct Deserialize(IGenericReader reader) - { - return new SimpleRecordStruct(); - } } } """; @@ -151,7 +120,7 @@ public static SimpleRecordStruct Deserialize(IGenericReader reader) } [Fact] - public void RecordStruct_WithoutDeserializeMethod_ReportsDiagnostic() + public void RecordStruct_WithStaticFactory_ReportsDiagnostic() { const string source = """ using ModernUO.Serialization; @@ -160,10 +129,15 @@ public void RecordStruct_WithoutDeserializeMethod_ReportsDiagnostic() namespace TestNamespace { [SerializationGenerator(0)] - public partial record struct BadRecordStruct + public partial record struct SimpleRecordStruct { [SerializableField(0)] private int _value; + + public static SimpleRecordStruct Deserialize(IGenericReader reader) + { + return new SimpleRecordStruct(); + } } } """; @@ -193,11 +167,6 @@ public partial struct MultiFieldStruct [SerializableField(2)] private int _z; - - public static MultiFieldStruct Deserialize(IGenericReader reader) - { - return new MultiFieldStruct(); - } } } """; diff --git a/ModernUO.Serialization.Generator/Diagnostics/DiagnosticDescriptors.cs b/ModernUO.Serialization.Generator/Diagnostics/DiagnosticDescriptors.cs index 42a870f..30c7e22 100644 --- a/ModernUO.Serialization.Generator/Diagnostics/DiagnosticDescriptors.cs +++ b/ModernUO.Serialization.Generator/Diagnostics/DiagnosticDescriptors.cs @@ -96,8 +96,8 @@ public static class DiagnosticDescriptors public static readonly DiagnosticDescriptor SG3009 = new( "SG3009", - "Struct/record must have a Deserialize method", - "'{0}' must have either a static 'Deserialize(IGenericReader)' factory method or an instance 'void Deserialize(IGenericReader)' method", + "Struct/record must not declare a Deserialize method", + "'{0}' must not declare a 'Deserialize(IGenericReader)' method or factory; the generator provides 'void Deserialize(IGenericReader)' for value types", "ModernUO.Serialization.Generator", DiagnosticSeverity.Error, true @@ -112,6 +112,24 @@ public static class DiagnosticDescriptors true ); + public static readonly DiagnosticDescriptor SG3011 = new( + "SG3011", + "Duplicate migration file ignored", + "Duplicate migration file '{0}' was ignored; another file already defines this class and version", + "ModernUO.Serialization.Generator", + DiagnosticSeverity.Warning, + true + ); + + public static readonly DiagnosticDescriptor SG3012 = new( + "SG3012", + "Stale migration file", + "Migration file for version {0} is above the current version {1} and is never read", + "ModernUO.Serialization.Generator", + DiagnosticSeverity.Warning, + true + ); + public static DiagnosticDescriptor GeneratorCrashedDiagnostic(Exception e) => new( "SG0001", diff --git a/ModernUO.Serialization.Generator/EntitySerializationGenerator.cs b/ModernUO.Serialization.Generator/EntitySerializationGenerator.cs index 121767d..555d805 100755 --- a/ModernUO.Serialization.Generator/EntitySerializationGenerator.cs +++ b/ModernUO.Serialization.Generator/EntitySerializationGenerator.cs @@ -14,9 +14,9 @@ *************************************************************************/ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; -using System.Globalization; using System.IO; using System.Text; using System.Threading; @@ -34,12 +34,11 @@ public EntitySerializationGenerator() : this(false) { } - public Dictionary Migrations { get; } = []; + // Populated concurrently: RegisterSourceOutput callbacks can run in parallel. + public ConcurrentDictionary Migrations { get; } = []; public void Initialize(IncrementalGeneratorInitializationContext context) { - var currentCulture = CultureInfo.DefaultThreadCurrentCulture; - // Gather all classes with [ModernUO.Serialization.SerializationGenerator] attribute var serializableClasses = context .SyntaxProvider @@ -65,7 +64,6 @@ public void Initialize(IncrementalGeneratorInitializationContext context) // Generate source code context.RegisterSourceOutput(classesWithMigrations, ExecuteIncremental); - CultureInfo.DefaultThreadCurrentCulture = currentCulture; } public static bool IsSerializationGeneratorSyntaxNode(SyntaxNode node, CancellationToken token) @@ -118,8 +116,8 @@ CancellationToken token return (null, [diagnostic]); } - // Validate that structs/records have deserialization capability - if (classSymbol!.IsValueType && !classSymbol.HasDeserializationCapability(compilation, out _)) + // The generator emits Deserialize for value types; a user-declared one collides with it. + if (classSymbol!.IsValueType && classSymbol.HasDeserializationCapability(compilation, out _)) { var typeName = classSymbol.Name; var diagnostic = typeNode.GenerateDiagnostic(DiagnosticDescriptors.SG3009, typeName); @@ -199,14 +197,15 @@ CancellationToken token defaultValueMethods.ToImmutable(), changedMethods.ToImmutable(), dirtyTrackingEntity, - ImmutableDictionary.Empty + ImmutableDictionary.Empty, + ImmutableArray.Empty ); return (record, []); } private static (SerializableClassRecord, Diagnostic[]) TransformToClassMigrationPairs( - ((SerializableClassRecord, Diagnostic[])?, ImmutableDictionary>) pair, + ((SerializableClassRecord, Diagnostic[])?, ImmutableDictionary) pair, CancellationToken token ) { @@ -228,7 +227,8 @@ CancellationToken token { classRecord = classRecord with { - Migrations = migs.ToImmutableDictionary() + Migrations = migs.Files.ToImmutableDictionary(), + DuplicateMigrationFiles = migs.Duplicates?.ToImmutableArray() ?? ImmutableArray.Empty }; } } @@ -236,14 +236,19 @@ CancellationToken token return (classRecord, diags); } - private static ImmutableDictionary> ToMigrationFileSet( + internal sealed record MigrationFileSet(Dictionary Files) + { + public List Duplicates { get; set; } + } + + private static ImmutableDictionary ToMigrationFileSet( ImmutableArray additionalTexts, CancellationToken token ) { token.ThrowIfCancellationRequested(); - var builder = ImmutableDictionary.CreateBuilder>(); + var builder = ImmutableDictionary.CreateBuilder(); foreach (var additionalText in additionalTexts) { @@ -258,13 +263,17 @@ CancellationToken token if (!builder.TryGetValue(className, out var classMigrationSet)) { - builder[className] = classMigrationSet = new Dictionary(); + builder[className] = classMigrationSet = new MigrationFileSet(new Dictionary()); } - // Only use the first migration file for each version to prevent silent overwrites - if (!classMigrationSet.ContainsKey(version)) + // Only use the first migration file for each version; the rest are reported (SG3011). + if (!classMigrationSet.Files.ContainsKey(version)) { - classMigrationSet[version] = additionalText; + classMigrationSet.Files[version] = additionalText; + } + else + { + (classMigrationSet.Duplicates ??= []).Add(path); } } @@ -302,6 +311,12 @@ private void ExecuteIncremental( context.CancellationToken ); + // Warnings are produced alongside successful generation; report either way. + for (var i = 0; i < (diags?.Length ?? 0); i++) + { + context.ReportDiagnostic(diags[i]); + } + if (classSource != null) { // Use arity notation for generic types to avoid invalid characters in filename @@ -315,13 +330,6 @@ private void ExecuteIncremental( Migrations[migration.Type] = migration; } } - else - { - for (var i = 0; i < diags.Length; i++) - { - context.ReportDiagnostic(diags[i]); - } - } } catch (Exception e) { diff --git a/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableClassRecord.cs b/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableClassRecord.cs index 196c117..b7f0c64 100644 --- a/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableClassRecord.cs +++ b/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableClassRecord.cs @@ -14,7 +14,8 @@ public record SerializableClassRecord( ImmutableArray<(ISymbol, AttributeData)> SerializableFieldDefault, ImmutableArray<(ISymbol, AttributeData)> SerializableFieldChanged, ISymbol? DirtyTrackingEntity, - ImmutableDictionary Migrations + ImmutableDictionary Migrations, + ImmutableArray DuplicateMigrationFiles ) { public bool IsValueType => ClassSymbol.IsValueType; diff --git a/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.Class.cs b/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.Class.cs index 3e1b1c2..a4c7e67 100644 --- a/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.Class.cs +++ b/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.Class.cs @@ -50,7 +50,8 @@ CancellationToken token defaultMethods, changedMethods, dirtyTrackingEntity, - migrations + migrations, + duplicateMigrationFiles ) = classRecord; var typeKeyword = typeNode.GetTypeKeyword(); @@ -466,6 +467,26 @@ CancellationToken token source.AppendLine(); } + var warnings = new List(); + + if (!duplicateMigrationFiles.IsDefaultOrEmpty) + { + foreach (var path in duplicateMigrationFiles) + { + warnings.Add(typeNode.GenerateDiagnostic(DiagnosticDescriptors.SG3011, path)); + } + } + + // The file at the current version is the schema record the migration tool maintains; + // anything beyond it is left over from a rolled-back version bump. + foreach (var migrationVersion in migrations.Keys) + { + if (migrationVersion > version) + { + warnings.Add(typeNode.GenerateDiagnostic(DiagnosticDescriptors.SG3012, migrationVersion, version)); + } + } + var migrationsBuilder = ImmutableArray.CreateBuilder(); for (var i = 0; i < version; i++) @@ -549,7 +570,8 @@ CancellationToken token serializableFieldSaveFlags, saveFlagMapping, saveFlagUseUlong, - saveFlagEnumCount + saveFlagEnumCount, + !isValueType ); source.AppendLine(); @@ -570,7 +592,8 @@ CancellationToken token serializableFieldSaveFlags, saveFlagMapping, saveFlagUseUlong, - saveFlagEnumCount + saveFlagEnumCount, + !isValueType ); } catch (DeserializeTimerFieldRequiredException e) @@ -661,7 +684,7 @@ CancellationToken token Properties = serializableFieldsForMigration.Length > 0 ? serializableFieldsForMigration : null } : null; - return (source.ToString(), newMigration, null); + return (source.ToString(), newMigration, warnings.ToArray()); } extension(StringBuilder source) diff --git a/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.DataStructure.cs b/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.DataStructure.cs index 30d42bc..d7dbea4 100644 --- a/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.DataStructure.cs +++ b/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.DataStructure.cs @@ -29,6 +29,12 @@ public static bool GenerateDataStructureMethods( var isList = propertyType.IsListInterface(compilation); var isCollection = propertyType.IsCollection(compilation); + // Non-collection generics (e.g. KeyValuePair) have type arguments but no Add/Clear surface. + if (!isArray && !isDictionary && !isList && !isCollection) + { + return false; + } + if (isDictionary) { var valueTypeName = namedTypeSymbol!.TypeArguments[1]; diff --git a/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.DeserializeMethod.cs b/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.DeserializeMethod.cs index 22dd144..24892dd 100644 --- a/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.DeserializeMethod.cs +++ b/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.DeserializeMethod.cs @@ -39,7 +39,8 @@ public void GenerateDeserializeMethod( SortedDictionary serializableFieldSaveFlagMethodsDictionary, Dictionary saveFlagMapping, bool saveFlagUseUlong, - int saveFlagEnumCount + int saveFlagEnumCount, + bool isVirtual = true ) { var genericReaderInterface = compilation.GetTypeByMetadataName(SymbolMetadata.GENERIC_READER_INTERFACE); @@ -50,7 +51,8 @@ int saveFlagEnumCount Accessibility.Public, isOverride, "void", - ImmutableArray.Create<(ITypeSymbol, string)>((genericReaderInterface, "reader")) + ImmutableArray.Create<(ITypeSymbol, string)>((genericReaderInterface, "reader")), + isVirtual ); var bodyIndent = $"{indent} "; diff --git a/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.Property.cs b/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.Property.cs index b5ac370..e0c7efc 100644 --- a/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.Property.cs +++ b/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.Property.cs @@ -69,7 +69,11 @@ public static void GenerateSerializableProperty( source.AppendLine($"{innerIndent}var oldValue = {fieldName};"); } - source.AppendLine($"{innerIndent}if (value != {fieldName})"); + var comparison = fieldSymbol.Type.HasInequalityOperator() + ? $"value != {fieldName}" + : $"!System.Collections.Generic.EqualityComparer<{fieldSymbol.Type.ToDisplayString()}>.Default.Equals(value, {fieldName})"; + + source.AppendLine($"{innerIndent}if ({comparison})"); source.AppendLine($"{innerIndent}{{"); source.AppendLine($"{innerIndent} {fieldName} = value;"); if (markDirtyMethod != null) diff --git a/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.SerializeMethod.cs b/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.SerializeMethod.cs index 113c4ba..74de7a2 100644 --- a/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.SerializeMethod.cs +++ b/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.SerializeMethod.cs @@ -32,7 +32,8 @@ public static void GenerateSerializeMethod( SortedDictionary serializableFieldSaveFlagMethodsDictionary, Dictionary saveFlagMapping, bool saveFlagUseUlong, - int saveFlagEnumCount + int saveFlagEnumCount, + bool isVirtual = true ) { var genericWriterInterface = compilation.GetTypeByMetadataName(SymbolMetadata.GENERIC_WRITER_INTERFACE); @@ -43,7 +44,8 @@ int saveFlagEnumCount Accessibility.Public, isOverride, "void", - ImmutableArray.Create<(ITypeSymbol, string)>((genericWriterInterface, "writer")) + ImmutableArray.Create<(ITypeSymbol, string)>((genericWriterInterface, "writer")), + isVirtual ); var bodyIndent = $"{indent} "; diff --git a/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializationEntityGeneration.ContentStruct.cs b/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializationEntityGeneration.ContentStruct.cs index bb0deff..f85f3b3 100644 --- a/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializationEntityGeneration.ContentStruct.cs +++ b/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializationEntityGeneration.ContentStruct.cs @@ -68,7 +68,7 @@ INamedTypeSymbol classSymbol source.GenerateEnumEnd($"{indent} "); } - source.AppendLine($"{indent} internal V{migration.Version}Content(IGenericReader reader, {classSymbol.ToDisplayString()} entity)"); + source.AppendLine($"{indent} internal V{migration.Version}Content(Server.IGenericReader reader, {classSymbol.ToDisplayString()} entity)"); source.AppendLine($"{indent} {{"); if (usesSaveFlags) diff --git a/ModernUO.Serialization.Generator/SerializableMigration/Rules/KeyValuePairMigrationRule.cs b/ModernUO.Serialization.Generator/SerializableMigration/Rules/KeyValuePairMigrationRule.cs index be37a33..9320db5 100644 --- a/ModernUO.Serialization.Generator/SerializableMigration/Rules/KeyValuePairMigrationRule.cs +++ b/ModernUO.Serialization.Generator/SerializableMigration/Rules/KeyValuePairMigrationRule.cs @@ -161,9 +161,17 @@ private static void GenerateDeserialize( string[] valueRuleArguments ) { + // Sub-rules assign to the property name they are given, so declare uniquely named + // locals for them to fill; multiple pair fields in one method must not collide. + var keyVariable = $"{propertyName.Replace(".", "")}Key"; + var valueVariable = $"{propertyName.Replace(".", "")}Value"; + + source.AppendLine($"{indent}{keyType} {keyVariable};"); + source.AppendLine($"{indent}{valueType} {valueVariable};"); + var serializableKeyProperty = new SerializableProperty { - Name = "key", + Name = keyVariable, Type = keyType, Rule = keyRule.RuleName, RuleArguments = keyRuleArguments @@ -179,7 +187,7 @@ string[] valueRuleArguments var serializableValueProperty = new SerializableProperty { - Name = "value", + Name = valueVariable, Type = valueType, Rule = valueRule.RuleName, RuleArguments = valueRuleArguments @@ -194,7 +202,7 @@ string[] valueRuleArguments ); source.AppendLine( - $"{indent}{propertyName} = new System.Collections.Generic.KeyValuePair<{keyType}, {valueType}>(key, value);" + $"{indent}{propertyName} = new System.Collections.Generic.KeyValuePair<{keyType}, {valueType}>({keyVariable}, {valueVariable});" ); } diff --git a/ModernUO.Serialization.Generator/SerializableMigration/Rules/TimerMigrationRule.cs b/ModernUO.Serialization.Generator/SerializableMigration/Rules/TimerMigrationRule.cs index fb07a4c..f210322 100644 --- a/ModernUO.Serialization.Generator/SerializableMigration/Rules/TimerMigrationRule.cs +++ b/ModernUO.Serialization.Generator/SerializableMigration/Rules/TimerMigrationRule.cs @@ -77,7 +77,7 @@ public override void GenerateDeserializationMethod( var readTimer = driftTimer ? "reader.ReadDeltaTime()" : "reader.ReadDateTime()"; var useVar = isMigration ? "" : "var "; source.AppendLine($"{indent}{useVar}{propertyName}Next = {readTimer};"); - source.AppendLine($"{indent}{useVar}{propertyName}Delay = {propertyName}Next == System.DateTime.MinValue ? System.TimeSpan.MinValue : {propertyName}Next - Core.Now;"); + source.AppendLine($"{indent}{useVar}{propertyName}Delay = {propertyName}Next == System.DateTime.MinValue ? System.TimeSpan.MinValue : {propertyName}Next - Server.Core.Now;"); } public override void GenerateSerializationMethod(StringBuilder source, string indent, SerializableProperty property) diff --git a/ModernUO.Serialization.Generator/SourceGeneration/SourceGeneration.Method.cs b/ModernUO.Serialization.Generator/SourceGeneration/SourceGeneration.Method.cs index 8aa44b8..91c5387 100644 --- a/ModernUO.Serialization.Generator/SourceGeneration/SourceGeneration.Method.cs +++ b/ModernUO.Serialization.Generator/SourceGeneration/SourceGeneration.Method.cs @@ -25,10 +25,11 @@ public static partial class SourceGeneration { public void GenerateMethodStart( string indent, string methodName, Accessibility accessors, bool isOverride, - string returnType, ImmutableArray<(ITypeSymbol, string)> parameters + string returnType, ImmutableArray<(ITypeSymbol, string)> parameters, bool isVirtual = true ) { - source.Append($"{indent}{accessors.ToFriendlyString()}{(isOverride ? " override" : " virtual")} {returnType} {methodName}("); + var modifier = isOverride ? " override" : isVirtual ? " virtual" : ""; + source.Append($"{indent}{accessors.ToFriendlyString()}{modifier} {returnType} {methodName}("); source.GenerateSignatureArguments(parameters); source.AppendLine($")\n{indent}{{"); } diff --git a/ModernUO.Serialization.Generator/SourceGeneration/SymbolMetadata/SymbolMetadata.UO.cs b/ModernUO.Serialization.Generator/SourceGeneration/SymbolMetadata/SymbolMetadata.UO.cs index ff39fa1..410d7b1 100644 --- a/ModernUO.Serialization.Generator/SourceGeneration/SymbolMetadata/SymbolMetadata.UO.cs +++ b/ModernUO.Serialization.Generator/SourceGeneration/SymbolMetadata/SymbolMetadata.UO.cs @@ -271,7 +271,7 @@ m.DeclaredAccessibility is Accessibility.Public or Accessibility.Internal { var dirtyTrackingEntityName = dirtyTrackingEntity != null ? $"{dirtyTrackingEntity}{(dirtyCanBeNull ? "?" : "")}." : ""; - return $"{dirtyTrackingEntityName}{markDirtyMethod.ToDisplayString()}()"; + return $"{dirtyTrackingEntityName}{markDirtyMethod.Name}()"; } return isSerializable switch @@ -281,6 +281,37 @@ m.DeclaredAccessibility is Accessibility.Public or Accessibility.Internal }; } + /// + /// True when value != field compiles for this type: reference types, enums, + /// primitives, type parameters constrained to a class, and types declaring + /// op_Inequality. Everything else compares via EqualityComparer. + /// + public bool HasInequalityOperator() + { + if (symbol.IsReferenceType || symbol.TypeKind is TypeKind.Enum or TypeKind.Pointer) + { + return true; + } + + if (symbol.SpecialType != SpecialType.None) + { + return true; + } + + if (symbol is ITypeParameterSymbol typeParameter) + { + return typeParameter.HasReferenceTypeConstraint; + } + + // Nullable lifts T's operators. + if (symbol is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } nullable) + { + return nullable.TypeArguments[0].HasInequalityOperator(); + } + + return symbol.GetMembers("op_Inequality").Length > 0; + } + public bool HasPublicSerializeMethod(Compilation compilation) { var genericWriterInterface = compilation.GetTypeByMetadataName(GENERIC_WRITER_INTERFACE);