177 Commits
Author SHA1 Message Date
Christoph Wille 1699b119c1 Prepare search terms once per run instead of per candidate name
AbstractSearchStrategy.IsMatch runs for every metadata row of every
loaded module on each keystroke, yet it re-stripped the operator prefix
per name and, for fuzzy terms, additionally lowercased both the name
and the term - the largest allocation source in interactive search.
The terms are invariant for the lifetime of a strategy (each keystroke
builds a new request and strategy), so the stripping and lowercasing
now happen once in the constructor, and the noncontiguous matcher
compares spans with per-char ToLowerInvariant. This switches fuzzy
matching from culture-sensitive to invariant lowercasing, which is the
appropriate semantic for matching metadata names.

The new IsMatchTests pin the +, -, =, ~ operator semantics (including
the pre-existing quirk that =Name compares against the backtick-
suffixed name of generic types) as observed before the change.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-10-10 18:30:53 +02:00
Christoph Wille 2ec4ee851d Go back to Sbom 4.1.5 bc of build warnings eg "Unhandled exception caught when trying to open "D:\\a\\ILSpy\\ILSpy\\ICSharpCode.ILSpyX\\bin\\Release\\ICSharpCode.ILSpyX.11.1.0.9858.c05ed6db.temp\\_manifest\\spdx_2.2\\manifest.spdx.json"" 2026-10-05 08:42:34 +02:00
Christoph Wille c2d51a80b9 Remove DiasymReader from restore entirely (update other NuGet packages as well) 2026-10-05 08:01:59 +02:00
Christoph Wille 1c7e0efef0 Generate SBOM only on integration build 2026-10-05 07:37:41 +02:00
Siegfried Pammer e61128fe30 Fix PDB dump review findings
Use the PDB table row width for StateMachineMethod external MethodDef references, and include method-level custom debug information in decoded PDB output. Also report a missing Portable PDB explicitly when a debug metadata table is requested from an assembly without one.

Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
2026-10-04 19:11:01 +02:00
Siegfried Pammer eec881e113 Honor bundle entries when dumping PDBs
The PDB dumper opened the outer package path directly, so --bundle-entry was ignored while dumping debug information. Load package inputs through the shared entry resolver and pass Cecil the selected assembly image while keeping ordinary files on the path-based code path for adjacent PDB discovery.

Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
2026-10-04 09:07:45 +02:00
Siegfried Pammer 30dbce447e Add --dump-pdb, a cross-platform replacement for Pdb2Xml
The Pdb2Xml command that just left the app was the only way to see a PDB's
contents, and it was Windows-only. Mono.Cecil's symbol readers cover the
Portable and the Windows (native MSF) format in managed code and pick the
right one themselves, so a single dump path serves both on every OS.

Cecil's object model, rather than raw metadata, is what makes this work for
Windows PDBs at all: they have no metadata tables for --dump-table to read.
That also settles the split between the two options - --dump-table stays the
raw row view of a Portable PDB, --dump-pdb is the decoded one.

Neither symbol reader exposes the document list, so the documents are
collected from the sequence points that reference them.

Assisted-by: Claude:claude-opus-5:Claude Code
2026-10-02 07:44:55 +02:00
Siegfried Pammer 58be31306e Dump the Portable PDB debug tables with --dump-table
The metadata-table dumper only knew the Cor tables, so the debug metadata a
Portable PDB carries had no command-line view at all; the GUI has shown those
eight tables for years. Moving the dumper into ILSpyX puts it next to the PDB
providers it now needs and makes it available to any host, not just ilspycmd.

Two things a user would otherwise trip over: a standalone .pdb is accepted as
an input file, and asking an assembly for a debug table resolves the embedded
or adjacent PDB instead of reporting an empty table. Reading a debug table
without either would silently print nothing at all.

The GUI's StateMachineMethod reader moves to a shared extension. Its own copy
read the MethodDef column with ReadInt16 and switched to the wide form at
ushort.MaxValue rather than 1 << 16, so a large enough method table would have
produced negative RIDs.

Assisted-by: Claude:claude-opus-5:Claude Code
2026-10-02 07:44:55 +02:00
Siegfried Pammer d814f4a47e Remove a set of assemblies from the list in one step
Deleting assemblies one at a time is not equivalent to deleting them
together: each removal raises its own collection change, and the
listeners of the assembly list react to one of those by pruning
navigation history, restarting a running search, re-querying every bound
command and re-decompiling whatever is still selected. Selecting an
expanded list and pressing Delete therefore paid for all of that once per
assembly, and froze the UI.

