Commit Graph
9597 Commits
Author SHA1 Message Date
Siegfried Pammer 826cf02254 Confine the receiver machinery to instance compound assignment operators
The reader's receiver materialization, the inlining slot restriction, and the
receiver-use predicate keyed on any instance operator, so C++/CLI-style
value-returning classic operators - which have an ordinary call spelling and
need none of it - got their receivers spilled into locals. CallBuilder had the
mirror problem: it took classic instance operators off the candidate path they
always used. One predicate now decides what the machinery applies to: an
instance operator under one of the op_*Assignment names, which classification
already guarantees has the C# 14 shape.

Assisted-by: Claude:claude-opus-5:Claude Code
2026-08-29 10:22:56 +02:00
Siegfried Pammer 67e22d7057 Flush pending side effects before materializing an operator receiver
The receiver slot is appended to the current block, but the expression stack
was only flushed inside the per-parameter loop - which the increment operators,
taking no parameters, never enter. A side effect still pending on the stack was
then emitted after the receiver read it should precede, so the increment
applied to a stale value of the field it targets.

Assisted-by: Claude:claude-opus-5:Claude Code
2026-08-29 10:18:05 +02:00
Siegfried Pammer e4fa167afc Render user-defined compound assignment operator declarations
TypeSystemAstBuilder writes the C# 14 "operator +=" declaration form, behind a
support flag like the other version-gated operator syntax; a non-public
operator has no legal operator declaration (CS9308) and falls back to a plain
method, except an explicit interface implementation, which is private in
metadata but still written in operator form. An instance operator hides by
signature like an ordinary method, so it can carry "new"; the
[CompilerFeatureRequired] marker the compiler emits is removed like the other
feature markers. Tooltips get the same rendering via a ConversionFlags bit;
widening ConversionFlags.All also turns on the existing checked-operator and
unsigned-right-shift flags, which the tooltip ambience now sets explicitly.

The test fixtures land here, where the pipeline is complete end to end: pretty
and IL round-trips, correctness runs against Roslyn's C# 14 binding (including
the operator-inheritance matrix and a ref-local target), and the ugly
configuration pins the output with the setting off.

Assisted-by: Claude:claude-opus-5:Claude Code
2026-08-29 09:39:10 +02:00
Siegfried Pammer 91dd944d29 Fold instance operator calls into compound assignment syntax
A call to an instance compound assignment operator becomes "x op= y" or "x++"
only when C# would accept the receiver in that position and bind the same
operator there: the receiver has to be an assignable variable (rather than
"this" in a class, a foreach or using variable, an "in" parameter, or a
readonly field outside its constructor), an operator declared in an interface
needs an interface- or type-parameter-typed receiver, and the operator form
cannot pick an overload by parameter modifier, so a call to an "in" overload
whose by-value sibling is applicable stays a call. A non-public operator has no
operator form at all (CS9308), and stays a call too.

In the other direction, "x = x + y" keeps the explicit spelling wherever
folding it to "x += y" would hand the statement to an applicable instance
operator.

Assisted-by: Claude:claude-opus-5:Claude Code
2026-08-29 09:39:10 +02:00
Siegfried Pammer d41032efbe Bind instance compound assignment operators when building expressions
"x op= y" and a prefix increment resolve in two phases: the instance operators
reachable from the static type of x come first wherever x is a variable -
whether or not the result is used - and the static operators only if none of
them is applicable. A postfix increment whose result is used is the one form
that always binds a static operator. CallBuilder models the first phase when it
checks that recompiling an instance operator call binds the same method; the
fallback phase is deliberately not modeled, since a call the instance
candidates cannot account for must give up the operator form rather than
collide with a static operator.

