From a8d841199d36b9908a5f0d339d15c2168d77b85a Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Fri, 8 May 2026 18:09:56 +0200 Subject: [PATCH 1/6] Lift IDisposable to MetadataFile and dispose evicted assemblies. MetadataFile now declares IDisposable using the canonical pattern (public non-virtual Dispose() + protected virtual Dispose(bool)). PEFile and WebCilFile become sealed and override Dispose(bool) to release the PEReader and MemoryMappedViewAccessor they own; ResourcesFile is also sealed. PortableDebugInfoProvider disposes the MetadataReaderProvider it owns. LoadedAssembly implements IDisposable and disposes both the loaded MetadataFile and the debug-info provider. AssemblyList.Unload / Clear / ReloadAssembly / HotReplaceAssembly now dispose the LoadedAssembly instances they evict, fixing a resource leak where every "Reload Assembly" held the previous PEReader (and the underlying file handle / memory-mapped view) alive until GC eventually finalized it. The disposal contract terminates at the AssemblyList tier: downstream holders of MetadataFile (MetadataModule, DecompilerTypeSystem, AssemblyListSnapshot, ...) hold borrowed references rather than owned ones, so making the base IDisposable does not cascade into CA1001 / CA2213 warnings elsewhere. Co-Authored-By: Claude Opus 4.7 (1M context) --- ICSharpCode.Decompiler/Metadata/MetadataFile.cs | 12 +++++++++++- ICSharpCode.Decompiler/Metadata/PEFile.cs | 8 +++++--- ICSharpCode.Decompiler/Metadata/WebCilFile.cs | 8 +++++--- ICSharpCode.Decompiler/Util/ResourcesFile.cs | 2 +- ICSharpCode.ILSpyX/AssemblyList.cs | 14 ++++++++++++-- ICSharpCode.ILSpyX/LoadedAssembly.cs | 11 ++++++++++- .../PdbProvider/PortableDebugInfoProvider.cs | 7 ++++++- 7 files changed, 50 insertions(+), 12 deletions(-) diff --git a/ICSharpCode.Decompiler/Metadata/MetadataFile.cs b/ICSharpCode.Decompiler/Metadata/MetadataFile.cs index 0e00052d63..6068800c41 100644 --- a/ICSharpCode.Decompiler/Metadata/MetadataFile.cs +++ b/ICSharpCode.Decompiler/Metadata/MetadataFile.cs @@ -45,7 +45,7 @@ namespace ICSharpCode.Decompiler.Metadata /// decompiled type systems. /// [DebuggerDisplay("{Kind}: {FileName}")] - public class MetadataFile + public class MetadataFile : IDisposable { public enum MetadataFileKind { @@ -297,6 +297,16 @@ public IModuleReference WithOptions(TypeSystemOptions options) return new MetadataFileWithOptions(this, options); } + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + } + private class MetadataFileWithOptions : IModuleReference { readonly MetadataFile peFile; diff --git a/ICSharpCode.Decompiler/Metadata/PEFile.cs b/ICSharpCode.Decompiler/Metadata/PEFile.cs index b3eb7be4cb..3d9cf168b2 100644 --- a/ICSharpCode.Decompiler/Metadata/PEFile.cs +++ b/ICSharpCode.Decompiler/Metadata/PEFile.cs @@ -31,7 +31,7 @@ namespace ICSharpCode.Decompiler.Metadata { [DebuggerDisplay("{FileName}")] - public class PEFile : MetadataFile, IDisposable, IModuleReference + public sealed class PEFile : MetadataFile, IModuleReference { public PEReader Reader { get; } @@ -55,9 +55,11 @@ public PEFile(string fileName, PEReader reader, MetadataReaderOptions metadataOp public override int MetadataOffset => Reader.PEHeaders.MetadataStartOffset; public override bool IsMetadataOnly => false; - public void Dispose() + protected override void Dispose(bool disposing) { - Reader.Dispose(); + if (disposing) + Reader.Dispose(); + base.Dispose(disposing); } IModule TypeSystem.IModuleReference.Resolve(ITypeResolveContext context) diff --git a/ICSharpCode.Decompiler/Metadata/WebCilFile.cs b/ICSharpCode.Decompiler/Metadata/WebCilFile.cs index 4843a5c4c2..47e7d4bd5b 100644 --- a/ICSharpCode.Decompiler/Metadata/WebCilFile.cs +++ b/ICSharpCode.Decompiler/Metadata/WebCilFile.cs @@ -32,7 +32,7 @@ namespace ICSharpCode.Decompiler.Metadata { - public class WebCilFile : MetadataFile, IDisposable, IModuleReference + public sealed class WebCilFile : MetadataFile, IModuleReference { readonly MemoryMappedViewAccessor view; readonly long webcilOffset; @@ -245,9 +245,11 @@ public override unsafe SectionData GetSectionData(int rva) return new MetadataModule(context.Compilation, this, TypeSystemOptions.Default); } - public void Dispose() + protected override void Dispose(bool disposing) { - view.Dispose(); + if (disposing) + view.Dispose(); + base.Dispose(disposing); } public struct WebcilHeader diff --git a/ICSharpCode.Decompiler/Util/ResourcesFile.cs b/ICSharpCode.Decompiler/Util/ResourcesFile.cs index 762edc53f8..4034b2d5a0 100644 --- a/ICSharpCode.Decompiler/Util/ResourcesFile.cs +++ b/ICSharpCode.Decompiler/Util/ResourcesFile.cs @@ -31,7 +31,7 @@ namespace ICSharpCode.Decompiler.Util /// /// .resources file. /// - public class ResourcesFile : IEnumerable>, IDisposable + public sealed class ResourcesFile : IEnumerable>, IDisposable { sealed class MyBinaryReader : BinaryReader { diff --git a/ICSharpCode.ILSpyX/AssemblyList.cs b/ICSharpCode.ILSpyX/AssemblyList.cs index e21894815e..cb4c425b9e 100644 --- a/ICSharpCode.ILSpyX/AssemblyList.cs +++ b/ICSharpCode.ILSpyX/AssemblyList.cs @@ -340,6 +340,8 @@ LoadedAssembly OpenAssembly(string file, Func load) { VerifyAccess(); file = Path.GetFullPath(file); + LoadedAssembly evicted; + LoadedAssembly newAsm; lock (lockObj) { if (!byFilename.TryGetValue(file, out LoadedAssembly? target)) @@ -348,7 +350,7 @@ LoadedAssembly OpenAssembly(string file, Func load) if (index < 0) return null; - var newAsm = new LoadedAssembly(this, file, stream: Task.FromResult(stream), + newAsm = new LoadedAssembly(this, file, stream: Task.FromResult(stream), fileLoaders: manager?.LoaderRegistry, applyWinRTProjections: ApplyWinRTProjections, useDebugSymbols: UseDebugSymbols); newAsm.IsAutoLoaded = target.IsAutoLoaded; @@ -356,8 +358,10 @@ LoadedAssembly OpenAssembly(string file, Func load) Debug.Assert(newAsm.FileName == file); byFilename[file] = newAsm; this.assemblies[index] = newAsm; - return newAsm; + evicted = target; } + evicted.Dispose(); + return newAsm; } public LoadedAssembly? ReloadAssembly(string file) @@ -387,6 +391,7 @@ LoadedAssembly OpenAssembly(string file, Func load) this.assemblies.Remove(target); this.assemblies.Insert(index, newAsm); } + target.Dispose(); return newAsm; } @@ -398,16 +403,21 @@ public void Unload(LoadedAssembly assembly) assemblies.Remove(assembly); byFilename.Remove(assembly.FileName); } + assembly.Dispose(); } public void Clear() { VerifyAccess(); + LoadedAssembly[] removed; lock (lockObj) { + removed = assemblies.ToArray(); assemblies.Clear(); byFilename.Clear(); } + foreach (var asm in removed) + asm.Dispose(); } public void Sort(IComparer comparer) { diff --git a/ICSharpCode.ILSpyX/LoadedAssembly.cs b/ICSharpCode.ILSpyX/LoadedAssembly.cs index 7f7e81eee1..a126221737 100644 --- a/ICSharpCode.ILSpyX/LoadedAssembly.cs +++ b/ICSharpCode.ILSpyX/LoadedAssembly.cs @@ -52,7 +52,7 @@ namespace ICSharpCode.ILSpyX /// * a file that is still being loaded in the background /// [DebuggerDisplay("[LoadedAssembly {shortName}]")] - public sealed class LoadedAssembly + public sealed class LoadedAssembly : IDisposable { /// /// Maps from MetadataFile (successfully loaded .NET module) back to the LoadedAssembly instance @@ -652,5 +652,14 @@ public AssemblyReferenceClassifier GetAssemblyReferenceClassifier(bool applyWinR } UniversalAssemblyResolver? universalResolver; + + public void Dispose() + { + if (loadingTask.Status == TaskStatus.RanToCompletion) + { + loadingTask.Result.MetadataFile?.Dispose(); + } + (debugInfoProvider as IDisposable)?.Dispose(); + } } } diff --git a/ICSharpCode.ILSpyX/PdbProvider/PortableDebugInfoProvider.cs b/ICSharpCode.ILSpyX/PdbProvider/PortableDebugInfoProvider.cs index a5432f47cb..d118c85c83 100644 --- a/ICSharpCode.ILSpyX/PdbProvider/PortableDebugInfoProvider.cs +++ b/ICSharpCode.ILSpyX/PdbProvider/PortableDebugInfoProvider.cs @@ -32,7 +32,7 @@ namespace ICSharpCode.ILSpyX.PdbProvider { - public class PortableDebugInfoProvider : IDebugInfoProvider + public sealed class PortableDebugInfoProvider : IDebugInfoProvider, IDisposable { string? pdbFileName; string moduleFileName; @@ -252,5 +252,10 @@ public MetadataFile ToMetadataFile() var kind = IsEmbedded || Path.GetExtension(SourceFileName).Equals(".pdb", StringComparison.OrdinalIgnoreCase) ? MetadataFileKind.ProgramDebugDatabase : MetadataFileKind.Metadata; return new MetadataFile(kind, SourceFileName, provider, options, 0, IsEmbedded); } + + public void Dispose() + { + provider.Dispose(); + } } } From 784379d012bb66284d1fa24760ffa913d5bbf382 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Fri, 8 May 2026 18:17:26 +0200 Subject: [PATCH 2/6] Annotate intentional analyzer-violations with [SuppressMessage]. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four cases where the analyzer rule conflicts with intentional design: * EmptyList.IDisposable.Dispose (CA1063) — explicit IDisposable on IEnumerator; making it public would conflict with the rest of the IList / IEnumerator surface. * MetadataFile.SectionHeaders (CA1065) — throw documents that this MetadataFileKind has no PE sections; PE-like derived kinds override. * LongSet.GetHashCode + LongSet itself (CA1065 + CA2231) — explicit guards against using LongSet in hash containers / via equality operators; SetEquals is the supported comparison and IEquatable.Equals is itself [Obsolete]. * AnnotationList.Clone (CA2002) — AnnotationList is a private nested type; the surrounding Annotatable class deliberately locks on the AnnotationList instance to serialize annotation reads/writes, and external code cannot obtain a reference to it. Co-Authored-By: Claude Opus 4.7 (1M context) --- ICSharpCode.Decompiler/CSharp/Syntax/IAnnotatable.cs | 3 +++ ICSharpCode.Decompiler/Metadata/MetadataFile.cs | 3 +++ ICSharpCode.Decompiler/Util/EmptyList.cs | 3 +++ ICSharpCode.Decompiler/Util/LongSet.cs | 5 +++++ 4 files changed, 14 insertions(+) diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/IAnnotatable.cs b/ICSharpCode.Decompiler/CSharp/Syntax/IAnnotatable.cs index a82a9d54f7..0dba3d6446 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/IAnnotatable.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/IAnnotatable.cs @@ -18,6 +18,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; @@ -115,6 +116,8 @@ public AnnotationList(int initialCapacity) : base(initialCapacity) { } + [SuppressMessage("Reliability", "CA2002:Do not lock on objects with weak identity", + Justification = "AnnotationList is a private nested type — the surrounding Annotatable class deliberately locks on the AnnotationList instance to serialize annotation reads/writes; external code cannot obtain a reference to it.")] public object Clone() { lock (this) diff --git a/ICSharpCode.Decompiler/Metadata/MetadataFile.cs b/ICSharpCode.Decompiler/Metadata/MetadataFile.cs index 6068800c41..398cf3fe06 100644 --- a/ICSharpCode.Decompiler/Metadata/MetadataFile.cs +++ b/ICSharpCode.Decompiler/Metadata/MetadataFile.cs @@ -22,6 +22,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection.Metadata; using System.Reflection.PortableExecutable; @@ -285,6 +286,8 @@ public virtual int GetContainingSectionIndex(int rva) throw new BadImageFormatException("This metadata file does not support sections."); } + [SuppressMessage("Design", "CA1065:Do not raise exceptions in unexpected locations", + Justification = "Throw signals that this MetadataFileKind has no PE sections; derived PE-like kinds override.")] public virtual ImmutableArray SectionHeaders => throw new BadImageFormatException("This metadata file does not support sections."); /// diff --git a/ICSharpCode.Decompiler/Util/EmptyList.cs b/ICSharpCode.Decompiler/Util/EmptyList.cs index 31f69f08a1..8d066332f5 100644 --- a/ICSharpCode.Decompiler/Util/EmptyList.cs +++ b/ICSharpCode.Decompiler/Util/EmptyList.cs @@ -20,6 +20,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; namespace ICSharpCode.Decompiler.Util { @@ -99,6 +100,8 @@ object IEnumerator.Current { get { throw new NotSupportedException(); } } + [SuppressMessage("Usage", "CA1063:Implement IDisposable Correctly", + Justification = "Explicit IDisposable implementation for IEnumerator; intentional no-op for the singleton.")] void IDisposable.Dispose() { } diff --git a/ICSharpCode.Decompiler/Util/LongSet.cs b/ICSharpCode.Decompiler/Util/LongSet.cs index e4e1329290..f4c64aa807 100644 --- a/ICSharpCode.Decompiler/Util/LongSet.cs +++ b/ICSharpCode.Decompiler/Util/LongSet.cs @@ -21,6 +21,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; namespace ICSharpCode.Decompiler.Util @@ -28,6 +29,8 @@ namespace ICSharpCode.Decompiler.Util /// /// An immutable set of longs, that is implemented as a list of intervals. /// + [SuppressMessage("Usage", "CA2231:Overload operator equals on overriding value type Equals", + Justification = "Equality on LongSet is intentionally only available via SetEquals — the IEquatable.Equals overload is itself [Obsolete] in favor of SetEquals.")] public struct LongSet : IEquatable { /// @@ -362,6 +365,8 @@ public override bool Equals(object? obj) return obj is LongSet && SetEquals((LongSet)obj); } + [SuppressMessage("Design", "CA1065:Do not raise exceptions in unexpected locations", + Justification = "Throw is an explicit guard against using LongSet in hash-based containers; use SetEquals for comparison.")] public override int GetHashCode() { throw new NotImplementedException(); From 317e33b3b843a1538698649bcca3d943a1d96167 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Fri, 8 May 2026 18:18:15 +0200 Subject: [PATCH 3/6] Fix five small analyzer warnings. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ExtensionDeclaration.SymbolKind (CA1065) — was throwing NotImplementedException; return SymbolKind.TypeDefinition to match TypeDeclaration / DelegateDeclaration, since `extension` declarations are type-level. * CustomAttribute.DecodeValue (CA2002) — replace `lock(this)` on the sealed-but-internal class with a private syncRoot field. * PlainTextOutput (CA1001) — implement IDisposable; track an ownsWriter flag so we only dispose the underlying TextWriter when the parameterless constructor created its own StringWriter. * DotNetCorePathFinder (CA1060) — move the libc realpath / free PInvokes into a private nested NativeMethods class. * ILSpy.ReadyToRun (CA1016) — add [assembly: AssemblyVersion("1.0.0.0")] in Properties/AssemblyInfo.cs to match the BamlDecompiler plugin. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Syntax/TypeMembers/ExtensionDeclaration.cs | 2 +- .../Metadata/DotNetCorePathFinder.cs | 15 +++++++++------ ICSharpCode.Decompiler/Output/PlainTextOutput.cs | 11 ++++++++++- .../TypeSystem/Implementation/CustomAttribute.cs | 3 ++- ILSpy.ReadyToRun/Properties/AssemblyInfo.cs | 3 ++- 5 files changed, 24 insertions(+), 10 deletions(-) diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/TypeMembers/ExtensionDeclaration.cs b/ICSharpCode.Decompiler/CSharp/Syntax/TypeMembers/ExtensionDeclaration.cs index 532d102d4b..019fd4e74b 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/TypeMembers/ExtensionDeclaration.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/TypeMembers/ExtensionDeclaration.cs @@ -24,7 +24,7 @@ public class ExtensionDeclaration : EntityDeclaration { public readonly static TokenRole ExtensionKeywordRole = new TokenRole("extension"); - public override SymbolKind SymbolKind => throw new System.NotImplementedException(); + public override SymbolKind SymbolKind => SymbolKind.TypeDefinition; public AstNodeCollection TypeParameters { get { return GetChildrenByRole(Roles.TypeParameter); } diff --git a/ICSharpCode.Decompiler/Metadata/DotNetCorePathFinder.cs b/ICSharpCode.Decompiler/Metadata/DotNetCorePathFinder.cs index b5cd4dd17d..d24cd5a0f8 100644 --- a/ICSharpCode.Decompiler/Metadata/DotNetCorePathFinder.cs +++ b/ICSharpCode.Decompiler/Metadata/DotNetCorePathFinder.cs @@ -318,7 +318,7 @@ static unsafe string GetRealPath(string path, Encoding encoding) fixed (byte* input = bytes) { - byte* output = GetRealPath(input, null); + byte* output = NativeMethods.GetRealPath(input, null); if (output == null) { return null; @@ -330,15 +330,18 @@ static unsafe string GetRealPath(string path, Encoding encoding) } byte[] result = new byte[len]; Marshal.Copy((IntPtr)output, result, 0, result.Length); - Free(output); + NativeMethods.Free(output); return encoding.GetString(result); } } - [DllImport("libc", EntryPoint = "realpath")] - static extern unsafe byte* GetRealPath(byte* path, byte* resolvedPath); + static class NativeMethods + { + [DllImport("libc", EntryPoint = "realpath")] + internal static extern unsafe byte* GetRealPath(byte* path, byte* resolvedPath); - [DllImport("libc", EntryPoint = "free")] - static extern unsafe void Free(void* ptr); + [DllImport("libc", EntryPoint = "free")] + internal static extern unsafe void Free(void* ptr); + } } } diff --git a/ICSharpCode.Decompiler/Output/PlainTextOutput.cs b/ICSharpCode.Decompiler/Output/PlainTextOutput.cs index 5899ebe9ec..0a8a363f52 100644 --- a/ICSharpCode.Decompiler/Output/PlainTextOutput.cs +++ b/ICSharpCode.Decompiler/Output/PlainTextOutput.cs @@ -28,9 +28,10 @@ namespace ICSharpCode.Decompiler { - public sealed class PlainTextOutput : ITextOutput + public sealed class PlainTextOutput : ITextOutput, IDisposable { readonly TextWriter writer; + readonly bool ownsWriter; int indent; bool needsIndent; @@ -44,11 +45,19 @@ public PlainTextOutput(TextWriter writer) if (writer == null) throw new ArgumentNullException(nameof(writer)); this.writer = writer; + this.ownsWriter = false; } public PlainTextOutput() { this.writer = new StringWriter(); + this.ownsWriter = true; + } + + public void Dispose() + { + if (ownsWriter) + writer.Dispose(); } public TextLocation Location { diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/CustomAttribute.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/CustomAttribute.cs index fb706994f2..b071bdbd0a 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/CustomAttribute.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/CustomAttribute.cs @@ -40,6 +40,7 @@ sealed class CustomAttribute : IAttribute CustomAttributeValue value; bool valueDecoded; bool hasDecodeErrors; + readonly object syncRoot = new object(); internal CustomAttribute(MetadataModule module, IMethod attrCtor, CustomAttributeHandle handle) { @@ -76,7 +77,7 @@ public bool HasDecodeErrors { void DecodeValue() { - lock (this) + lock (syncRoot) { try { diff --git a/ILSpy.ReadyToRun/Properties/AssemblyInfo.cs b/ILSpy.ReadyToRun/Properties/AssemblyInfo.cs index 61f1224e96..68b831944b 100644 --- a/ILSpy.ReadyToRun/Properties/AssemblyInfo.cs +++ b/ILSpy.ReadyToRun/Properties/AssemblyInfo.cs @@ -8,7 +8,8 @@ [assembly: TargetPlatform("Windows10.0")] [assembly: SupportedOSPlatform("Windows7.0")] -// General Information about an assembly is controlled through the following +// General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: NeutralResourcesLanguage("en-US")] +[assembly: AssemblyVersion("1.0.0.0")] From a541fc0ddf363794bdc459a5d99cb831ed6eeb81 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Fri, 8 May 2026 21:15:40 +0200 Subject: [PATCH 4/6] Fix CS9336 precedence bug in CompoundAssignmentInstruction. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The C# 9 IntPtr / UIntPtr guard in IsBinaryCompatibleWithType read type.Kind is not TypeKind.NInt or TypeKind.NUInt which parses as `(is not NInt) or (is NUInt)` — true unless Kind == NInt. The intent (per the surrounding comment "but not nint or C# 11 IntPtr") is "Kind is neither NInt nor NUInt", which needs parentheses around the alternation: type.Kind is not (TypeKind.NInt or TypeKind.NUInt) Effect: when Kind == NUInt the branch no longer mistakenly applies the C# 9 IntPtr-only restrictions (suppressing compound assignment without nint, disallowing shifts). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../IL/Instructions/CompoundAssignmentInstruction.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ICSharpCode.Decompiler/IL/Instructions/CompoundAssignmentInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/CompoundAssignmentInstruction.cs index c84711e832..eab1cbb290 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/CompoundAssignmentInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/CompoundAssignmentInstruction.cs @@ -215,7 +215,7 @@ internal static bool IsBinaryCompatibleWithType(BinaryNumericInstruction binary, return false; // operator not supported on pointer types } } - else if ((type.IsKnownType(KnownTypeCode.IntPtr) || type.IsKnownType(KnownTypeCode.UIntPtr)) && type.Kind is not TypeKind.NInt or TypeKind.NUInt) + else if ((type.IsKnownType(KnownTypeCode.IntPtr) || type.IsKnownType(KnownTypeCode.UIntPtr)) && type.Kind is not (TypeKind.NInt or TypeKind.NUInt)) { // If the LHS is C# 9 IntPtr (but not nint or C# 11 IntPtr): // "target.intptr *= 2;" is compiler error, but From fd6a81e7cb72fe4044338843a9714cc1a9ce8f42 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Fri, 8 May 2026 19:57:48 +0200 Subject: [PATCH 5/6] Suppress CA1063 / CA2213 on the ported ResXResourceWriter. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ResXResourceWriter is a verbatim port of the Mono implementation (see file header). Both warnings flag deliberate decisions in the upstream port that we preserve for fidelity: * CA1063 — Dispose() is virtual and the protected Dispose(bool) is not, the inverse of the canonical pattern; keeping the Mono shape. * CA2213 — the writer's stream / textwriter fields are caller-owned and intentionally not disposed by the writer. Co-Authored-By: Claude Opus 4.7 (1M context) --- ICSharpCode.Decompiler/Util/ResXResourceWriter.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ICSharpCode.Decompiler/Util/ResXResourceWriter.cs b/ICSharpCode.Decompiler/Util/ResXResourceWriter.cs index 09fa9f8650..bc27927589 100644 --- a/ICSharpCode.Decompiler/Util/ResXResourceWriter.cs +++ b/ICSharpCode.Decompiler/Util/ResXResourceWriter.cs @@ -31,6 +31,7 @@ // includes code by Mike Krüger and Lluis Sanchez using System; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Text; @@ -40,6 +41,10 @@ namespace ICSharpCode.Decompiler.Util { + [SuppressMessage("Usage", "CA1063:Implement IDisposable Correctly", + Justification = "Ported from the Mono ResXResourceWriter implementation; preserved verbatim for fidelity with the upstream.")] + [SuppressMessage("Usage", "CA2213:Disposable fields should be disposed", + Justification = "By design: the writer doesn't own the underlying stream/textwriter passed in by the caller. Mono behavior preserved.")] #if INSIDE_SYSTEM_WEB internal #else From eec031a220c609f62b16f20efd7ef3755402db33 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Fri, 8 May 2026 21:26:12 +0200 Subject: [PATCH 6/6] Make SessionSettings.FromString robust against malformed saved values. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The helper used to catch only FormatException, but the WPF type converters (e.g. RectConverter for the saved window placement) raise InvalidOperationException on malformed input — for instance, an empty string from a partially-written config file. That was enough to propagate up through SessionSettings.LoadFromXml and crash startup before the main window could even open, with no recovery path other than manually editing the on-disk ILSpy.xml. Treat any conversion failure as "use the default" instead, and treat empty strings the same as null at the entry. Effect: a single bad saved value falls back silently and the application starts. Co-Authored-By: Claude Opus 4.7 (1M context) --- ILSpy/SessionSettings.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ILSpy/SessionSettings.cs b/ILSpy/SessionSettings.cs index de72f0696f..48a1b12961 100644 --- a/ILSpy/SessionSettings.cs +++ b/ILSpy/SessionSettings.cs @@ -162,15 +162,18 @@ static string Unescape(string p) static T FromString(string s, T defaultValue) { - if (s == null) + if (string.IsNullOrEmpty(s)) return defaultValue; try { TypeConverter c = TypeDescriptor.GetConverter(typeof(T)); return (T)c.ConvertFromInvariantString(s); } - catch (FormatException) + catch (Exception) { + // TypeConverters for WPF types (e.g. Rect) throw InvalidOperationException, not + // FormatException, on malformed input. Treat any conversion failure as "use the + // default" so a single bad saved value can't crash startup. return defaultValue; } }