Emptying the list still reports a Remove rather than the Reset that
Clear() raises, because consumers read Reset as "re-read everything" and
several short-circuit their per-removal cleanup on it, which would leave
tabs and navigation history pointing at unloaded assemblies.

The tree's removal is reordered for the same reason: it detached and
announced one node at a time, so a listener reacting to the first
notification still found the rest of the same removal attached and took
them for a selection the user had made.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
2026-09-29 07:45:59 +02:00
Siegfried Pammer d0e7d1bd08 Merge pull request #4119 from icsharpcode/fix/2372-address-taken-by
Fix #2372: report a field's address loads on their own
2026-09-20 07:32:03 +02:00
Siegfried Pammer fa464968e1 Reorder the tree rows when the assembly list is sorted
Sorting reorders the assembly list in place and reports it as a Move, which
nothing downstream could act on: the tree node's handler had cases for Add,
Remove and Reset only, and neither the child collection nor the flattener had a
move at all. The rows therefore kept their pre-sort order until something else
forced a rebuild. Moving the node rather than removing and re-inserting it keeps
its identity, so an expanded subtree stays expanded and its row is not rebuilt.

A run longer than one row has to be reported the way the consumer reads it:
Avalonia's VirtualizingStackPanel applies a ranged move by removing OldItems.Count
rows and re-inserting them at NewStartingIndex - (Count - 1), so a run reported by
its final start index lands short by its own length. For a single row - a
collapsed node, and every move a sort makes - the two readings coincide.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-09-14 05:59:35 +02:00
Christoph Wille 18027c86bb Merge pull request #4125 from icsharpcode/christophwille/nu0926
September 2026 NuGet Updates
2026-09-09 08:15:36 +02:00
Christoph Wille 067122455a NuGet September 2026 Updates including .NET 11 RC1 with nearest Roslyn 2026-09-09 07:15:11 +02:00
Siegfried Pammer 7572b57eee Fix #2372: report a field's address loads on their own
Loading a field's address was counted as both a read and a write, so a
use that only reads - calling a method on a value-type field emits
ldflda and writes nothing - was listed under "Assigned By".

An address load says only that something needed a reference to the
field. What happens through that reference is up to the consumer, and
the IL scan here does not look at it, so the load is neither a read nor
a write and gets a category of its own. Special-casing the methods known
not to mutate their receiver would answer this one screenshot and leave
every ref argument still guessing.

Assisted-by: Claude:claude-opus-5:Claude Code
2026-09-07 23:00:53 +02:00
Siegfried Pammer 42022a3a9e Empty the panes when the assembly list is cleared
Removing assemblies one at a time cleaned up the tabs that showed them,
but clearing the whole list did not: Clear() raises a Reset whose
OldItems is null, and the handler pruned the history and returned before
the loop that empties the main tab and closes the orphaned ones. The
last decompiled member stayed on screen over an empty list.

Sorting had to be fixed first. It rebuilt the collection through Clear()
plus AddRange, so it raised the same Reset and dropped the whole
navigation history as a side effect - and once Reset closes tabs, it
would have closed all of those too. It now reorders in place, and a Move
is ignored where a removal would be handled, because a sort removes
nothing.

Assisted-by: Claude:claude-opus-5:Claude Code
2026-09-07 16:32:01 +02:00
Siegfried Pammer ad08d21884 Merge pull request #4106 from icsharpcode/perf/resolver-file-system-access
Read the file system once per reference closure, not per reference
2026-09-05 10:07:01 +02:00
Siegfried Pammer 2115bd9028 Read the file system once per reference closure, not per reference
Resolving one assembly resolves its whole reference closure, and every
reference in it asked the same framework directories the same questions.
The worst of it was the scan for the closest version folder of a shared
framework: a directory listing plus a recursive file search, repeated per
reference and per runtime pack - 42 scans for two distinct answers when
decompiling ICSharpCode.ILSpyX.dll.

The scan result is only safe to keep for a bounded time: a runtime can be
installed or removed while ILSpy runs, and reloading an assembly list has
to see that. So it is kept for the length of an explicitly opened scope,
which the type system opens around the closure it resolves and closes
again afterwards; outside a scope the file system is read as before. The
scope owns what was read, so two of them on one resolver do not stack -
the first to end takes it, and the other reads the file system again.