The same rule cuts the other way for the folds built from static operator
calls: a shadowed fold has to be written in one of the static-binding forms.
The binary operators become "x = x + y"; an increment becomes a postfix
increment whose result goes to a discard, "_ = x++;". That form only exists as
a statement, so the folds that would embed a shadowed increment in an
expression hold back and FixRemainingIncrements gives the increment a statement
of its own instead. A foreach variable cannot be such a receiver at all (it is
read-only), so the loop keeps the existing variable as a writable copy, both
for a plain loop variable and for a deconstruction.

Assisted-by: Claude:claude-opus-5:Claude Code
2026-08-29 09:39:03 +02:00
Siegfried Pammer 3ff2d0f108 Keep the receiver of an instance compound assignment operator a variable
An instance compound assignment operator has no callable spelling: its call
sites can only be written as "x op= y" or "x++", and C# requires x to be an
assignable variable whose static type binds the operator the call names. The
reader therefore parks an object-typed receiver in a stack slot typed with the
operator's declaring type, and the passes that would substitute or retype such
a receiver hold back. CallInstruction vetoes unfit receiver replacements
through SatisfiesSlotRestrictionForInlining - not writable, not a storage
location, or of a type that declares a hiding operator of the same name or its
checked/unchecked sibling (a declaration-existence check on the type system,
deliberately not a binding question) - which covers inlining; copy propagation
asks the same check per receiver load, the using-transform hands receiver
loads a writable copy of the read-only using variable, and stack slots that
were typed on purpose keep their type when translated.

Assisted-by: Claude:claude-opus-5:Claude Code
2026-08-29 09:38:34 +02:00
Siegfried Pammer 2291586659 Classify C# 14 user-defined compound assignment operators in the type system
C# 14 lets a type declare "void operator +=(T)" and friends: void-returning
instance methods under the op_*Assignment metadata names. Only that exact shape
becomes SymbolKind.Operator, and only while the new setting is on, because other
languages use the same names for unrelated methods: F# mangles
"static member (+=)" to a static, value-returning op_AdditionAssignment, and
C++/CLI emits value-returning instance operators. Those, and every method when
the setting is off, stay plain methods with a surfaced [SpecialName].

Assisted-by: Claude:claude-opus-5:Claude Code
2026-08-29 09:37:40 +02:00
Siegfried Pammer 46d8f8c4f4 Merge pull request #4067 from icsharpcode/docs/code-review-guidelines
Add code review guidance and require Assisted-by on every agent commit
2026-08-29 09:29:28 +02:00
Daniel Grunwald aa00e3fb84 Merge pull request #4068 from icsharpcode/type-inference
Type inference for switch-expressions
2026-08-29 09:23:19 +02:00
Daniel Grunwald 5d9cdce94c We need to properly compare the C# type, not just the StackType, otherwise our implicit conversions might end up converting to the wrong type.
This also matters in the `1 => DateTime.Now, 2 => null` case -- BestCommonType infers `DateTime` here, but we need `DateTime?` instead. But both had `StackType.O` so this went wrong prior to this commit.
2026-08-28 21:15:15 +02:00
Daniel Grunwald b54a073efe Improvements to type inference:
* merge object/dynamic distinctions like we do with tuple element names. This fixes BestCommonType(object, dynamic).
 * add a test that `new[] { 1, null }` has the "best common type" = `int`. The conversion error from `null` to `int` only happens later, it's not related to the best common type computation.
2026-08-28 21:05:19 +02:00
Siegfried Pammer 2d9f852cd0 Merge pull request #4065 from icsharpcode/fix-3683
Insert casts in switch expression to ensure a best common type exists
2026-08-28 19:03:54 +02:00
Siegfried Pammer 89719860b9 Require the Assisted-by trailer on every agent-written commit
The previous wording described what an AI-assisted commit looks like
without saying the trailer is mandatory, which left it readable as a
convention for substantial changes only. The marker is what lets a
reader tell which work came from an agent, so it has to hold across all
of it -- an exemption for trivial commits makes the absence of the
trailer meaningless. The same reasoning closed a matching gap in how
agents comment on PRs, where a single disclaimer on a review body left
eleven inline comments unmarked.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-08-28 18:51:19 +02:00
Siegfried Pammer 6c261321d5 Add a code review guide and point CLAUDE.md at it
An agent review of PR #4065 posted twelve comments and eight had to be
withdrawn. Most were output of a decompdiff corpus sweep run against the
PR branch, which measures the whole decompiler rather than the diff, and
one called a cast a regression when it was the correct emission -- the
old form compiled only because C# target-types the switch expression.

