The GAC probe only ever looked for the exact folder of the requested version.
For about a hundred assemblies the .NET Framework 4.7.2/4.8 reference assemblies
carry a higher version than the implementation ever installed in the GAC
(System.IO.Compression is 4.2.0.0 against 4.0.0.0 in the GAC, System.Runtime is
4.1.2.0, ...), because out-of-band packages shipped those versions and the ref
assemblies had to keep up. The runtime hides this behind assembly unification;
without an equivalent, every reference to one of them was reported as
unresolvable.
Matching on the major version keeps assemblies apart that share a name but are
different products, e.g. Microsoft.Build.Framework 4.0.0.0 and 15.x.
Assisted-by: Claude:claude-opus-5:Claude Code
The exporter dropped PresentationFramework, System.Xaml, System.Windows.Forms
and System.Drawing from every project it wrote, whatever the assembly used,
while a second list held the remaining WPF assemblies behind a WPF check. The
SDK draws the line elsewhere: Microsoft.NET.Sdk.WindowsDesktop.props promotes
the nine _WpfCommonNetFxReference items to _SDKImplicitReference only when
UseWPF is set, System.Windows.Forms only when UseWindowsForms is, and
WindowsFormsIntegration only when both are. A XAML-only assembly therefore lost
a reference that nothing supplied, and a WPF application that also used Windows
Forms lost the Windows Forms references while the project only said UseWPF.
Following the SDK there makes WPF and Windows Forms independent rather than
alternatives, which is what the flags enum is for: an assembly can use both,
and then both properties have to be written. Where an assembly looks like more
than one kind of project, the web SDK wins the Sdk attribute, because
Microsoft.NET.Sdk.Web imports Microsoft.NET.Sdk and so carries the desktop
targets, while Microsoft.NET.Sdk.WindowsDesktop carries no web targets.
System.Drawing stays unconditional: Microsoft.NET.Sdk.BeforeCommon.targets adds
it for every .NETFramework target rather than only for Windows Forms ones, and
on .NET Core it ships in the Microsoft.NETCore.App reference pack.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
An exported WPF project listed PresentationCore next to the implicit
Windows Desktop framework reference, which is a duplicate reference
(MSB3243) or an unresolvable one (MSB3245) once the hint path stops
pointing anywhere. The target-pack filter that should have caught it
asks the assembly resolver, which answers by probing the shared
frameworks installed on the machine running the export - so the same
assembly exported from Linux, or from a Windows box without the
desktop runtime, produced a different project file. What the SDK adds
for UseWPF is a fixed list, so match it by name instead.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Microsoft.NET.Sdk imports the Windows Desktop targets itself for .NET
Framework and for .NET 5 and later, and warns (NETSDK1137) about every
project that still names the separate SDK. Only .NET Core 3.x, where
those targets are not imported without a platform-suffixed moniker,
genuinely needs Microsoft.NET.Sdk.WindowsDesktop.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
A .NET 5 or later project that sets UseWPF or UseWindowsForms is
rejected outright (NETSDK1136) unless its target framework names the
Windows platform, so an exported WPF assembly produced a project that
could not build at all. The platform belongs to the assembly rather
than to WPF - TargetPlatformAttribute records it, SupportedOSPlatform
its minimum version - so the moniker follows the attributes wherever
they are present, and falls back to plain "windows" only for a desktop
project built before those attributes existed. Monikers older than
net5.0 take no platform suffix and must not grow one.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
The WPF markup compiler generates the program entry point from the
ApplicationDefinition item, so an exported project that lists App.xaml
as a Page has no Main at all and fails to build with CS5001. Both the
UI and ilspycmd already resolve the BAML root's partial class, which
makes deriving Application from System.Windows.Application the natural
signal. The module additionally has to have an entry point of its own:
a library that merely contains an Application subclass would otherwise
have MSBuild generate a Main into it.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Sanitizing a resource name is not injective: "a+b/logo.png", "a&b/logo.png"
and "a#b/logo.png" all come out as "a-b/logo.png". The writers create files
with FileMode.Create, so every colliding entry but the last was lost, and
nothing was written to the error list to say so - an assembly can be built
to make that happen to as many entries as it likes. A WPF probe assembly
with 20 resource entries exported as 13 files.
Uniquifying is enough because the exported item already pins the true name
in its LogicalName, so the file on disk has to be unique, not faithful. The
suffix search resumes where the previous collision on a name left off, so a
crafted pile of collisions stays linear rather than quadratic, and the name
is trimmed to keep the segment within the file system's limit.
Directory creation moves inside the per-entry error recovery for the same
reason: an entry named after a directory another entry needs makes it throw,
and that has to cost the one entry rather than the rest of the container.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
A rebuilt WPF project only resolves its own pack URIs when every entry of
"<AssemblyName>.g.resources" comes back under the resource ID it had before.
The file on disk cannot carry that ID: it is sanitized for the file system,
and the ID itself is escaped. Verified against a WPF assembly built for this:
the WPF build tasks re-escape whatever LogicalName they are given, so the item
has to hand them the decoded name, and an entry left as EmbeddedResource
rebuilds into a manifest resource of its own instead of into ".g.resources".
The adjustment is made where the items are collected, so it covers the base
class and both hosts that plug their own BAML handling into it without
widening the WriteResourceToFile or IResourceFileHandler contracts.
The Resource build action this gives them is also what #2253 asks for.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
The SDK-style writer only ever emitted EmbeddedResource items, so every other
item type the export produced never reached the project file. XAML recovered
from BAML lands in Page items: ILSpy wrote the .xaml files to disk and the
project referenced none of them, leaving an exported WPF project that cannot
rebuild.
Explicit items collide with the SDK's own globs - a UseWPF project globs
**/*.xaml into Page, and NETSDK1022 rejects the duplicate - so each include is
preceded by a remove of the same item type, the pattern the EmbeddedResource
path already uses. Setting EnableDefaultPageItems=false would work as well, but
it is WPF-specific and switches off a glob for the whole project, whereas the
remove is per item, applies to any item type, and keeps the SDK's item
definitions (XamlRuntime) in effect.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
WPF's build tasks key every Page and Resource item in
"<AssemblyName>.g.resources" by the item's relative path, lower-cased and
run through Uri.GetComponents(Path, UriEscaped) - so a folder named
"Resource Test" arrives as "resource%20test", and a folder named in any
non-ASCII script arrives as a run of UTF-8 percent escapes. Those escapes
are not part of the name; sanitizing them turned "resource%20test" into
"resource-20test" and any Chinese or umlaut folder into a line of hex.
Measured against a WPF assembly built for this: only space, '#', '{', '}'
and non-ASCII bytes are ever escaped, and Uri.UnescapeDataString is the
exact inverse - anything the escaper leaves alone contains no percent
sign, and a literal one arrives as %25. Only the WPF-generated containers
are affected, so a percent sign in any other .resources file stays part
of the name.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Roslyn passes a display class into a local function by ref, and a local
function that only forwards that parameter to a sibling has no closure
variable of its own. The closure analysis therefore found nothing to
anchor it and fell back to the root method body, which put it out of
reach of the callees it forwards to; CallBuilder then hit the assert
guarding a local function reference it cannot resolve and emitted the
raw metadata name of the target instead.
The constructor path also mixed use-site containers into a scope the
closure analysis had already determined; when the use-sites live in
separate function bodies there is no common container, and resetting to
the constructor body threw that scope away.
Assisted-by: Claude:claude-opus-5:Claude Code
Every mode loaded the input file with its own `new PEFile(fileName)` call, so a
bundle - which the UI opens fine, because LoadedAssembly goes through the file
loader registry - failed identically in all of them. The five call sites now
share one load helper that runs the same registry, which also makes archives
work and keeps one detection point for both front ends.
Which assembly inside a bundle is meant is not something to guess, so a package
without --bundle-entry lists its managed entries and exits non-zero. The entry
point is only annotated in that listing: the manifest has no marker for it, so
it is derived the way the host does it, from the runtime-config entry's name,
and stays unannotated where that does not resolve.
Assisted-by: Claude:claude-opus-5:Claude Code
The crash is a NullReferenceException in GetNodeByVisibleIndex, reached when a
background decompile realizes a node's children while the UI thread is indexing
the flattener. Eight ILSpyTreeNode.Decompile overrides call EnsureLazyChildren
from that task; two wrapped it in Dispatcher.UIThread.Invoke, six did not, and
one of the two lost its wrapper in the Avalonia port with no test noticing for a
release cycle. A rule every call site has to remember is a rule that gets broken
again, so EnsureLazyChildren marshals itself instead: SetOwner already named the
owning thread, and now also carries the host's way onto it. A call already on the
owner runs inline, so a blocking invoke cannot deadlock on itself and a nested
load costs no further hop; an unowned tree is left unmarshalled, which keeps
building a subtree on a worker and publishing it on the UI thread legal.
The affinity check stays as the regression detector, but its fail-fast throw was
worthless on its own: tree mutation happens inside callers that catch Exception,
so the throw ended up rendered into the decompiled output and the run passed. The
violation is now recorded before the throw, and an assembly-level NUnit test
action fails the test that produced one - an assembly-level teardown failure is
reported but leaves the exit code at zero.
Assisted-by: Claude:claude-opus-5:Claude Code
Issue #3290 is a NullReferenceException in GetNodeByVisibleIndex that is
provably unreachable single-threaded: TreeFlattener.Count and
GetNodeByVisibleIndex read the same totalListLength fields back to back, so a
stale index yields ArgumentOutOfRangeException, never an NRE. A stress harness
with reader threads racing an IsExpanded/Children mutator reproduces exactly
that NRE, so the crash requires a mutation from a foreign thread. The rule that
a displayed tree is only mutated from the UI thread was pure convention:
ICSharpCode.ILSpyX/TreeView contained no VerifyAccess, lock or dispatcher of any
kind, and two tree nodes already carry a Dispatcher.UIThread.Invoke workaround
for the same hazard, which means it has been hit before and fixed one site at a
time.
ICSharpCode.ILSpyX is host-agnostic and must not name a dispatcher, so ownership
is stated by the host instead of inferred: SetOwner(Thread) marks the thread
allowed to mutate a node and its subtree. Unowned means unchecked, which is what
makes the analyzer pattern legal - build a subtree on a worker, publish it on
the UI thread - without an exception carved into the rule.
The owner is resolved by walking up the model-parent chain to the nearest
explicit owner rather than stamped onto every node. That buys the propagation
rules for free: one call on the root covers the whole displayed tree, children
attached later inherit it with no bookkeeping, and a subtree built off-thread is
unchecked while it is being built yet inherits the owner the moment it is
attached - an attachment which is itself a checked mutation of the owned tree.
A subtree that already carries a different owner would otherwise leave one
displayed tree demanding two threads, so that case is reported once and the
incoming owner dropped, rather than reported on every later mutation. Re-owning
is allowed because handing a tree over is the point, but the handoff must come
from the current owner: a background thread taking a live tree away from the UI
is the race being hunted.
The check sits in SharpTreeNodeCollection.OnCollectionChanged, which every
Children mutator funnels through, and in the IsExpanded and IsHidden setters -
the three entry points that invalidate totalListLength. Checking in
OnCollectionChanged also means a violation is reported before OnChildrenChanged
rewrites the flat-list tree, so the AVL structure is left intact.
Violations are collected rather than fatal by default, with per-call-site
deduplication and a count, and the first hit of each site written straight
through to a log file so a long exploratory session can be read while it runs.
FailFast makes them throw so tests can observe one deterministically.
Everything is behind #if DEBUG plus [Conditional("DEBUG")], so the release build
has no field on SharpTreeNode and no call at any site; verified by decompiling
the release assembly.
Assisted-by: Claude:claude-opus-5:Claude Code
Issue #3290 reports a NullReferenceException inside
SharpTreeNode.GetNodeByVisibleIndex, reached from a WPF virtualizing panel's
measure pass. The frame is still live: the Avalonia SharpTreeView binds the
TreeFlattener straight to ItemsSource, so Avalonia's virtualization indexes the
same flat-list walk on every measure.
Driving the model directly shows the indexer cannot walk off the end on one
thread: TreeFlattener.Count and GetNodeByVisibleIndex read the same
totalListLength fields with nothing in between, so a stale index becomes an
ArgumentOutOfRangeException and never a null dereference. Randomized sweeps over
insert/remove/expand/collapse/hide/reparent found no state where the two
disagree. Only a mutation concurrent with the descent reproduces the reported
frame, and every tree mutation in the app is marshalled to the UI thread.
These tests pin the interleaving that the port changed: the flattened list
shrinking underneath a realized index range, and a lazy subtree loading and
reloading while scrolled. The first asserts the panel is actually virtualizing,
so it cannot quietly degrade into a non-virtualized run that proves nothing.
Assisted-by: Claude:claude-opus-5:Claude Code
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
Load_Dependencies_Resolves_References_And_Keeps_Them_In_The_List timed out on a loaded
CI agent. The idle predicate it waited on also covers the dispatcher queue, and
LoadDependenciesAsync ends with RefreshDecompiledView, so the test was waiting for a
decompilation to finish - work whose duration is a property of the machine, not of the
condition being asserted. Instrumenting the wait shows every assembly already loaded on
the first poll while dispatcher jobs stay queued for seconds, so the loads were never the
holdup.
Waiting for the list to show the resolved dependencies drops the dependency on machine
speed: under a deliberately shortened one-second deadline the previous wait failed every
run and this one passed every run.
Assisted-by: Claude:claude-opus-5:Claude Code
Obfuscators put arbitrary characters into BAML strings, and XML 1.0 has no
representation for most control characters - a numeric character reference is invalid
for them too. Writing such a document threw ArgumentException from XmlWriter, which
loses the resource on project export and shows an exception instead of the page in the
UI. The escapes are spelled the way the C# output spells them, so one convention covers
both languages. Namespace URIs have to be escaped where the XNamespace is created rather
than in the final pass: the URI is baked into every element name built from it, so
patching only the xmlns declaration would desync the two. Characters XML can carry stay
untouched, so ordinary documents decompile byte-identically.
Every BAML stream of an assembly lives in one .resources container, and the recovery
around resource writing sat outside the loop over its entries, so a single page that
could not be written discarded every other page sharing the container with it.
Assisted-by: Claude:claude-opus-5:Claude Code
An assembly can map one CLR namespace to several XML namespaces: PresentationFramework
maps its namespaces to both the winfx/2006 and the netfx/2007 presentation namespace. The
fallback used when the BAML xmlns records name no namespace for a type always preferred the
winfx/2006 one, so a document that binds the default prefix to netfx/2007 ended up with a
root start tag that declares one presentation namespace and needs the other for its own
name, which XmlWriter rejects. Only the root carries the xmlns declaration, which is why
skipping XClassRewritePass worked around it.
Assisted-by: Claude:claude-opus-5:Claude Code
GetPointerElementType existed because a pointer passing through a stack
slot could be typed IntPtr: ILReader replaced the slot type with
FindType(StackType) whenever the inferred type did not match the stack
type. With InferType() implemented on every ILInstruction that fallback
is gone (FlushExpressionStack now asserts the inferred type is
stack-accurate), so the target's inferred type is precise and the
definition chain no longer needs to be walked. Merged stack slots were
never recovered by the helper anyway (it required a single store).
Disabling the PointerType arm makes the uint*/byte* deconstruction
fixtures fail, so the sign-agnostic stobj.Type fallback remains guarded.
Assisted-by: Claude:claude-fable-5:Claude Code
Change InferType() to an abstract method and implement for every ILInstruction.
With this change, we now always have enough information to create a variable of an appropriate type to store the result of evaluating the instruction.
This previously was not the case for instructions producing "other value type", for which the stacktype-based fallback incorrectly produced `object`.
The naming rules have been warnings since they were introduced; they are
advisory conventions rather than build gates, and the noise they add to
every file listing outweighs their value.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
The nugetfuzz package cache is usually where the corpus itself came from,
so it holds exactly the dependency closure a corpus assembly references;
probing it resolves references the machine-wide NuGet cache does not have.
Reference assemblies name GAC-only internals such as SMDiagnostics and
System.ServiceModel.Internals, which ship in no pack and no package.
Reporting them as missing suggested --refs could supply them, so the
staging walk now tracks whether a name was reached through a reference
pack and drops those from the report.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
C# 12 allows both on the explicitly typed parameter list of a lambda, and
nowhere else: an anonymous method cannot declare either, and neither can a
lambda whose parameter list is about to be dropped. Guarded by a setting so
the output stays valid for earlier language versions.
Only what the anonymous function's own metadata declares is written. A lambda
may state a default the delegate does not have, a different one, or none where
the delegate has one, and reflection over the lambda's method reports what the
lambda declared - so filling either in from the delegate's Invoke would make
the recompiled assembly describe itself differently from the original. Call
sites are unaffected either way, because they bind against the delegate, which
still declares both.
Roslyn writes ParamArrayAttribute on the anonymous function's own method only
from version 5 on; before that it stands on the delegate type alone, where it
is not the lambda's to restate, so the fixture guards those cases on ROSLYN5.
The correctness test reads the metadata back through reflection, which is the
only place the difference is observable.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
The C# 10 grammar only allows attributes on a lambda or its parameters
when the parameter list is parenthesized, but LambdaNeedsParenthesis
predates attribute support and only considered the single parameter's
type and modifiers. An attributed lambda whose parameter type is erased
for being anonymous therefore printed as '[My] a => a.X', which does not
parse. Latent since attributed-lambda decompilation was added: every
other attributed lambda has explicitly typed parameters, which already
force the parenthesized form.
Assisted-by: Claude:claude-fable-5:Claude Code