BeginSnapshot is on IAssemblyResolver rather than an interface of its
own: it is core functionality of a resolver, and one implementation is
not an abstraction. This breaks the interface for implementors outside
this repository, who opt out by returning null - which is what the three
resolvers here that hold nothing do.

The remaining probes cost nothing to fix: the preferred runtime pack was
listed among the defaults it already belongs to, so its directory was
scanned twice for every reference that is not in it, and one package
folder was probed once per assembly the package contains.

Measured over 27 references with a fresh resolver each time: 3.3 ms per
assembly before, 3.1 ms without a scope, 1.1 ms with one.

Assisted-by: Claude:claude-opus-5:Claude Code
2026-09-04 19:55:39 +02:00
Siegfried Pammer 9a4b8e3012 Fix #2919: keep a settings file that cannot be parsed
ILSpy.xml is the only copy of everything a user puts in it - assembly
lists above all, which people build up over years and, as the report
shows, edit by hand. A file that fails to parse was replaced by defaults
on the next save, which happens for something as incidental as a window
position, so the data was gone before the user had a chance to notice
anything was wrong.

The file is now moved aside first, under a name that says what it is, and
an earlier copy is never replaced: two bad starts in a row must not cost
the file that still has the data. A typo in hand-written XML is usually
one edit away from readable, so what matters is that it still exists.

Telling the user is still not solved - that needs somewhere central to
report it from, which the settings do not have yet - but the file is
recoverable, and its name says why it is there.

Assisted-by: Claude:claude-opus-5:Claude Code
2026-09-04 14:40:46 +02:00
Siegfried Pammer 228041085b Fix #3290: marshal EnsureLazyChildren onto the tree's owning thread
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
2026-09-01 22:50:42 +02:00
Siegfried Pammer ca3e528ce2 Add a Debug-only thread-affinity check to the shared tree model
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
2026-09-01 22:50:42 +02:00
Siegfried Pammer af941339c0 Fix #2054: repair a chain of type forwarders that cycles
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
2026-09-01 12:20:56 +02:00
Christoph Wille 4980c011b8 Never size bundle entry decompression from the manifest's declared size
A single-file bundle manifest is attacker-controlled. Opening a compressed
entry pre-allocated a MemoryStream of the declared decompressed size
through an unchecked long-to-int cast, then inflated the whole deflate
stream before comparing lengths. A few-byte payload declaring ~2 GB thus
forced a ~2 GB allocation up front, sizes at or above 2 GB wrapped to a
negative capacity, and a decompression bomb was expanded in full before
the mismatch was noticed (CWE-789, CWE-197).

Grow the buffer only with bytes the deflate stream actually produces and
stop reading one byte past the declared size, which already proves the
entry corrupt. Reject declared sizes that cannot fit a single in-memory
buffer as invalid bundle data. Entry offsets need no extra check: the
UnmanagedMemoryStream over the mapping already validates them against the
view length.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-08-25 11:40:54 +02:00
Siegfried Pammer 3dfb59ac87 Resolve package entries by identity and end walks nobody reads
Searching a package resolves its entries by file name against a case-insensitive
cache, which is right for an assembly reference but wrong for an archive entry:
two entries differing only in case are two files, and they collapsed onto one
LoadedAssembly, so one was searched twice and the other never. Keying the cache
by the entry itself separates them, and the entry's package-relative path
becomes the assembly's file name, which is what tells the copies of one assembly
in a multi-target package apart wherever a search result shows a location.

Cancellation was only checked between top-level list entries, so a walk the user
had already replaced by typing another character kept extracting package entries
alongside the run they were waiting for. The omnibar had no way to end its run at
all: its view model is per document tab and nothing cancelled it when the tab
went away.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-08-17 22:39:27 +02:00
Siegfried Pammer 36fa5a3f90 Find package-nested assembly nodes without walking the tree
Resolving a type to its tree node scanned every descendant of the root, which
means every namespace node of every assembly - all of them built eagerly - to
find the one assembly node it needed. A package child records the bundle it
came from, so that chain leads straight to the single top-level node worth
descending into, and only that package's folders are searched from there.