The guide lives in .github/ rather than inline because most sessions
never review anything and should not carry it, and because it is as
useful to a human reviewer as to an agent. CLAUDE.md keeps only the two
rules that have to hold before you have decided you are reviewing at
all, plus the pointer.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-08-28 18:51:19 +02:00
Daniel Grunwald 1a784e2fa9 Fix throw switch-expr; by specifying the typeHint for throw. 2026-08-28 09:19:11 +02:00
Daniel Grunwald ca24b8f3af Fix #3683: insert casts in switch expression to ensure a best common type exists. 2026-08-27 23:14:42 +02:00
Siegfried Pammer 8997904b72 Merge pull request #4029 from icsharpcode/refactor/debug-steps-typed-il
Walk the whole decompiler pipeline in the Debug Steps pane
2026-08-27 10:51:03 +02:00
Siegfried Pammer 3a8340ef60 Stop pointing the ILAst docs at a language that is not there
The ilspycmd dump and the contributor notes described themselves by reference to
the UI's ILAst language; the pane is what walks that pipeline now.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-08-27 09:46:46 +02:00
Siegfried Pammer ea28552655 Name the new file's author, and require the group depth
The file this branch adds takes the contributor's name rather than
AlphaSierraPapa, and three comments it added drop their en-GB spelling.

EndOpenGroups now requires its target depth: zero is the one value that closes
groups the caller does not own, which is the misattribution the depth argument
was added to prevent.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-08-27 09:46:46 +02:00
Siegfried Pammer 90d563b796 Walk the step tree with the helper the repo already has
Three copies of the same pre-order walk across two test files become
TreeTraversal.PreOrder, and the stepper fixture builds its decompiler through
the file-name constructor instead of assembling the type system by hand.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-08-27 09:46:03 +02:00
Siegfried Pammer 72ed28f903 Report the halted-ILAst write through a return value
The ref parameter was only there because a partial method could not return one.
An extended partial can, so the caller reads the answer where it uses it. The
implementation stays behind DEBUG, which now needs a Release counterpart: an
extended partial must have one in every configuration, and there the step limit
is never set, so it never writes.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-08-27 09:46:03 +02:00
Siegfried Pammer 1e95c82a52 Show each step's pipeline half, and filter without building the tree
Two things about the step tree's presentation. Nothing said which half of the
pipeline a step belonged to, so each one now names its phase from what it points
at - instructions for the IL half, syntax nodes for the C# half - and a group
opener with no anchor takes it from the first step underneath.

And the wrappers are built on demand because a recorded type runs to tens of
thousands of steps, but the filter walked them and so built every one on the
first keystroke. It now asks the recorded steps whether a subtree contains a
match, and only descends into wrappers that already exist or are on a revealed
path.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-08-27 09:45:49 +02:00
Siegfried Pammer b8d68d9143 Give the builder output a step of its own
The state the ExpressionBuilder and StatementBuilder leave behind was only
reachable as "state before the first AST transform" - an entry that names a
transform rather than the state, and that sits below every member's group in a
tree with one group per member. It is now a top-level step at the seam, where
the whole type is converted and nothing has transformed it yet.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-08-27 09:45:49 +02:00
Siegfried Pammer 44116503d9 Take the halted function from the stepper
Two extra pieces of state existed to reconstruct what the stepper already
records: which member a halt belongs to. The step the limit stopped on answers
it, except on a member's opening step, which is where the previous member's
state ends - and there the last step actually recorded answers it instead.

