From 348d8cf7e30212728b77007c1f578496cdbfb73c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 23:05:53 +0000 Subject: [PATCH 1/2] Initial plan From e16ee191c07e4d34a1c1b09c0cc92fdb9a3196d1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 23:43:19 +0000 Subject: [PATCH 2/2] Changes before error encountered Co-authored-by: LeftofZen <7483209+LeftofZen@users.noreply.github.com> --- Common/CommonJsonContext.cs | 7 +++++++ Common/JsonFile.cs | 3 +++ Common/Logging/Logger.cs | 2 ++ Common/Logging/ReflectionLogger.cs | 3 +++ Common/VersionHelpers.cs | 2 +- Dat/FileParsing/AttributeHelper.cs | 7 ++++--- Dat/FileParsing/ByteHelpers.cs | 3 ++- Dat/FileParsing/ByteReader.cs | 12 +++++++++--- Dat/FileParsing/ByteWriter.cs | 5 ++++- Dat/FileParsing/ObjectAttributes.cs | 3 ++- Dat/FileParsing/SawyerStreamReader.cs | 7 +++++++ Dat/FileParsing/SawyerStreamWriter.cs | 5 +++++ Dat/Loaders/SoundObjectLoader.cs | 3 +++ .../Objects/Building/BuildingObject.cs | 2 ++ .../Objects/Common/BuildingComponents.cs | 3 +++ .../Objects/Industry/IndustryObject.cs | 6 ++++++ .../Objects/Scaffolding/ScaffoldingObject.cs | 3 +++ .../ObjectModels/Objects/Steam/SteamObject.cs | 3 +++ .../Objects/StreetLight/StreetLightObject.cs | 3 +++ .../Objects/TownNames/TownNamesObject.cs | 3 +++ .../Serialization/LocoObjectJsonConverter.cs | 4 ++++ .../ObjectModels/Validation/CountEqualTo.cs | 2 ++ Definitions/Web/Client.cs | 10 ++++++++++ Definitions/Web/ClientHelpers.cs | 5 +++++ Gui/EditorSettings.cs | 4 ++++ Gui/Gui.csproj | 1 + Gui/Models/ObjectEditorContext.cs | 6 ++++++ Gui/ViewLocator.cs | 3 +++ Gui/ViewModels/AudioViewModel.cs | 3 +++ Gui/ViewModels/Filters/FilterViewModel.cs | 14 +++++++++++--- Gui/ViewModels/FolderTreeViewModel.cs | 1 + Gui/ViewModels/Graphics/ImageTableViewModel.cs | 2 ++ Index/ObjectIndex.cs | 5 +++++ build.sh | 6 +++--- 34 files changed, 135 insertions(+), 16 deletions(-) create mode 100644 Common/CommonJsonContext.cs diff --git a/Common/CommonJsonContext.cs b/Common/CommonJsonContext.cs new file mode 100644 index 00000000..9ed8e52b --- /dev/null +++ b/Common/CommonJsonContext.cs @@ -0,0 +1,7 @@ +using System.Text.Json.Serialization; + +namespace Common; + +[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.Unspecified)] +[JsonSerializable(typeof(VersionCheckBody))] +internal partial class CommonJsonContext : JsonSerializerContext { } diff --git a/Common/JsonFile.cs b/Common/JsonFile.cs index 4a901d0c..d7bdbb3e 100644 --- a/Common/JsonFile.cs +++ b/Common/JsonFile.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; @@ -14,6 +15,7 @@ public static class JsonFile AllowTrailingCommas = true }; + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")] public static async Task SerializeToFileAsync(T obj, string filePath, JsonSerializerOptions? options = null) { await using (var fileStream = new FileStream( @@ -28,6 +30,7 @@ public static async Task SerializeToFileAsync(T obj, string filePath, JsonSer } } + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")] public static async Task DeserializeFromFileAsync(string filePath, JsonSerializerOptions? options = null) { await using (var fileStream = new FileStream( diff --git a/Common/Logging/Logger.cs b/Common/Logging/Logger.cs index b10fe727..79963dbd 100644 --- a/Common/Logging/Logger.cs +++ b/Common/Logging/Logger.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; namespace Common.Logging; @@ -10,6 +11,7 @@ public class Logger : ILogger public event EventHandler? LogAdded; + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "StackFrame.GetMethod() is used only for diagnostic logging of the caller's class name and is safe to call even if metadata is trimmed.")] public void Log(LogLevel level, string message, string callerMemberName = "", string sourceFilePath = "", int sourceLineNumber = 0) { // Get the class name using reflection diff --git a/Common/Logging/ReflectionLogger.cs b/Common/Logging/ReflectionLogger.cs index 4e8a0c0a..97c0a606 100644 --- a/Common/Logging/ReflectionLogger.cs +++ b/Common/Logging/ReflectionLogger.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text; @@ -5,12 +6,14 @@ namespace Common.Logging; public static class ReflectionLogger { + [RequiresUnreferencedCode("ReflectionLogger uses reflection to enumerate public properties and fields of objects, which may be trimmed.")] public static string ToString(T obj) { var sb = new StringBuilder(); return ToString(obj, sb).ToString(); } + [RequiresUnreferencedCode("ReflectionLogger uses reflection to enumerate public properties and fields of objects, which may be trimmed.")] static StringBuilder ToString(T obj, StringBuilder sb) { if (obj == null) diff --git a/Common/VersionHelpers.cs b/Common/VersionHelpers.cs index 0fe8b150..106fe06b 100644 --- a/Common/VersionHelpers.cs +++ b/Common/VersionHelpers.cs @@ -120,7 +120,7 @@ public static SemanticVersion GetLatestAppVersion(string productName, SemanticVe if (response.IsSuccessStatusCode) { var jsonResponse = response.Content.ReadAsStringAsync().Result; - var body = JsonSerializer.Deserialize(jsonResponse); + var body = JsonSerializer.Deserialize(jsonResponse, CommonJsonContext.Default.VersionCheckBody); var versionText = body?.TagName; return GetVersionFromText(versionText); } diff --git a/Dat/FileParsing/AttributeHelper.cs b/Dat/FileParsing/AttributeHelper.cs index 8928e3be..f80c3a77 100644 --- a/Dat/FileParsing/AttributeHelper.cs +++ b/Dat/FileParsing/AttributeHelper.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Reflection; namespace Dat.FileParsing; @@ -16,15 +17,15 @@ public static class AttributeHelper return attributes.Length == 1 ? attributes[0] as T : null; } - public static T? Get(Type t) where T : Attribute + public static T? Get([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type t) where T : Attribute => t.GetCustomAttribute(inherit: false); public static bool Has(PropertyInfo p) where T : Attribute => p.GetCustomAttribute(inherit: false) is not null; - public static bool Has(Type t) where T : Attribute + public static bool Has([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type t) where T : Attribute => t.GetCustomAttribute(inherit: false) is not null; - public static IEnumerable GetAllPropertiesWithAttribute(Type t) where T : Attribute + public static IEnumerable GetAllPropertiesWithAttribute([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type t) where T : Attribute => t.GetProperties().Where(Has); } diff --git a/Dat/FileParsing/ByteHelpers.cs b/Dat/FileParsing/ByteHelpers.cs index cd8eb8c4..b145f181 100644 --- a/Dat/FileParsing/ByteHelpers.cs +++ b/Dat/FileParsing/ByteHelpers.cs @@ -1,10 +1,11 @@ +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; namespace Dat.FileParsing; public static class ByteHelpers { - public static int GetObjectSize(Type type) + public static int GetObjectSize([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type) { var size = 0; if (type == typeof(byte) || type == typeof(sbyte)) diff --git a/Dat/FileParsing/ByteReader.cs b/Dat/FileParsing/ByteReader.cs index 351c8cec..b3f77d31 100644 --- a/Dat/FileParsing/ByteReader.cs +++ b/Dat/FileParsing/ByteReader.cs @@ -1,11 +1,13 @@ using Dat.Types; using Definitions.ObjectModels; +using System.Diagnostics.CodeAnalysis; namespace Dat.FileParsing; public static class ByteReader { - public static object ReadT(ReadOnlySpan data, Type t, int offset, int arrLength = 0, bool isVariableLoad = false) + [RequiresUnreferencedCode("ReadT uses reflection-based type handling and may call ReadLocoStruct which requires unreferenced code.")] + public static object ReadT(ReadOnlySpan data, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type t, int offset, int arrLength = 0, bool isVariableLoad = false) { if (t == typeof(uint8_t)) { @@ -178,10 +180,12 @@ static object ReadEnum(ReadOnlySpan data, Type t, int offset) } } + [RequiresUnreferencedCode("ReadLocoStruct uses reflection to read public properties and invoke constructors of the target type, which may be trimmed.")] public static T ReadLocoStruct(ReadOnlySpan data) where T : class => (T)ReadLocoStruct(data, typeof(T)); - public static ILocoStruct ReadLocoStruct(ReadOnlySpan data, Type t) + [RequiresUnreferencedCode("ReadLocoStruct uses reflection to read public properties and invoke constructors of the target type, which may be trimmed.")] + public static ILocoStruct ReadLocoStruct(ReadOnlySpan data, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicConstructors)] Type t) { var properties = t.GetProperties(); var args = new List(); @@ -258,7 +262,8 @@ public static ILocoStruct ReadLocoStruct(ReadOnlySpan data, Type t) return (ILocoStruct?)Activator.CreateInstance(t, [.. args]) ?? throw new InvalidDataException("couldn't parse"); } - public static IList ReadLocoStructArray(ReadOnlySpan data, Type t, int count, int structSize) // could get struct size from attribute, but easier just to pass in + [RequiresUnreferencedCode("ReadLocoStructArray uses reflection to read public properties and invoke constructors of the target type, which may be trimmed.")] + public static IList ReadLocoStructArray(ReadOnlySpan data, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicConstructors)] Type t, int count, int structSize) // could get struct size from attribute, but easier just to pass in { // cannot use ReadOnlySpan with yield return :| var list = new List(); @@ -285,6 +290,7 @@ public static IList ReadLocoStructArray(ReadOnlySpan data, Ty // return list; //} + [RequiresUnreferencedCode("ReadLocoStructArray uses reflection to read public properties and invoke constructors of the target type, which may be trimmed.")] public static IList ReadLocoStructArray(ReadOnlySpan data, int count, int structSize) where T : class { // cannot use ReadOnlySpan with yield return :| diff --git a/Dat/FileParsing/ByteWriter.cs b/Dat/FileParsing/ByteWriter.cs index 1aecdd0c..9a40c093 100644 --- a/Dat/FileParsing/ByteWriter.cs +++ b/Dat/FileParsing/ByteWriter.cs @@ -1,10 +1,12 @@ using Definitions.ObjectModels; +using System.Diagnostics.CodeAnalysis; namespace Dat.FileParsing; public static class ByteWriter { - public static void WriteT(Span data, Type t, int offset, object val) + [RequiresUnreferencedCode("WriteT uses reflection-based type handling and may call WriteLocoStruct which requires unreferenced code.")] + public static void WriteT(Span data, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type t, int offset, object val) { if (t == typeof(uint8_t)) { @@ -76,6 +78,7 @@ public static void WriteT(Span data, Type t, int offset, object val) } } + [RequiresUnreferencedCode("WriteLocoStruct uses reflection to enumerate public properties of the object type, which may be trimmed.")] public static ReadOnlySpan WriteLocoStruct(ILocoStruct obj) { ArgumentNullException.ThrowIfNull(obj); diff --git a/Dat/FileParsing/ObjectAttributes.cs b/Dat/FileParsing/ObjectAttributes.cs index 8fede182..f6e17647 100644 --- a/Dat/FileParsing/ObjectAttributes.cs +++ b/Dat/FileParsing/ObjectAttributes.cs @@ -1,11 +1,12 @@ using Dat.Data; using Definitions.ObjectModels; +using System.Diagnostics.CodeAnalysis; namespace Dat.FileParsing; public static class ObjectAttributes { - public static int StructSize() where T : ILocoStruct + public static int StructSize<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>() where T : ILocoStruct => AttributeHelper.Get(typeof(T))!.Size; //public static ObjectType ObjectType() // where T : ILocoStruct diff --git a/Dat/FileParsing/SawyerStreamReader.cs b/Dat/FileParsing/SawyerStreamReader.cs index 628f7360..8a0f0d48 100644 --- a/Dat/FileParsing/SawyerStreamReader.cs +++ b/Dat/FileParsing/SawyerStreamReader.cs @@ -8,6 +8,7 @@ using Definitions.ObjectModels.Graphics; using Definitions.ObjectModels.Objects.Sound; using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; using System.Text; namespace Dat.FileParsing; @@ -94,18 +95,21 @@ public static bool TryGetHeadersFromBytes(Stream stream, out (S5Header S5, Objec return true; } + [RequiresUnreferencedCode("LoadFullObject calls ValidateLocoStruct which uses DataAnnotations validation requiring reflection.")] public static (DatHeaderInfo DatFileInfo, LocoObject? LocoObject) LoadFullObject(string filename, ILogger logger, bool loadExtra = true) { using var fs = new FileStream(filename, FileMode.Open); return LoadFullObject(fs, logger, filename, loadExtra); } + [RequiresUnreferencedCode("LoadFullObject calls ValidateLocoStruct which uses DataAnnotations validation requiring reflection.")] public static (DatHeaderInfo DatFileInfo, LocoObject? LocoObject) LoadFullObject(byte[] data, ILogger logger, string filename = "", bool loadExtra = true) { using var ms = new MemoryStream(data); return LoadFullObject(ms, logger, filename: filename, loadExtra: loadExtra); } + [RequiresUnreferencedCode("LoadFullObject calls ValidateLocoStruct which uses DataAnnotations validation requiring reflection.")] public static (DatHeaderInfo DatFileInfo, LocoObject? LocoObject) LoadFullObject(Stream stream, ILogger logger, string filename, bool loadExtra = true) { logger.Info($"Full-loading \"{filename}\" with loadExtra={loadExtra}"); @@ -135,6 +139,7 @@ public static (DatHeaderInfo DatFileInfo, LocoObject? LocoObject) LoadFullObject } } + [RequiresUnreferencedCode("ValidateLocoStruct uses DataAnnotations validation which requires reflection and may not work correctly when trimming application code.")] static void ValidateLocoStruct(S5Header s5Header, ILocoStruct locoStruct, ILogger? logger) { var warnings = new List(); @@ -240,6 +245,7 @@ public static StringTable ReadStringTableStream(Stream stream, string[] stringNa } } + [RequiresUnreferencedCode("ReadImageTable calls ReadLocoStruct which uses reflection-based parsing requiring unreferenced code.")] public static (G1Header Header, List Table) ReadImageTable(LocoBinaryReader br) { if (br.BaseStream.Length - br.BaseStream.Position < ObjectAttributes.StructSize()) @@ -416,6 +422,7 @@ public static byte[] Decode(SawyerEncoding encoding, ReadOnlySpan data, in _ => throw new InvalidDataException("Unknown chunk encoding scheme"), }; + [RequiresUnreferencedCode("ReadChunk calls ReadLocoStruct which uses reflection-based parsing requiring unreferenced code.")] public static T ReadChunk(ref ReadOnlySpan data) where T : class => ByteReader.ReadLocoStruct(ReadChunkCore(ref data)); diff --git a/Dat/FileParsing/SawyerStreamWriter.cs b/Dat/FileParsing/SawyerStreamWriter.cs index 795463fb..c289a382 100644 --- a/Dat/FileParsing/SawyerStreamWriter.cs +++ b/Dat/FileParsing/SawyerStreamWriter.cs @@ -9,6 +9,7 @@ using Definitions.ObjectModels.Objects.Sound; using Definitions.ObjectModels.Types; using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; using System.Text; namespace Dat.FileParsing; @@ -81,6 +82,7 @@ public static byte[] SaveSoundEffectsToCSS(List<(SoundEffectWaveFormat locoWaveH } } + [RequiresUnreferencedCode("SaveMusicToDat calls WriteLocoStruct which uses reflection-based serialization requiring unreferenced code.")] public static byte[] SaveMusicToDat(DatMusicWaveFormat header, byte[] data) { using (var ms = new MemoryStream()) @@ -96,6 +98,7 @@ public static byte[] SaveMusicToDat(DatMusicWaveFormat header, byte[] data) } } + [RequiresUnreferencedCode("Save calls WriteLocoObject which uses DataAnnotations validation and WriteLocoStruct requiring reflection.")] public static void Save(string filename, string objName, ObjectSource objectSource, SawyerEncoding encoding, LocoObject locoObject, ILogger logger, bool allowWritingAsVanilla) { ArgumentNullException.ThrowIfNull(locoObject); @@ -369,6 +372,7 @@ public static byte[] EncodeRLEImageData(DatG1ElementFlags flags, byte[] imageDat return ms.ToArray(); } + [RequiresUnreferencedCode("WriteChunk calls WriteLocoStruct which uses reflection-based serialization requiring unreferenced code.")] public static ReadOnlySpan WriteChunk(ILocoStruct str, SawyerEncoding encoding) => WriteChunkCore(ByteWriter.WriteLocoStruct(str), encoding); @@ -490,6 +494,7 @@ public static void WriteLocoObject(Stream stream, LocoObject obj) } } + [RequiresUnreferencedCode("WriteLocoObject uses DataAnnotations validation (Validator.TryValidateObject) which requires reflection and may not work correctly when trimming application code.")] public static MemoryStream WriteLocoObject(string objName, ObjectType objectType, ObjectSource objectSource, SawyerEncoding encoding, ILogger logger, LocoObject obj, bool allowWritingAsVanilla) { var validationResults = new List(); diff --git a/Dat/Loaders/SoundObjectLoader.cs b/Dat/Loaders/SoundObjectLoader.cs index d4eee145..ec7c2b44 100644 --- a/Dat/Loaders/SoundObjectLoader.cs +++ b/Dat/Loaders/SoundObjectLoader.cs @@ -7,6 +7,7 @@ using Definitions.ObjectModels.Types; using System.ComponentModel; using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; namespace Dat.Loaders; @@ -160,6 +161,7 @@ internal record DatSoundObject( uint pcmDataLength; byte[] unkData; + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "LoadVariable calls ReadLocoStruct which uses reflection. The required types (DatSoundObjectData) are always statically referenced and preserved.")] public ReadOnlySpan LoadVariable(ReadOnlySpan remainingData) { // unknown structs @@ -183,6 +185,7 @@ public ReadOnlySpan LoadVariable(ReadOnlySpan remainingData) return remainingData[remainingData.Length..]; } + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "SaveVariable calls WriteLocoStruct which uses reflection. The required types (DatSoundObjectData) are always statically referenced and preserved.")] public ReadOnlySpan SaveVariable() { using (var ms = new MemoryStream()) diff --git a/Definitions/ObjectModels/Objects/Building/BuildingObject.cs b/Definitions/ObjectModels/Objects/Building/BuildingObject.cs index 25edfc6c..47f0f159 100644 --- a/Definitions/ObjectModels/Objects/Building/BuildingObject.cs +++ b/Definitions/ObjectModels/Objects/Building/BuildingObject.cs @@ -41,7 +41,9 @@ public static class Constants public IEnumerable Validate(ValidationContext validationContext) { +#pragma warning disable IL2026 // ValidationContext constructor uses reflection; this usage is safe as the target types are always present at runtime. var bcValidationContext = new ValidationContext(BuildingComponents); +#pragma warning restore IL2026 foreach (var result in BuildingComponents.Validate(bcValidationContext)) { yield return result; diff --git a/Definitions/ObjectModels/Objects/Common/BuildingComponents.cs b/Definitions/ObjectModels/Objects/Common/BuildingComponents.cs index 045ec00b..5057113f 100644 --- a/Definitions/ObjectModels/Objects/Common/BuildingComponents.cs +++ b/Definitions/ObjectModels/Objects/Common/BuildingComponents.cs @@ -2,12 +2,14 @@ using Definitions.ObjectModels.Validation; using System.ComponentModel; using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; namespace Definitions.ObjectModels.Objects.Common; [TypeConverter(typeof(ExpandableObjectConverter))] public class BuildingComponents : ILocoStruct { +#pragma warning disable IL2026 // LengthAttribute constructor uses reflection to get 'Count' on non-ICollection types; our properties use List which implements ICollection so Count is preserved. [Length(1, BuildingObject.Constants.MaxAnimationsCount)] [CountEqualTo(nameof(BuildingAnimations))] public List BuildingHeights { get; set; } = []; @@ -18,6 +20,7 @@ public class BuildingComponents : ILocoStruct [Length(1, BuildingObject.Constants.MaxVariationsCount)] public List> BuildingVariations { get; set; } = []; +#pragma warning restore IL2026 public IEnumerable Validate(ValidationContext validationContext) { diff --git a/Definitions/ObjectModels/Objects/Industry/IndustryObject.cs b/Definitions/ObjectModels/Objects/Industry/IndustryObject.cs index 1f957f34..4a1a4f40 100644 --- a/Definitions/ObjectModels/Objects/Industry/IndustryObject.cs +++ b/Definitions/ObjectModels/Objects/Industry/IndustryObject.cs @@ -9,8 +9,10 @@ public class IndustryObject : ILocoStruct, IHasBuildingComponents { public uint32_t FarmImagesPerGrowthStage { get; set; } public BuildingComponents BuildingComponents { get; set; } = new(); +#pragma warning disable IL2026 // LengthAttribute constructor uses reflection to get 'Count' on non-ICollection types; our properties use List which implements ICollection so Count is preserved. [Length(4, 4)] public List> AnimationSequences { get; set; } = []; // Access with getAnimationSequence helper method +#pragma warning restore IL2026 public List var_38 { get; set; } = []; // Access with getUnk38 helper method public uint8_t MinNumBuildings { get; set; } public uint8_t MaxNumBuildings { get; set; } @@ -25,6 +27,7 @@ public class IndustryObject : ILocoStruct, IHasBuildingComponents public uint8_t ScaffoldingSegmentType { get; set; } public Colour ScaffoldingColour { get; set; } +#pragma warning disable IL2026 // LengthAttribute constructor uses reflection to get 'Count' on non-ICollection types; our properties use List which implements ICollection so Count is preserved. [Length(2, 2)] public List InitialProductionRate { get; set; } = []; // 2 entries, min and max production rate @@ -33,6 +36,7 @@ public class IndustryObject : ILocoStruct, IHasBuildingComponents [Length(3, 3)] public List RequiredCargo { get; set; } = []; // Cargo required by this industry +#pragma warning restore IL2026 public Colour MapColour { get; set; } public IndustryObjectFlags Flags { get; set; } @@ -50,7 +54,9 @@ public class IndustryObject : ILocoStruct, IHasBuildingComponents public IEnumerable Validate(ValidationContext validationContext) { +#pragma warning disable IL2026 // ValidationContext constructor uses reflection; this usage is safe as the target types are always present at runtime. var bcValidationContext = new ValidationContext(BuildingComponents); +#pragma warning restore IL2026 foreach (var result in BuildingComponents.Validate(bcValidationContext)) { yield return result; diff --git a/Definitions/ObjectModels/Objects/Scaffolding/ScaffoldingObject.cs b/Definitions/ObjectModels/Objects/Scaffolding/ScaffoldingObject.cs index cb7238f2..48c9b302 100644 --- a/Definitions/ObjectModels/Objects/Scaffolding/ScaffoldingObject.cs +++ b/Definitions/ObjectModels/Objects/Scaffolding/ScaffoldingObject.cs @@ -1,14 +1,17 @@ using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; namespace Definitions.ObjectModels.Objects.Scaffolding; public class ScaffoldingObject : ILocoStruct { +#pragma warning disable IL2026 // LengthAttribute constructor uses reflection to get 'Count' on non-ICollection types; our properties use List which implements ICollection so Count is preserved. [Length(3, 3)] public List SegmentHeights { get; set; } = []; [Length(3, 3)] public List RoofHeights { get; set; } = []; +#pragma warning restore IL2026 public IEnumerable Validate(ValidationContext validationContext) => []; diff --git a/Definitions/ObjectModels/Objects/Steam/SteamObject.cs b/Definitions/ObjectModels/Objects/Steam/SteamObject.cs index 559886d8..ef1d9dcc 100644 --- a/Definitions/ObjectModels/Objects/Steam/SteamObject.cs +++ b/Definitions/ObjectModels/Objects/Steam/SteamObject.cs @@ -1,5 +1,6 @@ using Definitions.ObjectModels.Types; using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; namespace Definitions.ObjectModels.Objects.Steam; @@ -14,8 +15,10 @@ public class SteamObject : ILocoStruct public List FrameInfoType0 { get; set; } = []; public List FrameInfoType1 { get; set; } = []; +#pragma warning disable IL2026 // LengthAttribute constructor uses reflection to get 'Count' on non-ICollection types; our properties use List which implements ICollection so Count is preserved. [Length(9, 9)] public List SoundEffects { get; set; } = []; +#pragma warning restore IL2026 public IEnumerable Validate(ValidationContext validationContext) => []; diff --git a/Definitions/ObjectModels/Objects/StreetLight/StreetLightObject.cs b/Definitions/ObjectModels/Objects/StreetLight/StreetLightObject.cs index 42fa6910..6acb258a 100644 --- a/Definitions/ObjectModels/Objects/StreetLight/StreetLightObject.cs +++ b/Definitions/ObjectModels/Objects/StreetLight/StreetLightObject.cs @@ -1,11 +1,14 @@ using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; namespace Definitions.ObjectModels.Objects.Streetlight; public class StreetLightObject : ILocoStruct { +#pragma warning disable IL2026 // LengthAttribute constructor uses reflection to get 'Count' on non-ICollection types; our properties use List which implements ICollection so Count is preserved. [Length(3, 3)] public List DesignedYears { get; set; } = []; +#pragma warning restore IL2026 public IEnumerable Validate(ValidationContext validationContext) => []; diff --git a/Definitions/ObjectModels/Objects/TownNames/TownNamesObject.cs b/Definitions/ObjectModels/Objects/TownNames/TownNamesObject.cs index b67dce41..3a00a576 100644 --- a/Definitions/ObjectModels/Objects/TownNames/TownNamesObject.cs +++ b/Definitions/ObjectModels/Objects/TownNames/TownNamesObject.cs @@ -1,11 +1,14 @@ using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; namespace Definitions.ObjectModels.Objects.TownNames; public class TownNamesObject : ILocoStruct { +#pragma warning disable IL2026 // LengthAttribute constructor uses reflection to get 'Count' on non-ICollection types; our properties use List which implements ICollection so Count is preserved. [Length(6, 6)] public List Categories { get; set; } = []; +#pragma warning restore IL2026 public IEnumerable Validate(ValidationContext validationContext) => []; diff --git a/Definitions/ObjectModels/Serialization/LocoObjectJsonConverter.cs b/Definitions/ObjectModels/Serialization/LocoObjectJsonConverter.cs index a4843158..60a374e9 100644 --- a/Definitions/ObjectModels/Serialization/LocoObjectJsonConverter.cs +++ b/Definitions/ObjectModels/Serialization/LocoObjectJsonConverter.cs @@ -1,5 +1,6 @@ using Definitions.ObjectModels.Graphics; using Definitions.ObjectModels.Types; +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; @@ -13,6 +14,8 @@ public sealed class LocoObjectJsonConverter : JsonConverter private const string ImageTableProp = nameof(LocoObject.ImageTable); private const string ObjectClrTypeProp = "ObjectClrType"; + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "LocoObjectJsonConverter uses reflection-based JSON serialization and Type.GetType() for dynamic type resolution. Callers are responsible for ensuring required types are preserved.")] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2057", Justification = "The type name stored in JSON is always a valid assembly-qualified type name written by this same converter.")] public override LocoObject? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { using var doc = JsonDocument.ParseValue(ref reader); @@ -65,6 +68,7 @@ public sealed class LocoObjectJsonConverter : JsonConverter return new LocoObject(objectType, locoStruct, stringTable, imageTable); } + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "LocoObjectJsonConverter uses reflection-based JSON serialization and runtime type information. Callers are responsible for ensuring required types are preserved.")] public override void Write(Utf8JsonWriter writer, LocoObject value, JsonSerializerOptions options) { if (value is null) diff --git a/Definitions/ObjectModels/Validation/CountEqualTo.cs b/Definitions/ObjectModels/Validation/CountEqualTo.cs index 2b8054a3..7629cada 100644 --- a/Definitions/ObjectModels/Validation/CountEqualTo.cs +++ b/Definitions/ObjectModels/Validation/CountEqualTo.cs @@ -1,11 +1,13 @@ using System.Collections; using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; namespace Definitions.ObjectModels.Validation; [AttributeUsage(AttributeTargets.Property)] public class CountEqualToAttribute(string otherProperty) : ValidationAttribute { + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075", Justification = "ValidationContext.ObjectType is expected to have its public properties preserved when validation is performed via DataAnnotations.")] protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) { var otherPropertyInfo = validationContext.ObjectType.GetProperty(otherProperty); diff --git a/Definitions/Web/Client.cs b/Definitions/Web/Client.cs index ecb09b58..d7b9d5bc 100644 --- a/Definitions/Web/Client.cs +++ b/Definitions/Web/Client.cs @@ -1,5 +1,6 @@ using Common.Logging; using Definitions.DTO; +using System.Diagnostics.CodeAnalysis; using System.IO.Hashing; namespace Definitions.Web; @@ -8,6 +9,7 @@ public static class Client { public const string ApiVersion = RoutesV2.Prefix; + [RequiresUnreferencedCode("GetObjectListAsync uses reflection-based JSON deserialization via ClientHelpers.GetAsync.")] public static async Task> GetObjectListAsync(HttpClient client, ILogger? logger = null) => await ClientHelpers.GetAsync>( client, @@ -16,6 +18,7 @@ public static async Task> GetObjectListAsync(HttpCli null, logger) ?? []; + [RequiresUnreferencedCode("GetObjectAsync uses reflection-based JSON deserialization via ClientHelpers.GetAsync.")] public static async Task GetObjectAsync(HttpClient client, UniqueObjectId id, ILogger? logger = null) => await ClientHelpers.GetAsync( client, @@ -24,6 +27,7 @@ public static async Task> GetObjectListAsync(HttpCli id, logger); + [RequiresUnreferencedCode("UpdateObjectAsync uses reflection-based JSON serialization/deserialization via ClientHelpers.PutAsync.")] public static async Task UpdateObjectAsync(HttpClient client, UniqueObjectId id, DtoObjectPostResponse request, ILogger? logger = null) => await ClientHelpers.PutAsync( client, @@ -41,6 +45,7 @@ public static async Task> GetObjectListAsync(HttpCli ClientHelpers.ReadBinaryContentAsync, logger) ?? default; + [RequiresUnreferencedCode("UploadDatFileAsync uses reflection-based JSON serialization/deserialization via ClientHelpers.PostAsync.")] public static async Task UploadDatFileAsync(HttpClient client, string filename, byte[] datFileBytes, DateOnly creationDate, DateOnly modifiedDate, ILogger logger) { var xxHash3 = XxHash3.HashToUInt64(datFileBytes); @@ -53,6 +58,7 @@ public static async Task> GetObjectListAsync(HttpCli request); } + [RequiresUnreferencedCode("AddMissingObjectAsync uses reflection-based JSON serialization/deserialization via ClientHelpers.PostAsync.")] public static async Task AddMissingObjectAsync(HttpClient client, DtoObjectMissingPost entry, ILogger? logger = null) { logger?.Debug($"Posting missing object {entry.DatName} with checksum {entry.DatChecksum} to {client.BaseAddress?.OriginalString}{RoutesV2.Objects}{RoutesV2.Missing}"); @@ -64,6 +70,7 @@ public static async Task> GetObjectListAsync(HttpCli logger); } + [RequiresUnreferencedCode("GetLicencesAsync uses reflection-based JSON deserialization via ClientHelpers.GetAsync.")] public static async Task> GetLicencesAsync(HttpClient client, ILogger? logger = null) => await ClientHelpers.GetAsync>( client, @@ -72,6 +79,7 @@ public static async Task> GetLicencesAsync(HttpClie null, logger) ?? []; + [RequiresUnreferencedCode("GetAuthorsAsync uses reflection-based JSON deserialization via ClientHelpers.GetAsync.")] public static async Task> GetAuthorsAsync(HttpClient client, ILogger? logger = null) => await ClientHelpers.GetAsync>( client, @@ -80,6 +88,7 @@ public static async Task> GetAuthorsAsync(HttpClient null, logger) ?? []; + [RequiresUnreferencedCode("GetTagsAsync uses reflection-based JSON deserialization via ClientHelpers.GetAsync.")] public static async Task> GetTagsAsync(HttpClient client, ILogger? logger = null) => await ClientHelpers.GetAsync>( client, @@ -88,6 +97,7 @@ public static async Task> GetTagsAsync(HttpClient clien null, logger) ?? []; + [RequiresUnreferencedCode("GetObjectPacksAsync uses reflection-based JSON deserialization via ClientHelpers.GetAsync.")] public static async Task> GetObjectPacksAsync(HttpClient client, ILogger? logger = null) => await ClientHelpers.GetAsync>( client, diff --git a/Definitions/Web/ClientHelpers.cs b/Definitions/Web/ClientHelpers.cs index 5b51f106..55869f11 100644 --- a/Definitions/Web/ClientHelpers.cs +++ b/Definitions/Web/ClientHelpers.cs @@ -1,5 +1,6 @@ using Common.Logging; using Microsoft.AspNetCore.Http; +using System.Diagnostics.CodeAnalysis; using System.Net.Http.Json; namespace Definitions.Web; @@ -16,6 +17,7 @@ public static class ClientHelpers } } + [RequiresUnreferencedCode("ReadJsonContentAsync uses reflection-based JSON deserialization which may not work correctly when trimming application code.")] internal static async Task ReadJsonContentAsync(HttpContent content) => await content.ReadFromJsonAsync(); @@ -26,6 +28,7 @@ public static class ClientHelpers // return options; //} + [RequiresUnreferencedCode("GetAsync uses reflection-based JSON deserialization which may not work correctly when trimming application code.")] public static async Task GetAsync(HttpClient client, string apiRoute, string route, UniqueObjectId? resourceId = null, ILogger? logger = null) => await SendRequestAsync( client, @@ -42,6 +45,7 @@ public static async Task DeleteAsync(HttpClient client, string apiRoute, s null, logger) != null; + [RequiresUnreferencedCode("PostAsync uses reflection-based JSON serialization/deserialization which may not work correctly when trimming application code.")] public static async Task PostAsync(HttpClient client, string apiRoute, string route, TRequest request, ILogger? logger = null) => await SendRequestAsync( client, @@ -50,6 +54,7 @@ public static async Task DeleteAsync(HttpClient client, string apiRoute, s ReadJsonContentAsync, logger) ?? default; + [RequiresUnreferencedCode("PutAsync uses reflection-based JSON serialization/deserialization which may not work correctly when trimming application code.")] public static async Task PutAsync(HttpClient client, string apiRoute, string route, UniqueObjectId resourceId, TRequest request, ILogger? logger = null) => await SendRequestAsync( client, diff --git a/Gui/EditorSettings.cs b/Gui/EditorSettings.cs index f93bf1d3..f386f1fc 100644 --- a/Gui/EditorSettings.cs +++ b/Gui/EditorSettings.cs @@ -2,6 +2,7 @@ using Common.Logging; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Text; using System.Text.Json; @@ -72,6 +73,7 @@ public string IndexFileName [JsonIgnore] public const string DefaultFileName = "settings.json"; // "settings-dev.json" for dev, "settings.json" for prod + [RequiresUnreferencedCode("Load uses reflection-based JSON deserialization (JsonSerializer.Deserialize) which may not work correctly when trimming application code.")] public static EditorSettings Load(string filename, ILogger logger) { if (!File.Exists(filename)) @@ -88,9 +90,11 @@ public static EditorSettings Load(string filename, ILogger logger) return settings; } + [RequiresUnreferencedCode("Save uses reflection-based JSON serialization (JsonSerializer.Serialize) which may not work correctly when trimming application code.")] public void Save(string filename, ILogger logger) => Save(this, filename, logger); + [RequiresUnreferencedCode("Save uses reflection-based JSON serialization (JsonSerializer.Serialize) which may not work correctly when trimming application code.")] static void Save(EditorSettings settings, string filename, ILogger logger) { var text = JsonSerializer.Serialize(settings, options: JsonFile.DefaultSerializerOptions); diff --git a/Gui/Gui.csproj b/Gui/Gui.csproj index d560051f..fb5b8b2e 100644 --- a/Gui/Gui.csproj +++ b/Gui/Gui.csproj @@ -20,6 +20,7 @@ LeftofZen False Assets\loco_icon.ico + true diff --git a/Gui/Models/ObjectEditorContext.cs b/Gui/Models/ObjectEditorContext.cs index 563b2496..1951c1c6 100644 --- a/Gui/Models/ObjectEditorContext.cs +++ b/Gui/Models/ObjectEditorContext.cs @@ -14,6 +14,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Threading; @@ -62,6 +63,7 @@ public class ObjectEditorContext : IDisposable readonly ConcurrentQueue logQueue = new(); readonly SemaphoreSlim logFileLock = new(1, 1); // Allow only 1 concurrent write + [RequiresUnreferencedCode("ObjectEditorContext constructor calls LoadSettings which uses reflection-based JSON deserialization.")] public ObjectEditorContext() { Logger = new Logger(); @@ -121,6 +123,7 @@ async Task WriteLogsToFileAsync() } } + [RequiresUnreferencedCode("LoadSettings uses EditorSettings.Load which uses reflection-based JSON deserialization.")] void LoadSettings() { Settings = EditorSettings.Load(SettingsFile, Logger); @@ -151,6 +154,7 @@ string InitialiseDirectory(string folder, string defaultName) return folder; } + [RequiresUnreferencedCode("TryLoadObject calls SawyerStreamReader.LoadFullObject and EditorSettings.Load which use reflection-based operations.")] public bool TryLoadObject(FileSystemItem filesystemItem, out LocoUIObjectModel? uiLocoFile) { uiLocoFile = null; @@ -366,6 +370,7 @@ bool TryLoadLocalFile(FileSystemItem filesystemItem, out LocoUIObjectModel? loco static Task? indexerTask; static readonly SemaphoreSlim taskLock = new(1, 1); + [RequiresUnreferencedCode("LoadObjDirectoryAsync calls LoadObjDirectoryAsyncCore which uses reflection-based JSON serialization.")] public async Task LoadObjDirectoryAsync(string directory, IProgress progress, bool useExistingIndex) { // Check if a task is already running WITHOUT waiting on the semaphore @@ -395,6 +400,7 @@ public async Task LoadObjDirectoryAsync(string directory, IProgress progr await indexerTask; } + [RequiresUnreferencedCode("LoadObjDirectoryAsyncCore uses reflection-based JSON serialization and SawyerStreamReader operations.")] async Task LoadObjDirectoryAsyncCore(string directory, IProgress progress, bool useExistingIndex) { if (string.IsNullOrEmpty(directory) || !Directory.Exists(directory) || progress == null) diff --git a/Gui/ViewLocator.cs b/Gui/ViewLocator.cs index e9e5b867..4e68e270 100644 --- a/Gui/ViewLocator.cs +++ b/Gui/ViewLocator.cs @@ -2,11 +2,14 @@ using Avalonia.Controls.Templates; using Gui.ViewModels; using System; +using System.Diagnostics.CodeAnalysis; namespace Gui; public class ViewLocator : IDataTemplate { + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2057", Justification = "ViewLocator uses Type.GetType() to resolve view types by convention from ViewModel type names. All View types must be preserved via TrimmerRootDescriptor or explicit references.")] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072", Justification = "Activator.CreateInstance creates view types resolved via Type.GetType. Callers must ensure view types are preserved.")] public Control Build(object? data) { if (data == null) diff --git a/Gui/ViewModels/AudioViewModel.cs b/Gui/ViewModels/AudioViewModel.cs index 9ffcfc26..bb4c5636 100644 --- a/Gui/ViewModels/AudioViewModel.cs +++ b/Gui/ViewModels/AudioViewModel.cs @@ -55,6 +55,7 @@ public void Dispose() bool disposed; + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("AudioViewModel uses ReactiveUI APIs (WhenAnyValue, ReactiveCommand) which require reflection and may not work correctly when trimming application code.")] public AudioViewModel(ILogger logger, string soundName) { _ = this.WhenAnyValue(o => o.WaveStream) @@ -69,10 +70,12 @@ public AudioViewModel(ILogger logger, string soundName) ExportSoundCommand = ReactiveCommand.Create(ExportSoundAsync); } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("AudioViewModel uses ReactiveUI APIs (WhenAnyValue, ReactiveCommand) which require reflection and may not work correctly when trimming application code.")] public AudioViewModel(ILogger logger, string soundName, string filename) : this(logger, soundName) => ImportSoundFromFile(filename); + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("AudioViewModel uses ReactiveUI APIs (WhenAnyValue, ReactiveCommand) which require reflection and may not work correctly when trimming application code.")] public AudioViewModel(ILogger logger, string soundName, SoundEffectWaveFormat locoWaveFormat, byte[] pcmData) : this(logger, soundName) => WaveStream = new RawSourceWaveStream( diff --git a/Gui/ViewModels/Filters/FilterViewModel.cs b/Gui/ViewModels/Filters/FilterViewModel.cs index 3e7f13a5..95cd6ac3 100644 --- a/Gui/ViewModels/Filters/FilterViewModel.cs +++ b/Gui/ViewModels/Filters/FilterViewModel.cs @@ -11,6 +11,7 @@ using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using System.Reactive; @@ -21,8 +22,9 @@ namespace Gui.ViewModels.Filters; public class FilterTypeViewModel : ReactiveObject { - public Type Type { get; set; } - public string DisplayName { get; set; } + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] + public required Type Type { get; set; } + public required string DisplayName { get; set; } public string IconName { get; set; } = "CircleSmall"; public IBrush? BackgroundColour { get; set; } @@ -59,6 +61,7 @@ public bool IsValid public ObservableCollection AvailableFiltersList { get; set; } = []; + [RequiresUnreferencedCode("FilterViewModel uses ReactiveUI APIs (WhenAnyValue, ReactiveCommand) and reflection-based LINQ Expressions, which may not work correctly when trimming application code.")] public FilterViewModel(ObjectEditorContext editorContext, List availableFilters, Action onRemove) { _model = editorContext; @@ -232,6 +235,7 @@ private static bool IsNumericType(Type type) return null; } + [RequiresUnreferencedCode("BuildExpression uses reflection-based LINQ Expressions and ReactiveUI APIs which may not work correctly when trimming application code.")] public Func? BuildExpression(bool isLocal) { if (SelectedProperty == null || SelectedObjectType == null) @@ -257,6 +261,7 @@ private static bool IsNumericType(Type type) // return BuildFilterExpression()?.Compile(); //} + [RequiresUnreferencedCode("BuildObjectFilter uses reflection-based LINQ Expressions which may not work correctly when trimming application code.")] bool BuildObjectFilter(ObjectIndexEntry entry, bool isLocal) { if (!isLocal) @@ -286,6 +291,7 @@ bool BuildObjectFilter(ObjectIndexEntry entry, bool isLocal) return BuildObjectFilterExpression(SelectedObjectType.Type)?.Compile().Invoke(locoObject) ?? false; } + [RequiresUnreferencedCode("BuildFilterExpression uses System.Linq.Expressions.Expression.Property which requires reflection and may not work correctly when trimming application code.")] private Expression>? BuildFilterExpression() { if (SelectedProperty == null) @@ -299,7 +305,8 @@ bool BuildObjectFilter(ObjectIndexEntry entry, bool isLocal) return CreateFilterExpression(parameter, member); } - private Expression>? BuildObjectFilterExpression(Type t) + [RequiresUnreferencedCode("BuildObjectFilterExpression uses System.Linq.Expressions.Expression.Property which requires reflection and may not work correctly when trimming application code.")] + private Expression>? BuildObjectFilterExpression([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type t) { if (SelectedProperty == null) { @@ -312,6 +319,7 @@ bool BuildObjectFilter(ObjectIndexEntry entry, bool isLocal) return CreateFilterExpression(parameter, member); } + [RequiresUnreferencedCode("CreateFilterExpression uses System.Linq.Expressions.Expression.Property which requires reflection and may not work correctly when trimming application code.")] private Expression>? CreateFilterExpression(ParameterExpression parameter, MemberExpression member) { Expression? body = null; diff --git a/Gui/ViewModels/FolderTreeViewModel.cs b/Gui/ViewModels/FolderTreeViewModel.cs index 06d7b8e4..febb7244 100644 --- a/Gui/ViewModels/FolderTreeViewModel.cs +++ b/Gui/ViewModels/FolderTreeViewModel.cs @@ -112,6 +112,7 @@ public string DirectoryFileCount public FolderTreeViewModel() { } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("FolderTreeViewModel uses ReactiveUI APIs (WhenAnyValue, ReactiveCommand, Observable.FromEventPattern) and FilterViewModel which require reflection and may not work correctly when trimming application code.")] public FolderTreeViewModel(ObjectEditorContext editorContext) { EditorContext = editorContext; diff --git a/Gui/ViewModels/Graphics/ImageTableViewModel.cs b/Gui/ViewModels/Graphics/ImageTableViewModel.cs index cff8f09d..4a5543d3 100644 --- a/Gui/ViewModels/Graphics/ImageTableViewModel.cs +++ b/Gui/ViewModels/Graphics/ImageTableViewModel.cs @@ -12,6 +12,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reactive.Disposables; @@ -96,6 +97,7 @@ public string ImageCount ImageTable Model { get; init; } + [RequiresUnreferencedCode("ImageTableViewModel uses ReactiveUI APIs (WhenAnyValue, ReactiveCommand) which require reflection and may not work correctly when trimming application code.")] public ImageTableViewModel(ImageTable imageTable, ILogger logger) { ArgumentNullException.ThrowIfNull(imageTable); diff --git a/Index/ObjectIndex.cs b/Index/ObjectIndex.cs index acb27f94..7f7db6ac 100644 --- a/Index/ObjectIndex.cs +++ b/Index/ObjectIndex.cs @@ -8,6 +8,7 @@ using Definitions.ObjectModels.Types; using System.Collections.Concurrent; using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using System.IO.Hashing; using System.Text.Json.Serialization; @@ -50,15 +51,19 @@ public bool TryFind(ulong xxHash3, out ObjectIndexEntry? entry) // return entry != null; //} + [RequiresUnreferencedCode("SaveIndexAsync uses reflection-based JSON serialization via JsonFile.SerializeToFileAsync.")] public async Task SaveIndexAsync(string indexFile) => await JsonFile.SerializeToFileAsync(this, indexFile, JsonFile.DefaultSerializerOptions).ConfigureAwait(false); + [RequiresUnreferencedCode("LoadIndexAsync uses reflection-based JSON deserialization via JsonFile.DeserializeFromFileAsync.")] public static async Task LoadIndexAsync(string indexFile) => await JsonFile.DeserializeFromFileAsync(indexFile, JsonFile.DefaultSerializerOptions).ConfigureAwait(false); + [RequiresUnreferencedCode("LoadOrCreateIndex uses reflection-based JSON serialization/deserialization via ObjectIndex.LoadOrCreateIndexAsync.")] public static ObjectIndex LoadOrCreateIndex(string directory, ILogger logger, IProgress? progress = null) => LoadOrCreateIndexAsync(directory, logger, progress).Result; + [RequiresUnreferencedCode("LoadOrCreateIndexAsync uses reflection-based JSON serialization/deserialization via LoadIndexAsync and SaveIndexAsync.")] public static async Task LoadOrCreateIndexAsync(string directory, ILogger logger, IProgress? progress = null) { var indexPath = Path.Combine(directory, DefaultIndexFileName); diff --git a/build.sh b/build.sh index a551bc69..69feaf49 100644 --- a/build.sh +++ b/build.sh @@ -32,9 +32,9 @@ echo "$version" > Gui/version.txt # 3. Build the editor for different platforms echo "Building the ${FG_BLUE}Editor${RESET}" -dotnet publish Gui/Gui.csproj -c Release -p:WarningLevel=0 -p:PublishSingleFile=true -p:Version=$version --self-contained --runtime win-x64 -dotnet publish Gui/Gui.csproj -c Release -p:WarningLevel=0 -p:PublishSingleFile=true -p:Version=$version --self-contained --runtime linux-x64 -dotnet publish Gui/Gui.csproj -c Release -p:WarningLevel=0 -p:PublishSingleFile=true -p:Version=$version --self-contained --runtime osx-x64 +dotnet publish Gui/Gui.csproj -c Release -p:WarningLevel=0 -p:PublishSingleFile=true -p:PublishTrimmed=true -p:Version=$version --self-contained --runtime win-x64 +dotnet publish Gui/Gui.csproj -c Release -p:WarningLevel=0 -p:PublishSingleFile=true -p:PublishTrimmed=true -p:Version=$version --self-contained --runtime linux-x64 +dotnet publish Gui/Gui.csproj -c Release -p:WarningLevel=0 -p:PublishSingleFile=true -p:PublishTrimmed=true -p:Version=$version --self-contained --runtime osx-x64 echo "Building the ${FG_BLUE}Updater${RESET}" dotnet publish GuiUpdater/GuiUpdater.csproj -c Release -p:PublishSingleFile=true -p:Version=$version --self-contained --runtime win-x64