The two sibling lookups only ever considered the root's direct children, so
neither resolved anything inside a package at all. Sharing one helper fixes
them along the way, and it expands package folders on the descent because
search surfaces package contents whether or not the tree was ever opened
there.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-08-17 10:24:23 +02:00
Siegfried Pammer b2b82ce6a8 Stream the search's assembly walk instead of materializing it
Searching inside bundles and packages means expanding them, and the expansion
has to await each assembly's load result - which is what triggers the lazy
load in the first place. Building the full list up front (as the WPF pane did)
therefore means a search on a freshly restored list produces nothing at all
until the last assembly is off disk, and the blocking wait for it ignored the
cancellation token the pane fires on every keystroke.

The snapshot is still taken eagerly, before the first element is yielded, so
the set cannot change under a running walk; a failing assembly or an
unreadable package entry skips itself rather than abandoning the rest.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-08-17 09:23:39 +02:00
Christoph Wille cdeae656fe NuGet August 2026 Updates (#3981)
* Set v11 RTM
* Update features in README.md
* Remove the two 900-iteration process-list scroll tests
* Keep Svg.Controls.Skia.Avalonia at 12.0.0.13
* 10.0.11 and Roslyn for net11p7
* Fix module-scan test failing when PowerShell's NGen images are stale
* Opt Pack NuGets out of the MSBuild server to fix SBOM generation
2026-08-11 21:56:11 +02:00
Siegfried Pammer 5a6972323d Fix #633: add an Overrides analysis showing overridden base members
The analyzer panel could only walk downward (Overridden By, Implemented
By); finding what a member overrides required manually climbing base
types. The upward interface direction already exists as the Implements
analysis, so this adds the missing base-class direction as its dual.

The analyzer walks the override chain member by member via
InheritanceHelper.GetBaseMember instead of collecting all
signature-equal base members, so it stops at 'new virtual' shadow
boundaries, which hide any further base members from overriding. No
assembly-scope scan is needed; the walk is a direct type-system lookup.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-27 17:11:11 +02:00
Christoph Wille f5199fae24 Add ILSpyXEventSource for assembly load, resolve, search and analyzer tracing
ILSpyX had no instrumentation, yet most UI-visible latency bottoms out
here: lazy assembly loads, the first-resolve cascade that metadata-loads
every assembly in a list snapshot, per-module search strategy runs,
analyzer scope scans over all assemblies and their references, bundle/zip
entry extraction, and PDB loading. The provider mirrors the
ICSharpCode.Decompiler design: Start/Stop pairs, keyword gating, and
IsEnabled() guards at every call site; per-entry package extraction is
Verbose because of its volume.

AbstractSearchStrategy.Search is now a non-virtual template method that
wraps the span around a new protected SearchCore, so derived strategies
cannot bypass the instrumentation.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-22 21:10:41 +02:00
Christoph Wille b888153f24 NuGet 07/26 updates and .NET 11 P6 Roslyn 2026-07-14 20:46:08 +02:00
Siegfried Pammer 4d42b38af1 Handle truncated XALZ input via EndOfStreamException, not length checks
Pre-checking stream.Length before reading is racy (the stream can shrink
between check and read) and not every stream knows its length ahead of
time. Instead, catch EndOfStreamException at each read: a stream too
short for the magic is passed through as not-XALZ, and a truncated
header or payload after a confirmed magic is rejected as
InvalidDataException. stream.Length remains only a sizing hint for the
payload buffer.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-06 20:19:17 +02:00
Christoph Wille c685d5305b Bound XamarinCompressedFileLoader against crafted XALZ headers
The Xamarin XALZ loader sized its buffer allocations from an attacker-controlled
header field and ignored the partial-read length, so merely opening a crafted
file (the loader is registered first and runs on any XALZ-magic input) could
crash or over-allocate. The declared uncompressed length, a raw header uint cast
to int, had no sanity bound: a tiny file claiming ~2 GB forced a giant
ArrayPool.Rent (decompression-bomb amplification), and a high-bit value became
negative and made Rent throw ArgumentOutOfRangeException. The compressed length
was taken as the whole file (header included) and ReadAsync's return value was
discarded, leaving stale pooled bytes in the tail fed to the decoder; the
output MemoryStream then exposed the entire rented buffer, so PEFile parsed past
the real decompressed data into leftover pool contents.

Bound the declared length before renting (reject zero, > int.MaxValue, or more
than an LZ4 block could expand from this payload at its 255x maximum ratio),
read the payload that actually follows the header with ReadExactlyAsync, and
slice the output to the length LZ4Codec.Decode reports. Malformed input now
fails as a catchable InvalidDataException, consistent with the bundle and .rsrc
hardening; well-formed Xamarin modules load exactly as before.

Assisted-by: Claude:claude-opus-4-8:Claude Code
2026-07-06 20:19:17 +02:00
Siegfried Pammer 07d5a01d2b Add license headers to source files that lacked them
Backfills the standard MIT X11 header on hand-written files that never
got one, attributing each to its first-commit author and year from git
history. Code vendored from dotnet/runtime and Humanizr/Humanizer gets
its origin's license lines and a provenance note instead. Generated
files (Resources.Designer.cs, the version-info template), tool-managed
suppression files, and BAML test-case fixtures intentionally stay
header-less.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-05 21:09:46 +02:00
Siegfried Pammer 17b3539a20 Add a per-assembly target-framework override for reference resolution
ILSpy resolves an assembly's references against the target framework it
detects from the TargetFrameworkAttribute. When that attribute is missing,
wrong, or the user wants to force a different framework, there was no way to
hint the correct one, so references could resolve against the wrong runtime
pack or framework directory.

A LoadedAssembly can now carry a TargetFrameworkIdOverride that short-circuits
detection (it is the single value every LoadedAssembly-based resolution path
reads), is persisted in the assembly-list XML, and is carried across a reload
so a runtime change re-resolves against the new framework. The "Set Target
Framework" context-menu entry edits it through a dialog with a free-form text
box and an always-visible list of common monikers to pick from (the app forces
overlay popups, so a dropdown would be clamped inside the small dialog); input
is validated and converted from the short TFM users know (net48) to the long
FrameworkName form the resolver consumes (.NETFramework,Version=v4.8) via
NuGet. The direct DetectTargetFrameworkId callers in the decompiler core
(project export, language version) intentionally keep reading the real
attribute; only reference resolution is overridden.

Resurrects a 2020 prototype (branch tfmoverride) re-implemented against the
current ILSpyX/Avalonia code, whose surrounding structures no longer matched.

Assisted-by: Claude:claude-opus-4-8:Claude Code
2026-07-01 05:50:11 +02:00
Siegfried Pammer 9d38e6c962 Share one MutexProtector between settings and the bookmark list
BookmarkManager carried a byte-for-byte copy of ILSpySettings' private
MutexProtector, both guarding read-modify-write of a config sidecar in the same
settings directory so parallel ILSpy instances fold their edits in turn instead
of clobbering each other. Promote it to a single public type in
ICSharpCode.ILSpyX.Settings so the locking semantics live in one place.

Assisted-by: Claude:claude-opus-4-8:Claude Code
2026-06-30 01:43:00 +02:00
Christoph Wille da57bad452 Merge pull request #3758 from icsharpcode/christophwille/nu0626
NuGet June 2026 Updates
2026-06-10 16:21:11 +02:00
Siegfried Pammer e0cde005f9 Carry all publish RIDs in the core-library lock files
ICSharpCode.Decompiler / ILSpyX / BamlDecompiler set RestoreLockedMode but
declared no RuntimeIdentifiers, so their lock files were RID-agnostic. A
RID-specific 'dotnet publish -r <rid>' of ILSpy (which declares all four
distribution RIDs) propagated the RID to these locked project references and
failed restore with NU1004 unless --no-restore or --force-evaluate was used.
Declare the same win-x64;win-arm64;linux-x64;osx-arm64 set on the three
libraries and regenerate the locks so a locked-mode publish works for every
platform out of the box.

Assisted-by: Claude:claude-opus-4-8:Claude Code
2026-06-10 00:11:56 +02:00
Siegfried Pammer 87748e0cde Override vulnerable transitive packages from the DiaSymReader chain
The Microsoft.DiaSymReader / .PortablePdb packages pull NETStandard.Library
1.6.1, which drags in 4.3.0 builds of System.Net.Http, System.Private.Uri
and System.Text.RegularExpressions, all carrying known advisories (NU1902/
NU1903). They are framework-provided at runtime on net10.0; pin the patched
4.3.4 / 4.3.2 / 4.3.1 via central transitive pinning so NuGet audit passes.
Enabling transitive pinning also aligns a handful of other transitive
packages to their declared central versions.

Assisted-by: Claude:claude-opus-4-8:Claude Code
2026-06-10 00:11:56 +02:00
Siegfried Pammer 8c433ea3ff Add SharpTreeNode.ReloadChildren for the lazy-subtree reset
The UseNestedNamespaceNodes and HideEmptyMetadataTables handlers both
force a materialised node to re-create its children (clear, re-arm
LazyLoading, EnsureLazyChildren), guarded on it not already being lazy.
That's a general tree-node operation -- a soft, in-place rebuild that
re-runs LoadChildren without reloading the assembly -- so it belongs on
SharpTreeNode next to the lazy contract it builds on, not as a private
helper in the model. Both call sites now use node.ReloadChildren().

Assisted-by: Claude:claude-opus-4-8:Claude Code
2026-06-10 00:11:56 +02:00
Siegfried Pammer 667f6ad5f8 Add pwsh dev scripts, repo-wide lock files, and CI NuGet caching
Replace the Windows-only .bat helpers (clean / debugbuild / releasebuild /
restore / updatedeps and BuildTools/format) with cross-platform pwsh
scripts at the repo root: restore.ps1, build.ps1 (-Configuration), clean.ps1,
updatedeps.ps1 and BuildTools/format.ps1, alongside the existing publish.ps1.

Enable a packages.lock.json for every project by hoisting
RestorePackagesWithLockFile into the root Directory.Build.props (the four
core libraries set it individually before) and commit the generated locks,
so restores are repeatable and CI can cache packages off them.

Cache the NuGet packages folder in the three setup-dotnet workflows
(build-ilspy, build-frontends, codeql-analysis), keyed on the lock files
per the setup-dotnet caching guidance.

Scope the Debug "Verify package contents" check to the *.filelist outputs
it actually generates. A project's packages.lock.json is keyed only by
(framework, RID), with no host-OS axis, so a lock produced on Linux
legitimately differs from one produced on Windows whenever an OS-conditional
PackageReference applies (Debug+Windows pulls Microsoft.DiaSymReader*). The
Windows restore then rewrites those locks; that churn must not fail a step
whose job is to police the VSIX/MSI file lists.

Also drop the dead ILSpy.BamlDecompiler publish line from
publishlocaldev.ps1, mirroring the earlier publish.ps1 fix.

Assisted-by: Claude:claude-opus-4-8:Claude Code
2026-06-09 00:58:19 +02:00
Siegfried Pammer 4585be8de9 Raise atomic ranged events from the tree flattener
NodesInserted/NodesRemoved reported a multi-node change (a node plus its visible
descendants -- a contiguous run) as a sequence of single-item Add/Remove events,
but the underlying tree updates Count by the WHOLE run before they fire. A
consumer that reconciles the change one item at a time and re-indexes the source
mid-sequence then reads against a Count that has already dropped by the full run:
Avalonia's SelectionModel does exactly this (it re-reads SelectedItems while
handling each Remove), indexing past the end and throwing ArgumentOutOfRangeException
out of TreeFlattener's indexer whenever a selection was live during a bulk reshape
(e.g. toggling UseNestedNamespaceNodes). It only surfaced under full-suite timing,
which is why it read as flaky.