IL group openers gain an anchor so their position resolves. The member opener
deliberately keeps none: anchoring it to the member the pipeline is about to
start is exactly the misattribution the fallback exists to avoid.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-08-27 09:45:49 +02:00
Siegfried Pammer f24e7faa96 Carry step recording in the decompilation request
Whether a run records was read from the pane's own state at the moment the
decompiler was built, on a background task. A step index only means anything
against a run recorded the same way, so the full run and the step-limited replay
of one of its indices have to agree on it - and a view that can be attached or
detached between the two is the wrong place to ask.

The flag now travels in DecompilationOptions beside StepLimit and IsDebug, read
from the workspace on the UI thread when a run starts. Putting it on the tab
instead would miss the tabs opened after the pane, and the pane can be opened
before any tab exists at all.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-08-27 09:45:49 +02:00
Siegfried Pammer 1118f2c056 Let the pane's steps go when it closes
The recorded steps are pinned on the MEF-shared C# language, but the release
went through the pane's attached language, which is null whenever another
language is selected - open the pane on C#, switch to IL, close it, and the tree
stayed alive until the next full C# run. The language still raises StepperUpdated
at the end of every run, so a run that outlived the pane pinned its tree straight
back in. And opening the pane re-ran the decompile whatever the language,
discarding the view for a tree that only C# can produce.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-08-27 09:45:28 +02:00
Siegfried Pammer 0b4b2a3f08 Record the whole pipeline or none of it
Recording only gated the IL half, so a run with it off still numbered the C# AST
transforms into the shared stepper. That gave one pipeline two numbering scales,
and a step index is only meaningful against the scale it was recorded on: a tree
captured under one and replayed under the other selects a different step. It
also let the crashed-member attribution fire on a counter that had never moved -
a limit of zero matched at every throwing transform and rendered an unrelated
member's ILAst.

The flag now gates both halves, so steps exist exactly when recording is on, and
it lives on the pane instance rather than a static the background decompile read
across threads.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-08-27 09:45:28 +02:00
Siegfried Pammer a5bd230e14 Walk the whole decompiler pipeline in the Debug Steps pane
The pane used to split the pipeline across two languages: the ILAst language
stepped the IL transforms, the C# language stepped the AST transforms, and
nothing showed the seam between them, so a step index meant a different thing
depending on which language happened to be selected. Recording both halves into
one Stepper makes an index replayable across the whole pipeline; a limit that
lands in the IL phase has no C# to print, so the halted function is rendered as
ILAst instead.

Which function that is takes some care, because a member group's EndStep is the
next member's first step: a halt standing on a member's opening step belongs to
the member that just finished, a transform that throws where the limit was aimed
has to hand over the ILAst it half-transformed (what the ILAst language showed
as "ILAst after the crash"), and a step recorded on a helper function the
pipeline has not attached yet belongs to that function's own tree.

Retention stays opt-in twice over: the decompiler records IL steps only when
asked to, and the pane asks only while its view is on screen. Every kept step
pins the ILAst it captured, which for one type runs to tens of thousands of
nodes, so a closed pane would be paying for a tree nobody displays.

What is left of the ILAst language is its typed-IL dump, which runs no
transforms at all. That stays, as TypedILLanguage. IDebugStepProvider was down
to a single implementation and is removed.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-08-26 22:41:08 +02:00
Siegfried Pammer a980639f99 Pin the deconstruction shapes the transform has to keep matching
Twenty shapes that no fixture covered: nesting at depth three and at position
zero, inner discards on either side, property and no-conversion targets, and
deconstruction inside try, switch, if/else and while. All but one already
decompile correctly - they are checked in so a future change to
DeconstructionTransform cannot silently drop them.

The one that does not is left commented out with a pointer to #4059 rather than
as a red test: two back-to-back deconstructions share their out-slot
temporaries, and neither is recognized.

