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);