Raise one ranged event for the whole run instead, so the notification matches the
tree's state in a single step and Count/indices stay in agreement. The indexer
keeps throwing on genuine out-of-range access. Avalonia 12.4's virtualization and
selection handle ranged Add/Remove (every expand/collapse exercises it); full
headless suite is green.
2026-06-08 18:34:56 +02:00
Siegfried Pammer c5f8834e57 Stop the assembly list from disposing removed assemblies
Unload / Clear / ReloadAssembly / HotReplaceAssembly all called LoadedAssembly.Dispose() on the assembly they removed, which disposes its MetadataFile and unmaps the underlying file. But open document tabs and tree nodes can still hold that MetadataFile, and the list has no safe point at which to know those references are gone -- so disposing risked unmapping a file out from under a live reader (use-after-dispose). Drop the removed assembly and let the GC reclaim it once nothing references it instead. The DockWorkspace cancel-on-remove is now a courtesy, not a guard against an unmap race.
2026-06-08 18:34:55 +02:00
Siegfried Pammer dc1ae9c467 Show About page and seed running framework on first run
Two first-run behaviours that the previous version had and the port had
lost. When a launch has nothing to restore (no saved tree path, no
command-line target), greet the user with the About page in the main
tab instead of an empty "(no selection)" view; the greeting is fired
after the tree is ready and only takes the main tab when it's still
empty and active, so it can't steal activation from a tab the user
opened meanwhile (e.g. Compare...). And seed the default list with the
.NET framework ILSpy itself runs on - every managed assembly in the
running runtime's shared-framework directory. Headless tests opt out of
that seed (re-opening ~150 assemblies per test is ~10x slower) and fall
back to the minimal trio, leaving their expectations unchanged.