#4059

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-08-26 22:04:04 +02:00
Christoph Wille 27b45a4458 Fix #4054: Accept drops from sources that do not permit the Move effect
The assembly list answered DragOver with the Move effect only. OLE
intersects that answer with the effects the drag source permits, and
sources such as FileLocator Pro only permit Copy, so the negotiation
ended in None and the drop was refused although CF_HDROP was present
and the file path resolved fine. Explorer permits Move, which is why
drops from Explorer kept working. The WPF ILSpy offered Move, Copy and
Link and let the source pick; restore that.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-08-26 20:19:04 +02:00
Siegfried Pammer 5571b87074 Merge pull request #4051 from icsharpcode/docs/decompiler-engine-guardrails
Require spec references and design sign-off for engine changes
2026-08-26 08:29:51 +02:00
Siegfried Pammer 76ec7588e3 #2764: Add regression test for duplicate assembly references
A merging obfuscator can leave a module referencing two versions of the same
assembly. Loading both split every type they declare into two definitions that
compare unequal, so a signature naming such a type through one reference stopped
matching a base method naming it through the other, and a genuine override was
printed as virtual. ac0ef8a11 (#3253) dropped the lower-version duplicates, but
nothing covered that, and neither of the existing fixture kinds can carry the
three assemblies the situation needs.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-08-25 22:46:59 +02:00
Christoph Wille e930120ade Merge pull request #4047 from icsharpcode/fix/resources-file-length-validation
Bound every .resources length against the remaining stream before allocating
2026-08-25 18:03:58 +02:00
Christoph Wille 45470dfbed Merge pull request #4048 from icsharpcode/fix/bundle-entry-decompression-bound
Never size bundle entry decompression from the manifest's declared size
2026-08-25 18:03:28 +02:00
Christoph Wille 436a82a3f8 Merge pull request #4046 from icsharpcode/fix/lightjson-parser-depth-limit
Cap LightJson parser nesting depth to prevent stack overflow
2026-08-25 18:03:05 +02:00
Christoph Wille 629bed5677 Rename CheckLength to CheckLengthOrThrow
Assisted-by: Claude:claude-fable-5:Claude Code
2026-08-25 13:53:19 +02:00
Christoph Wille c74990d987 Share the bundle signature with the tests instead of copying it
Two test fixtures carried their own copy of the 32-byte signature, which
would silently drift from the real one. The signature is now an internal
member of SingleFileBundle and ILSpy.Tests gets internals access to the
decompiler assembly, matching what ILSpyX already grants it.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-08-25 13:52:27 +02:00
Christoph Wille feee85dc6c Detect a bundle signature that ends exactly at the end of the mapped view
IsBundle scanned up to but excluding the last position at which a full
signature fits, so a signature occupying the final 32 bytes of the region
was never compared. Windows hid this: a memory-mapped view there reports
the page-rounded region size, leaving trailing zero bytes after the file.
On Linux and macOS the view length is the exact file length, and the
LoadedPackage bundle tests, whose synthetic bundles end with the
signature, failed there with FromBundle returning null. Real bundles keep
apphost code after the signature, which is why this stayed latent.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-08-25 11:58:27 +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
Christoph Wille bdb4ffaa56 Bound every .resources length against the remaining stream before allocating
A .resources file's resource count, type count, name lengths, binary
resource lengths and serialized-object lengths all come from the file
and were only checked for being non-negative before sizing an allocation.
A crafted file can therefore request multi-gigabyte arrays from a
few-hundred-byte payload (CWE-789), turning a click on a resource node
into an out-of-memory condition. The serialization-format kind was
additionally an assert-only check that vanishes in Release builds.

Each element of these counts occupies at least one byte in the stream, so
a value needing more bytes than remain after the current position cannot
be honest. Reject it with the same BadImageFormatException the callers
already handle, and promote the format-kind assert into a real check.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-08-25 11:37:25 +02:00
Christoph Wille 9b3e33472d Cap LightJson parser nesting depth to prevent stack overflow
The JSON parser's value/object/array readers are mutually recursive with
no depth limit, so input nested tens of thousands of levels deep overflows
the stack with an uncatchable StackOverflowException (CWE-674) that kills
the process. This is reachable through DotNetCorePathFinder, which parses
the .deps.json shipped next to an opened assembly, so a crafted manifest
beside a target turns dependency resolution into a clean process kill.

Thread a depth counter through the readers and throw a catchable
JsonParseException once nesting passes a fixed cap. The cap (64) matches
the System.Text.Json default and is far beyond any real dependency graph.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-08-25 11:28:05 +02:00
Christoph Wille 24ec3327e7 Merge pull request #4045 from icsharpcode/dependabot/github_actions/github/codeql-action-4.37.8
Bump github/codeql-action from 4.37.7 to 4.37.8
2026-08-25 07:42:23 +02:00
dependabot[bot] 678699877b Bump github/codeql-action from 4.37.7 to 4.37.8
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.37.7 to 4.37.8.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v4.37.7...v4.37.8)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.37.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-08-24 22:43:43 +00:00
Christoph Wille 542d3b0407 Merge pull request #4044 from icsharpcode/fix/4040-search-result-icon-overlays
Fix #4040: Compose accessibility and static overlays onto search result icons
2026-08-24 18:36:24 +02:00
Christoph Wille 6e298af439 Cover the method, property, event and namespace-fallback search icon arms
The search icon tests only exercised the type and field delegations; the
method, property and event arms and the namespace LocationImage fallback
for top-level types were untested.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-08-24 12:54:20 +02:00
Christoph Wille c80a2ec36a Route remaining flat icon sites through the tree nodes' GetIcon helpers
The derived-types entries, the compare pane, and the analyzer tree nodes
still built their icons from bare base images (or private duplicates of
the helper logic), losing the accessibility/static overlays and the
kind-specific glyphs (enum value, literal, readonly field, indexer,
P/Invoke, virtual and extension methods). The WPF frontend routes all of
these through the tree nodes' static GetIcon helpers; do the same so
every pane composes icons identically by construction.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-08-24 12:51:09 +02:00
Christoph Wille 300c075ffe Map protected-internal types to the plain protected overlay
The WPF frontend uses a type-only overlay mapper that shows protected
internal types with the plain protected badge, while members get the
combined protected-internal badge; the Avalonia frontend ran both through
the shared Images.GetOverlay and so badged types differently. Restore the
type-only mapping in TypeTreeNode.GetIcon, which now also covers search
results and every other caller of the helper.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-08-24 12:38:56 +02:00
Christoph Wille c11eded3ea Fix #4040: Compose accessibility and static overlays onto search result icons
The search result factory built icons from the bare base images, bypassing
Images.GetIcon, so search results lost the private/internal/protected and
static mini-overlays (and flattened interfaces, structs, enums and delegates
to the class icon; constructors, operators and indexers to the plain member
icons). Delegate to the tree nodes' GetIcon helpers instead, as the WPF
frontend's SearchResultFactory did, so search icons match the assembly tree
by construction. TypeTreeNode and EventTreeNode get the same static GetIcon
extraction the other member tree nodes already had.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-08-24 10:27:03 +02:00
Christoph Wille 88dd08a947 Merge pull request #4039 from icsharpcode/fix/ui-regressions-4027-4028-4030
Fix five Avalonia UI regressions: mouse navigation, derived types, Enter, Delete, Analyze in the Analyzer pane
2026-08-24 07:16:09 +02:00
Christoph Wille 38f113f1ef Analyzer pane no longer duplicates entries analysed from different panes (#4030)
The pane deduped new entries by comparing IModule instances, but analyzer
results live in the type system each analyzer run builds, so the same
member analysed from a result row and from the assembly tree (or from a
re-run analysis after its rows were collapsed away) never matched and got
a second top-level row. The loaded MetadataFile is the identity that
survives across type systems.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-08-23 17:19:09 +02:00