A chain of type forwarders is followed by assembly name, and every name resolves relative
to the assembly being decompiled - so a chain that leaves for another framework can be
pulled straight back into the directory it started in. A .NET Standard 2.0 assembly
sitting among .NET Framework 4.6.1 facades lost System.Linq.Enumerable that way: the
chain went netstandard -> System.Core (from the shared framework) -> System.Linq (back to
the input directory) -> netstandard, arriving at an assembly it had already passed
through. Nothing in the closure defines the type, so it stayed unknown and every LINQ
call decompiled as a static call with a delegate cast.
Once the closure is loaded, chains that return to an assembly they already visited are
walked a second time, resolving each hop next to the assembly that forwards it. The
assembly ending the repaired chain is loaded only once it is confirmed to declare the
type; it then wins the deduplication against the assembly of the same name it displaces,
which version order says nothing about. An assembly that neither forwards nor declares
the type ends the walk with nothing loaded, so a failed repair cannot displace anything.
Only chains that are already broken are walked twice. Preferring the forwarder's own
directory as a resolution policy was tried first and rejected: measured against a corpus,
it moved a .NET 8 facade's System.Runtime reference out of a net4x compilation, splitting
type identities so that overrides printed as virtual. The two cases cannot be told apart
where references are resolved, because that layer sees assembly names, not the type whose
chain is or is not terminating.
AssemblyReference now knows the module that declares it, which is what lets a hop be
resolved next to its forwarder, and its metadata reader is that module's.
A chain that cannot be repaired is reported in the reference load log the UI already
shows, once per reference: a facade forwards hundreds of types and they all fail together.
Assisted-by: Claude:claude-opus-5:Claude Code
Four cases where the analyzer rule conflicts with intentional design:
* EmptyList<T>.IDisposable.Dispose (CA1063) — explicit IDisposable on
IEnumerator<T>; making it public would conflict with the rest of the
IList<T> / IEnumerator<T> 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<LongSet>.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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
While support for multi-module assemblies isn't fully working yet; it is clear at this point that we want
to treat each module in a multi-module assembly separately for the purposes of the type system.