Assisted-by: Claude:claude-opus-4-8:Claude Code
2026-06-08 18:34:55 +02:00
Siegfried Pammer bb114b27a7 Prevent FailFast when an assembly is unloaded mid-decompile
When the user removes an assembly while a decompile is still in flight,
the title-refresh paths (spinner tick, post-Task.Run InvokeAsync,
StopSpinner) walk node.Text -> LoadedAssembly.Text ->
metadata.GetAssemblyDefinition().Version, reading directly from the
PE file's MemoryMappedFile. AssemblyList.Unload calls Dispose
synchronously and unmaps that file; any continuation that lands on the
UI thread after Dispose dereferences freed pages and the CLR reports
the AV through FailFastIfCorruptingStateException -- a catch block
cannot intervene in .NET 5+.

Assisted-by: Claude:claude-opus-4-7:Claude Code
2026-06-08 18:34:54 +02:00
Siegfried Pammer 2510e70fc6 Search: <angle brackets> opt in to private/compiler-generated entities
The default ApiVisibility=PublicOnly setting filters search results
through CheckVisibility, which rejects private entities. Compiler-
generated names — async/iterator state machines (<Method>d__N),
display classes (<>c__DisplayClass), anonymous-method closures (<>c)
— are all private, so they were unfindable even when the user
typed the exact full name.

Assisted-by: Claude:claude-opus-4-7:Claude Code
2026-06-08 18:34:54 +02:00
Siegfried Pammer 184fe9b650 ImageListResourceEntryNode + factory wiring
Assisted-by: Claude:claude-opus-4-7:Claude Code

Assisted-by: Claude:claude-opus-4-7:Claude Code
2026-06-08 18:34:54 +02:00
Siegfried Pammer 83716e1801 Port ILSpy.Tests analyzer-library tests
Port all five analyzer test files from ILSpy.Tests/Analyzers (the
WPF-side test project that exercises the shared ICSharpCode.ILSpyX
library) into ILSpy.Avalonia.Tests/Analyzers/Library. The analyzer
logic itself is platform-agnostic, so the tests run unchanged once
the test assembly can reach the library's internals.

Assisted-by: Claude:claude-opus-4-7:Claude Code
2026-06-08 18:34:53 +02:00
Siegfried Pammer a9b6b0d318 Tree paints before decompile; AssemblyTreeNode observes load
Two ordering fixes for startup so the user sees the assembly tree before any
decompiled content shows up in the document area, and so the lazy-load story
actually holds.

Assisted-by: Claude:claude-opus-4-7:Claude Code
2026-06-08 18:34:53 +02:00
Siegfried Pammer 2d0a7418d7 Lazy LoadedAssembly + 2s cooldown sweep + perf test
Replace LoadedAssembly's eager Task.Run(LoadAsync) with a Lazy<Task<LoadResult>> so
construction is free and the load only starts on the first GetLoadResultAsync /
await / .Result access. Active assembly's load fires from the saved-path restore
and runs without competition; everything else stays cold until either the user
expands a tree node OR the cooldown sweep fires.

Assisted-by: Claude:claude-opus-4-7:Claude Code
2026-06-08 18:34:53 +02:00
Siegfried Pammer 6f955e0a1f Factory-walking resource dispatcher + Save scaffolding
Move IResourceNodeFactory to ILSpyX.Abstractions and GuessFileType to ILSpyX.Util
so both UIs can consume them. Wire ResourceEntryNode.Create(Resource) to walk the
MEF-discovered IResourceNodeFactory list (falls back to the generic node), make
Avalonia's ILSpyTreeNode implement ITreeNode for that contract, and register a
ResourcesFileTreeNodeFactory to keep .resources files routed correctly. Adds the
Save virtual on ILSpyTreeNode plus inline "Save" buttons + SaveAsync on
ResourceEntryNode/ResourceTreeNode using a new FilePickers helper around
StorageProvider.SaveFilePickerAsync.

Assisted-by: Claude:claude-opus-4-7:Claude Code
2026-06-08 17:30:07 +02:00
Siegfried Pammer e02097dccf Replace ISettingsFilePathProvider with Func<string> and move IResourceFileHandler to ILSpyX. 2026-06-08 07:34:42 +02:00